其他分享
首页 > 其他分享> > 动态规划系列(四)——俩海盗博弈问题

动态规划系列(四)——俩海盗博弈问题

作者:互联网

本⽂就借⽯头游戏来讲讲「假设两 个⼈都⾜够聪明,最后谁会获胜」这⼀类问题该如何⽤动态规划算法解决。

博弈类问题的套路都差不多,下⽂举例讲解,其核⼼思路是在⼆维 dp 的基 础上使⽤元组分别存储两个⼈的博弈结果。掌握了这个技巧以后,别⼈再问 你什么俩海盗分宝⽯,俩⼈拿硬币的问题,你就告诉别⼈:我懒得想,直接 给你写个算法算⼀下得了。

⼀、定义 dp 数组的含义

以下是对 dp 数组含义的解释:

dp[i][j].fir 表⽰,对于 piles[i...j] 这部分⽯头堆,先⼿能获得的最⾼分数。 

dp[i][j].sec 表⽰,对于 piles[i...j] 这部分⽯头堆,后⼿能获得的最⾼分数。 

举例理解⼀下,假设 piles = [3, 9, 1, 2],索引从 0 开始

dp[0][1].fir = 9 意味着:⾯对⽯头堆 [3, 9],先⼿最终能够获得 9 分。

dp[1][3].sec = 2 意味着:⾯对⽯头堆 [9, 1, 2],后⼿最终能够获得 2 分。

我们想求的答案是先⼿和后⼿最终分数之差,按照这个定义也就是

dp[0] [n-1].fir - dp[0] [n-1].sec ,即⾯对整个 piles,先⼿的最优得分和后⼿的 最优得分之差。

dp 数组最终的样⼦:

Screen Shot 2021-03-17 at 2.19.52 PM

下⽂讲解时,认为元组是包含 first 和 second 属性的⼀个类,⽽且为了节省

篇幅,将这两个属性简写为 fir 和 sec。⽐如按上图的数据,我们说

dp[1] [3].fir = 10 , dp[0] [1]sec = 3

⼆、状态转移⽅程

可以做的选择有两个:选择最左边的那堆⽯头,

或者选择最右边的那堆⽯头。 我们可以这样穷举所有状态:

n = piles.length 

for 0 <= i < n: 
  for j <= i < n: 
    for who in {fir, sec}: 
      dp[i][j][who] = max(left, right)

这道题的难点在于,两⼈是交替进⾏选择的,也就是说先⼿的选择会对 后⼿有影响

根据我们对 dp 数组的定义,很容易解决这个难点,写出状态转移⽅程:

dp[i][j].fir = max(piles[i] + dp[i+1][j].sec, piles[j] + dp[i][j-1].sec) 

# 解释:dp[i][j].fir = max( 选择最左边的⽯头堆 , 选择最右边的⽯头堆 ) 

#c我作为先⼿,⾯对 piles[i...j] 时,有两种选择: 

# 要么我选择最左边的那⼀堆⽯头,然后⾯对 piles[i+1...j] 

# 但是此时轮到对⽅,相当于我变成了后⼿; 

# 要么我选择最右边的那⼀堆⽯头,然后⾯对 piles[i...j-1] 

# 但是此时轮到对⽅,相当于我变成了后⼿。 

if 先⼿选择左边: 

  dp[i][j].sec = dp[i+1][j].fir 

if 先⼿选择右边: 

  dp[i][j].sec = dp[i][j-1].fir 

# 解释:我作为后⼿,要等先⼿先选择,有两种情况: 

# 如果先⼿选择了最左边那堆,给我剩下了 piles[i+1...j] 

# 此时轮到我,我变成了先⼿; 

# 如果先⼿选择了最右边那堆,给我剩下了 piles[i...j-1] 

# 此时轮到我,我变成了先⼿。 

根据 dp 数组的定义,我们也可以找出 base case,也就是最简单的情况:

dp[i][j].fir = piles[i] 

dp[i][j].sec = 0 

其中 0 <= i == j < n 

# 解释:i 和 j 相等就是说⾯前只有⼀堆⽯头 piles[i] 

# 那么显然先⼿的得分为 piles[i] 

# 后⼿没有⽯头拿了,得分为 0 

Screen Shot 2021-03-17 at 3.07.12 PM

这⾥需要注意⼀点,我们发现 base case 是斜着的,⽽且我们推算 dp[i] [j] 时 需要⽤到 dp[i+1] [j] 和 dp[i] [j-1]:

Screen Shot 2021-03-17 at 3.08.41 PM

所以说算法不能简单的⼀⾏⼀⾏遍历 dp 数组,⽽要斜着遍历数组

三、代码实现

如何实现这个 fir 和 sec 元组呢,你可以⽤ python,⾃带元组类型;或者使 ⽤ C++ 的 pair 容器;或者⽤⼀个三维数组 dp[n][n][2] ,最后⼀个维度就 相当于元组;或者我们⾃⼰写⼀个 Pair 类:

class Pair { 
  int fir, sec; 
  Pair(int fir, int sec) { 
    this.fir = fir;
    this.sec = sec; 
  } 
}

然后直接把我们的状态转移⽅程翻译成代码即可,可以注意⼀下斜着遍历数组的技巧

/* 返回游戏最后先⼿和后⼿的得分之差 */ 

int stoneGame(int[] piles) { 
  int n = piles.length; 
  // 初始化 dp 数组 
  Pair[][] dp = new Pair[n][n]; 
  for (int i = 0; i < n; i++) 
    for (int j = i; j < n; j++) 
      dp[i][j] = new Pair(0, 0); 
  // 填⼊ base case 
  for (int i = 0; i < n; i++) { 
    dp[i][i].fir = piles[i]; 
    dp[i][i].sec = 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].sec; 
      int right = piles[j] + dp[i][j-1].sec; 
      // 套⽤状态转移⽅程 
      if (left > right) { 
        dp[i][j].fir = left; 
        dp[i][j].sec = dp[i+1][j].fir; 
      } else { 
        dp[i][j].fir = right; 
        dp[i][j].sec = dp[i][j-1].fir; 
      } 
    } 
  }
  Pair res = dp[0][n-1]; 
  return res.fir - res.sec; 
} 

完整代码

#include <iostream>
#include <utility>
#include <vector>
#include<stdlib.h>

using namespace std;
typedef pair<int,int> Pair;

int stoneGame(vector<int> piles) { 
  int n = piles.size(); 
  // 初始化 dp 数组 
  vector<vector<Pair> > dp(n,vector<Pair>(n,make_pair(0, 0)));
  
  // 填⼊ base case 
  for (int i = 0; i < n; i++) { 
    dp[i][i].first = piles[i]; 
    dp[i][i].second = 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].second; 
      int right = piles[j] + dp[i][j-1].second; 
      // 套⽤状态转移⽅程 
      if (left > right) { 
        dp[i][j].first = left; 
        dp[i][j].second = dp[i+1][j].first; 
      } else { 
        dp[i][j].first = right; 
        dp[i][j].second = dp[i][j-1].first; 
      } 
    } 
  }
  Pair res = dp[0][n-1]; 
  return res.first - res.second; 
} 

int main(int argc, char **argv) 
{
    // vector<int> piles = {3,9,1,2};
    vector<int> piles;
    piles.push_back(3);
    piles.push_back(9);
    piles.push_back(1);
    piles.push_back(2);
    int res = stoneGame(piles);
    cout<< res <<endl;
    system("pause");
    return 0;
}

四、最后总结

本⽂给出了解决博弈问题的动态规划解法。博弈问题的前提⼀般都是在两个 聪明⼈之间进⾏,编程描述这种游戏的⼀般⽅法是⼆维 dp 数组,数组中通 过元组分别表⽰两⼈的最优决策。

之所以这样设计,是因为先⼿在做出选择之后,就成了后⼿,后⼿在对⽅做 完选择后,就变成了先⼿。这种⾓⾊转换使得我们可以重⽤之前的结果,典 型的动态规划标志。

标签:fir,博弈,piles,int,海盗,sec,数组,动态,dp
来源: https://www.cnblogs.com/satire/p/14550688.html