前言
数论问题
正文
问题来源
本问题来自leetcode上的486题。本以为已经了解到动态规划的思想了,没想到还是推导不出这道题的动态规划方程!
问题描述
给定一个表示分数的非负整数数组。 玩家1从数组任意一端拿取一个分数,随后玩家2继续从剩余数组任意一端拿取分数,然后玩家1拿,……。每次一个玩家只能拿取一个分数,分数被拿取之后不再可取。直到没有剩余分数可取时游戏结束。最终获得分数总和最多的玩家获胜。
给定一个表示分数的数组,预测玩家1是否会成为赢家。你可以假设每个玩家的玩法都会使他的分数最大化。
示例 1:
输入: [1, 5, 2]
输出: False
解释: 一开始,玩家1可以从1和2中进行选择。
如果他选择2(或者1),那么玩家2可以从1(或者2)和5中进行选择。如果玩家2选择了5,那么玩家1则只剩下1(或者2)可选。
所以,玩家1的最终分数为 1 + 2 = 3,而玩家2为 5。
因此,玩家1永远不会成为赢家,返回 False。
示例 2:
输入: [1, 5, 233, 7]
输出: True
解释: 玩家1一开始选择1。然后玩家2必须从5和7中进行选择。无论玩家2选择了哪个,玩家1都可以选择233。
最终,玩家1(234分)比玩家2(12分)获得更多的分数,所以返回True,表示玩家1可以成为赢家。
注意:
- 1 <= 给定的数组长度 <= 20.
- 数组里所有分数都为非负数且不会大于10000000。
- 如果最终两个玩家的分数相等,那么玩家1仍为赢家。
分析:
两人依次拿,如果Player1赢,则Player1拿的>Player2拿的。我们把Player1拿的视为”+”,把Player2拿的视为”-“,如果最后结果大于等于0则Player1赢。
因此对于递归来说,beg ~ end的结果为max(nums[beg] - partition(beg + 1, end), nums[end] - partition(beg, end + 1));对于非递归来说DP[beg][end]表示即为beg ~ end所取的值的大小(最终与零比较)。
递归java代码
public class Solution {
public boolean PredictTheWinner(int[] nums) {
return helper(nums, 0, nums.length-1) >= 0;
}
public int helper(int[] nums, int start, int end) {
if(start == end) return nums[start];
else return Math.max(nums[start]-helper(nums, start+1,end), nums[end]-helper(nums, start,end-1));
}
}
总结:
-
该问题没有直接比较一个选手所拿元素的和值,而是把问题转换为两个选手所拿元素的差值。这一点很巧妙,是关键的一步。
-
找出递推表达式:max(nums[beg] - partition(beg + 1, end), nums[end] - partition(beg, end + 1))
-
通过递推表达式构造递归算法是比较简单的。但是要构造一个非递归的算法难度较大。对于非递归算法,首先在dp中赋初始值,这是我们解题的第一步。在这个问题中,我们使用一个二位的数组dp来表示nums数组中任意开始和结束位置两人结果的差值。 C代码如下所示:
#define max(x,y) ((x)>(y)?(x) : (y))
int PredictTheWinner(int* nums, int numsSize) {
int** dp = NULL;
int i,j;
int ret;
if(numsSize == 0) return 0;
dp = (int**)malloc(sizeof(int*)*numsSize);
for(i=0;i<numsSize;i++)
{
dp[i] = (int*)malloc(sizeof(int)*numsSize);
}
for(i = 0; i < numsSize; i++) {
dp[i][i] = nums[i];
}
for(i = numsSize-2; i >= 0; i--) {
for(j = i+1; j < numsSize; j++) {
dp[i][j] = max(nums[i]-dp[i+1][j], nums[j]-dp[i][j-1]);
//printf("%d %d %d %d\n",i,j,nums[i],nums[j]);
}
}
ret = dp[0][numsSize-1];
for(i=0;i<numsSize;i++)
{
free(dp[i]);
}
free(dp);
return ret >= 0;
}
初始的时候,我们仅仅知道对角线上的值。dp[i][i] = nums[i]. 这一点很好理解。
接下来既然是求任意的开始和结束,对于二维数组,那肯定是一个双层的循环。通过dp中已知的元素和动态规划的递推表达式,我们就可以构造出我们的需要的结果。非递归的方式是从小问题到大问题的过程。
函数入参数:{1,5,233,7} 4
行列 | 0 | 1 | 2 | 3 |
---|---|---|---|---|
0 | 1,I | 4,4 | 229,5 | 222,6 |
1 | X | 5,I | 228,2 | -221,3 |
2 | X | X | 233,I | 226,1 |
3 | X | X | X | 7,I |
X代表计算全过程不参与计算,I代表对角线初始化
A,B前面A数值代表执行后该dp数组存的数值(计算的结果),B代表执行顺序
结语
还是需要多训练一下动态规划的思维。