[个人自训]一些CF1700~2000的博弈论相关(但可能并不是博弈论)的题目
碎碎念
板刷start
9.27 更新2题
9.28 更新1题
9.30 更新1题
10.2 更新2题
计组摸鱼,latex有空加
1728D Letter Picking
博弈论+DP
题意
给定一个偶数长度小写字母字符串 $ s $ , \(Alice\) 和 \(Bob\) 每人拥有一个初始为空的字符串,两人轮流操作。
每次操作可以选取\(s\)的最左端或者最右端的字母,放进自己的字符串的开头位置。
当 \(s\) 长度为0时游戏结束,自己的字符串字典序小的获胜,字典序相同则平局。
\(Alice\) 先手,两人均采取最优策略,输出游戏结果。
分析
从大状态(长字符串)考虑去转移到小状态(短字符串),则大状态时取到的字符一定在小状态的末尾而非开头,字典序不定,无法对游戏结果做出判断。
因此考虑以 \(dp\) 的方式从小状态去转移到大状态,这样小状态时的字符在大状态时的开头位置,对游戏的结果字典序具有决定性作用,一旦小状态取得必胜态,则大状态也必胜。
设 \(dp[l][r]\) 表示以 \(s[l,r]\) 作为游戏字符串得到的结果,取值为 \(1,0,-1\) 。\(1\) 为 \(Alice\) 胜, \(-1\) 为 \(Bob\) 胜, \(0\) 为平局。
\(r-l+1==2\) 时
不难发现,此时若 \(s[l]=s[r]\) ,则游戏平局,否则 \(Alice\) 总能获胜。
\(r-l+1>2\) 时
因为偶数字符串才有意义,此处只考虑 \(r-l+1==2,4,……n\) 的情况。
当小状态已经是必胜态时,大状态必是必胜态。当小状态平局, \(Alice\) 掌握选择权,总有办法使得大状态平局或必胜。对于 $ Alice$ 只能由小状态的必败态推出大状态的必败态,而对于 \(r-l+1==2\) 的最小状态不存在必败态,所以整个游戏对于 \(Alice\) 无必败态,只存在平局和 \(Alice\) 胜。
若 \(Alice\) 拿 \(s[l]\) ,且 \(Alice\) 想要获胜:
-
若Bob拿 \(s[r]\) : \(dp[l+1][r-1]\) 对于 \(Alice\) 已经是必胜态,或 \(dp[l+1][r-1]\) 是平局且 \(s[l]<s[r]\)
-
若Bob拿 \(s[l+1]\) :\(dp[l+2][r]\) 对于 \(Alice\) 已经是必胜态,或 \(dp[l+2][r]\) 是平局且 \(s[l]<s[l+1]\)
以上两种状况同时满足,则 \(Alice\) 必胜。否则平局。 \(Alice\) 拿 \(s[r]\) 同理。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define IOS ios::sync_with_stdio(false);cin.tie(0),cout.tie(0);
#define inf 0x3f3f3f3f
const int N=2100;
int t,n,q,dp[N][N];
char s[N];
void solve()
{
cin>>(s+1);
n=strlen(s+1);
for(int i=1;i<n;i++)
if(s[i]==s[i+1]) dp[i][i+1]=0;
else dp[i][i+1]=1;
for(int len=4;len<=n;len+=2)
for(int l=1;l<=n-len+1;l++)
{
int r=l+len-1;
if((s[l]<s[l+1]||dp[l+2][r])&&(s[l]<s[r]||dp[l+1][r-1])) dp[l][r]=1;
else if((s[r]<s[r-1]||dp[l][r-2])&&(s[l]>s[r]||dp[l+1][r-1])) dp[l][r]=1;
else dp[l][r]=0;
}
if(dp[1][n]) puts("Alice");
else puts("Draw");
}
main()
{
cin>>t;
while(t--) solve();
}
1665D GCD Guess
交互+位运算+ \(gcd\) (为啥标签有 \(games\) ?)
题意
题目要求猜一个 \(1e9\) 内的正整数 \(x\) ,最多 \(30\) 次询问机会,每次给 \(system\) 两个数 \(a,b\) , \(system\) 返回 \(gcd(x+a,x+b)\) 。猜出 \(x\) 则输出。
假设有两个二进制表示的数字: \(1010101000\) 与 \(1000\) ,会发现两数的最大公约数就等于较小数,即 \(gcd(1010101000,1000)=1000\) ,这样我们就能推出较大数的低位部分。假设这个数是 $1010101011 $ 呢?我们可以从低位开始猜。
注意到 \(gcd(1010101011,1)=gcd(1010101010,1)=1\) ,也就是说猜第一位时可能无法判断。我们可以询问第 \(k\) 位时询问 \(gcd(x+(1<<k),2^{k+1})\) ,比如猜第一位时询问
\(gcd(1010101011+1,10)\) ,得到结果为 \(10\) (二进制表示),则说明第一位为 \(1\) 。
使用一个数字 \(r\) 记录以及猜出的低位部分。如果猜出第 \(k\) 位为 \(1\) ,则 \(r+=(1<<k)\) ,下次询问时将 \(r\) 减去,低位就全为 \(0\) 了。我们使 \(k++\) ,继续猜测 \(gcd(x-r+(1<<k),2^{k+1})\) 。
从低位往高位猜,假设猜到第 \(k\) 位,我们设询问的数字为 \(q\) ,初始 \(q=x\) 。将 \(q\) 已经猜出的低位结果 \(r\) 减去(第 \(1-k\) 位置为 \(0\) ),然后将 \(q\) 的第 $ k$ 位加 \(1\) , \((q+=(1<<k))\) ,若 \(q\) 的第 $ k$ 位原本为 \(1\) ,则此时 \(q\) 进位。反之 \(q\) 的第 \(k\) 位此时为 \(1\) 。我们询问 \(gcd(q,2^{k+1})\) ,则此时若 \(q\) 此位原本为 \(1\) ,则返回 \(2^{k+1}\)。反之则不为 \(2^{k+1}\) 。
假设有一个二进制数字:\(101001\) ,设为 \(x\) ,我们需要去猜出它。模拟过程如下:
第一次我们询问 \(gcd(x+1,2^1)\) ,得到结果为 $ 2^1$ ,说明第一位为 $ 1$ 。此时 $ r=1$ 。
第二次我们询问 $ gcd(x-r+(1<<1),2^2)$ ,得到结果不为 \(2^2\) ,说明第二位为 $ 0$ 。 \(r=1\) 。
第3次同上。
第四次我们询问 \(gcd(x-r+(1<<3),2^4)\) ,得到结果为 \(2^4\) ,说明第四位为 \(1\) , \(r=1001\) 。
以此类推得到结果为 \(101001\) 。
这样每次可以猜测一位数字,猜测 \(30\) 次, \(2^{30}\) 的范围正好约为 \(1e9\) 。
问题在于,输入 \(a\) 与 \(b\) ,题目返回的值是 \(gcd(x+a,x+b)\) 。我们用一次更相减损术,化成 \(gcd(x+a,b-a)\) 。猜到第 \(k\) 位时,我们使 \(a=-r+(1<<k),b=2^{k+1}-r+(1<<k)\) 即可。
#include<bits/stdc++.h>
#define int long long
using namespace std;
int t,x,r,p[40];
main()
{
p[0]=1;
for(int i=1;i<=31;i++) p[i]=p[i-1]*2;
cin>>t;
while(t--)
{
r=0;
for(int k=0;k<30;k++)
{
int ans,a=-r+(1<<k),b=p[k+1]-r+(1<<k);
cout<<'?'<<' '<<a<<' '<<b<<'\n'<<'\n';
cin>>ans;
if(ans==p[k+1]) r+=(1<<k);
}
cout<<'!'<<' '<<r<<'\n';
}
}
1537D Deleting Divisors
一道接近10000人过的1700题 个人感觉评低一点会更中肯。
(当然思维难度还是有的,分类讨论很妙。可能因为代码很简洁所以过的人多)
题意
给定一个 \(1e9\) 范围的数 \(x\) ,\(Alice\) 和 \(Bob\) 轮流操作,每次可以使 \(x\) 减去一个除了 \(1\)和它本身的因子。下一个人再在现在的 \(x\) 上进行同样操作。不能操作的人输掉游戏。
\(Alice\) 和 \(Bob\) 都足够聪明,输出游戏结果。
假设 \(x\) 为质数或1,则其一定为奇数或2,此时Alice无法操作,先手必输。
若 \(x\) 为合奇数,分解 \(x\) 的因子,我们发现它一定是两个奇数相乘的结果。\((奇数*奇数=奇数,奇数*偶数=偶数,偶数*偶数=偶数)\) ,设为 \(a*b\) 。
第一次Alice无论使x-a或x-b,下一步Bob都可以跟着Alice减,使得Alice再次面临\(a*(b-2)\) 或 \((a-2)*b\) 即奇数乘奇数的局面,最后Alice将面临1*奇数的局面。
而奇数要么可以再次分解为奇数*奇数,进行类似操作,要么已经成为1或质数。总之,可以得到先手必输的结论。
上述为 \(x\) 为奇数或质数的讨论。若 $ x$ 为除了2之外的偶数,情况又会如何呢?
对一个偶数不断地进行除以2的操作,最终一定能够将它变为奇数。因此任意一个偶数都可以表示成\(2^k*t\)的形式,其中t为奇数。
假如t不为1,则Alice可以直接选择减去t,使x变为 \((2^k-1)*t\),此时Bob面对\(奇数*奇数\)的情况,同上述相同,故先手必胜。
若t已经为1,则 \(x=2^k\) ,Alice第一步只能选择一个 \(2^m\),\(x\) 变为 \(2^m*(2^{k-m}-1)\)。
若Alice使k-m>1,则Bob面临的局面仍为\(偶数*奇数\),他会减去奇数,让Alice面临\(奇数*奇数\)局面,,Alice必输。因此,Alice只能使m=k-1,选择减去 \(2^{k-1}\) ,则x变为 $2^{k-1} $。Bob选择时同理只能选择 \(2^{k-2}\) 。
若k为奇数,则进行偶数次操作,最后得到2,若k为偶数,则进行奇数次操作后得到2。因此,k为奇数时Alice必输,k为偶数时Alice必胜。
分析很多,代码很简单。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define IOS ios::sync_with_stdio(false);cin.tie(0),cout.tie(0);
#define inf 0x3f3f3f3f
int n,t;
void solve()
{
cin>>n;
if(n%2==1) cout<<"Bob"<<'\n';
else
{
int m=0;
while(n%2==0) m++,n/=2;
if(n>1) cout<<"Alice"<<'\n';
else if(m%2==0) cout<<"Alice\n";
else cout<<"Bob\n";
}
}
main()
{
cin>>t;
while(t--) solve();
}
1503B 3-Coloring
题意
给一个\(n*n\)的棋盘,每个格子可以填一种颜色,每个填完颜色的格子不能与上下左右四个格子的颜色相同。一共有三种颜色1 2 3。Alice每次指定本次填色不能填某种颜色,Bob在某个格子填色。如果填色发生矛盾,则Bob输,如果Bob顺利填完\(n*n\)的格子,Alice输。已知Bob有策略必赢,每次给出Alice的选择,输出Bob填的颜色和位置。
分析
做法假了好几次,WA麻了
其实棋盘用两种颜色就可以不冲突地填满,设为1 2,处理出一个能够填满棋盘只包含1 2的标准数组。
Alice每次只能指定一种颜色不能填,则除非已经将某种颜色填满,否则必定能填。
而假如我们已经将1所在的位置填完,观察可以发现,剩下的位置无论填2还是填3都不会再冲突,剩下同理。
代码是自己写的所以很丑。。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define IOS ios::sync_with_stdio(false);cin.tie(0),cout.tie(0);
#define inf 0x3f3f3f3f
const int N=200;
int t,n,q,x,bz[N][N],mp[N][N];
void solve()
{
cin>>n;
x=0;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(j==1)
{
if(bz[i-1][j]==2) bz[i][j]=1;
else bz[i][j]=2;
}
else
{
if(bz[i][j-1]==2) bz[i][j]=1;
else bz[i][j]=2;
}
//cout<<bz[i][j]<<' ';
}
//puts("");
}
for(int k=1;k<=n*n;k++)
{
cin>>x;
bool flag=0;
if(x!=1||x!=2)
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(bz[i][j]!=x&&mp[i][j]==0)
{
mp[i][j]=bz[i][j];
cout<<bz[i][j]<<' '<<i<<' '<<j<<endl<<endl;
flag=1;
break;
}
}
if(flag) break;
}
}
if(flag==0)
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
for(int l=1;l<=3;l++)
{
if(x!=l&&mp[i][j]==0&&mp[i-1][j]!=l&&mp[i+1][j]!=l&&mp[i][j-1]!=l&&mp[i][j+1]!=l)
{
mp[i][j]=l;
cout<<l<<' '<<i<<' '<<j<<endl<<endl;
flag=1;
break;
}
}
if(flag) break;
}
if(flag) break;
}
}
}
}
main()
{
solve();
}
C. Even Number Addicts
昨晚的题目,木有做出来TwT
题意
给定一个a数组,Alice和Bob轮流拿取其中的任意数字放进自己手中,Alice先手。当拿完全部数字时,如果Alice手中所有数之和为偶数则Alice胜,反之则Bob胜。
假设Alice和Bob都足够聪明,输出谁获胜。
分析
拿取数字的大小对结果无影响,而拿取数字的奇偶性对结果是有影响的。假设Alice此刻拿到的是奇数,则Alice手中数之和的奇偶性改变,反之不变。
可以从一个小的必败态或必胜态的状态向大状态递推。若小状态中存在必胜态,则大状态为必胜态。若小状态中均为必败态,则大状态也为必败态。
设 \(dp[i][j][k][l]\) 表示游戏目前有i个奇数 j个偶数 当前Alice手中的数字的奇偶性(0表示偶数 1表示奇数)此时是l(l取0 1,0表示Alice的回合,1表示Bob的回合)
dp数组值为0表示当前人必败,反之必胜。初始化为-1。具体解释在代码里。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define IOS ios::sync_with_stdio(false);cin.tie(0),cout.tie(0);
#define inf 0x3f3f3f3f
const int N=200;
int t,n,q,a[N];
int dp[N][N][2][2];
int dfs(int cnt0,int cnt1,int cur,int turn)
{
int &u=dp[cnt0][cnt1][cur][turn];
if(~u) return u; //如果u!=-1 表示此状态已经被遍历,直接返回
if((!cnt0)&&(!cnt1)) return u=(!cur^turn); //如果此时已经将全部数字拿完。 如果Alice手中为奇数且此时是Bob的回合,Bob必胜返回1 如果Alice手中为奇数且此时是Alice的回合 Alice必输返回0 如果Alice手中为偶数且此时是Alice的回合,Alice必胜返回1 如果Alice手中为偶数且此时是Bob的回合 Bob必输返回0。
int res=0;
if(cnt0) res|=!dfs(cnt0-1,cnt1,cur,!turn); //取非是因为 当前回合某人的必输=下一回合另一人的必赢
if(cnt1) res|=!dfs(cnt0,cnt1-1,cur^!turn,!turn); //取了一个奇数,如果取奇数的人是Bob,不影响Alice手中有的数的奇偶性。如果取奇数的人是Alice则会影响Alice手中有的数字的奇偶性。
return u=res;
}
void solve()
{
memset(dp,-1,sizeof dp);
cin>>n;
int ans=0;
for(int i=1;i<=n;i++)
{
cin>>a[i];
if(a[i]&1) ans++;
}
if(dfs(n-ans,ans,0,0)) cout<<"Alice"<<'\n';
else cout<<"Bob"<<'\n';
}
main()
{
cin>>t;
while(t--) solve();
}
1600E Array Game
题意
给定一个a数组,Alice和Bob轮流拿取,每次可以拿a数组最左的数字或者最右的数字放到一个初始为空的序列的末端,需要保证该序列为严格上升序列,不能拿取的玩家输掉游戏。
Alice和Bob都足够聪明,输出谁赢得游戏。
分析
纯纯和2022ICPC网络赛2原得差不多的题。。。
记录一下从最左与最右端的最长上升长度,记为len1,len2,指向a数组目前最左端的指针为p,目前最右端的指针为q。例如对于 5 6 7 8 6 4,len1=4,len2=3。
轮到某个玩家行动,若此时len1为奇数且a[p]>a[q],则当前玩家必赢。
同理若此时len2为奇数且a[q]>a[p],则当前玩家必赢。
若此时len1为偶数且a[p]>a[q],如果当前玩家只能选择拿a[p]则必输。如果当前玩家可以拿取a[q],则玩家只能拿取a[q]后接着递推。
若此时len2为偶数且a[q]>a[p],同理如上。
若目前局势不属于上述任何一种情况,则分两种情况拿a[p]和拿a[q]递推,如果小状态存在必胜态,则当前状态为必胜态。如果小状态均为必败态,则当前状态为必败态。
代码是自己写的所以很丑。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define IOS ios::sync_with_stdio(false);cin.tie(0),cout.tie(0);
#define inf 0x3f3f3f3f
const int N=2e5+100;
int t,n,a[N];
int dfs(int lenl,int lenr,int last,int p,int q,int turn)
{
//cout<<lenl<<' '<<lenr<<' '<<last<<' '<<p<<' '<<q<<' '<<turn<<'\n';
if((!lenl)&&(!lenr)) return 0;
if(p==q)
{
if(a[last]<a[p]) return 1;
return 0;
}
if(!lenl)
{
if(a[last]>=a[q]) return 0;
else return !dfs(lenl,lenr-1,q,p,q-1,!turn);
}
if(!lenr)
{
if(a[last]>=a[p]) return 0;
else return !dfs(lenl-1,lenr,p,p+1,q,!turn);
}
if((lenl&1)&&a[p]>a[q]) return 1;
if((lenr&1)&&a[p]<a[q]) return 1;
if((lenl&1==0)&&a[p]>a[q]&&a[last]<a[q]) return !dfs(lenl,lenr-1,q,p,q-1,!turn);
if((lenl&1==0)&&a[p]>a[q]&&a[last]>a[q]) return 0;
if((lenr&1==0)&&a[p]<a[q]&&a[last]<a[p]) return !dfs(lenl-1,lenr,p,p+1,q,!turn);
if((lenr&1==0)&&a[p]<a[q]&&a[last]>a[p]) return 0;
int res=0;
if(last==0||a[p]>a[last]) res|=!dfs(lenl-1,lenr,p,p+1,q,!turn);
if(last==0||a[q]>a[last]) res|=!dfs(lenl,lenr-1,q,p,q-1,!turn);
return res;
}
void solve()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
int len1=1,len2=1,p=1,q=n;
while(p+1<=n&&a[p+1]>a[p]) p++,len1++;
while(q-1>=1&&a[q-1]>a[q]) q--,len2++;
p=1,q=n;
int x=dfs(len1,len2,0,p,q,0);
if(x) cout<<"Alice";
else cout<<"Bob";
}
main()
{
solve();
}

浙公网安备 33010602011771号