mirror of
https://github.com/labuladong/fucking-algorithm.git
synced 2025-07-05 03:36:39 +08:00
翻译完成
This commit is contained in:
@ -1,64 +1,67 @@
|
||||
# Game Problems In Dynamic Programming
|
||||
|
||||
In the last article [几道智力题](../高频面试系列/一行代码解决的智力题.md) 中讨论到一个有趣的「石头游戏」,通过题目的限制条件,这个游戏是先手必胜的。但是智力题终究是智力题,真正的算法问题肯定不会是投机取巧能搞定的。所以,本文就借石头游戏来讲讲「假设两个人都足够聪明,最后谁会获胜」这一类问题该如何用动态规划算法解决。
|
||||
In the last article,we discussed a fun「stone game 」in [several puzzles](../高频面试系列/一行代码解决的智力题.md),By the constraints
|
||||
of the problem, the game is first to win.But intelligence questions are always intelligence questions,Real algorithmic problems are
|
||||
not solved by cutting corners. So this paper is going to talk about the stone game and assuming that both of these guys are smart enough, who's going to win in the end how do you solve this problem with dynamic programming.
|
||||
|
||||
Game problems follow a similar pattern,The core idea is to use tuples to store the game results of two people on the basis of two-dimensional dp array.Once you're mastered this technique,if someone asks you a similar question again,you can take it in stride.
|
||||
|
||||
博弈类问题的套路都差不多,下文举例讲解,其核心思路是在二维 dp 的基础上使用元组分别存储两个人的博弈结果。掌握了这个技巧以后,别人再问你什么俩海盗分宝石,俩人拿硬币的问题,你就告诉别人:我懒得想,直接给你写个算法算一下得了。
|
||||
We changed the stone game to be more general:
|
||||
|
||||
我们「石头游戏」改的更具有一般性:
|
||||
There is a pile of stones in front of you and your friends,it's represented by an array of piles,and piles[i] is how many stones are there in the ith heap.You take turns with the stones,one pile at a time,but you can only take the left or the right piles.After all the stones have been taken away, the last one who has more stones wins.
|
||||
|
||||
你和你的朋友面前有一排石头堆,用一个数组 piles 表示,piles[i] 表示第 i 堆石子有多少个。你们轮流拿石头,一次拿一堆,但是只能拿走最左边或者最右边的石头堆。所有石头被拿完后,谁拥有的石头多,谁获胜。
|
||||
The heap number of stones can be any positive integer,and the total number of stones can be any positive integer,That would break the situation in which one must win first.Let's say I have three piles of rocks: `piles = [1, 100, 3]`,Whether it's a 1or a 3,the 100 that's going to make the difference is going to be taken away by the back hand,and the back hand is going to win.
|
||||
|
||||
石头的堆数可以是任意正整数,石头的总数也可以是任意正整数,这样就能打破先手必胜的局面了。比如有三堆石头 `piles = [1, 100, 3]`,先手不管拿 1 还是 3,能够决定胜负的 100 都会被后手拿走,后手会获胜。
|
||||
**Assuming they are both smart**,design an algorithm that returns the difference between the final score of the first hand and the last hand,As in the example above,the first hand gets 4 points,the second hand gets 100 points, and you should return -96.
|
||||
|
||||
**假设两人都很聪明**,请你设计一个算法,返回先手和后手的最后得分(石头总数)之差。比如上面那个例子,先手能获得 4 分,后手会获得 100 分,你的算法应该返回 -96。
|
||||
|
||||
这样推广之后,这个问题算是一道 Hard 的动态规划问题了。**博弈问题的难点在于,两个人要轮流进行选择,而且都贼精明,应该如何编程表示这个过程呢?**
|
||||
With this design,this problem is a Hard dynamic programming problem.**The difficuty with gaming is that two people have to take turns choosing,and they're both smart.How do we program?**
|
||||
|
||||
还是强调多次的套路,首先明确 dp 数组的含义,然后和股票买卖系列问题类似,只要找到「状态」和「选择」,一切就水到渠成了。
|
||||
It's the approach that's been emphasized many times,The first step is to define the array,and then,like the stock buying and selling
|
||||
series,once you find the「status」and the「selection」,and then it's easy.
|
||||
|
||||
### 一、定义 dp 数组的含义
|
||||
### 1.Define the meaning of the dp array:
|
||||
|
||||
定义 dp 数组的含义是很有技术含量的,同一问题可能有多种定义方法,不同的定义会引出不同的状态转移方程,不过只要逻辑没有问题,最终都能得到相同的答案。
|
||||
Defining what a dp array means is very tachnical,The dp array of the same problem can be defined in several ways.Different definitions
|
||||
lead to different state transition equations,But as long as there's no logic problem,you end up with the same answer.I recommend that you don't get caught up in what looks like a great short technique,and that you end up with something that's a little bit more stable, something that's the most interpretable, and something that's the easiest to generalize,This paper gives a general design framework of game problem.
|
||||
|
||||
我建议不要迷恋那些看起来很牛逼,代码很短小的奇技淫巧,最好是稳一点,采取可解释性最好,最容易推广的设计思路。本文就给出一种博弈问题的通用设计框架。
|
||||
|
||||
介绍 dp 数组的含义之前,我们先看一下 dp 数组最终的样子:
|
||||
Before we introduce what a dp array means,let's take a look at what it ultimately looks like:
|
||||
|
||||

|
||||
|
||||
下文讲解时,认为元组是包含 first 和 second 属性的一个类,而且为了节省篇幅,将这两个属性简写为 fir 和 sec。比如按上图的数据,我们说 `dp[1][3].fir = 10`,`dp[0][1].sec = 3`。
|
||||
As explained below,tupels are considered to be a calss containing first and second atrributes,And to save space,these two atrributes are abbreviated to fir and sec.As shown in the figure above,we think `dp[1][3].fir = 10`,`dp[0][1].sec = 3`.
|
||||
|
||||
先回答几个读者可能提出的问题:
|
||||
Start by answering a few questions that readers might ask:
|
||||
|
||||
这个二维 dp table 中存储的是元组,怎么编程表示呢?这个 dp table 有一半根本没用上,怎么优化?很简单,都不要管,先把解题的思路想明白了再谈也不迟。
|
||||
This is a two-dimensional dp table that stores tuples.How do you represent that?Half of this array is useless,How do you optimize it?Very simple, do not care,first to think out the way to solve the problem again.
|
||||
|
||||
**以下是对 dp 数组含义的解释:**
|
||||
**Here's an explanation of what a dp array means:**
|
||||
|
||||
```python
|
||||
dp[i][j].fir 表示,对于 piles[i...j] 这部分石头堆,先手能获得的最高分数。
|
||||
dp[i][j].sec 表示,对于 piles[i...j] 这部分石头堆,后手能获得的最高分数。
|
||||
dp[i][j].fir represents the highest score the first hand can get for this section of the pile piles[i...j]
|
||||
dp[i][j].sec represents the highest score the back hand can get for this section of the pile 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 分。
|
||||
Just to give you an example,Assuming piles = [3, 9, 1, 2],The inedx starts at 0
|
||||
dp[0][1].fir = 9 means:Facing the pile of stones [3, 9],The first player eventually gets 9 points.
|
||||
dp[1][3].sec = 2 means:Facing the pile of stones [9, 1, 2],The second player eventually gets 2 points.
|
||||
```
|
||||
|
||||
我们想求的答案是先手和后手最终分数之差,按照这个定义也就是 $dp[0][n-1].fir - dp[0][n-1].sec$,即面对整个 piles,先手的最优得分和后手的最优得分之差。
|
||||
The answer we want is the difference between the final score of the first hand and the final score of the second hand,By thisdefinition, that is $dp[0][n-1].fir - dp[0][n-1].sec$ That is,facing the whole piles,the difference between the best score of the first hand and the best score of the second hand.
|
||||
|
||||
### 二、状态转移方程
|
||||
###2.state transition equation:
|
||||
|
||||
写状态转移方程很简单,首先要找到所有「状态」和每个状态可以做的「选择」,然后择优。
|
||||
It's easy to write the transition equation,The first step is to find all the states and the choices you can make for each state,and then pick the best.
|
||||
|
||||
根据前面对 dp 数组的定义,**状态显然有三个:开始的索引 i,结束的索引 j,当前轮到的人。**
|
||||
From the previous definition of the dp array,**there are obviously three states:the starting index i,the ending index j,and the person whose turn it is.**
|
||||
|
||||
```python
|
||||
dp[i][j][fir or sec]
|
||||
其中:
|
||||
range:
|
||||
0 <= i < piles.length
|
||||
i <= j < piles.length
|
||||
```
|
||||
|
||||
对于这个问题的每个状态,可以做的**选择有两个:选择最左边的那堆石头,或者选择最右边的那堆石头。** 我们可以这样穷举所有状态:
|
||||
For each state of the problem,**there are two choices you can make :Choose the pile to the left,or the pile to the right**.We can do all the states like this :
|
||||
|
||||
```python
|
||||
n = piles.length
|
||||
@ -69,57 +72,57 @@ for 0 <= i < n:
|
||||
|
||||
```
|
||||
|
||||
上面的伪码是动态规划的一个大致的框架,股票系列问题中也有类似的伪码。这道题的难点在于,两人是交替进行选择的,也就是说先手的选择会对后手有影响,这怎么表达出来呢?
|
||||
The pseudocode above is a rough framework for dynamic programming,and there is a similar pseudocode in the stock series problem.The difficulty of this problem is that two people choose alternately,that is to say,the choice of the first hand has effect on the second hand,how can we express this?
|
||||
|
||||
根据我们对 dp 数组的定义,很容易解决这个难点,**写出状态转移方程:**
|
||||
According to our definition of dp array,it is easy to solve this difficulty and **write down the state transition equation**:
|
||||
|
||||
```python
|
||||
dp[i][j].fir = max(piles[i] + dp[i+1][j].sec, piles[j] + dp[i][j-1].sec)
|
||||
dp[i][j].fir = max( 选择最左边的石头堆 , 选择最右边的石头堆 )
|
||||
# 解释:我作为先手,面对 piles[i...j] 时,有两种选择:
|
||||
# 要么我选择最左边的那一堆石头,然后面对 piles[i+1...j]
|
||||
# 但是此时轮到对方,相当于我变成了后手;
|
||||
# 要么我选择最右边的那一堆石头,然后面对 piles[i...j-1]
|
||||
# 但是此时轮到对方,相当于我变成了后手。
|
||||
dp[i][j].fir = max( Select the rock pile on the far left , Select the rock pile on the far right )
|
||||
# explanation:I,as a first hand,faced piles[i...j],I had two choices:
|
||||
# If I choose the pile of rocks on the far left,and I will face piles[i+1...j]
|
||||
# But when it came to the other side,I became the back hand.
|
||||
# If I choose the pile of rocks on the far right,and I will face piles[i...j-1]
|
||||
# But when it came to the other side,I became the back hand.
|
||||
|
||||
if 先手选择左边:
|
||||
if the first hand select the left:
|
||||
dp[i][j].sec = dp[i+1][j].fir
|
||||
if 先手选择右边:
|
||||
if the first hand select the right:
|
||||
dp[i][j].sec = dp[i][j-1].fir
|
||||
# 解释:我作为后手,要等先手先选择,有两种情况:
|
||||
# 如果先手选择了最左边那堆,给我剩下了 piles[i+1...j]
|
||||
# 此时轮到我,我变成了先手;
|
||||
# 如果先手选择了最右边那堆,给我剩下了 piles[i...j-1]
|
||||
# 此时轮到我,我变成了先手。
|
||||
# explanation:I,as a back hand ,have to wait for the first hand to choose,There are two condition:
|
||||
# If the first hand choose the pile of rocks on the far left,I will face piles[i+1...j]
|
||||
# then it's my turn, and i become the first hand.
|
||||
# If the first hand choose the pile of rocks on the far right,I will face piles[i...j-1]
|
||||
# then it's my turn, and i become the first hand.
|
||||
```
|
||||
|
||||
根据 dp 数组的定义,我们也可以找出 **base case**,也就是最简单的情况:
|
||||
According to the definition of the dp array, we can also find the **base case**,which is the simplest case:
|
||||
|
||||
```python
|
||||
dp[i][j].fir = piles[i]
|
||||
dp[i][j].sec = 0
|
||||
其中 0 <= i == j < n
|
||||
# 解释:i 和 j 相等就是说面前只有一堆石头 piles[i]
|
||||
# 那么显然先手的得分为 piles[i]
|
||||
# 后手没有石头拿了,得分为 0
|
||||
range: 0 <= i == j < n
|
||||
# explanation:i==j which means just a bunch of rocks piles[i] in the front of us
|
||||
# So obviously the first hand can get piles[i],
|
||||
# there are no stones int the back,so his score is 0
|
||||
```
|
||||
|
||||

|
||||
|
||||
这里需要注意一点,我们发现 base case 是斜着的,而且我们推算 dp[i][j] 时需要用到 dp[i+1][j] 和 dp[i][j-1]:
|
||||
One thing to note here is that we found that the base case is tilted in the table,and we need dp[i+1][j] and dp[i][j-1] to compute dp[i][j]:
|
||||
|
||||

|
||||
|
||||
所以说算法不能简单的一行一行遍历 dp 数组,**而要斜着遍历数组:**
|
||||
So the algorithm can not simply traverse the dp array row by row,but **traverse the array diagonally**.
|
||||
|
||||

|
||||
|
||||
说实话,斜着遍历二维数组说起来容易,你还真不一定能想出来怎么实现,不信你思考一下?这么巧妙的状态转移方程都列出来了,要是不会写代码实现,那真的很尴尬了。
|
||||
To be honest,traversing a two-dimensional array diagonally is easier said than done.
|
||||
|
||||
|
||||
### 三、代码实现
|
||||
### 3.code implementation
|
||||
|
||||
如何实现这个 fir 和 sec 元组呢,你可以用 python,自带元组类型;或者使用 C++ 的 pair 容器;或者用一个三维数组 `dp[n][n][2]`,最后一个维度就相当于元组;或者我们自己写一个 Pair 类:
|
||||
How do you implement this fir and sec tuple?You can either use python,with its own tuple type,or use the c++pair container,or use a three-dimensional array,the last dimension being the tuple,or we can write a pair class ourselves.
|
||||
|
||||
```java
|
||||
class Pair {
|
||||
@ -131,30 +134,30 @@ class Pair {
|
||||
}
|
||||
```
|
||||
|
||||
然后直接把我们的状态转移方程翻译成代码即可,可以注意一下斜着遍历数组的技巧:
|
||||
Then we can directly translate our state transition equation into code,and we can pay attention to technique of traversing through array diagonally:
|
||||
|
||||
```java
|
||||
/* 返回游戏最后先手和后手的得分之差 */
|
||||
/* Returns the difference between the last first hand and last hand */
|
||||
int stoneGame(int[] piles) {
|
||||
int n = piles.length;
|
||||
// 初始化 dp 数组
|
||||
//Initializes the dp array
|
||||
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
|
||||
// base case
|
||||
for (int i = 0; i < n; i++) {
|
||||
dp[i][i].fir = piles[i];
|
||||
dp[i][i].sec = 0;
|
||||
}
|
||||
// 斜着遍历数组
|
||||
// traverse the array diagonally
|
||||
for (int l = 2; l <= n; l++) {
|
||||
for (int i = 0; i <= n - l; i++) {
|
||||
int j = l + i - 1;
|
||||
// 先手选择最左边或最右边的分数
|
||||
// The first hand select the left- or right-most pile.
|
||||
int left = piles[i] + dp[i+1][j].sec;
|
||||
int right = piles[j] + dp[i][j-1].sec;
|
||||
// 套用状态转移方程
|
||||
// Refer to the state transition equation.
|
||||
if (left > right) {
|
||||
dp[i][j].fir = left;
|
||||
dp[i][j].sec = dp[i+1][j].fir;
|
||||
@ -169,20 +172,22 @@ int stoneGame(int[] piles) {
|
||||
}
|
||||
```
|
||||
|
||||
动态规划解法,如果没有状态转移方程指导,绝对是一头雾水,但是根据前面的详细解释,读者应该可以清晰理解这一大段代码的含义。
|
||||
Dynamic programming ,the most important is to understand the state transition equation,based on the previous detailed explanation,the reader should be able to clearly understand the meaning of this large piece of code.
|
||||
|
||||
而且,注意到计算 `dp[i][j]` 只依赖其左边和下边的元素,所以说肯定有优化空间,转换成一维 dp,想象一下把二维平面压扁,也就是投影到一维。但是,一维 dp 比较复杂,可解释性很差,大家就不必浪费这个时间去理解了。
|
||||
And notice that the calculation of 'dp[i][j]' only depends on the left and the bottom elements,so there must be room for optimization, for one-dimensional dp,But one-dimensional dp is a little bit more complicated,it's less interpretable,so you don't have to waste time trying to understand it.
|
||||
|
||||
### 四、最后总结
|
||||
### 4.summary:
|
||||
|
||||
本文给出了解决博弈问题的动态规划解法。博弈问题的前提一般都是在两个聪明人之间进行,编程描述这种游戏的一般方法是二维 dp 数组,数组中通过元组分别表示两人的最优决策。
|
||||
This paper presents a dynamic programming method to solve the game problem. The premise of game problems is usually between two smart people. The common way to describe such games is a one-dimensional array of dp, in which tuples represent the optimal decision of two people.
|
||||
|
||||
之所以这样设计,是因为先手在做出选择之后,就成了后手,后手在对方做完选择后,就变成了先手。这种角色转换使得我们可以重用之前的结果,典型的动态规划标志。
|
||||
The reason for this design is that when the first hand makes a choice, it becomes the second hand, and when the second hand makes a choice, it becomes the first hand. This role reversal allows us to reuse the previous results, typical dynamic programming flags.
|
||||
|
||||
读到这里的朋友应该能理解算法解决博弈问题的套路了。学习算法,一定要注重算法的模板框架,而不是一些看起来牛逼的思路,也不要奢求上来就写一个最优的解法。不要舍不得多用空间,不要过早尝试优化,不要惧怕多维数组。dp 数组就是存储信息避免重复计算的,随便用,直到咱满意为止。
|
||||
|
||||
希望本文对你有帮助。
|
||||
Those of you who have read this should understand how algorithms solve game problems. Learning algorithms, must pay attention to the template framework of the algorithm,rather than some seemingly awesome ideas, do not bend to write an optimal solution.Don't be afraid to use more space,don't try optimization too early, and don't be afraid of multidimensional arrays.A dp array is a way to store information and avoid double counting.
|
||||
|
||||
**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**:
|
||||
|
||||
I hope this article has been helpful.
|
||||
|
||||
**Focus on making the algorithm clear!Welcome to my public account labuladong,See more articles on learning to program**:
|
||||
|
||||

|
||||
|
Reference in New Issue
Block a user