Loading

LeetCode-Dynamic Programming-Easy 动态规划

1. 最大子序和(leetcode-53)

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 

示例: 
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

进阶: 
如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。 

Related Topics 数组 分治算法 动态规划 

1)动态规划

动态规划实现最大子序和

class Solution {
public int maxSubArray(int[] nums) {
    int maxv = nums[0];
    int pre = 0;

    for(int i : nums){
        pre = Math.max(pre+i, i);
        maxv = Math.max(maxv, pre);
    }

    return maxv;
}

}

2)分治算法

分治算法实现最大子序和

class Solution {
public int maxSubArray(int[] nums) {
    return maxSubArrayDivideWithBorder(nums, 0, nums.length-1);
}

private int maxSubArrayDivideWithBorder(int[] nums, int start, int end) {
    if (start == end) {
        // 只有一个元素,也就是递归的结束情况
        return nums[start];
    }

    // 计算中间值
    int center = (start + end) / 2;
    int leftMax = maxSubArrayDivideWithBorder(nums, start, center); // 计算左侧子序列最大值
    int rightMax = maxSubArrayDivideWithBorder(nums, center + 1, end); // 计算右侧子序列最大值

    // 下面计算横跨两个子序列的最大值

    // 计算包含左侧子序列最后一个元素的子序列最大值
    int leftCrossMax = Integer.MIN_VALUE; // 初始化一个值
    int leftCrossSum = 0;
    for (int i = center ; i >= start ; i --) {
        leftCrossSum += nums[i];
        leftCrossMax = Math.max(leftCrossSum, leftCrossMax);
    }

    // 计算包含右侧子序列最后一个元素的子序列最大值
    int rightCrossMax = nums[center+1];
    int rightCrossSum = 0;
    for (int i = center + 1; i <= end ; i ++) {
        rightCrossSum += nums[i];
        rightCrossMax = Math.max(rightCrossSum, rightCrossMax);
    }

    // 计算跨中心的子序列的最大值
    int crossMax = leftCrossMax + rightCrossMax;

    // 比较三者,返回最大值
    return Math.max(crossMax, Math.max(leftMax, rightMax));
}

}

2. 爬楼梯(leetcode-70)

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢? 

注意:给定 n 是一个正整数。 
示例 1: 
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1.  1 阶 + 1 阶
2.  2 阶 

示例 2: 
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1.  1 阶 + 1 阶 + 1 阶
2.  1 阶 + 2 阶
3.  2 阶 + 1 阶

Related Topics 动态规划 

1)动态规划

用 f(x) 表示爬到第 x 级台阶的方案数,考虑最后一步可能跨了一级台阶,也可能跨了两级台阶,所以

f(x)=f(x−1)+f(x−2)

爬到第 x 级台阶的方案数是爬到第 x - 1 级台阶的方案数和爬到第 x - 2 级台阶的方案数的和。

动态规划实现爬楼梯

class Solution {
public int climbStairs(int n) {
    int p = 0, q = 0, r = 1;
    for (int i = 1; i <= n; ++i) {
        p = q; 
        q = r; 
        r = p + q;
    }
    return r;
}

}

时间复杂度:循环执行 n 次,每次花费常数的时间代价,故渐进时间复杂度为 O(n)。
空间复杂度:这里只用了常数个变量作为辅助空间,故渐进空间复杂度为 O(1)。

2)矩阵快速幂

矩阵快速幂实现爬楼梯

public class Solution {

public int climbStairs(int n) {
int[][] q = {{1, 1}, {1, 0}};
int[][] res = pow(q, n);
return res[0][0];
}
public int[][] pow(int[][] a, int n) {
int[][] ret = {{1, 0}, {0, 1}};
while (n > 0) {
if ((n & 1) == 1) {
ret = multiply(ret, a);
}
n >>= 1;
a = multiply(a, a);
}
return ret;
}
public int[][] multiply(int[][] a, int[][] b) {
int[][] c = new int[2][2];
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j];
}
}
return c;
}
}

3)通项公式

通项公式实现爬楼梯

public class Solution {
public int climbStairs(int n) {
    double sqrt5 = Math.sqrt(5);
    double fibn = Math.pow((1 + sqrt5) / 2, n + 1) - Math.pow((1 - sqrt5) / 2, n + 1);
    return (int)(fibn / sqrt5);
}

}

3. 买卖股票的最佳时机(leetcode-121)

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),
设计一个算法来计算你所能获取的最大利润。 

注意:你不能在买入股票前卖出股票。 

示例 1: 
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

示例 2: 
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

Related Topics 数组 动态规划 

动态规划思路:遍历数组,求取当前i天卖出股票的最大收益,用prices[i]-(i天之前的最小值)。因此遍历时,记录当前最小值(0~i)与当前最大收益(prices[i]-(i天之前的最小值))。

动态规划实现买股票

class Solution {
public int maxProfit(int[] prices) {
    if(prices.length <2){
        return 0;
    }
    int currMin = Integer.MAX_VALUE;
    int maxv = 0;
    for(int i: prices){
        maxv = Math.max(maxv, i-currMin);
        currMin = Math.min(currMin, i);
    }
    return maxv;
}

}

4. 打家劫舍(leetcode-198)

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,
如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。 

示例 1: 
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。 

示例 2: 

输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。

提示: 
0 <= nums.length <= 100 
0 <= nums[i] <= 400 

Related Topics 动态规划 

1)动态规划

  1. 首先考虑最简单的情况。如果只有一间房屋,则偷窃该房屋,可以偷窃到最高总金额。如果只有两间房屋,则由于两间房屋相邻,不能同时偷窃,只能偷窃其中的一间房屋,因此选择其中金额较高的房屋进行偷窃,可以偷窃到最高总金额。

  2. 对于第 k~(k>2) 间房屋,有两个选项:

  • 偷窃第 k 间房屋,那么就不能偷窃第 k-1 间房屋,偷窃总金额为前 k-2 间房屋的最高总金额与第 k 间房屋的金额之和。

  • 不偷窃第 k 间房屋,偷窃总金额为前 k-1 间房屋的最高总金额。

在两个选项中选择偷窃总金额较大的选项,该选项对应的偷窃总金额即为前 k 间房屋能偷窃到的最高总金额。

  1. 用 dp[i] 表示前 i 间房屋能偷窃到的最高总金额,那么就有如下的状态转移方程:

dp[i]=max(dp[i−2]+nums[i],dp[i−1])

最终的答案即为 dp[n−1],其中 n 是数组的长度。

动态规划实现打家劫舍

class Solution {
public int rob(int[] nums) {
    if (nums == null || nums.length == 0) {
        return 0;
    }
    int length = nums.length;
    if (length == 1) {
        return nums[0];
    }
    int[] dp = new int[length];
    dp[0] = nums[0];
    dp[1] = Math.max(nums[0], nums[1]);
    for (int i = 2; i < length; i++) {
        dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
    }
    return dp[length - 1];
}

}

2)动态规划+滚动数组

解题思想同上,上述方法使用了数组存储结果。考虑到每间房屋的最高总金额只和该房屋的前两间房屋的最高总金额相关,因此可以使用滚动数组,在每个时刻只需要存储前两间房屋的最高总金额。

动态规划+滚动数组实现打家劫舍

class Solution {
public int rob(int[] nums) {
    if (nums == null || nums.length == 0) {
        return 0;
    }
    int length = nums.length;
    if (length == 1) {
        return nums[0];
    }
    int first = nums[0], second = Math.max(nums[0], nums[1]);
    for (int i = 2; i < length; i++) {
        int temp = second;
        second = Math.max(first + nums[i], second);
        first = temp;
    }
    return second;
}

}

时间复杂度:O(n),其中 n 是数组长度。只需要对数组遍历一次。

空间复杂度:O(1)。使用滚动数组,可以只存储前两间房屋的最高总金额,而不需要存储整个数组的结果,因此空间复杂度是 O(1)。

5. 粉刷房子 $ (leetcode-256)

6. 栅栏涂色 $ (leetcode-276)

7. 区域与检索 - 数组不可变(leetcode-303)

给定一个整数数组 nums,求出数组从索引 i 到 j (i ≤ j) 范围内元素的总和,包含 i, j 两点。 

示例: 
给定 nums = [-2, 0, 3, -5, 2, -1],求和函数为 sumRange()
sumRange(0, 2) -> 1
sumRange(2, 5) -> -1
sumRange(0, 5) -> -3 

说明: 
你可以假设数组不可变。 
会多次调用 sumRange 方法。 

Related Topics 动态规划

在构造函数中,预先计算从数字 0 到每个索引 i 的累积和。

区域与检索实现一

class NumArray {
private int[] dp;
private int length;
public NumArray(int[] nums) {
    length = nums.length;
    dp = new int[length];
    int t = 0;
    for(int i=0; i<length; i++){
        t = t+nums[i];
        dp[i] = t;
    }
}

public int sumRange(int i, int j) {
    if(i<0 || j>length || i>j)
        return 0;
    if(i==0)
        return dp[j];
    else
        return dp[j]-dp[i-1];
}

}

插入一个虚拟 0 作为 sum 数组中的第一个元素。这个技巧可以避免在 sumrange 函数中进行额外的条件检查。

区域与检索实现二

class NumArray {
private int[] sum;

public NumArray(int[] nums) {
    sum = new int[nums.length + 1];
    for (int i = 0; i < nums.length; i++) {
        sum[i + 1] = sum[i] + nums[i];
    }
}

public int sumRange(int i, int j) {
    return sum[j + 1] - sum[i];
}

}

8. 判断子序列(leetcode-392)

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。 
你可以认为 s 和 t 中仅包含英文小写字母。字符串 t 可能会很长(长度 ~= 500,000),而 s 是个短字符串(长度 <=100)。 

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。 

示例 1: 
s = "abc", t = "ahbgdc" 
返回 true. 

示例 2: 
s = "axc", t = "ahbgdc" 
返回 false. 

后续挑战 🍓🍓🍓: 
如果有大量输入的 S,称作S1, S2, ... , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码? 

Related Topics 贪心算法 二分查找 动态规划 

不用动态规划求解比较简单

展示代码

class Solution {
public boolean isSubsequence(String s, String t) {
    int i = 0;
    for (char ch : s.toCharArray()) {
        while (i < t.length() && t.charAt(i) != ch) i++;
        i++;
    }
    return i <= t.length();
}

}

后续挑战:
匹配一串字符需要 O(n),n 为 t 的长度。如果有大量输入的 S,称作 S1 , S2 , ... , Sk 其中 k >= 10 亿,你需要依次检查它们是否为 T 的子序列,这时候处理每一个子串都需要扫描一遍 T 是很费时的。

在这种情况下,需要在匹配前对 T 做预处理,利用一个二维数组记录每个位置的下一个要匹配的字符的位置,这里的字符是'a' ~ 'z',所以这个数组的大小是 dp[n][26],n 为 T 的长度。那么每处理一个子串只需要扫描一遍 Si 即可,因为在数组的帮助下对 T 是“跳跃”扫描的。

如t = adef,而s=df时,dp表如下

展示代码

class Solution {
public boolean isSubsequence(String s, String t) {
    // 预处理
    t = " " + t; // 开头加一个空字符作为匹配入口
    int n = t.length();
    int[][] dp = new int[n][26]; // 记录每个位置的下一个ch的位置
    for (char ch = 0; ch < 26; ch++) {
        int p = -1;
        for (int i = n - 1; i >= 0; i--) { // 从后往前记录dp
            dp[i][ch] = p;
            if (t.charAt(i) == ch + 'a') p = i;
        }
    }
    // 匹配
    int i = 0;
    for (char ch : s.toCharArray()) { // 跳跃遍历
        i = dp[i][ch - 'a'];
        if (i == -1) return false;
    }
    return true;
}

}

9. 使用最小花费爬楼梯(leetcode-746)

数组的每个索引作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](索引从0开始)。 
每当你爬上一个阶梯你都要花费对应的体力花费值,然后你可以选择继续爬一个阶梯或者爬两个阶梯。 
您需要找到达到楼层顶部的最低花费。在开始时,你可以选择从索引为 0 或 1 的元素作为初始阶梯。 

示例 1: 
输入: cost = [10, 15, 20]
输出: 15
解释: 最低花费是从cost[1]开始,然后走两步即可到阶梯顶,一共花费15。

示例 2: 
输入: cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
输出: 6
解释: 最低花费方式是从cost[0]开始,逐个经过那些1,跳过cost[3],一共花费6。

注意: 
cost 的长度将会在 [2, 1000]。 
每一个 cost[i] 将会是一个Integer类型,范围为 [0, 999]。 

Related Topics 数组 动态规划 

设 f[i] 为表示踩到第 i 个台阶时的最小花费。

计算花费 f[i] 有一个清楚的递归关系:f[i] = cost[i] + min(f[i-1], f[i-2])。

n 个台阶的最小花费为 min(f[n-1], f[n]),踩第 n 个台阶与不踩第 n 个台阶两种情况下的最小值。

动态规划实现一

class Solution {
public int minCostClimbingStairs(int[] cost) {
    if(cost == null || cost.length == 0)
        return 0;
    int len = cost.length;
    int dp[] = new int[len];
    dp[0] = cost[0];
    dp[1] = cost[1];  //注意第二个元素的初始化

    for(int i=2; i<len; i++){
        dp[i] = cost[i] + Math.min(dp[i-1], dp[i-2]);
    }
    return Math.min(dp[len-1], dp[len-2]);
}

}

动态规划+滚动数组

class Solution {
public int minCostClimbingStairs(int[] cost) {
    if(cost == null || cost.length == 0)
        return 0;
    int len = cost.length;
    int f1 = 0;
    int f2 = 0;  //注意第二个元素的初始化

    for(int i=0; i<len; i++){
        int f = cost[i] + Math.min(f1, f2);
        f2 = f1;
        f1 = f;
    }
    return Math.min(f2, f1);
}

}

10. 除数博弈(leetcode-1025)

爱丽丝和鲍勃一起玩游戏,他们轮流行动。爱丽丝先手开局。 
最初,黑板上有一个数字 N 。在每个玩家的回合,玩家需要执行以下操作: 
选出任一 x,满足 0 < x < N 且 N % x == 0 。 
用 N - x 替换黑板上的数字 N 。 

如果玩家无法执行这些操作,就会输掉游戏。 
只有在爱丽丝在游戏中取得胜利时才返回 True,否则返回 false。假设两个玩家都以最佳状态参与游戏。 

示例 1: 

输入:2
输出:true
解释:爱丽丝选择 1,鲍勃无法进行操作。

示例 2: 
输入:3
输出:false
解释:爱丽丝选择 1,鲍勃也选择 1,然后爱丽丝无法进行操作。

提示: 
1 <= N <= 1000 

Related Topics 数学 动态规划 

1)数学逻辑分析

当N=1时,Alice必输。
当N=2时,Alice必赢,因为Bob只能取1。

因此问题转变成了:当Bob面对1的情况的时候Bob必输,Alice只要先手拿到2就必赢。

因此实际上这是抢2问题,谁先拿到2谁就赢。

考虑N的取值情况,如果N为偶数,那么只要N>=2且N为偶数。
令N=2*x,那么只要Alice取x,Bob下一轮就只能拿到2。
推理知:只要N为偶数,Alice必赢(先手必赢)。

那么当N为奇数时,无论Alice怎么操作,只要Bob每次只减1,Alice就不可能在她的回合拿到偶数。所以奇数的话Alice必输

数学逻辑分析

class Solution {
public boolean divisorGame(int N) {
    return (N%2)==0;
}

}

2)动态规划

🍓 dp[]代表一个长度为n+1的数组,如果 dp[n-x]为false,则Alice会减去x,即 Bobdp[n-x]false,Alice胜。否则Alice输,因为不管x是多少,dp[n-x]为true,则dp[n]Alicefalse。

动态规划

class Solution {
public boolean divisorGame(int N) {

    if(N == 1) return false;
    if(N == 2) return true;

    boolean[] dp = new boolean[N+1];
    dp[1] = false;
    dp[2] = true;

    for(int i = 3; i<=N; i++){
        dp[i] = false;

        if((i % 1 == 0 && !dp[i - 1]) || (i % 2 == 0 && !dp[i - 2]))
            dp[i] = true;
    }
    return dp[N];
}

}

11. 三步问题(面试 08.01)

三步问题。有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶或3阶。实现一种方法,计算小孩有多少种上楼梯的方式。结果可能很大,你需要对结果模1000000007。 

示例1: 
输入:n = 3 
输出:4
说明: 有四种走法

示例2: 
输入:n = 5
输出:13

提示: 
n范围在[1, 1000000]之间 
Related Topics 动态规划 
动态规划走三步

class Solution {
    public int waysToStep(int n) {
        if (n < 4) {
            return n == 3 ? 4 : n;
        }
    int f1 = 1;
    int f2 = 2;
    int f3 = 4;

    int f = 0;

    for(int i=3; i<n; i++){
        //f = (f1+f2+f3)% 1000000007;  前两个相加可能会溢出!
        f = ((f1+f2)% 1000000007 +f3) % 1000000007 ;
        f1 = f2;
        f2 = f3;
        f3 = f;
    }
    return f;
}

}

12. 按摩师(面试 17.16)

一个有名的按摩师会收到源源不断的预约请求,每个预约都可以选择接或不接。在每次预约服务之间要有休息时间,因此她不能接受相邻的预约。给定一个预约请求序列,替按摩师找到最优的预约集合(总预约时间最长),返回总的分钟数。 

示例 1: 
输入: [1,2,3,1]
输出: 4
解释: 选择 1 号预约和 3 号预约,总时长 = 1 + 3 = 4。

示例 2: 
输入: [2,7,9,3,1]
输出: 12
解释: 选择 1 号预约、 3 号预约和 5 号预约,总时长 = 2 + 9 + 1 = 12。

示例 3: 
输入: [2,1,4,5,3,1,1,3]
输出: 12
解释: 选择 1 号预约、 3 号预约、 5 号预约和 8 号预约,总时长 = 2 + 4 + 3 + 3 = 12。

Related Topics 动态规划 

🍓 同打家劫舍思路一样!

不限定下标为 i 这一天是否接受预约,因此需要分类讨论:

  1. 接受预约,那么昨天就一定休息,由于状态 dp[i - 1] 的定义涵盖了下标为 i - 1 这一天接收预约的情况,状态只能从下标为 i - 2 的状态转移而来:dp[i - 2] + nums[i];

  2. 不接受预约,那么昨天可以休息,也可以不休息,状态从下标为 i - 1 的状态转移而来:dp[i - 1];

  3. 二者取最大值,因此状态转移方程为 dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])。

展示代码

class Solution {
public int massage(int[] nums) {
    int a = 0, b = 0;
    for (int i = 0; i < nums.length; i++) {
        int c = Math.max(b, a + nums[i]);
        a = b;
        b = c;
    }
    return b;
}

}

posted @ 2020-07-08 19:28  喵喵巫  阅读(205)  评论(0编辑  收藏  举报