/**
* Leetcode_877_StoneGame_Medium
* Alex and Lee play a game with piles of stones. There are an even number of piles
* arranged in a row, and each pile has a positive integer number of stones piles[i].
* The objective of the game is to end with the most stones. The total number of stones is odd,
* so there are no ties.
* Alex and Lee take turns, with Alex starting first. Each turn, a player takes
* the entire pile of stones from either the beginning or the end of the row.
* This continues until there are no more piles left, at which point the person with the most stones wins.
* Assuming Alex and Lee play optimally, return True if and only if Alex wins the game.
*
* Example 1:
* Input: [5,3,4,5]
* Output: true
* Explanation:
* Alex starts first, and can only take the first 5 or the last 5.
* Say he takes the first 5, so that the row becomes [3, 4, 5].
* If Lee takes 3, then the board is [4, 5], and Alex takes 5 to win with 10 points.
* If Lee takes the last 5, then the board is [3, 4], and Alex takes 4 to win with 9 points.
* This demonstrated that taking the first 5 was a winning move for Alex, so we return true.
* Note:
* 2 <= piles.length <= 500
* piles.length is even.
* 1 <= piles[i] <= 500
* sum(piles) is odd.
* 规则分析(重要):
* ①两个人选择都是采用最佳方法选择,因此,差别只有谁先开始之分,那么最佳选择的算法是一致的;
* ②数组piles的长度可为偶数,两人选择的次数相同;
* ③每个人只有两种选择,要么是选择left要么是选择right;
* ④上一人选择left,下一人就需要从[left+1,right]中选择;
* 上一人选择right,下一人就需要从[left,right-1]中选择。
* ⑤最终谁选择数之和大,就赢;正好是两者交替选择,因此,采用作差法即可。
* 作差法:减减等于加。A-(B-C) = A-B+C;A-(B-(C-D))=A-B+C-D;
*
* 方法0:直接返回true即可。
* 两人都是以最佳方式选择,且堆数为偶数,总和为奇数,因此谁先选择谁赢。
* 方法1:递归方法。利用Min-Max递归解法,返回最大的相对差值。
* 时间复杂度为O(N^2),无法通过。
* 方法2:利用动态规划解决。dp[][]
* dp[start][end]表示当前首选之人从start到end数组中最佳选择情况下,
* 选择数的和与第二个人选择数之和的差值。
* 如Alex首选,Lee后选:dp[0][len-1]表示Alex(首选之人)选择所有数之和比Lee大的值;
* 则dp[1][len-1]表示在Lee从1-(len-1)数组中选择所有数之和比Alex的差值;
* 实际利用的是dp[][]的右上三角;按照对角线遍历,利用perLen来进行遍历填值。
* 首先最长对角线初始化为piles值;然后递推公式:
* dp[start][end] = Math.max(piles[start] - dp[start + 1][end], piles[end] - dp[start][end - 1]);
* 时间复杂度:O(N^2);
* 空间复杂度:O(N).
* 方法3:利用动态规划解决,降低空间复杂度。dp[N]
* 从递推公式dp[start][end] = Math.max(piles[start] - dp[start + 1][end], piles[end] - dp[start][end - 1])
* 看出dp[start][end]只与相邻左边元素和下边元素有关,即上一个对角线上的两个元素,
* 每次perLen循环就是从旧的对角线更新到新的对角线,且对角线长度逐渐降低,因此元素覆盖即可。
*/
public boolean stoneGame1(int[] piles) {
return !(piles == null || piles.length == 0);
}
/**
* Leetcode提交超时不通过Time Limit Exceeded
* 时间复杂度:O(2^N),太大了
* 空间复杂度:O(N);递归深度为N
*/
public boolean stoneGame_Recursive(int[] piles) {
if (piles == null || piles.length == 0) return false;
return diff(piles, 0, piles.length - 1) > 0;
}
/***
* 由于两人都是最佳选择:所以抽象一个方法,从start到end时选择相对差值最大的。
*
* 数组从start到end最佳选择下返回的最大差值
* 递归终止条件:start==end返回本身即可;
* 否则,对应两种选择:1、自己选择start,然后减去对手从start+1到end的最佳选择最大差值;
* 2、自己选择end,然后减去对手从start到end-1的最佳选择的最大差值。
* 两种选择中取max,即自身选择从start到end的最大差值。
*/
public int diff(int[] piles, int left, int right) {
if (left < 0 || right >= piles.length || left > right) return 0;//边界条件判断
if (left == right) return piles[left];//递归终止条件
return Math.max(piles[left] - diff(piles, left + 1, right),
piles[right] - diff(piles, left, right - 1));
}
/**
* 由于递归存在大量重复问题,利用dp[left][right]解决重复问题.
* dp[left][right]表示从left到right中开始选择的最大的差值。
* dp[left][right] = Math.max(piles[left] - dp[left + 1][right],piles[right] - dp[left][right - 1]);
* 时间复杂度:O(N^2)
* 空间复杂度:O(N^2);仅仅利用矩阵dp[][]的右上三角矩阵。
*/
public boolean stoneGame_DP1(int[] piles) {
if (piles == null || piles.length == 0) return false;
int len = piles.length;
int[][] dp = new int[len][len];
//初始化--副对角线
for (int i = 0; i < len; i++) dp[i][i] = piles[i];
int left, right;
for (int perLen = 2; perLen <= len; perLen++) {
for (left = 0; left <= len - perLen; left++) {
right = left + perLen - 1;
dp[left][right] = Math.max(
piles[left] - dp[left + 1][right],
piles[right] - dp[left][right - 1]);
}
}
return dp[0][len - 1] > 0;//大于0,获胜
}
/**
* 时间复杂度:O(N^2)
* 空间复杂度:O(N);仅仅利用矩阵dp[][]的副对角线。
* dp[left] = Math.max(piles[left] - dp[left + 1], piles[right] - dp[left]);
* 这里的dp[left+1]相当于二维的dp[left+1][right];
* dp[left]相当于是dp[left][right-1];
* 之所以可以将维度,是因为可以利用长度perLen控制。
*/
public boolean stoneGame(int[] piles) {//最佳
if (piles == null || piles.length == 0) return false;
int[] dp = Arrays.copyOf(piles, piles.length);//相当于dp[][]的反对角线
int left, right;
for (int perLen = 2; perLen <= piles.length; perLen++) {
for (left = 0; left <= piles.length - perLen; left++) {
right = left + perLen - 1;
dp[left] = Math.max(piles[left] - dp[left + 1], piles[right] - dp[left]);
//等号右侧的dp代表的是上一次更新的dp值,即长度为perLen对应的差值。
}
}
return dp[0] > 0;
}
/********************Leetcode_1140_StoneGame_II_Medium*********************/
/**
* Alex and Lee continue their games with piles of stones.
* There are a number of piles arranged in a row, and each pile has
* a positive integer number of stones piles[i].
* The objective of the game is to end with the most stones.
*
* Alex and Lee take turns, with Alex starting first. Initially, M = 1.
*
* On each player's turn, that player can take all the stones
* in the first X remaining piles, where 1 <= X <= 2M. Then, we set M = max(M, X).
*
* The game continues until all the stones have been taken.
*
* Assuming Alex and Lee play optimally,
* return the maximum number of stones Alex can get.
*
* Example 1:
*
* Input: piles = [2,7,9,4,4]
* Output: 10
* Explanation: If Alex takes one pile at the beginning,
* Lee takes two piles, then Alex takes 2 piles again.
* Alex can get 2 + 4 + 4 = 10 piles in total.
* If Alex takes two piles at the beginning, then Lee
* can take all three piles left. In this case, Alex get
* 2 + 7 = 9 piles in total. So we return 10 since it's larger.
* Constraints:
*
* 1 <= piles.length <= 100
* 1 <= piles[i] <= 10 ^ 4
* 题目描述:
* 亚历克斯和李继续他们的石子游戏。许多堆石子 排成一行,每堆都有正整数颗石子 piles[i]。
* 游戏以谁手中的石子最多来决出胜负。
*
* 亚历克斯和李轮流进行,亚历克斯先开始。最初,M = 1。
*
* 在每个玩家的回合中,该玩家可以拿走剩下的 前 X 堆的 所有 石子,其中 1 <= X <= 2M。
* 然后,令 M = max(M, X)。
*
* 游戏一直持续到所有石子都被拿走。
*
* 假设亚历克斯和李都发挥出最佳水平,返回亚历克斯可以得到的最大数量的石头
* 样例
* 输入:piles = [2,7,9,4,4]
* 输出:10
* 解释:
* 如果亚历克斯在开始时拿走一堆石子,李拿走两堆,接着亚历克斯也拿走两堆。
* 在这种情况下,亚历克斯可以拿到 2 + 4 + 4 = 10 颗石子。
*
* 如果亚历克斯在开始时拿走两堆石子,那么李就可以拿走剩下全部三堆石子。
* 在这种情况下,亚历克斯可以拿到 2 + 7 = 9 颗石子。
*
* 所以我们返回更大的 10。
* 限制:
* 1 <= piles.length <= 100
* 1 <= piles[i] <= 10 ^ 4
*
*
* (动态规划) O(N^3)
* f(i,M) 表示当前位于第 i 位置M值时,先手能取到的最大的石子数量。
* 我们需要从后往前转移,因为最后的状态我们都是可以直接得到的。维护后缀和 sum。
* 有效位置的下标从 0 到 n−1,M 的下标从 1 到 n。
* 初始化:对于所有的 1≤M≤n,f(n,M)=0。这是边界情况。
* 转移时,我们需要枚举这一次先手取多少个石子 k,满足 1≤k≤2∗j,且 i+k≤n(i+k=n就是后边的石子全部取完),
* 我们转移 f(i,j)=max(f(i,j),sum(i)−f(i+k,max(j,k)))。
* 这里用 sum(i)sum(i) 减的原因是,此时我们交换了先后手,被转移的先手 以当前状态的角度看是后手,
* 所以需要从总和里去除。
* 最后答案就是 f(0,1),表示当前在第 0 个位置,且 M 为 1 时先手能取到的最大值。
* 时间复杂度: 状态数为 O(n^2) 个,转移需要 O(n)的时间,故时间复杂度为 O(n^3)。
* 空间复杂度:需要 O(n^2) 的数组记录状态,故空间复杂度为 O(n^2)。
*/
public int stoneGameII(int[] piles) {
if (piles == null || piles.length == 0) return 0;
if (piles.length == 1) return piles[0];
int len = piles.length;
int[] sum = new int[len + 1];
for (int i = len - 1; i >= 0; i--) sum[i] += sum[i + 1] + piles[i];//sum[i]是从i到len-1之和
int[][] dp = new int[len + 1][len + 1];//创建dp[][]数组;dp[len][*]=0很重要,也算是初始化了
for (int i = len - 1; i >= 0; i--)//i范围是0-len-1
for (int M = 1; M <= len; M++) {//M范围1-len
//X只需要遍历1-2*M即可,且当i+X==len时,说明后面一次即可全部获取,
//这时dp[i][M]=sum[i];下面的表达式满足,因为dp[len][*]=0;
for (int X = 1; X <= 2 * M && i + X <= len; X++) {
dp[i][M] = Math.max(dp[i][M], sum[i] - dp[i + X][Math.max(M, X)]);
//当i+X==0时,相当于dp[i][M]=Math.max(dp[i][M],sum[i])=sum[i];一次全收了肯定最大
}
}
return dp[0][1];
}