leetcode486预测赢家

leetcode279 Predict the Winner

Posted by BY on May 25, 2018

前言

数论问题

正文

问题来源

本问题来自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代表执行顺序

结语

还是需要多训练一下动态规划的思维。