dp

通用技巧

1.见到求绝对值就可以拆,直接变成对整体答案的贡献。转移上可以考虑按顺序转移,设一维状态为前i个未匹配的个数,转移的时候枚举向大或小转移;或者用状压表示大于或小于的状态。

2.在子序列计数的问题中,某些情况下可以用前缀和或bitset优化。

3.如果要求不能重复转移的问题,不妨使用填表法,并设一个辅助数组,强制某个点只能转移一次。不要先去重再计算,十分麻烦!!

4.刷表法一般来说不容易写错,但在某些时候是不好转移的。在对于有特殊限制并加入辅助状态的时候,填表法更能表现出某个状态是否被更新到,然后去更新后面。

5.高精除低精类似于模拟竖式,而高精除高精就只能一点一点去减了。注意判断除0情况。

6.在需要分别枚举如左右贡献a,b这两个变量时,我们不妨枚举d=b-a,推出其组合贡献,可以使用(这东西好像叫范德蒙德卷积) $$ \sum_{b=0}^{min(m,a)} C_b^a * C_{m-b}^{n-a} = C_m^n $$

7.对于 \(N^2\) 转移的dp,可以考虑转化为网格,观察其图形转移性质,从而化为更低维dp。

8.对于某些dp的转移,可以形象化理解,比如考虑在一个空间平面上,如何给下一层转移。或者通过最短路转移等等。

9.对于计算本质不同的子序列,可以 \(O(n^2)\) \(dp\),设 \(dp_i\) 表示以i结尾的本质不同的子序列个数。转移时钦定只对它靠一侧的匹配转移。

可以优化到 \(O(n)\) ,即设 \(dp_i\) 表示以 <=i结尾的本质不同的子序列个数。但是两种写法一定要分清楚不要混了。

10.对于只有两三种不同颜色去排列的方案,dp时一定要记得设上一次出现的位置为i,j,(k),再根据下一个放什么去转移!

11.dp时一定要看空间上界,有些维可能只有log级别!(笑死了dp写对了看不出来最多log次

12.开dp数组时不要信任评测机的空限,如果卡到顶了一定要滚动数组优化。

13.求lr区间覆盖的某些问题,考虑转化为二维平面,有时可以用二维差分/前缀和去做。

14.见到形如 \(\sum_{i=1}^{k} cnt_i^2\),直接转化为组合意义:选出两条满足限制的同色链的方案数。类似的,很多题目让求一个神秘东西的和,都是直接用组合意义转换。

15.DAG计数的技巧,每次枚举独立集作为入度为0的点,然后用容斥原理。如 CF1466H Finding satisfactory solutions。

16.推组合数的常见东西,\(\sum_{i=0}^x \binom{p+i}{i}=\binom{p+x+1}{x}\) ,具体证明只需要考虑组合数的递推式,并从小到大逐步合并即可。

17.在某些计数问题中,前面的选择对后面有影响,那么不妨把前面的一些选择先空下,在转移时延后计算dp贡献,如 CF1608F MEX counting

18.经典组合恒等式,\(\sum_{i=0}^m i^2 \binom{m}{i}=m(m+1)2^{m-2}\)\(\sum_{i=0}^m i \binom{m}{i}=m2^{m-1}\)\(\sum_{i=0}^m \binom{m}{i}=2^m\) 可以搭配记忆。

19.对于一堆线段求覆盖问题的dp,经常有两种思路,要么线段只相交不包含,那么左右端点都单增,可以用单调性;要么线段只包含不相交,那么可以构成树形结构。

20.如果对于某种单增式的矩阵难以计数,一定要考虑转化彻底,比如转化成若干\(\leq i,>i\) 的分界线,然后给它们分别向右下挪 \(i\) 位,转化成有向图不相交路径计数问题。

21.对于连续性的dp,也就是可以在实数域上选点,我们无法设计dp状态,不妨考虑给它离散化了,只要离散地足够细,就能得到更精准的答案;或不妨只考虑元素间的相对位置。

22.有种求正整数互异拆分数的方法,首先发现最多选择 \(O(\sqrt n)\) 个元素,然后考虑将行列反过来看(或形式化的,\(dp_x\) 表示 \(\forall i\leq x\) 选择的 \(\geq i\) 的数的个数之和),也就是考虑加入的元素,长度最多为 \(\sqrt n\) ,且钦定下一个元素只能比它大0或1。

状压dp

直接根据数据范围观察,小技巧是枚举子集

for(int s=t;s;s=(s-1)&t)

枚举子集的子集的时间复杂度是 \(O(3^n)\) ,可以把式子展开使用二项式定理证明。推广:枚举k次子集的时间复杂度是 \(O((k+1)^n)\) .

还有一个技巧吧,用子集的容斥原理的时候,直接减是会重复的。钦定一个点(比如1),把子集划分成含1的连通块和其他部分,再去枚举就能保证不重不漏了。

for(int i=1;i<=tmp;i++){
		f[i]=g[i];
		for(int j=(i-1)&i;j;j=(j-1)&i){
			if(!(j&1)) continue;
			f[i]=(f[i]-f[j]*g[i^j]%mod+mod)%mod;
		}
	}

计数dp

一般指求一个集合S的大小,并且常常范围非常大需要取模。

如果我们能将S分成若干无交的子集,那么S的元素个数就等于这些部分的元素个数和。

与最优化dp的异同:同,都是在一个范围内求一个大小值或最优值,这个值通过对范围内的所有元素做一次处理和整合得到;异,最优化dp我们只需要求满足这个范围的最值,即部分的并为范围,而计数dp我们则需要把范围分成若干个不交的部分。

计数dp的精髓就在于状态设计,而且多是多维dp,经常有很多想不到的神仙状态。

比如经常设计成0/1表示在左右或是否满足条件,包括设计成还需要多少才能满足条件这种([PA2021] Od deski do deski)。

预设型dp

我的理解是把需要求的限制直接加到dp状态中转移。

一般来说先想到设f[i][j](如,枚举到i位,放了j),但是发现有后效性、无法转移,而恰好题目又给了一个在每次转移都需要计算或满足的条件,那么不妨加入那一维条件。

比如求满足划分成若干段的方案,最后一维是段数;求达到奇怪度的方案数,最后一维是奇怪度([ABC134F] Permutation Oddness)。一般是数据范围小的高维dp。

(这样的dp一般最终求的是满足某个限制的方案数)

区间dp

我到现在都觉得区间dp很难……形式过于多样就很难想到。只能积累一些方法。

断环为链。

除了dp[l][r]之外,可以加入比如每个的对l,r的值的限制。尤其是对于删除添加色块的题目。

关于如何注意到要使用区间dp:多推性质,正想反想,时间倒流,删改成加等等。

如果状态实际不多但正常转移会tle或mle,可以考虑记忆化搜索。

斜率优化dp

用线性规划的方式优化dp,一般用于优化1D/1D的dp,即状态和转移都是一维

这样的dp一般有两种形式:

形如 \(f_i=A+B\) 的,其中A是一个只关于i的式子,B是一个只关于j的式子,一般可以使用单调栈/单调队列/数据结构维护。

形如 \(f_i=A+B+C\) 的,其中C是一个同时关于i,j的式子,比如 \(a_i*b_j\) 此时就要用斜率优化。

操作上,把转移方程写出来,对于当前i,假设从j转移比k更优,列出不等关系。然后开始化简,移项,把右边化成一个只关于i的式子。

具体维护时,用单调队列维护凸包,每次选择“相切”的点转移。

概率与期望(坑)

常用结论:
1.期望操作次数=1/得到目标的概率
2.(期望)e=(概率)P*(每次贡献)w
3.期望的线性性:E(x+y)=E(x)+E(y) 即和的期望等于期望的和
4.\(E(x)=\sum_i P(j=i)\times i\)\(E(i)=\sum_i P(j\geq i)\)
5.选到某个东西的期望等于从未选择它的概率之和(注意到这里要包含第0轮,或需要+1

第二个结论常常不仅用于求期望,有时还用于在抽象计数问题中每种情况的概率和期望比较好求时,求对于所有方案的某种操作次数总和( \(\sum w\)

一种特殊情况是在游走的时候还可能停在当前点,那么如何计算到达当前点的概率?先找定值,即从每个点转移(到别的点或者停留在本点)的概率一定是1,而最终在某个城市爆炸的概率是我们所要求的。其实就很像方程了,把在某个城市爆炸的概率设为x,转移到该城市的概率是好表示的,我们可以结合高斯消元去做。

例 [USACO Hol10] 臭气弹
//这个题最离谱的就是如何求无限循环的概率?? (还都是双向边 
//或者说根据题中给出的在某个城市结束的概率之和就是需求的概率,这样考虑列方程? 
//这种概率的问题是真的不好想!有点抽象,只知道每个xi,如何构造方程? 
//似乎先找单位1比较好,列出的方程为单位x的概率减掉每个能到达该边的点的转移概率等于零!!
//还有是从1开始,故1特殊为p/q,本来初始在这里就有值!! 
//注意每个可转移的点还要除以它的入度!!!这样才是最终的概率 
#include<bits/stdc++.h>
using namespace std;
const double eps=1e-10;
int n,m,p,q,c[305][305],d[305];
double a[305][305];
int main(){
	scanf("%d%d%d%d",&n,&m,&p,&q);
	double x=1.0*p/q;
	for(int i=1;i<=m;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		c[u][v]=c[v][u]=1;
		d[u]++,d[v]++;
	}
	a[1][n+1]=x;
	for(int i=1;i<=n;i++){
		a[i][i]=1.0;
		for(int j=1;j<=n;j++){
			if(c[i][j]) a[i][j]=-(1.0-x)/d[j];
		}
	}
	for(int i=1;i<=n;i++){
		for(int j=i;j<=n;j++){
			if(fabs(a[j][i])>eps){
				swap(a[j],a[i]);
				break;
			}
		}
		for(int j=n+1;j>=i;j--) a[i][j]/=a[i][i];
		for(int j=i+1;j<=n;j++){
			for(int k=n+1;k>=i;k--){
				a[j][k]-=a[i][k]*a[j][i];
			}
		}
	}
	for(int i=n-1;i>0;i--){
		for(int j=i+1;j<=n;j++){
			a[i][n+1]-=a[j][n+1]*a[i][j];
		}
	}
	for(int i=1;i<=n;i++){
		printf("%.9lf\n",a[i][n+1]);
	}
	return 0;
}

dp套dp

[TJOI2018] 游园会 是模版。

大概就是发现这个题需要维护一个lcs=i的限制,然而我们并不能直接得到lcs,lcs也是一种对整体状态dp出来的结果。而我们又发现求出来lcs的数组实际上只会两两相差1,也就是可以使用状压去维护。考虑状压维护的正确性,每次更新都是在之前更新过的dp数组的基础上新加入一个字符,相当于我们普通二维dp拆解开,一点一点更新。

然后lcs的dp可以预处理出来,再状压dp转移方案。如果理解清楚好像也还好。

还有一道例题[ZJOI2019] 麻将。还没有写……

树形dp

一般来说就是一维表示当前节点的状态,如果有需要,再加几维表示题目限制。

树上背包,在每个节点初始化状态时一定是只包含当前节点,然后去枚举子树和容量、分配,每次一般是方案相乘或取max,一定是对于当前x及其已经合并的子树和当前要合并的y子树操作。写状态转移的时候要想清楚。

关于树形背包的复杂度,看着是四个for实际上可以证明是O(n^3),看着是三个for(如一个for y,两个枚举siz)实际可以证明是O(n^2).因为有一个枚举子树的操作,那么我们只看枚举的x已有子树和y子树中的每对点,发现实际上每对点只在lca处被计算。([HNOI2015] 实验比较)

up and down,同样是经典套路。实际上是换根dp,一般来说up时从下向上遍历f[x]表示x子树内的贡献,down时从上向下遍历g[x]表示以x为根的全树贡献,g[1]=f[1].

矩阵快速幂加速dp

同样的,看数据范围。

一般来说,只要转移的条件不变,且数据范围比如n很大,并且转移状态又很小并且可以被矩阵所表示,那大概率是矩阵快速幂加速dp。(还有一种情况是转移状态并不需要依赖大范围的n,而是通过较小范围的比如m去设计dp,把n作为转移的参数。)

我觉得这个最难的就是矩阵的初始化和答案的输出,一定要把状态想清楚了,到底初始化后是不是需要转移n次,还是n-1次;输出的的答案是求和还是其中的某个值。

ps.如果要求前缀和,就给dp状态矩阵后面加一位sum,然后每次正常转移后再乘一个求前缀和的矩阵。转移时可以直接用转移矩阵乘上求前缀和的矩阵,得到新的转移矩阵。

填矩阵的方法:如果是从左到右进行快速幂,则转移矩阵(x,y)位置上的数代表从dp[x]转移到dp[y]的系数。

需要注意的点:first.矩阵乘的单位1是除主对角线为1外全0的矩阵

second.封装的时候一定记得清空,否则那个矩阵设出来实际上是有值的,影响计算

struct node{
	int a[30][30];
	void clear(){
		memset(a,0,sizeof(a));
	}
	node operator *(const node b){
		node res;
		res.clear();
		for(int i=1;i<=m;i++){
			for(int j=1;j<=m;j++){
				for(int k=1;k<=m;k++){
					res.a[i][j]=(res.a[i][j]+a[i][k]*b.a[k][j])%mod;
				}
			}
		} 	
		return res;
	}
}x,ans;

注:矩阵乘法有一个优化,可以把时间复杂度从 \(O(m^3logn)\) 变到 \(O(m^2logn)\),是对于 关于两条对角线都对称的矩阵 成立的。

我们会发现 \(A_{i,j}=A_{i-1,j-1}+A_{1,i+j-1}\) ,也就是我们只需要维护每个矩阵的第一行和第一列,就可以递推出来矩阵的所有信息。因为我们每次需要维护的信息是 \(O(m)\) 的,而进行一项的乘法操作是 \(O(m)\) 的,所以总体复杂度就是 \(O(m^2logn)\)

又,dp经常可以写成矩阵乘法的形式,如果想要得到l-r的转移,不妨直接用前缀和优化。需要求前缀逆的积。

基环树(树形dp

最常见条件:n个点,n条边(注意题目给的条件是否联通,可能是森林

基环内向树:每个点只有一条出边

基环外向树:每个点只有一条入边

处理手法:找环,断边,跑树形dp

void circle(int x,int p){
	vis[x]=1;
	for(int i=h[x];i;i=e[i].nxt){
		int y=e[i].to;
		if((i^1)==p) continue;
		if(vis[y]){
			xx=x,yy=y,k=i;
			continue;//不能return,要把联通块找完 
		}
		circle(y,i);
	}
}

轮廓线dp

就是状压的一种思路,把两行状态的枚举转化为逐格dp,感觉转移好像有一条轮廓线一样,把转移变为O(1)

一般来说是一种用于维护连通性的dp

注意到,虽然我们是逐格dp,但我们需要记录从当前格向前n格的状态。这里的状态指是否被覆盖。这也就是轮廓线:已决策状态和未决策状态的分界线。

总体上是分类讨论就行

感谢dalao blog让我学会了轮廓线dp

插头dp

真的太佩服了……陈丹琦在高中期间不仅发明了cdq分治,还发明了插头dp。

插头:一个格子某个方向的插头存在,表示这个格子在这个方向与相邻格子相连。

新开了一个 插头dp

高维前缀和/sosdp(子集dp)

用于对于集合的每个子集求解一些问题,枚举子集的复杂度就到了\(O(2^n)\),没法逐个计算,而高维前缀和可以做到 \(O(n*2^n)\)

关于理解这个式子为什么是这样的:实际上多维前缀和可以一维一维地算(比如二维三维前缀和,先算一横行,再把横行前缀和相加……),此处我们模拟的就是这个过程。

这个东西和 FMT/FWT 处理或卷积的第一步(typ=1)是一样的效果。(当typ=-1时,就是逆运算了)实际上FMT就是考虑构造了一个函数,使得用点值表示法可以直接计算出这个新函数,然后再通过逆运算变成系数表示。

FMT 处理或卷积
  for(int i=0;i<n;i++)
    for(int j=0;j<(1<<n);j++)
      if(j&(1<<i)) a[j]+=typ*a[j^(1<<i)];

另外,与之对应的还有高维后缀和(FMT 处理与卷积),但通过将所有东西取补集,就变成了高维前缀和,可以类似处理。以及还有高维差分,循环反向,加号变减号即可

其实我也还是不是很理解高维后缀和等一系列,看 奆佬blog

//要求最多O(log)求出对于每个子集中满足条件的个数
//如果不要求时间复杂度,转化成求一段区间内lst在l之前的,莫队差不多可以写 
//对于这一类求集合中子集的问题,理解方面,和容斥原理有关,考虑我们按照一定顺序枚举,并计算补集每次删掉的元素,从而保证转移是不重复的 
#include<bits/stdc++.h>
using namespace std;
int n=24,m,dp[1<<24],ans; 
int main(){
	scanf("%d",&m);
	for(int i=1;i<=m;i++){
		int s=0;
		for(int j=1;j<=3;j++){
			char c;
			scanf(" %c",&c);
			if(c>='y') continue;
			s|=(1<<(c-'a'));
		}
		dp[s]++;
	}
	for(int i=0;i<n;i++){
		for(int j=0;j<(1<<n);j++){
			if(j&(1<<i)) dp[j]+=dp[j^(1<<i)];
		}
	} 
	for(int i=0;i<(1<<n);i++) ans^=(m-dp[i])*(m-dp[i]);
	printf("%d",ans);
	return 0;
} 

决策单调性优化dp

决策单调性最常见的条件是满足四边形不等式。设 \(w(i,j)\) 为成本函数,即我们的转移式子形如 \(f_i=\min f_j+w(i,j)\) ,其中 \(\max\) \(\min\) 均可。我们实际上只需要证明成本函数满足四边形不等式,就可以得到决策单调的结论!(因为就算把那个 \(f_j\) 也带到四边形不等式的式子里,它也会被消掉!!

四边形不等式:对于 \(a\leq b\leq c\leq d\) ,有 \(w(a,c)+w(b,d)\leq w(a,d)+w(b,c)\) ,即“交叉小于相等”。(也有说大于的,但其实是一样的,就是满足某种单调性

推论,对于 \(i<j\) ,有 \(w(i,j)+w(i+1,j+1)\leq w(i,j+1)+w(i+1,j)\)

这样的形式我们很难理解,实际上移个项就好了, $ w(i+1,j+1)-w(i+1,j)\leq w(i,j+1)-w(i,j)$

即,随着i的增大,w的增量变小。那么这就相当于,我们对一个转移式子找到了决策单调性。

再说点人话,就是,如果在当前,i+1是更优的,则从今往后的所有转移,i+1相对i一定是更优的。

对于决策单调性dp的处理方式,一般就可以直接考虑单调队列/分治(整体二分)了。

例题: LOJ6039「雅礼集训 2017 Day5」珠宝

数位dp

(不知道是哪个古早时期讲了但根本听不懂的东西,这东西没别的,就是代码极其繁琐
oi-wiki上有数位dp的特征:题目条件转化为“数位”相关东西,最终目的为计数,上界很大(如 \(10^8\)

我们发现在计数的过程中,每一位的变化都可以独立进行,且会有一些数拥有很多相同的位,那我们不妨把这些过程归并起来,用递推或记忆化的方式转移

以 CF582D Number of Binominal Coefficients 为例。

有一个我根本不知道(但其实很显然)的结论: \(n!\) 中质因数 \(p\) 的个数为 \(\sum_{i=1} \lfloor \frac{n}{p^i} \rfloor\)

然后有根据这个结论的定理——库默尔定理:对于一个组合数 \(\binom{n}{m}\) ,它含有质因子 \(p\) 的个数为 \(n+m\)\(p\) 进制下计算时的进位次数。

然后我们将 \(A\) 转为 \(p\) 进制数后进行数位dp。令 \(f_{i,j,0/1,0/1}\) 表示当数位从高到低考虑到 \(A\) 的第 \(i\) 位,总共进位了 \(j\) 次,\(a+b\) 小于/等于 \(A\) 且 不接受/接受 低一位进位时 \(a,b\) 的取值方案数。以 \(i\) 为阶段,从大到小进行转移。

矩阵树定理

用于生成树计数,证明非常之复杂,因此我们会用就行。(依然是图论上的数数题

对于无向图,我们构造 Laplace 矩阵为度数矩阵-邻接矩阵,则该矩阵的所有 \(n-1\) 阶主子式都相等,且等于图的生成树个数;对于有向图,我们构造 Laplace 矩阵为出度/入度矩阵-邻接矩阵,则有向图的出度 Laplace 矩阵删去第 \(k\) 行第 \(k\) 列得到的主子式等于以 \(k\) 为根的根向树的个数,入度 Laplace 矩阵删去第 \(k\) 行第 \(k\) 列得到的主子式等于以 \(k\) 为根的叶向树的个数。

如果是带权的形式,那么它计算行列式统计的就是每种树边权乘积的和。如果要求边权和,可以给每个点赋值为 \(w_ix+1\) ,然后对 \(x^2\) 取模,最后统计一次项系数。(注:行列式用高消搞成上三角矩阵,\(ans=\prod a_{i,i}\)

LGV 引理

用于处理 DAG 上不相交路径计数等问题。

对于一个 DAG ,我们构造一个矩阵,其中 \(a_{i,j}\) 表示所有 \(A_i \to B_j\) 的路径条数,那么该矩阵的行列式就是所有 A 到 B 不相交路径的带符号和。

对于一些特殊的图,比如网格图,我们发现,只要存在逆序对,就必然会出现两个路径相交,那么此时的行列式就是不相交路径个数了。

拉格朗日插值

准确来说这是多项式算法,但是很多都是dp式子推出来,发现它是多项式,然后使用拉插

要求构造一个函数 \(f(x)\) 过点 \(P_1(x_1,y_1),P_2(x_2,y_2),...,P_n(x_n,y_n)\)

那么我们只需要构造n个函数,使得 \(f_i(t)\)\(t\neq x_i\) 时取值为0;在 \(t = x_i\) 时取值为 \(y_i\).

则题目所求为 \(f(x)=\sum_{i=1}^n f_i(x)\) .

此时显然函数是过 $ P_{1,2,...,n}$ 的.

那么可以设 \(f_i(x)=a\cdot\prod_{j\neq i}(x-x_j)\),将点 \(P_i(x_i,y_i)\) 代入得 \(a=\dfrac{y_i}{\prod_{j\neq i} (x_i-x_j)}\)

则 Lagrange 插值的形式为:

\[f(x)=\sum_{i=1}^ny_i\cdot\prod_{j\neq i}\dfrac{x-x_j}{x_i-x_j} \]

朴素实现的时间复杂度为 \(O(n^2)\),可以优化到 \(O(n\log^2 n)\)

对于横坐标连续的插值,可以做到O(n),可用阶乘推导。

注:一个n次多项式可以用n+1个不同点值用拉格朗日插值插出来(注意是n+1个点哦!!

另外,拉插还能搞生成函数,直接带点进来反推系数!

坑:自然数的k次方和是k+1次多项式,可以带k+2个点通过插值求得

另外,判断多项式次数的好方法是差分,原多项式次数就是差分后结果的次数+1(根据多项式的定义)

贪心 (乱入

Q:为什么把贪心和dp放一起呢?

A:贪心题中有时会借用dp排除一些情况([JOI 2022 Final]让我们赢得选举),而dp的最优策略选则有时需要使用贪心思想。

贪心,指在问题求解中总是做出在当前看来最好的选择,从局部最优解出发,得到整体最优解

重点在于贪心策略的选择

一般来说,先对问题建立数学模型,然后拆分问题为若干个子问题,从子问题最优解得到整体最优解

其实在很多时候,贪心策略的证明是很难的,但要发散想象力,敢想敢写

比如:

贪心常见设问是最大值最小和最小值最大(怎么和二分一样,当然二分中的check很多都是用了贪心的思想

考虑交换顺序的问题时,用相邻交换法,只想两个的情况,通过题目给的条件列式子分析,推出排序策略,很多都是按照max/min、和、差排序;注意一定先只想两个,但后面也一定要扩展到整体情况的(有时候贪心策略不具有传递性

johnson算法:对于推出来是min(ai,bj)<min(aj,bi)则i比j优的情况,直接这么排不具有传递性,但是我们可以知道a小的尽可能放前面,b小的尽可能放后面。这个式子可以直接拆开分五种情况讨论(含等于,但等于时可以随便排),于是我们得到了一个正确的排序方式。令d=(ai<=bi)?-1:1,先按照d排序,d同为-1时按照a升序排列,同为1时按b降序排列。

反悔贪心

挖坑待填

博弈论(乱入*2

(先手)必胜态和(先手)必败态

三条显然定理: 1.没有后继状态的状态是必败态 2.一个状态是必胜态当且仅当存在至少一个必败态为它的后继状态 3.一个状态是必败态当且仅当它的所有后继状态都为必胜态

典例:\(Nim\) 游戏

\(n\) 堆物品,每堆有 \(a_i\) 个,两个玩家轮流取走任意一堆的任意个物品,但不能不取。

定义 \(Nim\) 和= \(a_1\) ^ \(a_2\) ... \(a_n\)。当且仅当 \(Nim\) 和为0时,该状态为必败状态;否则必胜。

只需证:
1.没有后继状态的是必败状态
2.对于异或和不为 0 的局面,一定存在某种移动使得异或和为 0
3.对于异或和为 0 的局面,一定不存在某种移动使得异或和为 0

简单证明:
(1.全0局面,异或和为0
(2.设二进制最高位1的位置为d,易得一定有奇数个 \(a_i\) 使得d位为1,那么改 \(a_i\)\(a_i'=a_i\) ^ \(k\),则 \(a_i > a_i\) ^ \(k\).移动合法
(3.如果将 \(a_i\) 改为 \(a_i'\) ,根据异或运算,\(a_i=a_i'\)不合法

扩展到一般情况:SG函数

定义mex函数为不属于集合s中的最小非负整数。SG函数为 SG(x)=mex{ SG(y) | x to y},即x后继状态SG函数的mex值。

当且仅当对于组合游戏的所有起点 \(s_1,s_2,...,s_n\)\(SG(s_1)\) ^ \(SG(s_2)\)...^ \(SG(s_n)\neq 0\) 时,这个游戏先手必胜。同时这是一个组合游戏的游戏状态x的SG值。

适用于任何公平的两人游戏,可以决定游戏的输赢。一般需要把游戏抽象成有向图。

posted @ 2024-10-14 17:28  theWeeds'Defense  阅读(26)  评论(0)    收藏  举报