【Leetcode】486. Predict the Winner

本文深入探讨了LeetCode上预测赢家问题的解决策略,通过分析游戏规则,提出了两种解决方案:记忆化搜索和动态规划,详细解释了算法原理,并对代码进行了逐行解读,最后讨论了算法的时间和空间复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目地址:

https://leetcode.com/problems/predict-the-winner/

给定一个非负整数数组。设计一个两人的游戏,比如甲乙两个人,甲可以从数组左右两端点取一个分数,然后乙接着拿,直到拿完为止。如果甲总分高于等于乙,就返回true,否则返回false。甲是先手方。

思路是这样的:首先如果总共只有一个数字,或有偶数个数字,那么先手必胜。证明如下:
只有一个数字时显然先手必胜;
如果有偶数个数字,我们把整个数组从左向右数第奇数个数都染成红色,第偶数个数都染成蓝色。如果红色数字的和大于等于蓝色数字的和,那么每轮先手都去取红色的数字,就可以必胜;反之每轮都取蓝色的数字就必胜。因为按照这种策略,先手可以保证自己取的数字都是一种颜色,而让对手都取另一种颜色。这样先手就可以稳操胜券。

接下来考虑一般情况:
如果只剩两个数字了,那么结论可以很轻易的得到。如果多于两个数字,那么就枚举先手方拿左右端这两种情况,这样就把大问题化为了更小规模的问题。但困难之处在于,仅仅知道小规模问题谁胜谁负,不足以知道大规模问题谁胜谁负,于是想到我们可以把谁胜谁负转化为分差。如果我们知道了 n n n规模的问题中,先手方比后手方的分差 s n s_n sn,那么对于规模 n + 1 n+1 n+1的问题,先手方比后手方的分差就是先手方拿一个分数,再减去规模为 n n n的问题中先手方和后手方的分差(此时先后手对调了)。最后只需要返回分差是否非负即可。

法1:记忆化搜索。代码如下:

public class Solution {
    public boolean PredictTheWinner(int[] nums) {
    	// 如果只有一个数字,或者有偶数个数字,返回true,先手必胜
        if (nums.length == 1 || (nums.length & 1) == 0) {
            return true;
        }
        // f[i][j]记录对于数组nums[i,...,j],先手方与后手方的分差
        // 做记忆化,防止重复搜索
        int[][] f = new int[nums.length][nums.length];
        return dfs(nums, 0, nums.length - 1, f) >= 0;
    }
    
    private int dfs(int[] nums, int i, int j, int[][] f) {
    	// 递归出口
        if (j - i == 1) {
            f[i][j] = Math.abs(nums[i] - nums[j]);
            return f[i][j];
        }
        // 如果f里有记忆,直接调取记忆;否则接着暴搜
        // score1是先手选了数组左端的情况下,后手与先手的分差
        int score1 = f[i + 1][j] != 0 ? f[i + 1][j] : dfs(nums, i + 1, j, f);
        // score2是先手选了数组右端的情况下,后手与先手的分差
        int score2 = f[i][j - 1] != 0 ? f[i][j - 1] : dfs(nums, i, j - 1, f);
        // 返回之前做一下记忆
        f[i][j] = Math.max(nums[i] - score1, nums[j] - score2);
        return f[i][j];
    }
}

时空复杂度 O ( n 2 ) O(n^2) O(n2)。时间复杂度的分析可以这么看,在暴搜那个二维矩阵的时候,每搜到叶子节点回溯的时候,一路上都会去做记忆化,所以这个二维矩阵每个entry最多被搜一次。

法2:动态规划。本质上和记忆化搜索是一样的。只需要注意更新顺序,一定要以已知来更新未知。代码如下:

public class Solution {
    public boolean PredictTheWinner(int[] nums) {
        if (nums.length == 1 || (nums.length & 1) == 0) {
            return true;
        }
        
        int[][] dp = new int[nums.length - 1][nums.length];
        for (int j = 1; j < nums.length; j++) {
            for (int i = j - 1; i >= 0; i--) {
                if (j - i == 1) {
                    dp[i][j] = Math.abs(nums[i] - nums[j]);
                } else {
                    dp[i][j] = Math.max(nums[i] - dp[i + 1][j], nums[j] - dp[i][j - 1]);
                }
            }
        }
        
        return dp[0][nums.length - 1] >= 0;
    }
}

时空复杂度一样。

接下来考虑优化空间复杂度,直观的想法是只用一维数组,重用数组覆盖掉以后不用的数值。我们想象某时刻我们在更新矩阵的第 i i i行第 j j j列,这个数字只依赖与第 i i i行第 j − 1 j-1 j1列,以及第 i + 1 i+1 i+1行第 j j j列的数,所以我们可以按行滚动更新,具体的意思是,我们初始化一维数组,代表某一行,然后新行就用这个旧的行来更新,在更新行的时候, j j j需要从左向右更新。在空间优化的时候,决定更新顺序的关键,是一定要搞清楚当前值是由更新后的值算出来的,还是更新前的值算出来的,只有这样才能保证答案正确。代码如下:

public class Solution {
    public boolean PredictTheWinner(int[] nums) {
        if (nums.length == 1 || (nums.length & 1) == 0) {
            return true;
        }   
            
        int[] dp = new int[nums.length];
        for (int i = nums.length - 2; i >= 0; i--) {
            for (int j = i + 1; j < nums.length; j++) {
            	dp[j] = Math.max(nums[i] - dp[j], nums[j] - dp[j - 1]);
            }
        }
        
        return dp[nums.length - 1] >= 0;
    }
}

时间复杂度一样,空间 O ( n ) O(n) O(n)

注意:
首先一个自然的问题是,可不可以不用int数组,而用布尔数组。答案是不可以,因为小规模问题的胜负不足以决定大规模问题的胜负。一个很简单的反例是, ( 1 , 3 , 1 ) (1,3,1) (1,3,1) ( 1 , 0 , 1 ) (1,0,1) (1,0,1),前者甲必败,后者甲必胜。如果布尔数组可以解决问题的话,当只剩两个数的时候,布尔数组都记录为真(因为两个数的时候甲必胜),那么在更新到三个数的时候,仅仅靠端点的数值和两个数时的胜败,无法推断出三个数的时候的胜败。比如上面两个反例,端点都是 1 1 1,但最后胜负却完全不同。所以必须用int数组引入分差来做。

第二个自然的问题是,能不能像数组长度为偶数一样,算奇数位和偶数位的数字和来判断。答案也是不可以。虽然长度为偶数时甲必胜,但上面的那个必胜策略并不能保证赢的分差最大。比如,如果甲取了第一个数,接下来算一下奇偶位的数之和的差的绝对值(已经取过的数不计入),这是乙用必胜策略打败甲时的分差。这个分差如果不够大,使得甲取的那个数减去这个分差仍然非负,就会得到甲胜的错误的结论。

具体的反例是: ( 0 , 0 , 7 , 6 , 5 , 6 , 1 ) (0,0,7,6,5,6,1) (0,0,7,6,5,6,1)。我们先来递推一下分差:

i↓ j→0123456
0007142-1
1007-1601
20071621
30006156
40000510
50000065
60000001

如果甲先取 0 0 0,那么乙的最优策略的分差是 1 1 1;如果甲先取 1 1 1,那么乙的最优策略的分差是 2 2 2。而如果算一下甲取 1 1 1后,乙的必胜策略的分差是 0 + 7 + 5 − 0 − 6 − 6 = 0 0+7+5-0-6-6=0 0+7+5066=0,并不是最优的 2 2 2,这就会得到甲胜的错误的结果。

所以如果数组长度为奇数的话,必须用动态规划来递推。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值