博弈论合集

前言:此合集是整理上个学期开学时做的博弈论题目,没大有难度,都是板子。自己这断断续续补了一个月(还没补完)。
之前一直以为sg函数是解决小部分博弈问题的。
这次突然发现,sg函数可以解决很多很多博弈问题,相当于是博弈的板子。

中间经历过了一些事情,虽然真的没有可能变好,但是希望可以慢慢变好。
分类:
1、sg函数类
2、dp类
3、威佐夫博弈
4、巴什博弈
5、思维类

1、SG函数

sg函数可以解决很大部分的两人博弈题目

HDU2176 取m堆石子

先放一个 可用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函数的

HDU1536 S-nim

套板子即可 不过数太大 要写循环不要写搜索
而且那里写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;
}

HDU - 1847

题意是只能拿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;
}

UVA1378

这个有些难度 题意是一个序列 只能把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;
}

2、威佐夫博弈

威佐夫博弈是一种比较经典的博弈
是一组组不重复的数对 所以对应取两堆石子(貌似可以用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;
}

3、巴什博弈

HDU2149

就是两个人轮流加价 一次最多加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;
}

4、dp类

LightOJ 1031

一段序列 两个人从左右开始取 可以取一段但是不能不取 问先手最多取多少
区间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;
}

CF 859C

一堆数 从左到右开始选 可以选择获得这个数 然后决策权交给下一个人
或者选择不获得这个数 决策权留给自己
倒着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;
}

5、思维类

HDU4388

这题是个很强的思维题 关键是发现原来的二进制中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;
}

POJ2348

再上一个模拟博弈 两个数 每次可用大数减去小数的任意倍数
就按它说的搜索即可 要满足各个条件 这是赵鑫大佬的算法 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;
}

你可能感兴趣的:(博弈,总结)