🚩DP - 动态规划

最近发现笔试题的区分度主要在动态规划,本文按照题型归类整理。

背包问题

0-1 背包问题

给定一组物品,每个物品有两个属性:重量(w)和价值(v)。同时给定一个背包的容量(bag)。目标是选择一些物品放入背包中,使得在不超过背包容量的前提下,物品的总价值最大。在01背包问题中,每个物品只能选择放入背包一次(要么放入,要么不放入),不能重复放入。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public static int bag01(int[] w, int[] v, int bag) {
  int N = w.length;
  // 对于前 i 个物品可能的选择,若背包剩余容量为 j,能达到的最大价值为 dp[i][j]
  // i 属于 [0,N]
  // rest 属于 [0,bag]
  int[][] dp = new int[N + 1][bag + 1];
  for (int i = 1; i <= N; i++) {
    for (int j = 1; j <= bag; j++) {
      // w[i-1] v[i-1] 表示第 i 件物品的重量和价值
      int wi = w[i - 1], vi = v[i - 1];
      // 不选择第 i 件物品
      int p1 = dp[i - 1][j];
      // 选择第 i 件物品,要看背包剩余容量是否能放下物品 i
      int p2 = j >= wi ? dp[i - 1][j - wi] + vi : 0;
      dp[i][j] = Math.max(p1, p2);
    }
  }
  // 对于所有(N个)物品,背包容量为 bag 时的最大价值
  return dp[N][bag];
}
  • 时间复杂度:O(N x bag)
  • 空间复杂度:O(N x bag)

其中N为物品的数量,bag为背包的容量。

WY 厂笔试

将 nums 数组分成两部分,使得每一部分都包含至少一个元素,并且元素之和相加大于 k。

转换思路:从nums中选取1~N-1个元素,使得他们的和在一定范围[k,sum-k]内

再·转换思路 :0-1 背包问题,背包大小范围在[k,sum-k]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public static int countPartitionsBag(int[] nums, int k) {
  int sum = 0;
  for (int num : nums) {
    sum += num;
  }
  // 下界大于上界
  if (k > sum - k) {
    return 0;
  }
  // 转换成 0-1 背包问题,背包最大容量为 sum-k,每个数的重量为数值,价值上一状态的价值
  // 在计算过程中 背包容量从 1~sum-k 的情况都一并计算了,涵盖了[k,sum-k]
  int N = nums.length;
  int bag = sum - k;
  int[][] dp = new int[N + 1][bag + 1];
  dp[0][0] = 1;
  for (int i = 1; i <= N; i++) {
    for (int j = 0; j <= bag; j++) {
      // nums[i-1] 表示第 i 个数(的重量)
      int wi = nums[i - 1];
      // 默认不选择
      dp[i][j] = dp[i-1][j];
      // 选择,要看背包剩余容量是否能放下
      dp[i][j] +=j >= wi ? dp[i - 1][j - wi] : 0;
    }
  }
  // 统计背包范围在 [k,sum-k] 的情况数
  int res = 0;
  for (int i = k; i <= sum - k; i++) {
    res += dp[N][i];
  }
  return res;
}

子集背包问题

给定一个非空数组 nums,判断数组是否可以被分割成两个子集,使两个子集元素和相等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static boolean canPartition(int[] nums) {
  int sum = 0, n = nums.length;
  for (int num : nums) {
    sum += num;
  }
  if ((sum & 1) == 1) {
    return false;
  }
  sum /= 2;
  boolean[] dp = new boolean[sum + 1];
  dp[0] = true;
  for (int num : nums) {
    // 从后往前遍历,防止前面的的结果影响后面
    for (int i = sum; i >= 0; i--) {
      // 选择是否使用 num 来组成剩下的和
      if (i - num >= 0) {
        dp[i] = dp[i] || dp[i - num];
      }
    }
  }
  return dp[sum];
}
  • 时间复杂度:O(n^2^)
  • 空间复杂度:O(n)

完全背包问题

物品的数量无限,求刚好装满背包的组合数。

[322]零钱兑换.java

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1

你可以认为每种硬币的数量是无限的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public static int coinChange(int[] coins, int amount) {
  // 要凑到 i 元钱有 dp[i] 种情况
  int[] dp = new int[amount + 1];
  // 不赋值 Integer.MAX_VALUE 是担心 +1 溢出
  Arrays.fill(dp, amount + 1);
  dp[0] = 0;
  for (int coin : coins) {
    for (int i = coin; i <= amount; i++) {
      // 目标金额通过这一个硬币,与【目标-硬币】金额联系起来,选择硬币数更少的情况
      dp[i] = Math.min(dp[i], dp[i - coin] + 1);
    }
  }
  return dp[amount] == amount + 1 ? -1 : dp[amount];
}
  • 时间复杂度:O(n^2^)
  • 空间复杂度:O(n)

股票问题

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。设计一个算法来计算你所能获取的最大利润。返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

lv1. 你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Solution {
    public int maxProfit(int[] prices) {
        int res = 0;
        int min = Integer.MAX_VALUE;
        for (int price : prices) {
            if (price <= min) {
                min = Math.min(min, price);
            } else {
                res = Math.max(res, price - min);
            }
        }
        return res;
    }
}

lv2. 在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Solution {
    public int maxProfit(int[] prices) {
        if (prices.length < 2) {
            return 0;
        }
        int res = 0;
        int pre = prices[0];
        for (int i = 1; i < prices.length; i++) {
            int cur = prices[i];
            if (cur > pre) {
                res += cur - pre;
            }
            pre = cur;
        }
        return res;
    }
}

lv3. 每次交易(买入+卖出)需要交手续费

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Solution {
    public int maxProfit(int[] prices, int fee) {
        int n = prices.length;
        int[][] dp = new int[n][2];
        // 0 不持有  1 持有
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        for (int i = 1; i < n; i++) {
            // i-1 昨天
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee);
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
        }
        return dp[n - 1][0];
    }
}

lv4. 不限次数,但含冷冻期:卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        if (n <= 1) return 0;
        // 0 不持有,不在冷冻期
        // 1 持有
        // 2 不持有,在冷冻期 - 昨天一定持有,且今天要卖
        int[][] dp = new int[n][3];
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        dp[0][2] = 0;
        for (int i = 1; i < n; i++) {//从[1]...[n-1]
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][2]);
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
            dp[i][2] = dp[i - 1][1] + prices[i];
        }
        return Math.max(dp[n - 1][0], dp[n - 1][2]);
    }
}

lv5. 最多可以完成 两笔 交易,不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
    public int maxProfit(int[] prices) {
        if (prices.length < 2) {
            return 0;
        }
        int n = prices.length;
        int[][] dp = new int[n][5];
        // 0 没有任何买卖
        // 1 买入第一次
        dp[0][1] = -prices[0];
        // 2 卖出第一次
        // 3 买入第二次
        dp[0][3] = -prices[0];
        // 4 卖出第二次
        for (int i = 1; i < n; i++) {
            // i-1 表示昨天
            // 买入花钱,所以是 -price[i]
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
            // 卖出得钱,所以是 +price[i]
            dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
            dp[i][3] = Math.max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
            dp[i][4] = Math.max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
        }
        // 最后一天的第二次卖出
        return dp[n - 1][4];
    }
}

lv6. 你最多可以完成 k 笔交易。也就是说,你最多可以买 k 次,卖 k 次,你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Solution {
    public int maxProfit(int k, int[] prices) {
        int len = prices.length;
        if (len < 2) {
            return 0;
        }
        // 0 未买入
        // k+1 第 k 次买入 - 奇数
        // k+2 第 k 次卖出 - 偶数
        int[][] dp = new int[len][2 * k + 1];
        // 每个时刻第一次买入的初始情况
        for (int i = 0; i < k; i++) {
            dp[0][2 * i + 1] = -prices[0];
        }
        for (int i = 1; i < len; i++) {
            // i-1 表示昨天
            for (int j = 1; j <= 2 * k; j++) {
                if ((j & 1) == 1) {
                    // 当天考虑买入
                    // 不买,延续昨天的
                    // 买,要在昨天不持有的基础上买
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - 1] - prices[i]);
                } else {
                    // 当天考虑卖出
                    // 不卖,延续昨天的
                    // 卖,要在昨天持有的基础上卖
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - 1] + prices[i]);
                }
            }
        }
        return dp[len - 1][2 * k];
    }
}

lv999. 限制K笔交易,冷冻期1天,有手续费?

字符串比较问题

最长公共子序列

给定两个字符串 text1text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。例如,"ace""abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
    public int longestCommonSubsequence(String text1, String text2) {
        int n1 = text1.length(),n2 =text2.length();
        int[][] dp = new int[n1+1][n2+1];
        for(int i=1;i<=n1;i++){
            int char1 = text1.charAt(i-1);
            for(int j=1;j<=n2;j++){
                int char2 = text2.charAt(j-1);
                if(char1==char2){
                    dp[i][j] = dp[i-1][j-1] +1;
                }else{
                    dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);
                }
            }
        }
        return dp[n1][n2];
    }

编辑距离

给你两个单词 word1word2请返回将 word1 转换成 word2 所使用的最少操作数

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public int minDistance(String word1, String word2) {
        int len1 = word1.length();
        int len2 = word2.length();

        // 多开一行一列是为了保存边界条件,即字符长度为 0 的情况,这一点在字符串的动态规划问题中比较常见
        int[][] dp = new int[len1 + 1][len2 + 1];
        // 初始化:当 word2 长度为 0 时,将 word1 的全部删除即可
        for (int i = 1; i <= len1; i++) {
            dp[i][0] = i;
        }
        // 当 word1 长度为 0 时,插入所有 word2 的字符即可
        for (int j = 1; j <= len2; j++) {
            dp[0][j] = j;
        }

        char[] word1Array = word1.toCharArray();
        char[] word2Array = word2.toCharArray();
        // 递推开始,注意:填写 dp 数组的时候,由于初始化多设置了一行一列,横纵坐标有个偏移
        for (int i = 1; i <= len1; i++) {
            for (int j = 1; j <= len2; j++) {
                // 这是最佳情况
                if (word1Array[i - 1] == word2Array[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1];
                    continue;
                }
                // 否则在以下三种情况中选出步骤最少的,这是「动态规划」的「最优子结构」
                // 1、在下标 i 处插入一个字符
                int insert = dp[i][j - 1] + 1;
                // 2、替换一个字符
                int replace = dp[i - 1][j - 1] + 1;
                // 3、删除一个字符
                int delete = dp[i - 1][j] + 1;
                dp[i][j] = Math.min(Math.min(insert, replace), delete);
            }
        }
        return dp[len1][len2];
    }
0%