动态规划解决博弈问题
石头游戏:
你和你的朋友⾯前有⼀排⽯头堆,⽤⼀个数组 piles 表⽰,piles[i] 表⽰第 i 堆⽯⼦有多少个。你们轮流拿⽯头,⼀次拿⼀堆,但是只能拿⾛最左边或者 最右边的⽯头堆。所有⽯头被拿完后,谁拥有的⽯头多,谁获胜。 ⽯头的堆数可以是任意正整数,⽯头的总数也可以是任意正整数,这样就能 打破先⼿必胜的局⾯了。⽐如有三堆⽯头 piles = [1,100, 3] ,先⼿不管 拿 1 还是 3,能够决定胜负的 100 都会被后⼿拿⾛,后⼿会获胜。 假设两⼈都很聪明,请你设计⼀个算法,返回先⼿和后⼿的最后得分(⽯头 总数)之差。⽐如上⾯那个例⼦,先⼿能获得 4 分,后⼿会获得 100 分,你 的算法应该返回 -96。
分析
首先 这个问题和常规的问题不同,先手和后手会相互影响,所以在dp table 中存在两个状态 fir, sec;
1 明确dp[i][j] = (fir, sec)的含义
对于第i到第j堆的石头,先手最多拿到fir个,后手最多拿到sec的个
2.初始化base情况
1)因为i<j 所以
if i>j dp[i][j] == 0
2)如果i==j 必然只有一个石头堆,先手获得 后手为0 即 dp[i][j] = {pile(i),0};
2 确定状态转移方程
当前dp[i][j] 和那些因素有关呢?
1.对于先手来说
1)他可以选最左边 即pile[i], 选择了之后 对于 dp[i-1][j] (i+1~j 石头堆)来说就是后手了,所以 dp[i][j]fir = dp[i+1][j].sec + pile[i];
2)他也可以选左边 即pile[j] ,选择了之后 对于dp[i]j-1来说就是后手了,所以 dp[i][j] sec = dp[i][j-1].sec + pile[j];
先手要选择最大的即 max(dp[i+1][j].sec + pile[i], dp[i][j-1].sec + pile[j]);
2.对于后手来说
后手的选择 取决于先手的选择
1)若先手选择了左边 则剩下i+1 ~j 的石头堆来说,后手变成了先手 即dp[i][j].sec = dp[i+1][j].fiir;
2)若先手选择了右边 则剩下i ~j-1的石头堆来说,后手变成了先手 即dp[i][j].sec = dp[i][j-1].fiir;
总结
不管先手还是后手 dp[i][j]的值只和dp[i+1][j] 和dp[i][j-1]的值有关 类似于图
此时的dp table 采用斜向遍历的方式 具体代码
int game(int[] pile){
int n = piles.length;
//三维数组 最后一个维度代表两个状态 先手和后手
int[][][] dp = new int[n][n][2]
//base 根据上文分析
for(int i = 0; i < n;i++){
//先手拿到
dp[i][i][0] = piles[i];
//后手没有
dp[i][i][1] = 0;
}
//斜向便利数组
for (int l=2;l<=n;l++) {
for(int i =0;i<=n-l;i++) {
int j = l + i- 1;
// 先⼿选择最左边或最右边的分数
int left = piles[i] + dp[i+1][j][1];
int right = piles[j] + dp[i][j-1][1];
// 套⽤状态转移⽅程
if (left > right) {
dp[i][j][0] = left;
dp[i][j][1] = dp[i+1][j][0];
}else{
dp[i][j][0]=right;
dp[i][j][1] =dp[i][j-1][0];
}
}
}
return dp[0][n][1] - dp[0][n][0];
}