0877-石子游戏

Raphael Liu Lv10

Alice 和 Bob 用几堆石子在做游戏。一共有偶数堆石子, 排成一行 ;每堆都有 整数颗石子,数目为 piles[i]

游戏以谁手中的石子最多来决出胜负。石子的 总数奇数 ,所以没有平局。

Alice 和 Bob 轮流进行, Alice 先开始 。 每回合,玩家从行的 开始结束 处取走整堆石头。
这种情况一直持续到没有更多的石子堆为止,此时手中 石子最多 的玩家 获胜

假设 Alice 和 Bob 都发挥出最佳水平,当 Alice 赢得比赛时返回 true ,当 Bob 赢得比赛时返回 false

示例 1:

**输入:** piles = [5,3,4,5]
**输出:** true
**解释:**
Alice 先开始,只能拿前 5 颗或后 5 颗石子 。
假设他取了前 5 颗,这一行就变成了 [3,4,5] 。
如果 Bob 拿走前 3 颗,那么剩下的是 [4,5],Alice 拿走后 5 颗赢得 10 分。
如果 Bob 拿走后 5 颗,那么剩下的是 [3,4],Alice 拿走后 4 颗赢得 9 分。
这表明,取前 5 颗石子对 Alice 来说是一个胜利的举动,所以返回 true 。

示例 2:

**输入:** piles = [3,7,2,3]
**输出:** true

提示:

  • 2 <= piles.length <= 500
  • piles.length偶数
  • 1 <= piles[i] <= 500
  • sum(piles[i])奇数

前言

这道题是「486. 预测赢家 」的特例。和第 486 题相比,这道题增加了两个限制条件:

  • 数组的长度是偶数;

  • 数组的元素之和是奇数,所以没有平局。

这道题可以使用第 486 题的解法进行求解。如果充分利用上述两个限制条件,还可以使用数学方法进行求解。

方法一:动态规划

由于每次只能从行的开始或结束处取走整堆石子,因此可以保证剩下的石子堆一定是连续的。

如果只剩下一堆石子,则当前玩家只能取走这堆石子。如果剩下多堆石子,则当前玩家可以选择从行的开始或结束处取走整堆石子,然后轮到另一个玩家在剩下的石子堆中取走石子。这是一个递归的过程,因此可以使用递归进行求解,递归过程中维护一个总数,表示 Alice 和 Bob 的石子数量之差,当游戏结束时,如果总数大于 0,则 Alice 赢得比赛,否则 Bob 赢得比赛。

如果有 n 堆石子,则递归的时间复杂度为 O(2^n),无法通过所有的测试用例。递归的时间复杂度高的原因是存在大量重复计算。由于存在重复子问题,因此可以使用动态规划降低时间复杂度。

定义二维数组 dp,其行数和列数都等于石子的堆数,dp}[i][j] 表示当剩下的石子堆为下标 i 到下标 j 时,即在下标范围 [i, j] 中,当前玩家与另一个玩家的石子数量之差的最大值,注意当前玩家不一定是先手 Alice。

只有当 i \le j 时,剩下的石子堆才有意义,因此当 i>j 时,dp}[i][j]=0。

当 i=j 时,只剩下一堆石子,当前玩家只能取走这堆石子,因此对于所有 0 \le i < \textit{nums}.\text{length,都有 dp}[i][i]=\textit{piles}[i]。

当 i<j 时,当前玩家可以选择取走 piles}[i] 或 piles}[j],然后轮到另一个玩家在剩下的石子堆中取走石子。在两种方案中,当前玩家会选择最优的方案,使得自己的石子数量最大化。因此可以得到如下状态转移方程:

\textit{dp}[i][j]=\max(\textit{piles}[i] - \textit{dp}[i+1][j], \textit{piles}[j] - \textit{dp}[i][j-1])

最后判断 dp}[0][\textit{piles}.\text{length}-1] 的值,如果大于 0,则 Alice 的石子数量大于 Bob 的石子数量,因此 Alice 赢得比赛,否则 Bob 赢得比赛。

<fig1,fig2,fig3,fig4,fig5,fig6,fig7,fig8,fig9,fig10,fig11,fig12>

[sol11-Java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public boolean stoneGame(int[] piles) {
int length = piles.length;
int[][] dp = new int[length][length];
for (int i = 0; i < length; i++) {
dp[i][i] = piles[i];
}
for (int i = length - 2; i >= 0; i--) {
for (int j = i + 1; j < length; j++) {
dp[i][j] = Math.max(piles[i] - dp[i + 1][j], piles[j] - dp[i][j - 1]);
}
}
return dp[0][length - 1] > 0;
}
}
[sol11-C#]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Solution {
public bool StoneGame(int[] piles) {
int length = piles.Length;
int[,] dp = new int[length, length];
for (int i = 0; i < length; i++) {
dp[i, i] = piles[i];
}
for (int i = length - 2; i >= 0; i--) {
for (int j = i + 1; j < length; j++) {
dp[i, j] = Math.Max(piles[i] - dp[i + 1, j], piles[j] - dp[i, j - 1]);
}
}
return dp[0, length - 1] > 0;
}
}
[sol11-C]
1
2
3
4
5
6
7
8
9
10
11
12
bool stoneGame(int* piles, int pilesSize) {
int dp[pilesSize][pilesSize];
for (int i = 0; i < pilesSize; i++) {
dp[i][i] = piles[i];
}
for (int i = pilesSize - 2; i >= 0; i--) {
for (int j = i + 1; j < pilesSize; j++) {
dp[i][j] = fmax(piles[i] - dp[i + 1][j], piles[j] - dp[i][j - 1]);
}
}
return dp[0][pilesSize - 1] > 0;
}
[sol11-C++]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
bool stoneGame(vector<int>& piles) {
int length = piles.size();
auto dp = vector<vector<int>>(length, vector<int>(length));
for (int i = 0; i < length; i++) {
dp[i][i] = piles[i];
}
for (int i = length - 2; i >= 0; i--) {
for (int j = i + 1; j < length; j++) {
dp[i][j] = max(piles[i] - dp[i + 1][j], piles[j] - dp[i][j - 1]);
}
}
return dp[0][length - 1] > 0;
}
};
[sol11-Golang]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func stoneGame(piles []int) bool {
length := len(piles)
dp := make([][]int, length)
for i := 0; i < length; i++ {
dp[i] = make([]int, length)
dp[i][i] = piles[i]
}
for i := length - 2; i >= 0; i-- {
for j := i + 1; j < length; j++ {
dp[i][j] = max(piles[i] - dp[i+1][j], piles[j] - dp[i][j-1])
}
}
return dp[0][length-1] > 0
}

func max(x, y int) int {
if x > y {
return x
}
return y
}
[sol11-Python3]
1
2
3
4
5
6
7
8
9
10
class Solution:
def stoneGame(self, piles: List[int]) -> bool:
length = len(piles)
dp = [[0] * length for _ in range(length)]
for i, pile in enumerate(piles):
dp[i][i] = pile
for i in range(length - 2, -1, -1):
for j in range(i + 1, length):
dp[i][j] = max(piles[i] - dp[i + 1][j], piles[j] - dp[i][j - 1])
return dp[0][length - 1] > 0
[sol11-JavaScript]
1
2
3
4
5
6
7
8
9
10
11
12
13
var stoneGame = function(piles) {
const length = piles.length;
const dp = new Array(length).fill(0).map(() => new Array(length).fill(0));
for (let i = 0; i < length; i++) {
dp[i][i] = piles[i];
}
for (let i = length - 2; i >= 0; i--) {
for (let j = i + 1; j < length; j++) {
dp[i][j] = Math.max(piles[i] - dp[i + 1][j], piles[j] - dp[i][j - 1]);
}
}
return dp[0][length - 1] > 0;
};

上述代码中使用了二维数组 dp。分析状态转移方程可以看到,dp}[i][j] 的值只和 dp}[i + 1][j] 与 dp}[i][j - 1] 有关,即在计算 dp 的第 i 行的值时,只需要使用到 dp 的第 i 行和第 i+1 行的值,因此可以使用一维数组代替二维数组,对空间进行优化。

[sol12-Java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public boolean stoneGame(int[] piles) {
int length = piles.length;
int[] dp = new int[length];
for (int i = 0; i < length; i++) {
dp[i] = piles[i];
}
for (int i = length - 2; i >= 0; i--) {
for (int j = i + 1; j < length; j++) {
dp[j] = Math.max(piles[i] - dp[j], piles[j] - dp[j - 1]);
}
}
return dp[length - 1] > 0;
}
}
[sol12-C#]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Solution {
public bool StoneGame(int[] piles) {
int length = piles.Length;
int[] dp = new int[length];
for (int i = 0; i < length; i++) {
dp[i] = piles[i];
}
for (int i = length - 2; i >= 0; i--) {
for (int j = i + 1; j < length; j++) {
dp[j] = Math.Max(piles[i] - dp[j], piles[j] - dp[j - 1]);
}
}
return dp[length - 1] > 0;
}
}
[sol12-C]
1
2
3
4
5
6
7
8
9
10
11
12
bool stoneGame(int* piles, int pilesSize) {
int dp[pilesSize];
for (int i = 0; i < pilesSize; i++) {
dp[i] = piles[i];
}
for (int i = pilesSize - 2; i >= 0; i--) {
for (int j = i + 1; j < pilesSize; j++) {
dp[j] = fmax(piles[i] - dp[j], piles[j] - dp[j - 1]);
}
}
return dp[pilesSize - 1] > 0;
}
[sol12-C++]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
bool stoneGame(vector<int>& piles) {
int length = piles.size();
auto dp = vector<int>(length);
for (int i = 0; i < length; i++) {
dp[i] = piles[i];
}
for (int i = length - 2; i >= 0; i--) {
for (int j = i + 1; j < length; j++) {
dp[j] = max(piles[i] - dp[j], piles[j] - dp[j - 1]);
}
}
return dp[length - 1] > 0;
}
};
[sol12-Golang]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func stoneGame(piles []int) bool {
length := len(piles)
dp := make([]int, length)
for i := 0; i < length; i++ {
dp[i] = piles[i]
}
for i := length - 2; i >= 0; i-- {
for j := i + 1; j < length; j++ {
dp[j] = max(piles[i] - dp[j], piles[j] - dp[j - 1])
}
}
return dp[length - 1] > 0
}

func max(x, y int) int {
if x > y {
return x
}
return y
}
[sol12-Python3]
1
2
3
4
5
6
7
8
9
10
class Solution:
def stoneGame(self, piles: List[int]) -> bool:
length = len(piles)
dp = [0] * length
for i, pile in enumerate(piles):
dp[i] = pile
for i in range(length - 2, -1, -1):
for j in range(i + 1, length):
dp[j] = max(piles[i] - dp[j], piles[j] - dp[j - 1])
return dp[length - 1] > 0
[sol12-JavaScript]
1
2
3
4
5
6
7
8
9
10
11
12
13
var stoneGame = function(piles) {
const length = piles.length;
const dp = new Array(length).fill(0);
for (let i = 0; i < length; i++) {
dp[i] = piles[i];
}
for (let i = length - 2; i >= 0; i--) {
for (let j = i + 1; j < length; j++) {
dp[j] = Math.max(piles[i] - dp[j], piles[j] - dp[j - 1]);
}
}
return dp[length - 1] > 0;
};

复杂度分析

  • 时间复杂度:O(n^2),其中 n 是数组的长度。需要计算每个子数组对应的 dp 的值,共有 n(n+1)}{2 个子数组。

  • 空间复杂度:O(n),其中 n 是数组的长度。空间复杂度取决于额外创建的数组 dp,如果不优化空间,则空间复杂度是 O(n^2),使用一维数组优化之后空间复杂度可以降至 O(n)。

方法二:数学

假设有 n 堆石子,n 是偶数,则每堆石子的下标从 0 到 n-1。根据下标将 n 堆石子分成两组,每组有 n}{2 堆石子,下标为偶数的石子堆属于第一组,下标为奇数的石子堆属于第二组。

初始时,行的开始处的石子堆位于下标 0,属于第一组,行的结束处的石子堆位于下标 n-1,属于第二组,因此作为先手的 Alice 可以自由选择取走第一组的一堆石子或者第二组的一堆石子。如果 Alice 取走第一组的一堆石子,则剩下的部分在行的开始处和结束处的石子堆都属于第二组,因此 Bob 只能取走第二组的一堆石子。如果 Alice 取走第二组的一堆石子,则剩下的部分在行的开始处和结束处的石子堆都属于第一组,因此 Bob 只能取走第一组的一堆石子。无论 Bob 取走的是开始处还是结束处的一堆石子,剩下的部分在行的开始处和结束处的石子堆一定是属于不同组的,因此轮到 Alice 取走石子时,Alice 又可以在两组石子之间进行自由选择。

根据上述分析可知,作为先手的 Alice 可以在第一次取走石子时就决定取走哪一组的石子,并全程取走同一组的石子。既然如此,Alice 是否有必胜策略?

答案是肯定的。将石子分成两组之后,可以计算出每一组的石子数量,同时知道哪一组的石子数量更多。Alice 只要选择取走数量更多的一组石子即可。因此,Alice 总是可以赢得比赛。

fig1

[sol2-Java]
1
2
3
4
5
class Solution {
public boolean stoneGame(int[] piles) {
return true;
}
}
[sol2-C#]
1
2
3
4
5
public class Solution {
public bool StoneGame(int[] piles) {
return true;
}
}
[sol2-C]
1
2
3
bool stoneGame(int* piles, int pilesSize) {
return true;
}
[sol2-C++]
1
2
3
4
5
6
class Solution {
public:
bool stoneGame(vector<int>& piles) {
return true;
}
};
[sol2-Golang]
1
2
3
func stoneGame(piles []int) bool {
return true
}
[sol2-Python3]
1
2
3
class Solution:
def stoneGame(self, piles: List[int]) -> bool:
return True
[sol2-JavaScript]
1
2
3
var stoneGame = function(piles) {
return true;
};

复杂度分析

  • 时间复杂度:O(1)。

  • 空间复杂度:O(1)。

 Comments
On this page
0877-石子游戏