Predict the Winner题解
今天做了一道非常有代表性的leetcode题目Predict the Winner。题目的大意为两个玩家在一起玩游戏。给定一序列的得分币。每次每个玩家只能从序列的头或者尾部获取得分币直到没有得分币剩下。然后比较两人的得分,得分高的人获胜,问先手的玩家是否能赢。原题链接: 486-predict-the-winner
这道题看起来挺复杂的。每一次选择,都会影响到下一次的结果。如果仅仅想得到局部的最大值,肯定无法得到最终的最大值。但是仔细分析一下,每一次只有两种选择,头或者尾。假设我们从(x:xs)先选择了头x,那么接下来的玩家就从xs中获取最大值。选择尾部元素也是同样的结果。所以看起来有点像一棵二叉树。这样看来可以使用递归的方式遍历数来获取结果。但是目前还有个问题没有解决。就是比较大小的问题。如何才能够在遍历树结构的时候将两个玩家的得分值进行比较呢?换个思路,并不需要得到最后的总分才能够比较。只需要在每次遍历时,用先手玩家的得分减去后手玩家的得分的值的正负来确定大小。所以直观的递归思路的话代码应该是下面这个样子:
def PredictTheWinner(nums: Array[Int]): Boolean = {
def winner(nums:Array[Int],left:Int,right:Int,turn:Int):Int = {
if(left == right)
return turn * nums(left)
turn * math.max(turn * (turn * nums(left) + winner(nums,left + 1,right,-turn)),
turn * (turn * nums(right) + winner(nums,left,right - 1,-turn)))
}
winner(nums,0,nums.length - 1,1) >= 0
}
很容易分析出时间复杂度为 O(2n) O ( 2 n ) 。
其实这段代码已经可以ac本题。但是作为一个算法学习者来说,这种时间复杂度是无法忍受的。于是我们可以采用数组作为缓存来优化时间复杂度。优化后的代码看起来就好多了:
def PredictTheWinner1(nums: Array[Int]): Boolean = {
def winner(nums:Array[Int],left:Int,right:Int,dp:Array[Array[Int]]):Int = {
if(left == right)
return nums(left)
if(dp(left)(right) != 0)
return dp(left)(right)
dp(left)(right) = math.max( nums(left) - winner(nums,left + 1,right,dp),
nums(right) - winner(nums,left,right - 1,dp))
dp(left)(right)
}
val dp = Array.ofDim[Int](nums.length,nums.length)
winner(nums,0,nums.length - 1,dp) >= 0
}
这里并没有跟之前一样采用标识turn来控制得分的比较。而是利用每位玩家轮流获取分数的规则来直接相减比较大小。优化后的代码时间复杂度降低到 O(n2) O ( n 2 ) 。
上面的结果已经很好了,但是我们知道,非尾递归由于会开辟新的调用栈,所以仍然不是一个最好的选择。考虑到虽然玩家当前的操作的确会影响到接下来选择的结果,但是已经完成的选择却不会受到当前操作的影响。所以我们如果我们知道剩余得分序列先手玩家的得分值与后手玩家的得分值的差值,那么每一次当前选择都能够确定下来而不影响到接下来的选择。动态规划的状态转移方程为:
i i 和分别代表剩余得分序列的头和尾。根据上面的状态转移方程,我们可以写出对应的 bottom−upde b o t t o m − u p d e 动态规划版本:
def PredictTheWinner2(nums: Array[Int]): Boolean = {
val n = nums.length
val dp = Array.ofDim[Int](n + 1,n)
for(i <- n to (0,-1)){
for(j <- i + 1 until n){
dp(i)(j) = math.max(nums(i) - dp(i + 1)(j),nums(j) - dp(i)(j - 1))
}
}
dp(0)(n - 1) >= 0
}
时间复杂度仍然是
O(n2)
O
(
n
2
)
。
当然,如果题目对于空间复杂度有更高的要求,可以将二维数组换成一维数组以节约空间。