取火柴问题变种

2069 字
10 分钟
取火柴问题变种

目录#

  • 起因
  • 问题构建 & 前置知识
  • 手推过程
  • DSDS 过程
  • 验证结论
  • 补充

起因#

高中毕业,整理材料的时候,发现自己之前在草稿纸上探究到一半的变种取火柴问题。于是就展开来探究了一下。

问题构建 & 前置知识#

正常的取火柴问题是:一堆火柴一共有 nn 根,有两个足够聪明的人轮流取火柴,每次可以取 1∼a1\sim a 根,取到最后一根火柴的人胜利,问先手必胜还是后手必胜。

例如,每次可以取 1∼31\sim 3 根。很容易知道,当 4∣n4 \mid n 时,后手必胜,否则先手必胜。也就是说,对于取 1∼a1\sim a 的情况,当且仅当 nn 是 (a+1)(a+1) 的倍数时,后手必胜。这里不展开赘述。

现在我们要讨论的是,如果在 1∼a1\sim a 中跳过一个数不能取,周期会发生什么变化?如果跳过不止一个数,周期又会发生什么变化?

在后续的问题探讨中,可以采用线性递推的方式,例如,你已经知道总火柴数为 1∼n1\sim n 时分别的必胜情况,那么对于总数 (n+1)(n+1) 的情况,只要能够一次取若干根跳到一个后手必胜的局面,那么该情况一定是先手必胜;若找不到,则后手必胜。

手推过程#

跳过一个数#

问题变为:每次可以取 1∼a1\sim a 或 (a+2)(a+2)

这个问题依然比较经典,经过几个例子的尝试后可以发现,后手必胜情况的周期为断点位置,即后手必胜当且仅当 n=k(a+1),kn=k(a+1),k 为正整数。

后面不止一个数#

问题变为:每次可以取 1∼a1\sim a 或 (a+2)∼b(a+2)\sim b

我们尝试举例。

尝试 11 22 44 :后手必胜周期 33

尝试 11 33 44 :后手必胜周期 2/52/5 交替,大周期 77

注意到,我们似乎需要比较前后两段数字的长短,也就是 aa 和 (b−a−1)(b-a-1) 的大小关系。

似乎,前半段较短时,会出现交替后手必胜周期;后半段较短时则不会?

尝试 1∼61\sim 6 8∼138\sim 13 :后手必胜周期 77

尝试 1∼61\sim 6 8∼148\sim 14 :后手必胜周期 7/157/15 交替,大周期 2222

推论:

  • 当 2a+1≥b2a+1\geq b :后手必胜周期 (a+1)(a+1)
  • 当 2a+1<b2a+1 < b :后手必胜周期 (a+1)/(b+1)(a+1)/(b+1) 交替,大周期 (a+b+2)(a+b+2)

经过多轮验证,应该成立。

开头一般化#

问题变为:每次可以取 a∼ba\sim b 或 (b+2)∼c(b+2)\sim c

这种情况不符合题目情况,总数为特定数时会出现最后几根怎么也取不走的情况,游戏无法进行。

故我们不讨论这种情况。

跳过若干数#

问题变为:每次可以取 1∼a1\sim a 或 (a+3)∼b(a+3)\sim b

我们毫无头绪,于是举例:


尝试 11 22 55 :后手必胜周期 33

尝试 11 22 66 :后手必胜周期 3/43/4 交替,大周期 77

尝试 11 22 77 :后手必胜周期 33

尝试 11 22 88 :后手必胜周期 33

尝试 11 22 99 :后手必胜周期 3/3/43/3/4 交替,大周期 1010

尝试 11 22 1010 :后手必胜周期 33

尝试 11 22 1111 :后手必胜周期 33

尝试 11 22 1212 :后手必胜周期 3/3/3/43/3/3/4 交替,大周期 1313


尝试 11 33 44 :后手必胜周期 2/52/5 交替,大周期 77

尝试 11 44 55 :后手必胜周期 2/62/6 交替,大周期 88

尝试 11 55 66 :后手必胜周期 2/2/72/2/7 交替,大周期 1111

尝试 11 66 77 :后手必胜周期 2/2/82/2/8 交替,大周期 1212

尝试 11 77 88 :后手必胜周期 2/2/2/92/2/2/9 交替,大周期 1515

尝试 11 88 99 :后手必胜周期 2/2/2/102/2/2/10 交替,大周期 1616


尝试 11 22 55 66 :后手必胜周期 3/43/4,大周期 77

尝试 11 22 66 77 :后手必胜周期 3/53/5,大周期 88

尝试 11 22 77 88 :后手必胜周期 33

尝试 11 22 88 99 :后手必胜周期 3/3/43/3/4,大周期 1010


感觉似乎是有规律的,但是我搞不明白,于是,我转向了 DeepseekDeepseek 。

DSDS 过程#

经过 DSDS,我们发现,当取 1∼a1 \sim a c∼bc \sim b (c≥a+2)(c \ge a+2) 的时候:

如果不考虑 c∼bc \sim b 的部分,大于 aa 的部分如果不是 (a+1)(a+1) 的倍数,一定先手胜,所有 (a+1)(a+1) 的倍数都是后手胜。

加入考虑, c∼bc \sim b 中可能影响后手胜位置的,便是其中是 (a+1)(a+1) 倍数的数。例如含有 (2a+2)(2a+2) ,那么此时比 (2a+2)(2a+2) 略大的数,跳到的 (2a+2)(2a+2) 就会变成先手胜。

这个时候我们需要讨论,通过 1∼31\sim 3 10∼1310\sim 13 和 1∼31\sim 3 10∼1210\sim 12 两个例子,我们发现前者是 4/4/144/4/14 , 1414 接在后边构成 2222 大周期,后者是 1313 把 4/44/4 包在里面形成 1313 大周期。

于是我又 DSDS 了一下,对于 1∼a1 \sim a c∼bc \sim b ,记 L=a+1L=a+1 ,他给出的结论是:

if 后半段连续区间包含m*L(m为能取所有值的最小值):
if m>=2 and 后半段区间长>=L:
T=(m-1)L+(b+1) 接尾型
else:
T=b+1 包裹型
else:
T=a+1 断点型

对于分支一,通俗的解释是: 1∼a1\sim a 覆盖所有可到达的非倍数余数,如果无 LL 的倍数,那么所有后手胜的位置都不会受到影响,等于是先手胜的位置自己之间加了一些连接。

对于分支二,通俗的解释是: bb 是能减去的最大数, (b+1)(b+1) 之前的数都按照 LL 的倍数规则,其中 c∼bc\sim b 全部标为先手胜,那么如果 (b+1)(b+1) 跳不到任何后手胜点,他自己就是后手胜,否则会被推迟到 (m−1)L+(b+1)(m-1)L+(b+1) 。

验证结论#

验证方案#

验证规则成立:对拍

对拍,具体来说就是,对比“暴力递推”和“规律求解”两种方法得出的周期是不是都相同。

输入#

输入所有可以取的数,然后输出 nn 为 1∼10001\sim 1000 分别是谁必胜,记先手必胜为 00 ,后手必胜为 11 。

规律求解法#

于是我先写了一个按照规则输出周期的代码 answer.cpp 。

暴力递推法#

在写这个代码的时候,我发现,我需要根据获得的 0/10/1 ansans 数组来求它的最小周期,这个时候我们可以使用 KMPKMP 匹配算法,此处不展开讲述,代码写入 duipai.cpp 。

随机数据生成#

这里我们使用 C++ 中的均匀整数生成工具,代码如 random.cpp 。

比对数据#

我写了一个程序,让他们自动生成 10001000 次数据,比对 10001000 次,看结果是否有差异。代码如 check.cpp 。

参考代码#

所有代码如下:

answer.cpp
#include<bits/stdc++.h>
using namespace std;
int n,A[105],m,L,a,c,b,T;
bool flag;
int read(){
int x=0,f=0;
char c=getchar();
while(c<'0'||c>'9'){
if(c=='-') f=1;
c=getchar();
}
while(c>='0'&&c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar();
return f?-x:x;
}
int main(){
n=read();
for(int i=1;i<=n;++i) A[i]=read();
//1.successive?
flag=1;
for(int i=2;i<=n;++i) if(A[i]!=A[i-1]+1){
flag=0;
a=A[i-1],c=A[i],b=A[n];
break;
}
if(flag) T=A[n]+1;
else{
L=a+1;
//2.mL?
flag=0;
for(int i=c;i<=b;++i) if(i%L==0){
flag=1;
m=i/L;
break;
}
if(flag){
if(m>=2&&b-c+1>=L) T=(m-1)*L+b+1;
else T=b+1;
}else T=L;
}
cout<<T;
return 0;
}
duipai.cpp
#include<bits/stdc++.h>
using namespace std;
int n,a[105],pi[1005];
bool flag,ans[1005];
int read(){
int x=0,f=0;
char c=getchar();
while(c<'0'||c>'9'){
if(c=='-') f=1;
c=getchar();
}
while(c>='0'&&c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar();
return f?-x:x;
}
int main(){
n=read();
for(int i=1;i<=n;++i) a[i]=read();
for(int i=0;i<1000;++i){
flag=1,ans[i]=0;
for(int j=1;j<=n;++j) if(i-a[j]==0||(i-a[j]>0&&ans[i-a[j]])) flag=0;
if(flag) ans[i]=1;
}
//find circle
for(int i=1;i<1000;++i){
int j=pi[i-1];
while(j>0&&ans[i]!=ans[j]) j=pi[j-1];
if(ans[i]==ans[j]) ++j;
pi[i]=j;
}
cout<<1000-pi[999];
return 0;
}
random.cpp
#include<bits/stdc++.h>
#include<random>
using namespace std;
int a,c,b,n,A[55];
int main(){
default_random_engine e;
uniform_int_distribution<int> u(1,20);
e.seed(time(0));
a=u(e);
c=u(e)+a;
b=u(e)+c;
for(int i=1;i<=b;++i) if(i<=a||(c<=i&&i<=b)) A[++n]=i;
cout<<n<<"\n";
for(int i=1;i<=n;++i) cout<<A[i]<<" ";
return 0;
}
check.cpp
#include<bits/stdc++.h>
#include<windows.h>
using namespace std;
int main(){
int T=1000;
while(T--){
printf("#%d\n",1000-T);
system("random.exe > random.txt");
system("duipai.exe < random.txt > duipai.txt");
double st=clock();
system("answer.exe < random.txt > answer.txt");
double ed=clock();
if(system("fc duipai.txt answer.txt")) return(printf("Wrong Answer %.0lfms",ed-st))&&0;
else if(ed-st>1000) return(printf("Time Limit Exceeded %.0lfms",ed-st))&&0;
else printf("Accepted %.0lfms\n",ed-st);
puts("");
}
return 0;
}

结果#

一开始,第 7474 个比对出现差异,原因是当时 answer.cpp 第 3333 行的 break 忘记加了。

更正后, 10001000 次答案均无差异。

因此,推断结论成立。

若后半段不是连续区间呢?

似乎没有确切规律,或者说规律太过复杂,有兴趣的读者可以自行探究。

补充#

最后问了一下 DSDS 这在博弈论里是什么问题,他是这么说的,仅供参考:

是的,这个问题属于组合博弈论(Combinatorial Game Theory)的范畴,具体是无偏博弈(impartial game)中的减法游戏(subtraction game),也可以看成是单堆 Nim 的变种。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
取火柴问题变种
https://blog.juzi75.top/posts/match/
作者
橘子75
发布于
2026-09-13
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
橘子75
苏世独立,横而不流。
公告
新站创立,请多多支持!
分类
标签
站点统计
文章
13
分类
3
标签
13
总字数
11,753
运行时长
0 天
最后活动
0 天前

文章目录