文章目录
7.博弈论
7.1 巴什博弈
取石子:
有一堆石子,一共N个石子,两个人轮流拿石子,每次最多取M个,最少取1个。先取完的人获胜。(两人足够聪明)
输入
两个整数 n, m。1 <= n <= 100, 1<= m <= 100。
输出
如果先手不能赢输出 -1
如果享受能赢,则输出任意一种先手能赢的第一次取的石子数
分析
博弈论里的巴什博弈
结论: 若n%(m+1) == 0则先手必败。否则先手必胜
原因分析 先手想必胜,肯定是要使自己能取走最后一颗石子,而后手无法取走最后一个石子。
突破口: 把 n 分解成 x * (m+1) + k 即可 先手先取走k个石子,此时剩下的 x * (m+1)个石子,对于每份 m + 1, 轮到后手取的时候每次都取不干净m + 1个石子,而再轮到先手的时候,刚好可以把每份后手没取完的m+1个石子取完。。。所以这样就可以保证最后一个石子是先手拿走的。
对于 n < m时,上述策论也成立,因为先手可以直接一次取完.
这题加了如果先手能赢,还有输出一种第一次取的个数,这样直接开一个循环就好,i从1–m取,(n - i)%(m+1) == 0则第一次取i个就是答案。
#include <bits/stdc++.h>
using namespace std;
int main()
{
int i, n, m;
scanf("%d %d", &n, &m);
if(n%(m+1)==0)
printf("-1");
else
{
for(i = 1; i <= m; i++)
{
if((n-i)%(m+1)==0)
{
printf("%d\n", i);
break;
}
}
}
return 0;
}
7.2 尼姆博弈
取火柴
有两个玩家在玩取火柴的游戏,一共有N堆火柴,每个玩家每次至少取一根火柴,至多可以把一堆火柴全部拿走.问最后谁能够取光最后一根火柴,谁就是赢家.
1、我们首先以一堆为例: 假设现在只有一堆石子,你的最佳选择是将所有石子全部拿走,那么你就赢了。
2、如果是两堆:假设现在有两堆石子且数量不相同,那么你的最佳选择是取走多的那堆石子中多出来的那几个,使得两堆石子数量相同,这样,不管另一个怎么取,你都可以在另一堆中和他取相同的个数,这样的局面你就是必胜。比如有两堆石子,第一堆有3个,第二堆有5个,这时候你要拿走第二堆的三个,然后两堆就都变成了3个,这时你的对手无论怎么操作,你都可以“学”他,比如他在第一堆拿走两个,你就在第二堆拿走两个,这样你就是稳赢的。
3、如果是三堆 ,我们用(a,b,c)表示某种局势,首 先(0,0,0)显然是奇异局势,无论谁面对奇异局势,都必然失败。第二种奇异局势是 (0,n,n),只要与对手拿走一样多的物品,最后都将导致(0,0,0)。仔细分析一下,(1,2,3)也是奇异局势,无论对手如何拿,接下来都可以变为(0,n,n)的情型。
从中我们要明白两个理论:
一个状态是必败状态当且仅当它的所有后继都是必胜状态
一个状态是必胜状态当且仅当它至少有一个后继是必败状态
有了这两个规则,就可以用递推的方法判断整个状态图的每一个结点都是必胜还是必败状态。
这里引入L . Bouton在1902年给出的定理:状态(x1,x2,x3)为必败状态当且仅当x1 XOR x2 XOR x3=0,这里的XOR是二进制的逐位异或操作,也成Nim和。也就是当Nim和!= 0时,先手胜利,否则失败。
任何奇异局势(a,b,c)都有a(+)b(+)c =0。
如果我们面对的是一个非奇异局势(a,b,c),要如何变为奇异局势呢?
(3,4,5):
3| 0 1 1
4| 1 0 0
5| 1 0 1
+| 0 1 0
Nim和为010,取最左边的一个1(第二位),在原来的数中随便找一个该位同样是1的,发现只有3,所以求出除了3之外剩余数的异或和001,将3更新为001即可,得(1,4,5)
原理:c=a(+)b,a(+)b(+)c=a(+)b(+)(a(+)b)=(a(+)a)(+)(b(+)b)=0(+)0=0
#include<iostream>
using namespace std;
int main()
{
int n, a;
while(scanf("%d", &n)!=EOF)
{
int ans = 0;
for(int i=0; i<n; ++i)
{
scanf("%d", &a);
ans ^= a;
}
if(ans) printf("Yes\n");
else printf("No\n");
}
return 0;
}
Nim游戏基本定理: Nim博弈先手必胜,当且仅当 A 1 x o r A 2 x o r A 3 . . . x o r A n ≠ 0 A_1 xor A_2xor A_3...xor A_n\ne0 A1xorA2xorA3...xorAn=0
公平组合游戏ICG
- 两名玩家交替行动;
- 在游戏进程的任意时刻,可以执行的合法行动与轮到哪名玩家无关;
- 不能行动的玩家判负。
有向图游戏 给定一个有向无环图,图中有一个唯一的起点,在起点上放一枚棋子,两名玩家交替的把这枚棋子沿有向边进行移动,每次可以移动一步,无法移动者判负。该游戏被称为有向图游戏,任何一个公平组合游戏都可以转化为有向图游戏。 有向图游戏的某个局面必胜,当且仅当该局面对应节点的SG函数值大于0; 有向图游戏的某个局面必败,当且仅当该局面对应节点的SG函数值等于0。
mex运算 设S表示一个非负整数集合。定义 m e x ( S ) mex(S) mex(S)为求出不属于集合S的最小非负整数的运算: m e x ( S ) = m i n mex(S)=min mex(S)=min ( x ) ( x (x)(x (x)(x ∈ \in ∈ N , x ,x ,x ∉ \notin ∈/ S)
SG函数 在有向图游戏中,对于每个节点x,设从x出发共有k条有向边,分别到达节点
y
1
,
y
2
,
.
.
.
y
k
y_1,y_2,...y_k
y1,y2,...yk,定义SG(x)为x的后继节点
y
1
,
y
2
,
.
.
.
y
k
y_1,y_2,...y_k
y1,y2,...yk的SG函数值构成的集合再执行mex运算的结果,即:
S
G
(
x
)
=
m
e
x
(
{
S
G
(
y
1
)
,
S
G
(
y
2
)
,
.
.
.
,
S
G
(
y
k
)
}
)
SG(x)=mex(\{SG(y_1),SG(y_2),...,SG(y_k)\})
SG(x)=mex({SG(y1),SG(y2),...,SG(yk)})
特别地,整个有向图游戏G的SG函数值被定义为有向图游戏起点start的SG函数值。
有向图游戏的和 有向图游戏的和的SG函数值等于它包含的各个子游戏SG函数值的异或和。 S G ( G ) = S G ( G 1 ) x o r S G ( G 2 ) x o r S G ( G 3 ) x o r . . . x o r S G ( G m ) SG(G)=SG(G_1) xor SG(G_2) xor SG(G_3) xor...xor SG(G_m) SG(G)=SG(G1)xorSG(G2)xorSG(G3)xor...xorSG(Gm)
因为第一阶拿到地面要拿一次,第二阶拿两次,第三阶拿三次…所以可以看成第二阶有两堆石子,第三阶有三堆…偶数阶石子为偶数堆,异或为0;奇数阶异或后就是原本石子数目,所以可以把原本所有奇数阶的石子进行异或,得到的就是答案。
#include<bits/stdc++.h>
using namespace std;
int n,x,ans;
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>x;
if(i%2) ans^=x;
}
if(ans) cout<<"Yes";
else cout<<"No";
}
#include<bits/stdc++.h>
using namespace std;
int mem[10010],n,k,a[110],ans,box[10010][110];
int sg(int x)
{
if(mem[x]!=-1) return mem[x];
for(int i=1;i<=k;i++)
if(a[i]<=x) box[x][sg(x-a[i])]=1;//枚举集合内的数
for(int i=0;;i++)
if(!box[x][i]) return mem[x]=i;
}
int main()
{
cin>>k;
memset(mem,-1,sizeof mem);
for(int i=1;i<=k;i++)
cin>>a[i];
cin>>n;
for(int i=1;i<=n;i++)
{
int x;
cin>>x;
ans^=sg(x);
}
if(ans) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
return 0;
}
#include<bits/stdc++.h>
using namespace std;
int n,k,a[110],ans,maxn,mem[110],box[110][200];
int sg(int x)
{
if(mem[x]!=-1) return mem[x];
for(int i=0;i<x;i++)
for(int j=0;j<=i;j++)//枚举更小的两个堆
box[x][sg(i)^sg(j)]=1;
for(int i=0;;i++)
if(!box[x][i]) return mem[x]=i;
}
int main()
{
cin>>n;
memset(mem,-1,sizeof mem);
for(int i=1;i<=n;i++)
{
cin>>a[i];
ans^=sg(a[i]);
}
if(ans) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
return 0;
}
就是建有向图了。
#include<bits/stdc++.h>
using namespace std;
const int M=6010,N=2010;
int n,m,k,tot,ans,mem[N],box[N][2*N];
int head[N],ver[M],nex[M];
void add(int x,int y)
{
ver[++tot]=y,nex[tot]=head[x],head[x]=tot;
}
int SG(int u)
{
if (mem[u]!=-1) return mem[u];值已经被计算过了,直接返回
set<int> S;
for (int i=head[u];i;i=nex[i])
S.insert(SG(ver[i]));
for (int i=0;;i++)
if (!S.count(i))
return mem[u]=i;
}
int main()
{
cin>>n>>m>>k;
memset(mem,-1,sizeof mem);
for(int i=1;i<=m;i++)
{
int x,y;
scanf("%d%d",&x,&y);
add(x,y);
}
for(int i=1;i<=k;i++)
{
int x;
scanf("%d",&x);
/*for(int j=head[i];j;j=nex[j])
cout<<ver[j]<<' ';
cout<<endl;*/
ans^=SG(x);
//cout<<"x:"<<x<<" sg(x):"<<sg(x)<<endl;
}
if(ans) cout<<"win";
else cout<<"lose";
return 0;
}
7.3 威佐夫博弈
奇异局势(先手必输):(0,0)(1,2)(3,5)(4 ,7)(6,10)(8,13)(9 , 15)(11 ,18)(a[k],b[k])我们会发现他们的差值是递增的,分别是0,1,2,3,4,5,6,7…n
这些局势的第一个值是未在前面出现过的最小的自然数。
继续分析我们会发现,每种奇异局势的第一个值(这里假设第一堆数目小于第二堆的数目)总是等于当前局势的差值乘上1.618
a[k] = (int)((b[k] - a[k])*1.618)
#include<bits/stdc++.h>
using namespace std;
int main()
{
int a,b;
while(cin>>a>>b)
{
if(a>b) swap(a,b);
int temp=(b-a)*(sqrt(5.0)+1.0)/2.0;
if(temp!=a)
cout<<"1"<<endl;
else
cout<<"0"<<endl;
}
return 0;
}
8.动态规划
8.1 背包
8.1.1 01背包
给定n种物品(每种仅一个)和一个容量为c的背包,要求选择物品装入背包,使得装入背包中物品的总价值最大。
#include<bits/stdc++.h>
using namespace std;
int dp[405][1505];
int w[405],v[405];
int main()
{
int n,c,i,j;
while(~scanf("%d%d",&n,&c))
{
for(i=0;i<405;i++)
for(j=0;j<1505;j++)
dp[i][j]=0;
for(i=1;i<=n;i++)
scanf("%d",&w[i]);
for(i=1;i<=n;i++)
scanf("%d",&v[i]);
int bag=c;
int ma=0;
for(i=1;i<=n;i++)
{
for(j=1;j<=bag;j++)
{
if(j<w[i])
dp[i][j]=dp[i-1][j];
else
{
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
ma=max(ma,dp[i][j]);
}
//cout<<dp[i][j]<<' ';
}
//cout<<endl;
}
cout<<ma<<endl;
}
return 0;
}
#include <iostream>
using namespace std;
int w[105],v[105];
int dp[2][1005];//滚动数组优化01背包
//滚动数组优化01背包
int main()
{
int t,m,res;
scanf("%d%d",&t,&m);
//读入数据
for(int i=1;i<=m;i++)
scanf("%d%d",&w[i],&v[i]);
for(int i=1;i<=m;i++)
{
for(int j=t;j>=0;j--)
{
if(j>=w[i])
dp[i%2][j]=max(dp[(i-1)%2][j-w[i]]+v[i],dp[(i-1)%2][j]);
else dp[i%2][j]=dp[(i-1)%2][j];
}
}
}
//输出最后一个数据
printf("%d",dp[m%2][t]);
}
//一维优化01背包
#include <iostream>
using namespace std;
int w[105],v[105];
int dp[1000];
int main()
{
int t,m,res;
scanf("%d%d",&t,&m);
for(int i=1; i<=m; i++)
scanf("%d%d",&w[i],&v[i]);
for(int i=1; i<=m; i++)
for(int j=t; j>=w[i]; j--)
dp[j]=max(dp[j-w[i]]+v[i],dp[j]);
printf("%d",dp[t]);
return 0;
}
8.1.2 多重背包
有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
二进制优化多重背包
#include <iostream>
using namespace std;
int n,m;//n个种类,m代表包总体积
int v[11010],w[11010];//v代表体积,w代表价值
int dp[2010];
int main()
{
scanf("%d%d",&n,&m);
int cnt=0;//cnt统计新的种类
for(int i=1; i<=n; i++)
{
int a,b,s;//体积,价值,数量
scanf("%d%d%d",&a,&b,&s);
//将s件用二进制转换为log2s堆
for(int k=1; k<=s; k<<=1)
{
v[++cnt]=k*a;//前++,第1种,第二种.....
w[cnt]=k*b;
s-=k;
}
if(s)//s有剩余,自立为新品种
{
v[++cnt]=s*a;
w[cnt]=s*b;
}
}
for(int i=1; i<=cnt; i++)
for(int j=m; j>=v[i]; j--)
dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
//动态转移方程和01背包完全相同
printf("%d",dp[m]);
return 0;
}
8.1.3 完全背包
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的体积是v[i],价值是val[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
#include <iostream>
using namespace std;
int N,V;
int v[1010],val[1010];
int dp[1010][1010];
int main()
{
scanf("%d%d",&N,&V);
for(int i=1; i<=N; i++)
{
scanf("%d%d",&v[i],&val[i]);
}
for(int i=1; i<=N; i++)
for(int j=0; j<=V; j++)
{
dp[i][j]=dp[i-1][j];//继承上一个背包
if(j>=v[i])
dp[i][j]=max(dp[i-1][j],dp[i][j-v[i]]+val[i]);
}
printf("%d",dp[N][V]);
return 0;
}
一维优化
#include <iostream>
using namespace std;
int N,V;
int v[1010],val[1010];
int dp[1010];
int main()
{
scanf("%d%d",&N,&V);
for(int i=1; i<=N; i++)
{
scanf("%d%d",&v[i],&val[i]);
}
for(int i=1; i<=N; i++)
for(int j=0; j<=V; j++)
{
dp[j]=dp[j];//此时右边的dp[j]是上一层i-1的dp[j],然后赋值给了当前i的dp[i]
if(j>=v[i])
dp[j]=max(dp[j],dp[j-v[i]]+val[i]);
//dp[j-v[i]],已经被算过
}
printf("%d",dp[V]);//输出最大体积,即最优解
return 0;
}
8.1.4 混合背包
//混合背包,k==1表示只取一件(01背包),k>1多重背包,k==-1完全背包
#include<bits/stdc++.h>
using namespace std;
int dp[20005],w,v,k,s,n;
int main()
{
cin>>n>>s;
while(n--)
{
cin>>w>>v>>k;
if(k>=1)//多重背包与01背包
{
int x=1;
while(k>x)
{
for(int j=s;j>=x*w;j--)
f[j]=max(f[j-w*x]+v*x,f[j]);
k-=x;
x<<=1;
}
for(int j=s;j>=w*k;j--)
f[j]=max(f[j-w*k]+v*k,f[j]);
}
else //完全背包
for(int j=w;j<=s;j++)
f[j]=max(f[j-w]+v,f[j]);
}
cout<<f[s]<<endl;
return 0;
}
8.1.5有依赖的01背包
P1064 [NOIP2006 提高组] 金明的预算方案
#include<bits/stdc++.h>
using namespace std;
int num,sum;
int bag[32500];
int v[65][3];
int w[65][3];
int main()
{
cin>>sum>>num;
for(int i=1;i<=num;i++)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
if(!z) v[i][0]=x*y,w[i][0]=x;
else if(!v[z][1]) v[z][1]=x*y,w[z][1]=x;
else v[z][2]=x*y,w[z][2]=x;
}
for(int i=1;i<=num;i++)
{
for(int j=sum;j>=w[i][0];j--)
{
bag[j]=max(bag[j],bag[j-w[i][0]]+v[i][0]);
if(j>=w[i][0]+w[i][1])
bag[j]=max(bag[j],bag[j-w[i][0]-w[i][1]]+v[i][0]+v[i][1]);
if(j>=w[i][0]+w[i][2])
bag[j]=max(bag[j],bag[j-w[i][0]-w[i][2]]+v[i][0]+v[i][2]);
if(j>=w[i][0]+w[i][1]+w[i][2])
bag[j]=max(bag[j],bag[j-w[i][0]-w[i][1]-w[i][2]]+v[i][0]+v[i][1]+v[i][2]);
}
}
cout<<bag[sum];
return 0;
}
8.2 区间DP
设有 N堆石子排成一排,每堆石子有一定的质量 m(m ≤1000)。现在要将这 N 堆石子合并成为一堆。每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻。合并时由于选择的顺序不同,合并的总代价也不相同。试找出一种合理的方法,使总的代价最小,并输出最小代价。
#include<bits/stdc++.h>
using namespace std;
int a[310],sum[310],dp[310][310],node[310][310];
///a:储存石子数;
//sum:储存前缀和;
//dp:记录每个对每个区间动态规划后的值(第一维左端点,第二维右端点);
//node:标记每个区间动态规划得到最小值的相应分隔点 (最优分隔点下标)
int n;
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
dp[i][j]=0x3ffffff;//要求最小值,首先全部初始化为无穷大
for(int i=1;i<=n;i++)
{
cin>>a[i];
sum[i]=sum[i-1]+a[i];//先算出前缀和,避免后面需要反复求和
dp[i][i]=0;//在未合并时没有额外消耗,初始值为0
node[i][i]=i;//对于单个结点形成的区间来说,最优分隔点就是它自己
}
for(int len=1;len<=n;len++)//枚举长度,从底向上
{
for(int j=1;j+len<=n+1;j++)//枚举左端点
{
int ends=j+len-1;//求出右端点位置
for(int i=node[j][ends-1];i<=node[j+1][ends];i++)//四边形不等式,最优分隔点必位于这个区间
{
if(dp[j][ends]>dp[j][i]+dp[i+1][ends]+sum[ends]-sum[j-1])
{
dp[j][ends]=dp[j][i]+dp[i+1][ends]+sum[ends]-sum[j-1];//更新最小值
node[j][ends]=i;//记录对应的结点位置
}
}
}
}
cout<<dp[1][n];//输出最上层的结果
return 0;
}
8.3 状压DP
(有21个点,1-21,编号互质的两点之间可通行,现从编号为1的点出发,问有多少种哈密顿回路的走法)
定义状态 f ( S , x ) f(S,x) f(S,x):不重复经过点集 S S S,当前所在点为 x x x的方案数;
状态转移:对状态 f ( S , x ) f(S,x) f(S,x),对任意 y ∉ S y \notin S y∈/S且 x x x, y y y之间有连边的 y y y,可以转移到 f ( S ⋃ y , y ) f(S\bigcup y,y) f(S⋃y,y)。(换一句话说,前一个状态是达成后一个状态的策略之一);
#include<bits/stdc++.h>
using namespace std;
const int N=21,M=1<<N;
long long road[N][N],f[M][N];
int main()
{
for(int i=0;i<N;i++)
for(int j=0;j<N;j++)
if(__gcd(i+1,j+1)==1)
road[i][j]=1;
f[1][0]=1;//集合为{1},停留在点1(在数组中处理将其减一故第二维为0)
for(int i=0;i<(1<<N);i++)//集合状态
for(int j=0;j<N;j++)//走过当前集合且最后停留在点j
if(i>>j&1)//看j点有没有到(在不在编码i代表的集合S中),这里为真代表在
for(int k=0;k<N;k++)//要往点k走
if(!(i>>k&1))//k不在S中
if(road[j][k])
f[i|(1<<k)][k]+=f[i][j];
long long ans=0;
for(int i=0;i<N;i++)
ans+=f[(1<<N)-1][i];//全集对应的编码为(1<<N)-1,这里i全循环一遍是因为教学楼1和所有教学楼都连边
cout<<ans<<endl;
return 0;
}
枚举一个集合的子集:
for (int x = S; x; x = (x-1)&S)
8.4 树形DP
#include<bits/stdc++.h>
using namespace std;
const int N=6e3+10;
vector<int>vec[N];
int a[N],n,f[N][2],v[N],root;
void dp(int x)
{
f[x][0]=0;
f[x][1]=a[x];
for(int i=0;i<vec[x].size();i++)
{
int son=vec[x][i];
dp(son);
f[x][0]+=max(f[son][0],f[son][1]);
f[x][1]+=f[son][0];
}
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<n;i++)
{
int l,k;
scanf("%d%d",&l,&k);
v[l]=1;
vec[k].push_back(l);
}
for(int i=1;i<=n;i++)
{
if(!v[i])
{
root=i;
break;
}
}
dp(root);
cout<<max(f[root][0],f[root][1]);
return 0;
}
#include<bits/stdc++.h>
using namespace std;
const int N=1520;
vector<int>vec[N];
int a[N],n,f[N][2],v[N],root;
void dp(int x)
{
f[x][0]=0;
f[x][1]=1;
for(int i=0;i<vec[x].size();i++)
{
int son=vec[x][i];
dp(son);
f[x][0]+=f[son][1];
f[x][1]+=min(f[son][0],f[son][1]);
}
}
int main()
{
cin>>n;
for(int i=0;i<n;i++)
{
int x,k;
scanf("%d%d",&x,&k);
while(k--)
{
int son;
scanf("%d",&son);
v[son]=1;
vec[x].push_back(son);
}
}
for(int i=0;i<n;i++)
{
if(!v[i])
{
root=i;
break;
}
}
dp(root);
cout<<min(f[root][0],f[root][1]);
return 0;
}