博弈论题解

P5675 [GZOI2017]取石子游戏

有两种 \(Alice\) 不胜利的方法:

  1. \(SG\) 之和为 \(0\) ,肯定不成功
  2. \(SG\) 之和不为 \(0\) ,选择的堆的石子的个数,一定小于其他石子堆石子个数的异或和。

为什么呢?从二进制考虑:无论从该堆选择多少石子,都无法将异或和变为 \(0\) ,因此后手肯定是必胜的情况。

看数据范围,支持 \(O(n^2-n^3)\) 级别的算法。

考虑 \(dp\) ,设 \(dp[i][j]\) 表示前 \(i\) 堆,异或和为 \(j\) 的方案数。

我们发现,因为 \(a[i] \leq 200\) ,因此异或和最大为 \(2^8-1\)

因此,第一维枚举选择的第一堆石子,第二维表示前 \(j\) 堆,第三维表示前 \(j\) 堆的异或和为 \(k\).

递推计算即可。

// P5675 [GZOI2017]取石子游戏
#include<bits/stdc++.h>
using namespace std;
#define int long long 
const int N=305,mod=1e9+7;
int n,a[N],dp[N][N],ans;
signed main(){
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    dp[0][0]=1;
    for(int i=1;i<=n;i++){// 第 i 堆作为首选的堆
        for(int j=1;j<=n;j++)
            for(int k=0;k<256;k++){//k为有多少种选择方法异或和=k
                if(i==j) dp[j][k]=dp[j-1][k];//跳过枚举第一维石子
                else dp[j][k]=(dp[j-1][k]+dp[j-1][k^a[j]])%mod;//继承+异或和为0的个数
            }
        for(int j=a[i];j<256;j++) ans=(ans+dp[n][j])%mod;//统计第 i 堆作为第一堆的答案
    }
    cout<<ans<<endl;
    return 0;   
}

P2964 [USACO09NOV]A Coin Game S

如果是正着求,我们要记录的二倍会有很多很多情况,记录下来十分麻烦,所以我们将这道题目的题意转换为两个人轮流放硬币,一个人放硬币时需要满足这一次放的硬币数的两倍大于等于上一次另一个人放的硬币数。

\(sum[i]\) 表示放了 \(i\) 枚硬币后的总面值。

\(dp[i][j]\) 表示已经放了 \(i\) 个硬币,下一个人要放 \(j\) 个硬币,最多放 \(2j\) 个的情况。

每一次转移, \(k\) 表示这一次取得的硬币数量,枚举 \(k=[1,2j]\) ,在 \(2j\)\(sum[i]- dp[i-k][k]\) 中取一个最大值, \(dp[i-k][k]\) 记录上一个人在当前这个人放 \(k\) 个时,上一个人最多能放的个数。

\(sum[i]-dp[i-k][k]\) 即为当前这个人放 \(k\) 个时放的总硬币数。

考虑优化:

只有优化第三个循环,我们才能过了这题。观察 \(dp[i][j],dp[i][j-1]\) 的关系。

通过定义,我们发现:\(dp[i][j]\) 是严格包含 \(dp[i][j−1]\) 的,我们只需要在 \(dp[i][j−1]\) 的基础上继续枚举 \(k=2 \times j-1\)\(k = 2 \times j\) 这两种状态即可。

#include<bits/stdc++.h>
using namespace std;
const int N=2e3+5;
int n,a[N],sum[N],dp[N][N];
int main(){
    cin>>n;
    for(int i=n;i>=1;i--) cin>>a[i];
    for(int i=1;i<=n;i++) sum[i]=sum[i-1]+a[i];//sum[i]记录放了i个硬币后的总面值 
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++){
			int k=2*j-1; 
			dp[i][j]=dp[i][j-1];//由上面分析可知,dp[i][j]严格包含了dp[i][j-1] 
			//在dp[i][j-1]的基础上更新两个状态。 
			if(k<=i) dp[i][j]=max(dp[i][j],sum[i]-dp[i-k][k]); //当k<=i时,才能取max 
			if(k+1<=i) dp[i][j]=max(dp[i][j],sum[i]-dp[i-k-1][k+1]); //当k+1<=i时,才能取max 
		}
	printf("%d\n",dp[n][1]);
	//这里dp[n][1]表示已经放了n个马上要再放1个,但还没有放。
	//马上下一个人要放1个硬币,也就意味着上一次只能放1个或2个,即为第一次取的情况。 
    system("pause");
    return 0;
}

P4101 [HEOI2014]人人尽说江南好

合并次数为奇数先手必胜,偶数后手必胜,那么两个人都会尽可能向着自己想要的方向去发展(即拉到总合并次数为奇数/偶数)

如果拉到最长的操作次数我们必胜的话,那么不管对面怎么操作,我们都能拉回来

同样如果最长的操作是对面必胜,不管我们怎么合并,对面也能用拉回来

以及

我们已经发现最长的情况我们必胜,无论对手怎么操作我们都能拖到最长,我们必胜

对手发现最长他必胜,无论我们怎么操作,他也肯定能往下拖。

所以对最长必胜的那一方来说,一直拖到最长就是最优策略。

而我们已经分析了必胜的那一方总是能拖下去。

即这是一种必然取胜的方法,且如果对手不按套路出牌我们也能拖回来。

因此最终答案就是最长能拖到的次数,如果为奇先手胜,如果为偶后手胜。

最长次数判断:

  1. \(n\leq m\) 判断出为 \(n-1\)
  2. \(n>m\) ,则最后最大合并后堆数一定是 \(\{m,m,m,m,n\%m\}\)

因此,就有计算公式:

\[ans=(n/m)*(m-1)+n\%m?n\%m-1:0; \]

然后判断奇偶即可。

// P4101 [HEOI2014]人人尽说江南好
#include<bits/stdc++.h>
using namespace std;
int T;
long long int n,m;
int main(){
	scanf("%d",&T);
	while(T--){
		scanf("%lld%lld",&n,&m);
		long long ans=(n/m)*(m-1)+((n%m)?(n%m-1):0);
		if(ans%2) printf("0\n");
		else printf("1\n");
	}
    system("pause");
	return 0;
}
posted @ 2021-09-16 18:00  Evitagen  阅读(44)  评论(0编辑  收藏  举报