(简记)一类博弈论 DP

经典博弈论 DP

我们曾经在(笔记)交互题 构造题中提到过类似的 DP 技巧,具体理论支持在(笔记)博弈论 公平组合游戏。具体地,我们需要令 DP 状态中 \(0\) 作为必败态,然后通过其他分别为 \(1,2,3\) 的非必败态进行转移。具体来说,构成一个非必败态,我们只需要存在任意一个可以来自的必败态,然后就可以把对手搞过去。但是构成一个必败态则需要我们没有一条入边是来自必败态的,显然后者更加苛刻。这也符合了博弈论的 DAG 游戏转移理论中利用 \(\text{mex}\) 进行状态转移的技巧。

具体转移方式已经在链接中展开,这里不再多讲。

可以发现一般博弈论的 DP 题都是 从后往前 DP,即从确定的终止状态向初始状态 DP,因为 绝顶聪明 这一条件使得双方都能预测到他们当前的行为对后续局面的影响,可以说只有 后效性 而没有 前效性:若 \(i,j\) 确定,则两人之前的决策对当前决策无影响。

——@Alex_Wei题解:CF1628D2 Game on Sum (Hard Version)

因此双方都会依此做出最优决策,而对应地每个时刻的答案就都是固定的。

双人博弈:\(\min\max\) DP

对答案 DP

P4765 [CERC2014] The Imp

其实不难。首先我们需要证明一个贪心结论,即博弈时应按照 \(v\) 从小到大的顺序取用。证明简单,考虑两个不同物品分别有 \(v_1,v_2,c_1,c_2\),则从小到大取的收益为 \(\min(v_1-c_1,v_2-c_1-c_2)\),反之则为 \(\min(v_2-c_2,v_1-c_1-c_2)\),而显然 \(v_1\le v_2\implies min(v_1-c_1,v_2-c_1-c_2)\geq v_1-c_1-c_2\)。也就是说,选升序显然更优。

再考虑 DP 的细节,敌手可以考虑把物品用魔法干掉或者直接让其成为最后购买的一件,让我们直接离开。表现在转移方程里就是 \(\min(v_i-c_i,dp_{i,j-1}-c_i)\)。当然我们也可以选择不选这项物品,所以总的方程:

\(dp_{i,j}\leftarrow \max(dp_{i-1,j},\min(v_i-c_i,dp_{i,j-1}-c_i))\)

然后我们发现我们必须选到一个最终带走的物品(当然也有可能选不到),那么我们不如一开始就先把它确定下来,这样我们的 DP 顺序就是按 \(v\) 从大到小即可,然后边界就是 \(dp_{i,0}=v_i-c_i\)

时间复杂度 \(O(Tnk)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const LL INF=1e14;
const int N=1.5e5+5;
int n,k;
struct Q{LL v,c;}q[N];
bool cmp(Q x,Q y){return x.v<y.v;}
LL dp[N][10];
int main(){
    int Tn;scanf("%d",&Tn);
    while(Tn--){
        scanf("%d%d",&n,&k);
        for(int i=1;i<=n;i++)
            scanf("%lld%lld",&q[i].v,&q[i].c);
        sort(q+1,q+1+n,cmp);
        for(int i=n+1;i>=1;i--)
            for(int j=k;j>=0;j--)
                dp[i][j]=0;
        for(int i=n;i>=1;i--){
            dp[i][0]=max(dp[i+1][0],q[i].v-q[i].c);
            for(int j=k;j>=1;j--)
                dp[i][j]=max(dp[i+1][j],min(dp[i+1][j-1]-q[i].c,q[i].v-q[i].c));
        }
        printf("%lld\n",dp[1][k]);
    }
    return 0;
}

CF1628D2 Game on Sum (Hard Version)

首先想到需要博弈论 DP,在这点上我就失败了。令 \(f_{i,j}\) 表示 \(m=i,n=j\) 局面下的答案。显然有转移:\(f_{i,j}=\max_{t\in[0,k]}\min(f_{i-1,j-1}+t,f_{i-1,j}-t)\),外面那层 \(t\) 可以由 Alice 取,里面那层 \(\min\) 由 Bob 取,那么取到最大值即为 \(t=\frac{f_{i-1,j}-f_{i-1,j-1}}{2}\),根据感性理解 \(t\in[0,k]\),所以 \(f_{i,j}=\frac{f_{i-1,j-1}+f_{i-1,j}}{2}\),边界是 \(f_{i,0}=0,f_{i,i}=ik\)

考虑优化该过程,发现 \(f_{i,i}\)\(f_{n,m}\) 的贡献就是 \((i+1,i)\)\((n,m)\) 只走右下或下方的路径种数除一个 \(2\) 的幂次,这和杨辉三角求组合数的过程是相同的。为什么不是 \((i,i)\)?因为 \(f_{i,i}\) 无法转移到 \(f_{i+1,i+1}\),下移一格正好可以覆盖除了 \(f_{i+k,i+k}\) 的所有情况。预处理组合数可以做到 \(\Theta(n)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const LL MOD=1e9+7;
const int N=3e6+5;
LL inv[N],jc[N],jcinv[N],npw2[N];
inline LL C(int m,int n){
	return jc[m]*jcinv[n]%MOD*jcinv[m-n]%MOD;
}
void init(){
	jc[0]=jc[1]=inv[0]=inv[1]=
	jcinv[0]=jcinv[1]=npw2[0]=1;
	npw2[1]=(MOD+1)/2;
	for(int i=2;i<=N-5;i++){
		jc[i]=jc[i-1]*i%MOD;
		inv[i]=(MOD-MOD/i)*inv[MOD%i]%MOD;
		jcinv[i]=jcinv[i-1]*inv[i]%MOD;
		npw2[i]=npw2[i-1]*npw2[1]%MOD;
	}
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	int Tn;init();cin>>Tn;
	while(Tn--){
		int n,m,k;cin>>n>>m>>k;
		if(n==m){cout<<1ll*k*m%MOD<<'\n';continue;}
		LL res=0;
		for(int i=1;i<=m;i++)
			res=(res+1ll*i*k%MOD*C(n-i-1,m-i)%MOD*npw2[n-i]%MOD)%MOD;
		cout<<res<<'\n';
	}
	return 0;
}

DAG 上 DP

P12576 [UOI 2021] 数字图

一开始一直在想 DAG 上缩点然后博弈论 DP,但是在一个 SCC 中显然不一定能全部取到点值,可能取部分就出去了。我们考虑点权只有 \(1,2\) 的弱化情况,并且只在点权不同的点之间连边,因为玩家如果移动肯定是移动到点权不同的点上摆脱对自己的劣势局面。转化后模型与点权无关,在出度为 \(0\) 的点上先手必败,然后一个点如果有任意后继是必败点,这个点就是必胜点。如果一个点所有后继都是必胜点,那么它就是必败点。考虑到定出必败点比必胜点更加严格,我们在 DP 时不能对所有点进行拓扑,而是计算出一个必胜点后直接让它转移到下一个必败点,对所有必败点进行拓扑,因为必胜点要求宽松,可能在一个环里,不一定能把其所有出度减成 \(0\)。然后外层套一个二分答案就可以 \(O((n+m)\log V)\) 解决。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
int n,m,a[N],val[N];
int U[N],V[N];
vector<int>g[N];
int hd,tl,q[N],sg[N],deg[N];
bool check(int mid){
	hd=1,tl=0;
	for(int i=1;i<=n;i++)
		val[i]=(a[i]>=mid),
		sg[i]=-1,g[i].clear(),deg[i]=0;
	if(val[1])return 1;
	for(int i=1;i<=m;i++){
		int u=U[i],v=V[i];
		if(val[u]!=val[v]){
			deg[u]++;
			g[v].push_back(u);
		}
	}
	for(int i=1;i<=n;i++)
		if(!deg[i])
			q[++tl]=i;
	while(hd<=tl){
		int u=q[hd++];
		sg[u]=0;
		for(int v:g[u]){
			if(sg[v]==-1){
				sg[v]=1;
				for(int w:g[v]){
					deg[w]--;
					if(sg[w]==-1&&!deg[w])
						q[++tl]=w;
				}
			}
		}
	}
	return (sg[1]==1);
}
int main(){
	//freopen("graph.in","r",stdin);
	//freopen("graph.out","w",stdout);
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	for(int i=1;i<=m;i++)
		scanf("%d%d",&U[i],&V[i]);
	int l=1,r=1000000000,res=1;
	while(l<=r){
		int mid=(l+r)>>1;
		if(check(mid))res=mid,l=mid+1;
		else r=mid-1;
	}
	printf("%d",res);
	return 0;
}
posted @ 2025-07-14 23:18  TBSF_0207  阅读(34)  评论(0)    收藏  举报