Codeforces Round #659 (Div. 2)A-D题解

Codeforces Round #659 (Div. 2)A-D题解

//写于rating值2032/2056
这一场cf是补的,题目分布并没有按照往常的难度梯度递增。
这场是同时有div1和div2,对于div2的人来说这场如果策略得到及时跟过题人数多的题去写C的话是个上分的好机会。
A题难度为一般div2场的B
B1B2题难度为一般div2场的DE
CD难度对应一般div2场的DE

Codeforces Round #659 (Div. 2)

A题难度系数1200
一个简单的构造
对于除了第一个和最后一个字符串外的每个字符串来说,它和前一个字符串有长度为x1的前缀子串是相同的,和后一个字符串有长度为x2的前缀子串是相同的。那么这个字符串的总长度不可以小于x1和x2,也就是至少是max(x1,x2)才能满足构造需求。并且由于字符串长度不能为0,因此也不能小于1。
由此得到每个字符串的长度后,就依照下标顺序依次构造过来就可。

#include<bits/stdc++.h>
#define ll long long
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;

char next(char in)
{
    
    
    return 'a'+(in-'a'+1)%26;
}

int32_t main()
{
    
    
    IOS;
    int t;
    cin>>t;
    while(t--)
    {
    
    
        ll n;
        cin>>n;
        vector<ll>num(n);//num[i]表示字符串i和字符串i+1的最长公共前缀的长度
        for(auto &x:num) cin>>x;
        vector<string>s(n+1);
        for(ll i=0;i<=num[0];i++) s[0]+='a';
        for(ll i=1;i<n;i++)
        {
    
    
            ll len=max(num[i-1],num[i])+1;
            //字符串s[i]的长度,需要满足不小于num[i-1]和num[i-2]这两个长度,且长度不能为0因此+1
            for(ll j=0;j<num[i-1];j++)  
            //num[i-1]为字符串s[i-1]和字符串s[i]前缀公共的部分,直接复制过来
                s[i]+=s[i-1][j];
            if(num[i-1]<s[i-1].size()) s[i]+=next(s[i-1][num[i-1]]);
            //检验下一个与字符串s[i-1]不匹配的位置字符串s[i-1]是否存在字符
            //如果存在就使用那个字符对应字母表里的下一个字母替换
            while(s[i].size()<len) s[i]+='a';//后面部分随意补
        }
        for(ll i=0;i<num[n-1];i++)
            s[n]+=s[n-1][i];
        s[n]+=next(s[n-1][num[n-1]]);
        for(ll i=0;i<=n;i++) cout<<s[i]<<endl;
    }
}

B1题难度系数1900
贪心,dp,为B2题的解法提供基础
对于任意的时间t1和t2,如果t1%(2k)==t2%(2k)的话,t1和t2这两个时间点每个位置的水深是相同的。
接着题意中一个条件为,如果我们希望在t(对2k取模过)时间出现在距离为d的地点的话,首先在t这个时间点的时候距离为d的地点水深不可超过l的水深限制,并且我们必须可以在(t-1+2k)%(2k)的时间点出现在距离为d-1的地点。
接着就是对于距离为d的地点,我们可以采取贪心的策略,如果我们在t时间可以出现在这个地点,那么如果t+1的时间点我们待在原地不动且该处水深没超过l的话,也就意味着我们可以在t+1的时间点出现在这个地点。
由此我们得到了从位置d-1的出现时间点情况推至位置d的出现时间点情况
以下在为B2的解法做铺垫
我们注意到水深是[0,1,2…k-1,k,k-1,k-2…1]这样分布的,对于k这个时间点如果我们可以出现,那么k+1这个时间点的水深更浅,我们同样可以出现,同理一直推到时间点0;但是从时间点0开始就不一定了,因为后面水深是越来越深,可能在某一个时刻我们不能继续在这个地点待下去。。
我们从时间点k开始往后推,如果当前时间可以出现当前位置,那么下一个时间点如果当前位置的水深没有超过限制,意味着下一个时间点也可以出现在当前位置。
我们刚从位置d-1的情况推到位置d的情况时,时间点k可能是无法出现在位置d的,但是经过上面两段的贪心处理后,在时间点k是可能出现在位置d的。一旦时间点k这个最深水深的情况可以出现在位置d,那么所有的时间我们都可以出现在位置的。因此我们循环一次2k是不够的,需要循环两次2k。

#include<bits/stdc++.h>
#define ll long long
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
const ll maxn=1e2+7;

ll dp[maxn][2*maxn];//dp[i][j]为在时间点j能否出现在距离为i的地点
ll deep[maxn];//deep[i]为距离为i处的初始水深
ll n,k,l;

ll cal(ll x)//计算时间点x水深增加了多少
{
    
    
    x%=(2*k);
    if(x<=k) return x;
    else return 2*k-x;
}

int32_t main()
{
    
    
    IOS;
    int t;
    cin>>t;
    while(t--)
    {
    
    
        cin>>n>>k>>l;
        for(ll i=1;i<=n;i++) cin>>deep[i];
        memset(dp,0,sizeof(dp));
        for(ll i=0;i<2*k;i++)
            dp[0][i]=1;
        for(ll i=1;i<=n;i++)//i为距离
        {
    
    
            for(ll j=0;j<2*k;j++)//j为时间点
            {
    
    
                if(deep[i]+cal(j)<=l)//预处理,从i-1位置的出现情况推至当前i位置的出现情况
                {
    
    
                    if(dp[i-1][(j-1+2*k)%(2*k)]) dp[i][j]=1;
                }
            }
            for(ll j=0;j<4*k;j++)//在当前位置停留,注意是循环两次2k
            {
    
    
                ll now=(k+1+j)%(2*k);//从k+1这个时间点开始循环检测4k个点
                ll pre=(now-1+2*k)%(2*k);
                if(deep[i]+cal(now)<=l&&dp[i][pre]) dp[i][now]=1;
            }
        }
        bool flag=0;
        for(ll i=0;i<2*k;i++)//是否能在距离n的地点出现
            if(dp[n][i]) flag=1;
        if(flag) cout<<"Yes"<<endl;
        else cout<<"No"<<endl;
    }
}



B2题难度系数2200
其实总结一下我们B1题的解法,我们会发现,经过我们在某一个位置的停留处理后,在这个位置的可能出现的时间点其实是一个连续的时间区间(0到2k-1的一个循环区间,一部分区间在最左侧,一部分区间在最右侧也是连续的)。
由于是循环的时间区间,我们重定义一下各个时间点的水深增加量为[k-1,k-2…1,0,1,2,3…k-1,k]
经过这样的重定义后,我们会发现我们的时间区间就变成必然是连续的一段,而不会出现一部分在最左一部分在最右了,方便讨论。
详细看代码注释吧,难点在于前一个位置的时间区间右移1之后可能会出现的区间循环问题,这里用了flagl记录右移1之后最左侧是否存在区间,flagr记录除了最左侧外是否还有区间,分类讨论。

#include<bits/stdc++.h>
#define ll long long
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
const ll maxn=3e5+7;

ll n,k,l;
ll deep[maxn];
ll prel,prer;//记录上一个地点可出现的时间区间的左右下标
ll nowl,nowr;//当前位置的可出现的时间区间的左右下标
bool flagl,flagr;//记录pre区间整体右移1个位置后,最左侧和除了最左侧外是否存在区间

int32_t main()
{
    
    
    IOS;
    int t;
    cin>>t;
    while(t--)
    {
    
    
        cin>>n>>k>>l;
        for(ll i=1;i<=n;i++) cin>>deep[i];
        prel=0;prer=2*k-1;//初始地点任意时间点都可出现
        bool flag=1;
        for(ll i=1;i<=n;i++)
        {
    
    
            ll temp=l-deep[i];//记录当前位置水深最多能增加多少
            if(temp<0)
            {
    
    
                flag=0;
                break;
            }
            nowr=min(k-1+temp,2*k-1);//只考虑水深的情况下左右下标的界限是多少
            nowl=max(0ll,k-1-temp);//注意这里对应的水深是[k-1,k-2....1,0,1,2...k-1,k]
            prel++;prer++;//前一个位置时间区间整体+1
            if(prel==2*k)//特判,如果左下标超出了限制,代表只有一个时间点2k-1且右移后变成了0
            {
    
    
                flagr=0;
                flagl=1;//由于只右移一个位置,因此flagl指示的最左侧区间只有一个0时间点
            }
            else if(prer==2*k)//右下标超出了限制而左下标没有,则最左侧存在区间,除了最左侧外也有区间
            {
    
    
                prer--;
                flagl=flagr=1;
            }
            else//左右下标都未超出界限
            {
    
    
                flagl=0;
                flagr=1;
            }
            if(flagl&&nowl==0);//存在左时间区间并且当前时间区间为0的时候就不要做修改了
            else//左侧区间考虑完毕,再考虑右侧区间
            {
    
    
                if(flagr)//存在右区间的情况
                {
    
    
                    if(nowl>prer||nowr<prel)//前面位置的区间与当前区间没有匹配部分,那就没有方案了
                    {
    
    
                        flag=0;
                        break;
                    }
                    else nowl=max(nowl,prel);//由于我们可以原地停留,因此更新区间左下标就可以了
                }
                else//如果没有右侧区间那就是没有可行方案了
                {
    
    
                    flag=0;
                    break;
                }
            }
            prel=nowl;
            prer=nowr;
            if(prer==2*k-1) prel=0;
        }
        if(flag) cout<<"Yes"<<endl;
        else cout<<"No"<<endl;
    }
}

C题难度系数1700
贪心,理清思路就会比较简单。
首先确定一下无法从字符串a构造成字符串b的情况,我们只能把字符的值变得更大,因此如果初始的时候字符串a中某一个下标位置的字符比字符串b中该位置的大,那就无法构造。
接着考虑可以构造的情况。
对于字符串a中的某个字符,需要把这个字符构造成字符串b中对应位置的字符,看成一个路径映射。
这里可以把相同相同的路径映射当做一起来处理,因为如果相同的构造路径你采取了不同的构造方法,如果其中一种是最优的整体策略话,那么另一种必然不可能更优。由此我们对所有相同的构造路径采取相同的构造方法。
之后我们来思考贪心的策略。
我们首先存储下所有需要构造的路径,然后取起点值最小的那一个。
以第一个样例
3
aab
bcc
为例。
这里有a->b,a->c,b->c三个路径。
我们选取起点最小的a,a的终点有b和c两个。
a必然要构造成b和c,那么我们先构造出最近的b也就是构造a->b这个路径(所有的a可以一起被改变),那么此时,我们原本的a->c路径其实已经变成了b->c,搭上了b->c的这个“顺风车”。
原本的三个路径由此变成了两个路径。
以下代码便是基于此思路。
(感觉没什么好注释的)

#include<bits/stdc++.h>
#define ll long long
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
ll road[21][21];

int32_t main()
{
    
    
    IOS;
    int t;
    cin>>t;
    while(t--)
    {
    
    
        ll n;
        cin>>n;
        string a,b;
        cin>>a>>b;
        memset(road,0,sizeof(road));
        bool f=1;
        for(ll i=0;i<n;i++)
        {
    
    
            if(a[i]>b[i]) f=0;
            else road[a[i]-'a'][b[i]-'a']=1;
        }
        if(f)
        {
    
    
            ll ans=0;
            for(ll i=0;i<20;i++)
            {
    
    
                for(ll j=i+1;j<20;j++)
                {
    
    
                    if(road[i][j])
                    {
    
    
                        ans++;
                        for(ll k=j+1;k<20;k++)
                            if(road[i][k]) road[j][k]=1;
                        break;
                    }
                }
            }
            cout<<ans<<endl;
        }
        else cout<<-1<<endl;
    }
}

D题难度系数1900
位运算,博弈
这里要用到一种“整体思想”。
tip:奇数个1位运算异或得到1,偶数个1位运算异或得到0,参与位运算异或的0的个数对最终结果没有影响。
首先我们必然是考虑1所在的值最大的那个位置。
如果这个位置上出现1的次数为偶数,那么针对这一个位置上的1,如果这偶数个1被两个人任意取的情况可以分成了两种,一种是一个人拿了奇数个1,一个人拿了偶数个1(最后两个人都是1)另一种是一个人拿了偶数个1,另一个人拿了偶数个1(最后两个人都是0)。也就是说不管怎么取,两个人在这一个位置上的最终数值都是相等的,我们不必考虑这一个位置上的1。
由此一直向更低的位置去找,如果找到最低位了都没找到出现奇数个1的位置,那么两人就是平局。
现在讨论存在奇数个1的位置的情况。
这个时候又要根据这个奇数个1的数值情况分为两类讨论:
这个位置上有x个1,x是个奇数,总共的数字个数是n,也就是说还有n-x个0,n-x记为y。
先推一个前提结论(很重要),当1的个数x和0的个数y均为偶数的时候。先手的人或者后手的人如果自己希望让双方平分1和0的个数的话,另一个人不管怎么取都是可以,都是做到平分的。
如果是后手的人希望平分1和0的个数的话,他只要跟着先手的人拿就是了。
如果是先手的人希望平分1和0的个数的话,那他先手先随便拿一个,假设拿的1,如果后手的人拿1那最好,正合心意。如果后手的人拿0,那就跟着拿0。由于0的个数是偶数,到最后迟早后手的人还是要怪怪回来拿1.
推出这个结论后,后面讨论能轻松很多。
第一种情况:[x/2]+1是奇数的情况
这种情况下先手的人想要胜利,那么他就希望自己能拿到[x/2]+1最后为1,后手的人拿到[x/2]最后为0。
这种情况的抉择是必然先手胜利的。下面为推导过程。
1.如果0的个数y为偶数,这种情况下,先手的人选择先拿掉一个1,那么1和0的个数剩下的个数都是偶数。接下来利用上面的前提结论,让先手后手各自拿走一半,那么最后两人手里的1的个数就是[x/2]+1和[x/2],先手赢。
2.如果0的个数y为奇数,这种情况下,先手的先拿掉一个1,这样的话1的个数变为x-1是偶数,0的个数仍然为y是奇数。这种情况下,后手的人是不敢去拿0的,因为后手的人去拿0的话,剩下的1和0的个数都会变成偶数,这个时候利用上面的前提结论是可以做到让双方平分1和0的。这样的话先手就赢了,所以后手只能去拿1,但是后手去拿1的话,容易得到这种情况下依旧是先手赢。
第二种情况:[x/2]+1是偶数的情况
这种情况下先手的人想要胜利,那么他就希望自己能拿到[x/2]最后为0,后手的人拿到[x/2]+1最后为0。
但是这样的策略并不是所有情况都能如他所愿的。需要根据0的个数y来分类讨论。
1.如果0的个数y为奇数,这种情况下先手的人先拿掉一个0,那么0的个数变为y-1是偶数,1的个数仍然为x是奇数,接下来后手的人拿什么,先手的就跟着拿什么,到最后会剩下一个1被后手拿走,后手比先手多拿一个1,先手胜利。
2.如果0的个数y为偶数,这种情况下先手的人必败。因为1的个数x是奇数,0的个数y为偶数,先手拿什么,后手就跟着拿什么,最后会剩下一个1被先手拿走,先手的人比后手多拿一个1,后手胜利。

#include<bits/stdc++.h>
#define ll long long
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;

ll num[30];//1<<29就超过1e9的范围了,可以自己写个简短的代码跑一下得到这个数字

int32_t main()
{
    
    
    IOS;
    int t;
    scanf("%d",&t);
    while(t--)
    {
    
    
        int n;
        scanf("%d",&n);
        memset(num,0,sizeof(num));
        for(ll i=0;i<n;i++)
        {
    
    
            ll a;
            scanf("%lld",&a);
            ll temp=1;
            for(ll j=0;j<30;j++)
            {
    
    
                if(temp&a) num[j]++;
                temp<<=1;
            }
        }
        int f=0;//1为先手胜,0为平局,-1为后手胜
        for(ll i=29;i>=0;i--)
            if(num[i]&1)
            {
    
    
                if(num[i]/2&1)
                {
    
    
                    if((n-num[i])&1) f=1;
                    else f=-1;
                }
                else f=1;
                break;
            }
        if(f==1) printf("WIN\n");
        else if(f==0) printf("DRAW\n");
        else printf("LOSE\n");
    }
}

猜你喜欢

转载自blog.csdn.net/StandNotAlone/article/details/107603615