2 条题解
-
0
题意分析
本题是一个关于博弈游戏的问题,游戏中玩家轮流选择大于1的整数,有禁选规则(已选数字及其倍数、已选数字倍数之和不能选),先手为Christine,后手为Matt,若玩家无合法可选数字则输。要求根据给定的可选数字集合,判断其中的“必赢选项”。
解题思路
- 状态表示:
- 用一个整数
pos
表示当前已选数字的状态,每一位对应一个数字,若该位为1表示对应的数字已被选,初始时pos
为所有数字都未选的状态,即(1 << 21) - 3
(因为1不参与游戏,所以去掉前两位)。 - 对于每个测试用例,根据输入的可选数字集合,更新
pos
,将已选数字对应的位设为0。 - 用
dp[posb]
表示状态posb
(剩余可选数字的状态)下是否必赢,-1
表示未计算,0
表示必输,1
表示必赢。
- 用一个整数
- 深度优先搜索(DFS):
- 定义
DFS(int posa, int posb)
函数,posa
表示当前已选数字的状态,posb
表示剩余可选数字的状态。 - 若
posb == 0
,表示没有可选数字了,当前玩家必输,返回0。 - 若
dp[posb] != -1
,表示该状态已经计算过,直接返回dp[posb]
。 - 遍历所有可选数字,对于每个可选数字
a[i]
:- 先复制当前状态
ita = posa
,itb = posb
。 - 如果
itb
中该数字对应的位为1(即该数字可选),则根据规则更新ita
(将已选数字加上a[i]
对应的位)和itb
(去掉已选数字的倍数和已选数字倍数之和对应的位)。 - 递归调用
DFS(ita, itb)
,若返回0(即对手必输),则当前玩家必赢,将dp[posb]
设为1并返回1。
- 先复制当前状态
- 如果遍历完所有可选数字都没有找到必赢的情况,则当前玩家必输,将
dp[posb]
设为0并返回0。
- 定义
- 寻找必赢选项:
- 在
main
函数中,对于每个测试用例,先初始化dp
数组为-1
,然后对输入的可选数字进行排序。 - 遍历所有可选数字,对于每个数字
a[i]
,根据规则更新ita
和itb
,然后调用DFS(ita, itb)
。 - 如果
DFS(ita, itb)
返回0(即对手必输),则将该数字加入必赢选项数组ans
中。
- 在
- 输出结果:
- 按照输出格式要求,输出测试用例编号、必赢选项或提示没有必赢选项的信息。
复杂度分析
- 时间复杂度:
- 对于每个测试用例,遍历所有可选数字,对于每个数字又要进行一些状态更新和递归调用。假设可选数字个数为
n
,状态更新和递归调用的时间复杂度与数字的范围和状态表示有关,这里粗略估计为指数级,所以整体时间复杂度为指数级,具体为(因为每个数字都可能有选或不选两种情况,且对于每个状态需要遍历所有数字进行状态更新)。
- 对于每个测试用例,遍历所有可选数字,对于每个数字又要进行一些状态更新和递归调用。假设可选数字个数为
- 空间复杂度:
- 主要空间消耗是
dp
数组,其大小为(因为状态最多有种),以及存储必赢选项的数组ans
,其大小最多为n
,所以空间复杂度为。
- 主要空间消耗是
代码实现
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> using namespace std; const int maxn = 21; int n, pos, a[maxn], ans[maxn], cnt; int dp[2097158]; // 深度优先搜索函数,判断状态(posa, posb)下是否必赢 bool DFS(int posa, int posb) { if (posb == 0) return 0; if (dp[posb] != -1) return dp[posb]; int ita, itb; for (int i = 0; i < n; i++) { ita = posa; itb = posb; if (itb & (1 << i)) { // 更新ita,将已选数字加上a[i]对应的位 for (int j = 0; j + a[i] < 21; j++) if (ita & (1 << j)) ita |= 1 << (j + a[i]); // 更新itb,去掉已选数字的倍数和已选数字倍数之和对应的位 for (int j = 0; j < n; j++) if (itb & (1 << j) && (ita & (1 << a[j]))) itb ^= 1 << j; if (!DFS(ita, itb)) return dp[posb] = 1; } } return dp[posb] = 0; } int main() { int cas = 1; while (scanf("%d", &n) && n) { memset(dp, -1, sizeof(dp)); cnt = 0; pos = (1 << 21) - 3; for (int i = 0; i < n; i++) { scanf("%d", &a[i]); pos ^= 1 << a[i]; } sort(a, a + n); for (int i = 0; i < n; i++) { int ita = pos; int itb = (1 << n) - 1; // 更新ita,将已选数字加上a[i]对应的位 for (int j = 0; j + a[i] < 21; j++) if (ita & (1 << j)) ita |= 1 << (j + a[i]); // 更新itb,去掉已选数字的倍数和已选数字倍数之和对应的位 for (int j = 0; j < n; j++) if (ita & (1 << a[j])) itb ^= 1 << j; if (!DFS(ita, itb)) { dp[ita] = 1; ans[cnt++] = a[i]; } } printf("Test Case #%d\n", cas++); if (!cnt) { printf("There's no winning move.\n\n"); continue; } printf("The winning moves are:"); for (int i = 0; i < cnt; i++) printf(" %d", ans[i]); printf("\n\n"); } return 0; }
- 状态表示:
-
0
🧠 题解(Solution)
🔍 本质:
这是一道典型的 博弈论搜索题(Impartial Game, Game Theory),可用记忆化搜索 + MinMax 判断解决。
✅ 状态定义:
状态由“当前还可以选择的数字集合”定义;
若从当前状态出发,任意选择一个合法数字后,对手必输,则当前状态是必赢态(Winning Position);
如果无法选出使对方进入输局的数字,则当前为必输态(Losing Position)。
✅ 核心步骤:
对当前状态中每个合法数字 ,模拟选中它后的新状态:
移除 ;
移除所有 的倍数;
移除所有可以由选中集合线性组合得到的数(如 等);
递归判断新状态是否是必输状态(对方无必胜策略);
若存在某个 能使新状态是必输态,则 是必赢选项。
✅ 优化:
由于数字范围最多 ,状态可以用位图(bitmap)表示;
所有状态总数最多为 ,可使用记忆化搜索避免重复判断;
所有组合可预处理(剪枝优化)。
📌 举个例子(第三组):
当前可选集合为 ,Christine 可选择:
→ 移除 ⇒ 无剩余 ⇒ 对方输, 是必胜;
→ 同理判断;
找到所有使对方陷入无可选项的数字。
代码实现:
using namespace std; int timemark[21]; //每次搜索时,对数字是否可选的标记 int N; //输入数字的个数 int a[21]; //储存输入的数字 int record[524288]; //保存局面策略的数组 int cifang(int m,int n){//求m的n次方 if(n==0) return 1; int s=m; for(int i=1;i<n;i++) s=s*m; return s; } int bin2dec(int c[],int n){//将数组c对应的局面2进制数转化为十进制数字 int s=0; for(int i=1;i<=n;i++){ s=s+cifang(2,c[i]-2); } return s; } int pd(int a[],int n){ //a是储存当前局面数字的数组,n是数组的长度 int b2d=bin2dec(a,n); //求这个局面对应的十进制索引 if(record[b2d]!=0){ //如果是已搜索过的局面,则直接返回结果 return record[b2d]; } else{//否则对这个局面开始搜索 int winmove[21];//储存胜利策略的数组 int winCount=1;//储存胜利策略的数量 int flag=0;//标记是否为必胜局面的变量,0必输,1必胜 for(int i=1;i<=n;i++){//循环取出a中第i个数字 int count=1;//取出数字的个数 for(int ii=2;ii<=20;ii++){//标记若数字t在数组a中,则标记为1,否则为0,去掉第i个元素,所以timemark[i]=0 timemark[ii]=0; } for(int ii=1;ii<=n;ii++){ if(ii==i) continue; timemark[a[ii]]=1; } for(int k=2;a[i]+k<=20;k++){//将因为第i个数字被取出而影响的数字剔除 if(timemark[k]==0&&timemark[a[i]+k]!=0){ count++; timemark[a[i]+k]=0; } } int b[21];//储存a去掉取出的数字和受影响的数字后的数组,用来进行下一层搜索 for(int indb=1;indb<=n-count;){ for(int inda=1;inda<=n;inda++){ if(timemark[a[inda]]==1){ b[indb]=a[inda]; indb++; } } } int next=pd(b,n-count);//求下一层局面的胜负情况 if(next==-1){//如果下一层为必输局面,这一层就是必胜局面,同时当前取出的数字成为一个必胜策略 winmove[winCount++]=a[i]; flag=1; } if(i==n&&flag==0){//遍历本次所有可取的数字后,记录输赢的标记flag为0,则说明无论取哪个数字,下一个局面都是一个必胜局面,从而当前局面为必输局面 record[bin2dec(a,n)]=-1; return -1; } else if(i==n&&flag==1){//遍历本次所有可取的数字后,记录输赢的标记flag为0,从而当前局面为必赢局面 record[bin2dec(a,n)]=bin2dec(winmove,winCount-1);//将必胜策略转化为十进制数字储存在record中 return 1; } } } } void printWin(int t){//打印必胜时的策略 int count = 2; while(t!=0){ if(t%2==1) cout<<count<<' '; count++; t=t/2; } cout<<endl; } int main(){ cin>>N; int count=1; record[0]=-1; for(int i=1;i<524288;i++){ record[i]=0; } for(int i=2;i<=20;i++){ int a[2]; a[1]=i; record[bin2dec(a,1)]=bin2dec(a,1); } while(N){ for(int i=1;i<=N;i++) cin>>a[i]; cout<<"Test Case #"<<count++<<endl; if(N==1){ cout<<"The winning moves are: "<<a[1]<<endl; } else{ int t=pd(a,N); if(t==-1) cout<<"There's no winning move."<<endl; else{ cout<<"The winning moves are: "; printWin(record[bin2dec(a,N)]); } } cout<<endl; cin>>N; } return 0; }
- 1
信息
- ID
- 144
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者