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 数组,并利用判断条件决定是否可以返回 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:
`
输入: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,j 从 0 开始),则有:
初始条件为:当 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) 被重复计算了两次,随着问题规模的扩大,重复计算的位置会越来越多。
为了保证每个位置只计算一次,我们可以将计算的结果存入备忘录 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、9 和 16 都是完全平方数,而 3 和 11 不是。
- 示例 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 看成是 j 和 i-j 的和,其中 j 是比 i 小的完全平方数,则有:
在动态规划的过程中,为了记录比当前整数小的完全平方数,我们使用一个列表 list 来存放当前已经出现的所有的完全平方数。
如果一个数是完全平方数,则将 dp 设为 1,同时将该数加入 list。如果一个数不是完全平方数,则遍历 list(j 表示 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;
}