对于某些dp,是在博弈的基础上的,针对这些题目,我们不妨设
dp[某一阶段],为该阶段先手的最优子结构
那么对于他的状态转移:
dp[下一状态] = max(忽视对手的最优情况 - dp[上一状态])
尝试理解:因为博弈中,一个状态的先手,在上一状态是后手,而对手是先手,所以dp[上一状态]其实是对手的最优状态,所以有在忽视对手的最优情况 - 上一状态对手的最优情况。
比较抽象,下面看两个例题:
LightOJ - 1031 Easy Game:
对于这个题目,先处理出前缀和sum[n] 我们设dp[i][j]为区间[i,j]先手与后手的最大分差,对于它来说,忽视对手的最优情况就是把这一区间的所有数都取完,这样先手的得分是sum[j] - sum[j-1],后手是0分。而对于它的状态转移:
dp[i][j] = max( sum[j] - sum[k] - dp[i][k], sum[k] - sum[i-1] - dp[k][j]) (i <= k <= j)
对于每次转移,先后手就会转换,所以分差会变成原来的相反数。
下面是ac代码:
#include
#include
#include
#define ll long long
using namespace std;
ll dp[128][128];
ll sum[128];
int main()
{
int tr;
cin >> tr;
for (int t = 1; t <= tr; t++)
{
int n;
scanf("%d", &n);
sum[0] = 0;
memset(dp, -0x3f, sizeof(dp));
for (int i = 1; i <= n; i++)
{
scanf("%lld", &sum[i]);
dp[i][i] = sum[i];
sum[i] += sum[i - 1];
}
for (int i = n - 1; i >= 1; i--)
{
for (int j = i + 1; j <= n; j++)
{
for (int k = i; k <= j; k++)
{
if (k == j)
{
dp[i][j] = max(dp[i][j], sum[j] - sum[i - 1]);
continue;
}
dp[i][j] = max(dp[i][j], max(sum[k] - sum[i - 1] - dp[k + 1][j], sum[j] - sum[k] - dp[i][k]));
}
}
}
printf("Case %d: %d\n", t, dp[1][n]);
}
return 0;
}
HDU - 4597 Play Game
这个题虽然比较难一些,但是和上面的题目思路一致,而且更贴合上述的思想。我们设dp[al][ar][bl][br]为在第一堆牌(al,ar)第二堆牌(bl,br)的区间阶段中,先手的最大分数。我们进行记忆化搜索。
显然,无视对手的最优状态就是假定这一区间的所有牌都是先手取(实际不可能)
sum = suma[i] - suma[j-1] + sumb[g] - sumb[k-1]
而状态转移方程:
dp[al][ar][bl][br] = max(dp[al][ar][bl][br], sum - dfs(al, ar-1, bl, br));
dp[al][ar][bl][br] = max(dp[al][ar][bl][br], sum - dfs(al + 1, ar, bl, br));
dp[al][ar][bl][br] = max(dp[al][ar][bl][br], sum - dfs(al, ar, bl, br - 1));
dp[al][ar][bl][br] = max(dp[al][ar][bl][br], sum - dfs(al, ar, bl + 1, br));
注意一下边界的判定就可以了。
下面是ac代码
#include
#include
#include
#include
#include
#include
#include
#include
#define ll long long
using namespace std;
const int N = 1e5+5;
int dp[26][26][26][26];
int a[26], b[26], suma[26], sumb[26];
void init()
{
memset(suma, 0, sizeof(suma));
memset(sumb, 0, sizeof(sumb));
memset(dp, 0, sizeof(dp));
}
int dfs(int al, int ar, int bl, int br)
{
if (dp[al][ar][bl][br]) return dp[al][ar][bl][br];
if (al > ar && bl > br)
{
//printf("%d %d %d %d : %d\n", al, ar, bl, br, dp[al][ar][bl][br]);
return 0;
}
int sum = suma[ar] - suma[al-1] + sumb[br] - sumb[bl - 1];
if (al <= ar) dp[al][ar][bl][br] = max(dp[al][ar][bl][br], sum - dfs(al, ar-1, bl, br));
if (al <= ar) dp[al][ar][bl][br] = max(dp[al][ar][bl][br], sum - dfs(al + 1, ar, bl, br));
if (bl <= br) dp[al][ar][bl][br] = max(dp[al][ar][bl][br], sum - dfs(al, ar, bl, br - 1));
if (bl <= br) dp[al][ar][bl][br] = max(dp[al][ar][bl][br], sum - dfs(al, ar, bl + 1, br));
//printf("%d %d %d %d : %d\n", al, ar, bl, br, dp[al][ar][bl][br]);
return dp[al][ar][bl][br];
}
int main()
{
int t;
cin >>t;
while(t--)
{
init();
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
{
scanf("%d", &a[i]);
suma[i] = suma[i-1] + a[i];
}
for (int i = 1; i <= n; i++)
{
scanf("%d", &b[i]);
sumb[i] = sumb[i-1] + b[i];
}
printf("%d\n", dfs(1, n, 1, n));
}
return 0;
}
感觉这个题记忆化搜索比区间dp要好懂的多,当然也可以用区间dp跑。