【C++前缀和 动态规划 博弈】1140. 石子游戏 II|2034

本文涉及的基础知识点

C++算法:前缀和、前缀乘积、前缀异或的原理、源码及测试用例 包括课程视频
C++动态规划
博弈:往往后续状态已知,前续状态未知

LeetCode1140. 石子游戏 II

Alice 和 Bob 继续他们的石子游戏。许多堆石子 排成一行,每堆都有正整数颗石子 piles[i]。游戏以谁手中的石子最多来决出胜负。
Alice 和 Bob 轮流进行,Alice 先开始。最初,M = 1。
在每个玩家的回合中,该玩家可以拿走剩下的 前 X 堆的所有石子,其中 1 <= X <= 2M。然后,令 M = max(M, X)。
游戏一直持续到所有石子都被拿走。
假设 Alice 和 Bob 都发挥出最佳水平,返回 Alice 可以得到的最大数量的石头。
示例 1:
输入:piles = [2,7,9,4,4]
输出:10
解释:如果一开始 Alice 取了一堆,Bob 取了两堆,然后 Alice 再取两堆。Alice 可以得到 2 + 4 + 4 = 10 堆。
如果 Alice 一开始拿走了两堆,那么 Bob 可以拿走剩下的三堆。在这种情况下,Alice 得到 2 + 7 = 9 堆。返回 10,因为它更大。
示例 2:
输入:piles = [1,2,3,4,5,100]
输出:104
提示:
1 <= piles.length <= 100
1 <= piles[i] <= 104

错误解法:动态规划+前缀和

N = piles ,2M大于n没意义,所以本题假定其小于等于n。

动态规划的状态表示

m为当前可以移除的石子数量,i为已经移除石头堆数。
dp0[i][m] 表示,当前行动者是Alice,Alice最小分数。
dp1[i][m] 表示 当前行动者是Bom,Alice的最大分数。
空间复杂度:O(nM)

动态规划的转移方程

枚举前置状态dp0[i][m],令 Alice移除了x块石头,1 <= x ,i+x <= N
m1 = max(m,2x)
MaxSelf(dp1[i+x][m1],preSum[i+x]- dp0[i][m])
单个状态时间复杂度O(n),总时间复杂度:O(nMn)
枚举前置状态dp1,类似。

动态规划的填表顺序

i,m,x 从小到大

动态规划的初始值

			const int iNotMay = 1e8;
			vector<vector<int>> dp0(N + 1, vector<int>(N + 1, iNotMay));
			vector<vector<int>> dp1(N + 1, vector<int>(N + 1,- iNotMay));
			dp0[0][min(2,N)] = 0;
			正负iNotMay 表示	非法。转移时忽略非法数据。

动态规划的返回值

max(dp0.back().back(),preSum.back()-dp1.back().back())

错误原因

{1,2,3} Alice 选择{1} Bob 选择{2} Alice选择 {3}
实际上:Bob 选择 {2,3}

错误代码

class Solution {
		public:
			int stoneGameII(vector<int>& piles) {
				vector<int> preSum(1);
				for (const auto& n : piles) {
					preSum.emplace_back(n + preSum.back());
				}
				const int N = piles.size();
				const int iNotMay = 1e8;
				vector<vector<int>> dp0(N + 1, vector<int>(N + 1, iNotMay));
				vector<vector<int>> dp1(N + 1, vector<int>(N + 1,- iNotMay));
				dp0[0][min(2,N)] = 0;
				for (int i = 0; i <= N; i++) {
					for (int m = 1; m <= N; m++) {
						if (dp0[i][m] < iNotMay) {
							for (int x = 1; (x <= m )&&(i + x <= N); x++) {
								int m1 = max(m, 2 * x);
								m1 = min(m1, N);
								dp1[i + x][m1] = max(dp1[i + x][m1], preSum[i + x] - preSum[i] + dp0[i][m]);
							}
						}
						if (dp1[i][m] >= 0) {
							for (int x = 1; (x <= m) && (i + x <= N); x++) {
								int m1 = max(m, 2 * x);
								m1 = min(m1, N);
								dp0[i + x][m1] = min(dp0[i + x][m1], dp1[i][m]);
							}
						}
					}
				}
				int i1 = *std::max_element(dp1.back().begin(), dp1.back().end());		
				for (const auto& n : dp0.back()) {
					if (iNotMay == n) { continue; }
					i1 = max(i1, n);
				}
				return i1;
			}
		};

动态规划+前缀和

动态规划的状态表示

dp[i]][m] 当前行动者可以选择[1,m]堆石子,已经选择了i堆,当前行动者从剩余的石子中能获得的最大分数。

动态规划的转移方程

后续状态已知,前置状态未知。
枚举前置状态,枚举需求了x堆石头。 1 <= x <= m 且 i+x <= N
dp[i][m] = max(preSum[i+x]-dp[i+x][m1])

动态规划的填表顺序

i从N-1到0,m从1到N , x从1到m

动态规划的初始值

dp[N].assign(N,0)

动态规划的返回值

dp[0][min(2,N)]

代码

核心代码

class Solution {
		public:
			int stoneGameII(vector<int>& piles) {
				vector<int> preSum(1);
				for (const auto& n : piles) {
					preSum.emplace_back(n + preSum.back());
				}
				const int N = piles.size();
				vector<vector<int>> dp(N + 1, vector<int>(N + 1, 0));
				for (int i = N - 1; i >= 0; i--) {
					for (int m = 1; m <= N; m++) {
						int& cur = dp[i][m];
						for (int x = 1; (x <= m) && (i + x <= N); x++) {
							int m1 = max(2 * x, m);
							m1 = min(m1, N);
							cur = max(cur, preSum.back() - preSum[i] - dp[i + x][m1]);
						}

					}
				}			
				return dp[0][min(2,N)];
			}
		};

单元测试

	vector<int> piles;
		TEST_METHOD(TestMethod1)
		{
			piles = { 1};
			auto res = Solution().stoneGameII(piles);
			AssertEx(1, res);
		}
		TEST_METHOD(TestMethod2)
		{
			piles = { 1,2 };
			auto res = Solution().stoneGameII(piles);
			AssertEx(3, res);
		}
		TEST_METHOD(TestMethod3)
		{
			piles = { 1,2,3 };
			auto res = Solution().stoneGameII(piles);
			AssertEx(3, res);
		}
		TEST_METHOD(TestMethod4)
		{
			piles = { 1,2,3,4 };
			auto res = Solution().stoneGameII(piles);
			AssertEx(5, res);
		}
		TEST_METHOD(TestMethod11)
		{
			piles = { 2, 7, 9, 4, 4 };
			auto res = Solution().stoneGameII(piles);
			AssertEx(10, res);
		}
		TEST_METHOD(TestMethod12)
		{
			piles = { 1,2,3,4,5,100 };
			auto res = Solution().stoneGameII(piles);
			AssertEx(104, res);
		}

扩展阅读

我想对大家说的话
工作中遇到的问题,可以按类别查阅鄙人的算法文章,请点击《算法与数据汇总》。
学习算法:按章节学习《喜缺全书算法册》,大量的题目和测试用例,打包下载。重视操作
有效学习:明确的目标 及时的反馈 拉伸区(难度合适) 专注
闻缺陷则喜(喜缺)是一个美好的愿望,早发现问题,早修改问题,给老板节约钱。
子墨子言之:事无终始,无务多业。也就是我们常说的专业的人做专业的事。
如果程序是一条龙,那算法就是他的是睛
失败+反思=成功 成功+反思=成功

视频课程

先学简单的课程,请移步CSDN学院,听白银讲师(也就是鄙人)的讲解。
https://edu.csdn.net/course/detail/38771
如何你想快速形成战斗了,为老板分忧,请学习C#入职培训、C++入职培训等课程
https://edu.csdn.net/lecturer/6176

测试环境

操作系统:win7 开发环境: VS2019 C++17
或者 操作系统:win10 开发环境: VS2022 C++17
如无特殊说明,本算法用**C++**实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

闻缺陷则喜何志丹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值