在题目之前,还是要一如既往地复习一下下这周学的算法--组合博弈
说到博弈,众所周知的有三种博弈:巴什博奕(Bash Game)、尼姆博奕(Nimm Game)、威佐夫博奕(Wythoff Game)
我们引入两个重要的点:
P点--必败点
N点--必胜点
首先
巴什博弈:
给了一堆石子,数目为n个,现在在这么多石子中取石子,每次取石子数量不超过m个,最终将物品拿完者获胜。那么这个问题背分成以下几种情况:
Case1:n<m+1,则先手胜
Case2:n=m+1,则后手胜
Case3:n=k(m+1)+r,(0<=r<=m)
a.r=0,n=k(m+1),后手必胜
b.r!=0,n=k(m+1)+r,先手必胜
抽象不,还是看代码吧
if(n%(m+1)==0)
cout<<"xianshou"<<endl;
else
cout<<"houshou"<<endl;
如果用P点和N点来说的话,是这样的:
可是如果,m的值不是连续的,而是间断的该咋整呢?
那么这是引入几条胜败定律:
1、所有终结点都是P点(必败点)
2、以任何N点(必胜点)操作,至少存在有一种方法能进入P点
3、无论如何操作,从P点都能走到的点是N点
对于这个问题,还可以把它倒着看,从终结点开始,然后往前推导,真不戳。。
然后,比较相似的还有走迷宫(从一点只能往下走一格,往左走一格,或者走到左下角的格),判断先后手赢面-------------->转化一下:两堆牌,抽走某一堆中的一张,或者两堆各一张,抽完者胜。这很好想,就是从0|0开始,往上遍历就完事儿了。
尼姆博奕:
有n堆石子,俩人轮流从石子堆中的一个取任意数量的石子,至少一个,最后拿光石子的人胜利
n=1: 先手必胜。
n=2:俩情况:一种两堆石子相同,另一种一堆比另一堆少
(m,m) ,先手必输。
(m,M),先手先从多的一堆中拿出(M-m)个,先手必胜。
(正经人称(m,m)的局面为奇异局势)
n=3:(m,m,M),先手必胜(先手可以先拿M,变成(m,m,0))
那么,如何用编程来实现呢?
我们再引入Nim-Sum运算:
Nim-Sum运算:
假设(,......,)和(,......,)的nim-sum是(,......,),则有:
(,......,) ^(,......,) = (,......,)
( = + (mod 2))
人话就是异或运算
代码:
for(int i=1;i<=n;i++)
{
cin>>a[i];
s^=a[i];
}
如果最后算得s=0,那么必输,反之如果s!=0,那么有必胜的策略
威佐夫博弈:
两堆若干个物品,两个人轮流从任一堆取至少一个或同时从两堆中取同样多的物品,最后取光者胜
如何应对?
两个人如果都采用正确操作,那么面对非奇异局势,先手必胜;反之,后手必胜
很难想到这题居然和黄金分割比有关:
1.618=(sqrt(5.0)+1)/2
ans=(int)((b-a)*((sqrt(5.0)+1)/2));
若ans与两堆物品较小值相等,那么后手胜,反之先手胜
博弈介绍到这,现在我们来看看万能大法--SG函数(Graph Games&Sqrague--Grundy Function)
SG函数
经典问题:尼姆博奕(above)
x节点的SG值是除去x的后继节点的SG值后的最小非负整数
再有关键的判断:
sg(x)=0;——必败
sg(x)!=0;——必胜
众所周知,博弈问题很多可以当做图问题来做,因此SG函数的用途非常广泛
那么现在我们就有底气解决大多博弈问题了,当然,还有可能有将数个问题组合起来的问题,那么就再来一个定理:
若图游戏G由若干子图游戏Gi组成,即:G=G1+G2+...+Gn,假设gi是Gi的SG函数值,那么图游戏G的SG函数值的计算:
最后,如果异或和为0,那么当前的合并状态是必败的;
如果异或和非0,那么当前状态是必胜的。
P1288 取数游戏 II
这题其实不难,我们先按照最效率的方式来走,能发现,直接一次走到对面和来回走几次再过去情况一样,那么就一直直接走,容易得出:最后先手的输赢和非0路径的奇偶有关
代码:
#include<bits/stdc++.h>
using namespace std;
int n;
const int N=35;
int a;
int tot;
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a;
if(a!=0)
tot++;
}
if(tot%2==0)
cout<<"NO"<<endl;
else
cout<<"YES"<<endl;
}
P2197 【模板】Nim 游戏
略略略~~~~~~~~~~~~~~~
#include<bits/stdc++.h>
using namespace std;
int t;
int n;
const int N=1e4+5;
int stone[N];
int main()
{
cin>>t;
while(t--)
{
int s=0;
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>stone[i];
s^=stone[i];
}
if(s==0)
cout<<"No"<<endl;
else
cout<<"Yes"<<endl;
}
}
P1113 杂务
拓扑排序:
首先,我们明确一下:有向无环图一定是拓扑序列(一定不能有环!!!)
无向图没有拓扑序列
对于拓扑序列,我们需要引入度——进入某点的路径数为入度,某点指向其他点的路径数为出度,总结一下,拓扑序列中只有从前指向后的边,没有从后指向前的边
对于一个有向无环图,一定有一个点的入度为0,如果找不到一个入度为0的点,这个图一定是带环的
对于拓扑排序,思路如下:
1、记录各个点的入度
2、将入度为 0 的点放入队列
3、找出所有这个点发出的边,删除边,同时边的另一侧的点的入度 -1。
4、如果所有点都进过队列,则可以拓扑排序,输出所有顶点。否则不可以进行拓扑排序。
本题:
首先来存个图,然后把所有入度为0的点放入队列中,再依次遍历,寻找这个点连接的其他点,并将连通关系抹除,同时下一点入度-1,同时dp一下,来一个状态转移方程:
表示完成i点任务后的耗时,为了保证其他路径到达i点时任务能够完成,故取max
代码:
#include<bits/stdc++.h>
using namespace std;
int n;
const int N=1e4+5;
int u,v;
int minute[N];//u为起点,v为前驱点,minute为耗时
int ind[N];
int f[N];
bool connect[N][N];
queue<int> q;
int ans;
void bfs()
{
int t;
for(int i=1;i<=n;i++)
if(!ind[i])
q.push(i);
while(q.size())
{
t=q.front();
q.pop();
for(int i=1;i<=n;i++)
if(connect[t][i])
{
f[i]=max(f[i],f[t]+minute[i]);
connect[t][i]=connect[i][t]=false;
ind[i]--;
if(!ind[i])
q.push(i);
}
}
for(int i=1;i<=n;i++)
ans=max(ans,f[i]);
cout<<ans;
}
int main()
{
cin>>n;
memset(connect,false,sizeof(connect));
for(int i=1;i<=n;i++)
{
cin>>u>>minute[i];
while(1)
{
cin>>v;
if(v==0)
break;
ind[u]++;
connect[u][v]=connect[v][u]=true;
}
}
ind[1]=0;
f[1]=minute[1];
bfs();
}
P1002 [NOIP2002 普及组] 过河卒
关于dp乱入博弈。。。
以每个位置累计路径数,若能走就加上前一次走过的点位的dp值,如果不能走,就跳过啦
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int fx[]={0,-2,-1,1,2,2,1,-1,-2};
const int fy[]={0,1,2,2,1,-1,-2,-2,-1};
int bx,by,mx,my;
ll f[40][40];
bool s[40][40];
int main()
{
cin>>bx>>by>>mx>>my;
bx+=2;by+=2;mx+=2;my+=2;
//防越界
f[2][1]=1;
s[mx][my]=1;//标记马的位置
for(int i=1;i<=8;i++)
s[mx+fx[i]][my+fy[i]]=1;
for(int i=2;i<=bx;i++)
{
for(int j=2;j<=by;j++)
{
if(s[i][j])
continue;//被马拦住就直接跳过
f[i][j]=f[i-1][j]+f[i][j-1];
}
}
cout<<f[bx][by];
return 0;
}