//写于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
#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
#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
#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
#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
#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");
}
}