每周 Leetcode: 1217、55、62、121、279

396 阅读6分钟

2021 05 27

本周的五道 Leetcode 分别是:

  • 1217 玩筹码
  • 55 跳跃游戏
  • 62 不同路径
  • 121 买卖股票的最佳时机
  • 279 完全平方数

1217 玩筹码

题目描述

数轴上放置了一些筹码,每个筹码的 位置 存在数组 chips 当中。

你可以对 任何筹码 执行下面两种操作之一(不限操作次数,0 次也可以):

  • 将第 i 个筹码向左或者右移动 2 个单位,代价为 0。
  • 将第 i 个筹码向左或者右移动 1 个单位,代价为 1。 最开始的时候,同一位置上也可能放着两个或者更多的筹码。

返回将所有筹码移动到同一位置(任意位置)上所需要的最小代价。

  • 示例 1:
输入:chips = [1,2,3]
输出:1
解释:第二个筹码移动到位置三的代价是 1,第一个筹码移动到位置三的代价是 0,总代价为 1。
  • 示例 2:
输入:chips = [2,2,2,3,3]
输出:2
解释:第四和第五个筹码移动到位置二的代价都是 1,所以最小总代价为 2。

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/mi…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一 贪心思想

贪心的基本思想是尽量选择局部最优解,也就是每次尽量按照代价 0 移动筹码,如果没有代价为 0 的移动,再考虑代价为 1

由题意,我们可以知道 chips 存储的是筹码的位置,

  • 奇数位置到奇数位置 或者 偶数位置到偶数位置 的代价为 0
  • 奇数位置到偶数位置 或者 偶数位置到奇数位置 的代价为 1

所以先将所有的奇数位筹码移动到其中的某一个位置,再将所有偶数位筹码移动到其中的某一个位置,此时代价为 0

由于最后的两个位置一定是奇数位和偶数位,所以移动的代价都是 1,选择筹码数更小的哪个位置进行移动,移动的代价就是 筹码数 x 1

以上移动奇数位和偶数位的过程也可以表示为 计算奇数(偶数)个数

// 贪心思想
class Solution {
    public int minCostToMoveChips(int[] position) {
        int even = 0;   // 偶数位置个数
        int odd = 0;    // 奇数位置个数
        for(int i = 0; i < position.length; ++i){
            if(position[i] % 2 == 0){
                even++;
            }else {
                odd++;
            }
        }
        return Math.min(even,odd);
    }
}

55 跳跃游戏

题目描述

给定一个非负整数数组 nums ,你最初位于数组的 第一个下标

数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标。

 - 示例 1:

输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
  • 示例 2:
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/ju…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一 动态规划

如果当前位于位置 i,能够到达最后一个下标的条件为 i 到最后的距离 小于等于 nums[i],即 nums[i] + i <= nums.length,这就可以作为判断是否返回 true 的条件。

dp[i] 表示到达位置 i 后,下一跳能够到达的最远的位置,则有

dp[i]=max(dp[i1],nums[i]+i)dp[i] = max(dp[i-1],nums[i] + i)

不断地计算 dp 数组,并利用判断条件决定是否可以返回 true

还需要注意的是,如果当前位置 i 无法到达即 dp[i-1] < i ,则直接返 false

如果数组只有一个元素,最后一个下标就是初始位置,永远可以到达。

public boolean canJump(int[] nums) {
    if(nums.length == 1){
        return true;
    }
    int[] dp = new int[nums.length];
    dp[0] = nums[0];
    for(int i = 1; i < dp.length; i++){
        if(dp[i-1] < i){
            return false;
        }
        dp[i] = Math.max(dp[i-1],nums[i] + i);
        if(dp[i] >= nums.length - 1){
            return true;
        }
    }
    return false;
}

解法二 贪心思想

贪心法与动态规划思想很接近,贪心法总是考虑当前位置 i 能够到达的最大位置,并不断地更新最大位置。判断条件和更新的方法都和动态规划一样。

public boolean canJump(int[] nums) {
    int maxDistance = 0;
    for(int i = 0; i < nums.length; ++i){ 
        if(i > maxDistance){
            return false;
        }
        maxDistance = Math.max(maxDistance,nums[i] + i);
        if(maxDistance >= nums.length-1){
            return true;
        }
    }
    return false;
}

62 不同路径

题目描述

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

  • 示例 1: image.png`
输入:m = 3, n = 7
输出:28
  • 示例 2:
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/un…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一 动态规划

对于网格中的任意位置 (i,j),到达该位置只可能从该位置的 上侧网格左侧网格 经过向下一步和向右一步 到达。

dp[i][j] 表示到达 (i,j) 的不同路径数(其中,i,j0 开始),则有:

dp[i][j]=dp[i1][j]+dp[i][j1]dp[i][j] = dp[i-1][j] + dp[i][j-1]

初始条件为:当 i == 0 || j ==0 即到达的位置与 Start 位置处于同行同列时,不同路径只有唯一的一条。

// 方法一 动态规划
public int uniquePaths(int m, int n) {
    int[][] dp = new int[m][n];
    for(int i = 0; i < m; i++){
        for(int j = 0; j < n; j++){
            if(i == 0 || j == 0){
                dp[i][j] = 1;
            }else {
                dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }
    }
    return dp[m-1][n-1];
}   

解法二 递归 + 备忘录

以上过程也可以使用递归函数来完成,假设 uniquePaths(i,j) 表示到达位置 (i,j) 的不同路径数,则在递归函数中如下调用即可,同时递归的 终止条件i ==1 || j == 1

int uniquePaths(int i, int j) {
    if(i == 1 || j == 1){
        return 1;
    }
    return uniquePaths(i-1,j) + unqiuePaths(i,j-1);
}

但是,递归会不断的重复计算,比如 m == 3, n == 2 时,递归树如下所示,其中绿色节点 (2,2) 被重复计算了两次,随着问题规模的扩大,重复计算的位置会越来越多。

image.png

为了保证每个位置只计算一次,我们可以将计算的结果存入备忘录 int[][] result。计算 (i,j) 时,先从备忘录中获取,如果备忘录的结果是 0,再递归调用函数,并将结果存入备忘录中。

// 方法二 递归 + 备忘录
public int uniquePaths(int m, int n) {
    // 初始化备忘录,默认都是 0,表示都没有计算过
    int[][] results = new int[m][n];
    return uniquePaths(results, m, n);
} 

// 递归函数
public int uniquePaths(int[][] results, int m, int n){
    // 终止条件
    if(m == 1 || n == 1){
        results[m-1][n-1] = 1;
        return 1;
    }
    // 先从备忘录中获取结果
    int upResult = results[m-2][n-1];
    int leftResult = results[m-1][n-2];
    // 如果没有,则递归计算,并将结果存入备忘录
    if(upResult == 0){
        upResult = uniquePaths(results,m-1,n);
        results[m-2][n-1] = upResult;
    }
    if(leftResult == 0){
        leftResult = uniquePaths(results,m,n-1);
        results[m-1][n-2] = leftResult;
    }
    // 返回结果
    return upResult + leftResult;
}

121 买卖股票的最佳时机

题目描述

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0

  • 示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
  • 示例 2:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/be…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一 动态规划

dp[i] 表示第 i 天卖出股票所能获得的最大利润,那么第 1 天卖出的最大利润为 0

假设当前要计算 dp[i],下面详细分析各种情况:

  • (1) 如果前一天卖出的最大利润 dp[i-1] = 0,说明前 i-1 天的价格是 非升序的
    • 如果 prices[i] > prices[i-1],那么当天卖出的最大利润就是 prices[i] - prices[i-1]
    • 否则,最大利润为 0
  • (2) 如果前一天卖出的最大利润 dp[i-1] > 0,那么前 i-1 天中价格最低那天的价格为 prices[i-1] - dp[i-1],这时将
    • 如果 prices[i] > prices[i-1],那么利润比前一天卖出的利润更高,dp[i] = dp[i-1] + prices[i] - prices[i-1]
    • 否则,那么就将当天的价格和 prices[i-1] - dp[i-1] 比较
      • prices[i] < prices[i-1] - dp[i-1],说明前 i-1 天的价格都比当天高,利润为 0
      • 否则,说明当天卖出有利润,dp[i] = prices[i] - (prices[i-1] - dp[i-1])

将上面的情况总结一下,令 diff = prices[i] - prices[i-1],temp = diff + dp[i-1],有

if(dp[i-1] == 0){
    dp[i] = diff > 0 ? diff : 0;
}else{
    dp[i] = diff > 0 ? temp : (temp < 0 ? 0 : temp);
}

代码实现如下:

// 方法一 动态规划
public int maxProfit(int[] prices) {
    int[] dp = new int[prices.length];
    int max = dp[0];
    for(int i = 1; i < prices.length; i++){
        int diff = prices[i] - prices[i-1];
        int temp = diff + dp[i-1];
        if(dp[i-1] == 0){
            dp[i] = diff > 0 ? diff : 0;
        }else{
            dp[i] = diff > 0 ? temp : (temp < 0 ? 0 : temp);
        }
        max = Math.max(dp[i],max);
    }
    return max;
}

解法二 最低价格买入

一种简单的思路是,只要能在 价格最低的那一天 买入股票,之后只需要看看当天的价格能获取的最大利润,并不断更新这个最大利润。

遍历价格数组,记录目前的最低价格,并在之后的每一天更新最大利润。

注意的是,如果某一天的价格比最低价格还要低,就更新最低价格,同时,不需要计算利润。

// 方法二 最低价格买入
public int maxProfit(int[] prices) {
    int minPrice = Integer.MAX_VALUE;
    int maxProfit = 0;
    for(int i = 0; i < prices.length; i++){
        if(prices[i] < minPrice){
            minPrice = prices[i];
        }else if(maxProfit < prices[i] - minPrice){
            maxProfit = prices[i] - minPrice;
        }
    }
    return maxProfit;
}

279 完全平方数

题目描述

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

给你一个整数 n ,返回和为 n 的完全平方数的 最少数量

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、916 都是完全平方数,而 311 不是。

  • 示例 1:
输入:n = 12
输出:3 
解释:12 = 4 + 4 + 4
  • 示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/pe…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一 动态规划

dp[i] 表示整数和为 i 的完全平方数的最少数量。

如果 i 本身就是完全平方数,那么 dp[i] = 1

如果 i 不是完全平方数,我们可以将 i 看成是 ji-j 的和,其中 j 是比 i 小的完全平方数,则有:

dp[i]=minj<ij是完全平方数(dp[j]+dp[ij]dp[i] = min_{j<i且j是完全平方数} (dp[j] + dp[i-j]

在动态规划的过程中,为了记录比当前整数小的完全平方数,我们使用一个列表 list 来存放当前已经出现的所有的完全平方数。

如果一个数是完全平方数,则将 dp 设为 1,同时将该数加入 list。如果一个数不是完全平方数,则遍历 listj 表示 list 中的元素),使用上面的公式计算 dp[i]

最终返回 dp[n] 即可。

// 动态规划
public int numSquares(int n) {
    int[] dp = new int[n+1];
    // list 记录当前已经遍历过的完全平方数
    List<Integer> list = new ArrayList<>();
    dp[1] = 1;
    list.add(1);
    for(int i = 1; i <= n; ++i){
        // 如果是完全平方数,结果为 1,加入 list
        if(isSquare(i)){
            dp[i] = 1;
            list.add(i);
            continue;
        }
        // 如果不是完全平方数
        // 为了比较最小值,将 dp[i] 初始设为一个较大值,也可以在 new 的时候一起设置
        dp[i] = n+1;
        for(int j : list){
            dp[i] = Math.min(dp[j] + dp[i-j],dp[i]);
        }
    }
    return dp[n];
}

// 判断 num 是否为完全平方数
public boolean isSquare(int num){
    int i = (int)Math.sqrt(num);
    return i*i == num;
}