【数据结构与算法】之深入解析“石子游戏IX”的求解思路与算法示例

一、题目描述

  • Alice 和 Bob 再次设计了一款新的石子游戏,现有一行 n 个石子,每个石子都有一个关联的数字表示它的价值,给你一个整数数组 stones ,其中 stones[i] 是第 i 个石子的价值。
  • Alice 和 Bob 轮流进行自己的回合,Alice 先手,每一回合,玩家需要从 stones 中移除任一石子。
    • 如果玩家移除石子后,导致所有已移除石子的价值总和可以被 3 整除,那么该玩家就输掉游戏;
    • 如果不满足上一条,且移除后没有任何剩余的石子,那么 Bob 将会直接获胜(即便是在 Alice 的回合)。
  • 假设两位玩家均采用最佳决策,如果 Alice 获胜,返回 true;如果 Bob 获胜,返回 false。
  • 示例 1:
输入:stones = [2,1]
输出:true
解释:游戏进行如下:
- 回合 1Alice 可以移除任意一个石子。
- 回合 2Bob 移除剩下的石子。 
已移除的石子的值总和为 1 + 2 = 3 且可以被 3 整除。因此,Bob 输,Alice 获胜。
  • 示例 2:
输入:stones = [2]
输出:false
解释:Alice 会移除唯一一个石子,已移除石子的值总和为 2 。 
由于所有石子都已移除,且值总和无法被 3 整除,Bob 获胜。
  • 示例 3:
输入:stones = [5,1,2,4,3]
输出:false
解释:Bob 总会获胜。其中一种可能的游戏进行方式如下:
- 回合 1Alice 可以移除值为 1 的第 2 个石子。已移除石子值总和为 1- 回合 2Bob 可以移除值为 3 的第 5 个石子。已移除石子值总和为 = 1 + 3 = 4- 回合 3Alices 可以移除值为 4 的第 4 个石子。已移除石子值总和为 = 1 + 3 + 4 = 8- 回合 4Bob 可以移除值为 2 的第 3 个石子。已移除石子值总和为 = 1 + 3 + 4 + 2 = 10.
- 回合 5Alice 可以移除值为 5 的第 1 个石子。已移除石子值总和为 = 1 + 3 + 4 + 2 + 5 = 15.
Alice 输掉游戏,因为已移除石子值总和(15)可以被 3 整除,Bob 获胜。

二、求解算法:构造移除序列,计算回合数

  • 由于只关心总和能否被 3 整除,我们可以将 stones 按照模 3 的结果分为 3 组,即 0、1 和 2。
  • 根据题意,第一回合不能移除 0,否则直接输掉游戏,因此第一回合只能移除 1 或者 2,可以枚举这两种情况,如果其中一种可以让 Alice 获胜就返回 true,否则返回 false。
  • 下面以第一回合移除 1 来说明。在不考虑移除 0 的前提下,后面的移除由于要满足总和不能被 3 整除,因此移除的石子是固定的,整体构成一个 112121212… 循环的序列。
  • 对于 0,由于移除之后不会改变总和模 3 的结果,因此不会改变后续 1 和 2 的移除顺序,所以可以在序列的任意非开头位置插入 0。
  • 两人为了不让自己输掉,必然会按照上述序列进行,直到没有石子,或某一方只能移除导致总和被 3 整除的石子时分出胜负。因此需要求出让总和不能被 3 整除的最大的回合数,这相当于 112121212… 序列的最长长度,加上 0 的个数。
  • 若该回合数为奇数,且还有剩余石子,那么下一回合要轮到 Bob 移除石子,且他只能移除一枚让总和被 3 整除的石子,于是 Alice 获胜;否则 Bob 获胜。
  • 对于第一回合移除 2 的情况,同样会构成一个 221212121… 循环的序列,做法同上。
  • 算法示例如下:
func check(c [3]int) bool {
	if c[1] == 0 {
		return false
	}
	c[1]-- // 开头为 1
	turn := 1 + min(c[1], c[2])*2 + c[0] // 计算回合数
	if c[1] > c[2] { // 序列末尾可以再加个 1
		turn++
		c[1]--
	}
	return turn%2 == 1 && c[1] != c[2] // 回合数为奇数,且还有剩余石子
}

func stoneGameIX(stones []int) bool {
	c := [3]int{}
	for _, v := range stones {
		c[v%3]++
	}
	return check(c) || check([3]int{c[0], c[2], c[1]}) // 枚举第一回合移除的是 1 还是 2
}

func min(a, b int) int {
	if a > b {
		return b
	}
	return a
}
  • 注意到对于回合数,只需考虑其奇偶性,因此可以去掉恒为偶数的 min(c[1], c[2])*2,然后按照 c[0] 的奇偶性分类讨论:
    • 若 c[0] 为偶数,要使回合数为奇数,c[1] > c[2] 必须不成立,可以选择 c[1] 和 c[2] 中的较小值当作第一回合移除的石子,这样做除了让 c[1] > c[2] 不成立外,由于 c[1]-- 的缘故,还可以使 c[1] != c[2] 成立。因此在 c[0] 为偶数的情况下,需要满足 min(c[1],c[2])>0,即 c[1]>0 且 c[2]>0 时 Alice 才可以获胜;
    • 若 c[0] 为奇数,要使回合数为奇数,c[1] > c[2] 必须成立。在执行了两次 c[1]-- 后,由于要满足最后的 c[1] != c[2],相当于在初始时满足 c[1] - 2 > c[2]。因此在 c[0] 为奇数的情况下,需要满足 c[1]−2>c[2] 或 c[2]−2>c[1] 时 Alice 才可以获胜。
func stoneGameIX(stones []int) bool {
	c := [3]int{}
	for _, v := range stones {
		c[v%3]++
	}
	if c[0]%2 == 0 {
		return c[1] > 0 && c[2] > 0 // min(c[1], c[2]) > 0
	}
	return c[1]-2 > c[2] || c[2]-2 > c[1]
}

三、博客之星

  • 今年是我第一次参加博客之星,需要各位大佬的支持,麻烦百忙之中,抽出一点宝贵的时间,给我投一下票:https://bbs.csdn.net/topics/603955258 给我一个五星(列表会一一全部回复),不胜感激!
本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>