前言:此合集是整理上个学期开学时做的博弈论题目,没大有难度,都是板子。自己这断断续续补了一个月(还没补完)。
之前一直以为sg函数是解决小部分博弈问题的。
这次突然发现,sg函数可以解决很多很多博弈问题,相当于是博弈的板子。
。
中间经历过了一些事情,虽然真的没有可能变好,但是希望可以慢慢变好。
分类:
1、sg函数类
2、dp类
3、威佐夫博弈
4、巴什博弈
5、思维类
。
sg函数可以解决很大部分的两人博弈题目
先放一个 可用SG函数解决但是可以不用的题目——取m堆石子
题意是从m堆石子里取石子 每次只能从一堆取 不能不取 问谁先取完
这题相对于S-nim问题的特殊之处是:S-nim只可以去特定数量的元素 而取m堆石子 每次可取任意数量元素。所以这题简化 可以直接用堆数求异或和。
//异或和的含义其实就是看看有多少个数在某一位有数
然后如果可以从某一堆取 问你可以怎样取。当然是先求除了这一堆a[i],剩下的异或和tp去和当前的这一堆比较 如果小于当前这一堆 那么可以取 (比如a[i]=111,tp=011,那么就可以拿掉1 这样拿可以保证相当于是后手
下面是AC代码
const int maxn=1e4+10;
int n;
int a[200010];
int main() {
while(sd(n)!=EOF){
if(n==0) break;
int ans=0;
rep(i,1,n){
sd(a[i]);
ans^=a[i];
}
if(ans){
printf("Yes\n");
rep(i,1,n){
int tp=ans^a[i];//除了这个的 异或和
//下面这里还必须是小于
//因为tp是剩下的 如果等于就相当于不拿
if(tp<a[i]) printf("%d %d\n",a[i],tp);//tp就是剩下的数量 (应该异或上其他的等于0
}
}else printf("No\n");
}
return 0;
}
然后放几个使用SG函数的
套板子即可 不过数太大 要写循环不要写搜索
而且那里写memset就可以 写for就T 不知道为什么
const int maxn=110;
// SG[0]=0,f[]={1,3,4},
// x=1时可以取走1-f{1}个石子,剩余{0}个,所以SG[1]=mex{ SG[0] }= mex{0} = 1;
// x=2时可以取走2-f{1}个石子,剩余{1}个,所以SG[2]=mex{ SG[1] }= mex{1} = 0;
// x=3时可以取走3-f{1,3}个石子,剩余{2,0}个,所以SG[3]=mex{SG[2],SG[0]}=mex{0,0} =1;
// x=4时可以取走4-f{1,3,4}个石子,剩余{3,1,0}个,所以SG[4]=mex{SG[3],SG[1],SG[0]}=mex{1,1,0}=2;
// x=5时可以取走5-f{1,3,4}个石子,剩余{4,2,1}个,所以SG[5]=mex{SG[4],SG[2],SG[1]}=mex{2,0,1}=3;
int n;
int f[maxn];
int sg[10010];
bool s[10010];//记录当前这个值有没有存在
void get_sg(){
sort(f+1,f+n+1);
sg[0]=0;
for(int i=1;i<=10000;i++){//从sg[1]开始找
//sg[i]代表 当前石子数为i时 后继状态的sg函数集合的mex值
//mex值为 不属于该集合的 最小非负整数
cl(s,0);//这里如果改成for循环就t 懵逼的一批
rep(j,1,n){//对于所有可取的数
if(i<f[j]) break;//不能超过总数
//这里应该是当前的石子数和可取的比较!!写成n了!!!
int rem=i-f[j];//rem是拿了之后剩下的是几
s[sg[rem]]=true;//意思是标记集合 集合中的数是 剩下的数的sg值(后继值
}
int p=0;
while(s[p]&&p<=10000) ++p;//找到第一个非负的数 不是! 反了!!!
sg[i]=p;
}
}
int main() {
while(scanf("%d",&n)!=EOF){
if(n==0) break;
rep(i,1,n) scanf("%d",&f[i]);
get_sg();
int t;//组数
scanf("%d",&t);
rep(i,1,t){
int k,sum=0;
scanf("%d",&k);//堆数
rep(j,1,k){
int num;
scanf("%d",&num);
sum^=sg[num];//最后异或的是所有堆中数量的sg值
}
if(sum) putchar('W');
else putchar('L');
}
putchar('\n');
}
return 0;
}
题意是只能拿1 2 4 8 ……
套板子即可。
const int maxn=1010;
int sg[maxn];
int p[12];
int n;
void dfs(int i){//i是当前这个数
if(sg[i]!=-1) return;
bool con[maxn];
cl(con,0);
for(int j=0;p[j]<=i;j++){
dfs(i-p[j]);
con[sg[i-p[j]]]=true;//即 后继状态
}
for(int j=0;j<=1000;j++){
if(!con[j]){
sg[i]=j;
break;
}
}
return;
}
int main() {
p[0]=1;
rep(i,1,10) p[i]=p[i-1]*2;
cl(sg,-1);
dfs(1000);
while(sd(n)!=EOF){
printf(sg[n]?"Kiki\n":"Cici\n");
}
return 0;
}
这个有些难度 题意是一个序列 只能把a[i]的拿走一个然后a[j] a[k]各加一个 要求i
预处理sg 从右向左倒着来。sg[i]意为到最后一位的距离是多少
不过后态成了sg[j]^sg[k]
不算板子题 有点难度。
下面是AC代码
const int maxn=1010;
int n,cas;
int a[30];
int sg[30];
bool con[maxn];
void get_sg(){
//开头可以相同
for(int i=0;i<=22;i++){//无法从最后一堆选
cl(con,0);
for(int j=0;j<i;j++){
for(int k=j;k<i;k++){
con[sg[j]^sg[k]]=true;
}
}
for(int j=0;j<maxn;j++){
if(!con[j]){
sg[i]=j;
break;
}
}
}
return;
}
void fun(){
int ans=0;
rep(i,0,n-1){
sd(a[i]);
if(a[i]&1) ans^=sg[n-1-i];
}
//这里也不能用ans来判断
for(int i=0;i<=n-1;i++){
if(!a[i]) continue;//这一个不能是0!
for(int j=i+1;j<=n-1;j++){
for(int k=j;k<=n-1;k++){
if(ans==(sg[n-1-i]^sg[n-1-j]^sg[n-1-k])){
printf("%d %d %d\n",i,j,k);
return;
}
}
}
}
printf("-1 -1 -1\n");
return;
}
int main() {
cas=0;
get_sg();
while(sd(n)!=EOF){
if(!n) break;
printf("Game %d: ",++cas);
fun();
}
return 0;
}
威佐夫博弈是一种比较经典的博弈
是一组组不重复的数对 所以对应取两堆石子(貌似可以用sg函数 但我不会
这样的数对有如下性质
对于数对ak bk bk-ak=k 且 ak=k*(sqrt(5.0)+1)/2;
不存在重复的数。
下面是该题的进阶版 就是问你先手能不能赢 如果能赢 就输出第一次取的方法
之前水过了一次 然后上次做 自己把自己给hack了
新的思路稳妥一点
大致思路就是 看看能不能在a和b里面找到这两个数 分情况 同在a 同在b 在b a 三种情况
讨论一下就可以了 现在心情不行 没法想这些东西
const int maxn=1e6+10;
int a[maxn],b[maxn];
int main() {
for(int i=0;i<maxn;i++){
a[i]=(int)(i*(sqrt(5.0)+1)/2);
b[i]=a[i]+i;
}
int x,y;
while(1){
scanf("%d%d",&x,&y);
if(x==0&&y==0) break;
//x<=y
if(x==0){
printf("1\n0 0\n");
continue;
}
int k=y-x;
if(x==a[k]&&y==b[k]){//奇异局势
printf("0\n");
}else{
printf("1\n");
//若当前不是奇异局势
bool aflag=true,bflag=true;
int pa=lower_bound(a+1,a+maxn,x)-a;
int pb=lower_bound(b+1,b+maxn,y)-b;
if(x!=a[pa]) {
pa=lower_bound(b+1,b+maxn,x)-b;
aflag=false;
}
if(y!=b[pb]){
pb=lower_bound(a+1,a+maxn,y)-a;
bflag=false;
}
//现在true代表 x在a中找到了坐标 y在b中找到了坐标 各得其所
//false 代表x在b中 y在a中 找到了坐标
if(x==y){//若是同一个
printf("0 0\n");
if(bflag){//若在b中找到了
if(x>a[pb]) printf("%d %d\n",a[pb],b[pb]);
}
}else{//x
if(x>a[k]) printf("%d %d\n",a[k],b[k]);
if(aflag&&bflag){//若在a b中找到了
//都在各自的地方找到了 说明是低阶奇异局势共同加上一个数
if(x>a[pb]) printf("%d %d\n",a[pb],b[pb]);
if(y>b[pa]) printf("%d %d\n",a[pa],b[pa]);
}else if(aflag&&!bflag){//两个都在a中找到
//若a对应的b 不大于当前的b
if(b[pa]<a[pb]) printf("%d %d\n",a[pa],b[pa]);
}else if(!aflag&&bflag){//两个都在b中找到
//若b对应的a 不小于当前的a
if(a[pb]>b[pa]) printf("%d %d\n",a[pa],b[pa]);
}else{//都没找到
printf("%d %d\n",a[pa],b[pa]);
}
}
}
}
return 0;
}
就是两个人轮流加价 一次最多加m 最少加1 问谁先超过n
%(m+1)就行
然后输出第一次加价方法的时候要分析一下
int main() {
int n,m;
while(cin>>n>>m){
if(n%(m+1)) {//若前者胜
if(n>m+1){
printf("%d\n",n%(m+1));
}else{//小于
for(int i=n;i<m;i++){
printf("%d ",i);
}printf("%d\n",m);
}
}else {
printf("none\n");
}
}
return 0;
}
一段序列 两个人从左右开始取 可以取一段但是不能不取 问先手最多取多少
区间dp 核心代码:
dp[i][j]=b[j]-b[i-1];
rep(k,i,j-1){
dp[i][j]=max(dp[i][j],b[k]-b[i-1]-dp[k+1][j]);
dp[i][j]=max(dp[i][j],b[j]-b[k]-dp[i][k]);
}
从短到长进行dp 要么取ik这一块 要么取剩下那一块
const int maxn=110;
int n;
int a[maxn],b[maxn];
int dp[maxn][maxn];
int main() {
int t;sd(t);
rep(cas,1,t){
sd(n);
b[0]=0;
rep(i,1,n){
sd(a[i]);
b[i]=b[i-1]+a[i];
}
rep(l,1,n){
rep(i,1,n){
int j=i+l-1;
if(j>n) break;
dp[i][j]=b[j]-b[i-1];
rep(k,i,j-1){
dp[i][j]=max(dp[i][j],b[k]-b[i-1]-dp[k+1][j]);
dp[i][j]=max(dp[i][j],b[j]-b[k]-dp[i][k]);
}
}
}
printf("Case %d: %d\n",cas,dp[1][n]);
}
return 0;
}
一堆数 从左到右开始选 可以选择获得这个数 然后决策权交给下一个人
或者选择不获得这个数 决策权留给自己
倒着dp即可
下面是核心代码:
dp[i]=max(dp[i+1],b[i+1]-dp[i+1]+a[i]);
下面是AC代码
int n;
int a[60],b[60];
int dp[60];
int main() {
sd(n);
rep(i,1,n) {
sd(a[i]);
}
b[n+1]=0;
for(int i=n;i>=1;i--){
b[i]=b[i+1]+a[i];
dp[i]=max(dp[i+1],b[i+1]-dp[i+1]+a[i]);
}
printf("%d %d\n",b[1]-dp[1],dp[1]);
return 0;
}
这题是个很强的思维题 关键是发现原来的二进制中1的数量k与分成的堆数同奇偶
所以只要统计各堆所有数字二进制1之和即可
//这题本来以为是sg 没想到是思维!
//太强的思维了!
//k-1 与 m-1 同奇偶
//操作m-1次 奇数的话 先手胜
//则 某一堆 若k-1是奇数 先手胜
//若某一堆k-1是偶数 不会影响胜负
//所以求出所有堆的k-1值 加起来
//若是奇数就先手胜 否则后手胜
int num(int x){
int ans=0;
while(x){
if(x&1) ans++;
x>>=1;
}
return ans-1;
}
int main() {
int T;
sd(T);
rep(i,1,T){
int n;
sd(n);
int ans=0;
rep(j,1,n){
int x;
sd(x);
ans+=num(x);
}
printf("Case %d: %s",i,(ans&1)?"Yes\n":"No\n");
}
return 0;
}
再上一个模拟博弈 两个数 每次可用大数减去小数的任意倍数
就按它说的搜索即可 要满足各个条件 这是赵鑫大佬的算法 tql
//多的一堆可减去少的一堆的倍数 变为0就赢了
bool dfs(ll a,ll b){
if(a<b){
ll tp=a;a=b;b=tp;
}
if(b==0) return false;
if(a%b==0) return true;
if(a>=2*b) return true;
return !dfs(a-b,b);
}
int main() {
ll a,b;
while(scanf("%lld%lld",&a,&b)!=EOF){
if(a==0&&b==0) break;
printf(dfs(a,b)?"Stan wins\n":"Ollie wins\n");
}
return 0;
}