题目链接:UVA - 10891 - Game of Sum - (区间DP)
题意:有n个数字的序列,有两个人A,B来玩游戏, 每个人每次可以从两端(左或右)中的任意一端取走一个或若干个数(获得价值为取走数之和), 但是他取走的方式一定要让他在游戏结束时价值尽量的高,最头疼的是两个人都很聪明,所以每一轮两人都将按照对自己最有利的方法去取数字,计算一下在游戏结束时,先取数的人A的价值与后取数人B的价值之差(不要求绝对值)。
题解全部来自下面的博客:
https://blog.csdn.net/nixinyis/article/details/50733202
https://blog.csdn.net/basementman/article/details/16905657
https://blog.csdn.net/tengfei461807914/article/details/50885531
解析:
有如下经典题目:两个玩家A和B在玩一个取石子游戏,且每个石子都有它们各自的价值。在游戏中有这样一个规则:每次取一个石子必行从两端取,要么是最左端,要么是最右端,直到取完为止。两个玩家都非常聪明,他们每次都会去最优的结果。给他们N个石子,你能计算出玩家A,B各自的最后结果吗?假设总是玩家A先开局。
因为分数总和是一定的,所以一个人的得分越高,另一个就越低。而且不管怎么取,游戏的状态始终是一段连续子序列。所以可以用数组best[i,j]来表示在由第i个到第j个元素组成的子序列时先手所能得到的最高分。因为两个人都是用最优策略,那么先手后手的问题可以随意定义一个就好。所以我们可以分析,另一个人在所能取得所有最优选择中得分最低的分数 m ,sum-m即为我的最高得分。
对于这个经典问题,我们考虑best[i][j]表示开局者玩家A的i到j部分得到最大和,sum[i][j]表示从i到j和,由于只能从两端取石子,要么有从最左端,要么从最右端。则有转移方程:
best[i][j]=sum[i][j]-min(best[i][j-1],best[i+1][j]); 目标状态:best[1][n]
那么推广到这题,可以从两端取连续的,我们同样稍改变下即可。在状态转移时,我们要枚举从左边和从右边取以及取多少个,这等价于枚举给对手剩下了怎么样的子序列,是(k,j)(i<k<=j),还是(k,j)(i<=k<j)。所以有
best[i,j]=sum(i,j)-min{best[i+1,j],best[i+2,j]……,best[j,j],best[i,j-1],best[i,j-2]……,best[i,i],0}
其中sum(i,j)是元素i到j的书之和,其中“0”表示取完所有的数。
所以A的得分是d[1,n],B的得分是sum(1,n)-d[1,n],答案便是d[1,n]-(sum(1,n)-d[1,n])=2*d[1,n]-sum(1,n)
代码:
#include<bits/stdc++.h> using namespace std; typedef long long ll; const ll maxN=105; int sum[maxN],a[maxN]; int n,dp[maxN][maxN]; bool vis[maxN][maxN]; int fdp(int l,int r) { if(vis[l][r]) return dp[l][r]; vis[l][r]=true; int tmp=0; for(int k=l+1;k<=r;k++) tmp=min(tmp,fdp(k,r)); for(int k=l;k<r;k++) tmp=min(tmp,fdp(l,k)); dp[l][r]=sum[r]-sum[l-1]-tmp; return dp[l][r]; } int main() { while(scanf("%d",&n)&&n) { for(int i=1;i<=n;i++) { scanf("%d",&a[i]); sum[i]=sum[i-1]+a[i]; } memset(vis,false,sizeof(vis)); int ans=2*fdp(1,n)-sum[n]; printf("%d\n",ans); } return 0; }