算法-动态规划

警告
本文最后更新于 2022-08-14,文中内容可能已过时。

Dynamic Programming

定义:

  • 存在「重叠子问题」
  • 具备「最优子结构」

步骤:

  • 确定「初始条件」
  • 确定「状态」
  • 确定「可选择列表」
  • 确定「状态转移方程」
  • 数组优化时间
  • 降维优化空间

思路:

  • 自顶向下
1
2
def fib(n):
    return fib(n - 1) + fib(n - 2)
  • 自底向上
1
2
3
4
def fib(n):
    for i in range(n + 1):
        dp[i] = dp[i - 1] + dp[i - 2]
    return dp[n]

Longest Increasing Subsequence

  • 状态定义:

$$ dp[i]=s[0:i]的最长上升子序列长度 $$

  • 状态初始化:

$$ dp[0] = 0 $$

  • 状态转移方程:

$$ dp[i]=\max(dp[i],dp[j] + 1) \quad \text{if } j \lt i \text{ and } s_j \lt s_i $$

Longest Common Subsequence

  • 状态定义:

$$ dp[i][j]=x[0:i]和y[0:j]的最长公共子序列长度 $$

  • 状态初始化:

$$ dp[i][0] = dp[0][j] = 0 $$

  • 状态转移方程:

$$ dp[i+1][j+1]= \begin{aligned} \begin{cases} dp[i][j] + 1& \text{if } x_i = y_j \newline \max(dp[i+1][j],dp[i][j+1])& \text{if } x_i \ne y_j \end{cases} \end{aligned} $$

Longest Common Increasing Subsequence

  • 状态定义:

$$ dp[i][j]=x[0:i]和y[0:j]的最长公共上升子序列长度 $$

  • 状态初始化:

$$ dp[i][0] = dp[0][j] = 0 $$

  • 状态转移方程:

$$ dp[i+1][j+1]= \begin{aligned} \begin{cases} dp[i][j] + 1& \text{if } x_i = y_j \newline \max(dp[i+1][j],dp[i][j+1])& \text{if } x_i \ne y_j \end{cases} \end{aligned} $$

  • 状态定义:

$$ dp[i][j]=x[0:i]到y[0:j]的最小操作次数 $$

  • 状态初始化:

$$ \begin{aligned} dp[i][0] &= i \newline dp[0][j] &= j \end{aligned} $$

  • 状态转移方程:

$$ dp[i+1][j+1]= \begin{aligned} \begin{cases} dp[i][j]& \text{if } x_i = y_j \newline 1+\min(dp[i+1][j],dp[i][j+1],dp[i][j])& \text{if } x_i \ne y_j \end{cases} \end{aligned} $$

  • 状态定义:

$$ dp[i][j]=只选前i个物品所能获得的最大价值 $$

  • 状态初始化:

$$ dp[0] = 0 $$

  • 状态转移方程:

$$ dp[i]= \begin{aligned} \begin{cases} dp[i-1] + v_i& \text{if } w + w_i \le W \newline 1+\min(dp[i+1][j],dp[i][j+1],dp[i][j])& \text{if } x_i \ne y_j \end{cases} \end{aligned} $$

  • 状态定义:

$$ dp[i][j]=s[i:j+1]是否是回文串 $$

  • 状态初始化:

$$ \begin{aligned} &dp[i][i] = true &dp[i][i+1] = true \quad \text{ if } s_i = s_{i+1} \end{aligned} $$

  • 状态转移方程:

$$ dp[i][i+j]=dp[i+1][i+j-1] \land s_i = s_{i+j} $$

121. 买卖股票的最佳时机

  1. 定义状态:$dp[i]$ 表示前 $i$ 天的最大利润。
  2. 状态转移:

$$ dp[i] = \max(dp[i-1], prices[i] - 前i-1天的最低价格) $$

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        int[] dp = new int[n];
        int minPrice = prices[0];
        for (int i = 1; i < n; i++) {
            dp[i] = Math.max(dp[i - 1], prices[i] - minPrice);
            minPrice = Math.min(minPrice, prices[i]);
        }
        return dp[n - 1];
    }
}
  1. 空间优化:每一天的状态只与前一天的状态有关。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Solution {
    public int maxProfit(int[] prices) {
        int ans = 0;
        int minPrice = Integer.MAX_VALUE;
        for (int price : prices) {
            ans = Math.max(ans, price - minPrice);
            minPrice = Math.min(minPrice, price);
        }
        return ans;
    }
}

122. 买卖股票的最佳时机 II

  1. 定义状态:$dp[i][0]$ 表示第 $i$ 天未持有股票的最大利润,$dp[i][1]$ 表示第 $i$ 天持有股票的最大利润。

  2. 状态转移:

    • 未持有->未持有。
    • 未持有->持有。(购买)
    • 持有->未持有。(出售)
    • 持有->持有。

$$ dp[i][0] = \max(dp[i-1][0],dp[i-1][1] + prices[i]) \newline dp[i][1] = \max(dp[i-1][1],dp[i-1][0] - prices[i]) $$

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        int[][] dp = new int[n][2];
        dp[0][1] = -prices[0];
        for (int i = 1; i < n; i++) {
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
        }
        return dp[n - 1][0];
    }
}
  1. 空间优化:每一天的状态只与前一天的状态有关。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        // 最开始不可能持有股票,状态不存在,置为负无穷
        int[] dp = { 0, Integer.MIN_VALUE };
        for (int price : prices) {
            int[] temp = {
                Math.max(dp[0], dp[1] + price),
                Math.max(dp[1], dp[0] - price)
            };
            dp = temp;
        }
        return dp[0];
    }
}

123. 买卖股票的最佳时机 III

  1. 定义状态:

    • $dp[i][0][0]$ 表示第 $i$ 天未持有股票且前 $i$ 天未进行过购买的最大利润。
    • $dp[i][1][0]$ 表示第 $i$ 天未持有股票且前 $i$ 天进行过 1 次购买的最大利润。
    • $dp[i][2][0]$ 表示第 $i$ 天未持有股票且前 $i$ 天进行过 2 次购买的最大利润。
    • $dp[i][0][1]$ 表示第 $i$ 天持有股票且前 $i$ 天未进行过购买的最大利润。
    • $dp[i][1][1]$ 表示第 $i$ 天持有股票且前 $i$ 天进行过 1 次购买的最大利润。
    • $dp[i][2][1]$ 表示第 $i$ 天持有股票且前 $i$ 天进行过 2 次购买的最大利润。
  2. 状态转移:

    • 未持有->未持有。
    • 未持有->持有。(购买,交易数+1)
    • 持有->未持有。(出售)
    • 持有->持有。

$$ \begin{aligned} dp[i][0][0] &= dp[i-1][0][0] \newline dp[i][1][0] &= \max(dp[i-1][1][0],dp[i-1][1][1] + prices[i]) \newline dp[i][2][0] &= \max(dp[i-1][2][0],dp[i-1][2][1] + prices[i]) \newline dp[i][0][1] &= -\infty \newline dp[i][1][1] &= \max(dp[i-1][1][1],dp[i-1][0][0] - prices[i]) \newline dp[i][2][1] &= \max(dp[i-1][2][1],dp[i-1][1][0] - prices[i]) \newline \end{aligned} $$

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        int[][][] dp = new int[n][3][2];
        dp[0][0][1] = -100000; // 防止溢出
        dp[0][1][1] = -prices[0];
        dp[0][2][1] = -prices[0];
        for (int i = 1; i < n; i++) {
            dp[i][0][0] = dp[i - 1][0][0];
            dp[i][1][0] = Math.max(dp[i - 1][1][0], dp[i - 1][1][1] + prices[i]);
            dp[i][2][0] = Math.max(dp[i - 1][2][0], dp[i - 1][2][1] + prices[i]);
            dp[i][0][1] = -100000; // 防止溢出
            dp[i][1][1] = Math.max(dp[i - 1][1][1], dp[i - 1][0][0] - prices[i]);
            dp[i][2][1] = Math.max(dp[i - 1][2][1], dp[i - 1][1][0] - prices[i]);
        }
        return dp[n - 1][2][0];
    }
}
  1. 空间优化:每一天的状态只与前一天的状态有关。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Solution {
    public int maxProfit(int[] prices) {
        int sell1 = 0;                 // 已经卖出 1 次的最大利润,等价于 dp[i][1][0]
        int sell2 = 0;                 // 已经卖出 2 次的最大利润,等价于 dp[i][2][0]
        int buyy1 = Integer.MIN_VALUE; // 已经买入 1 次的最大利润,等价于 dp[i][1][1]
        int buyy2 = Integer.MIN_VALUE; // 已经买入 2 次的最大利润,等价于 dp[i][2][1]
        for (int price : prices) {
            sell2 = Math.max(sell2, buyy2 + price);
            buyy2 = Math.max(buyy2, sell1 - price);
            sell1 = Math.max(sell1, buyy1 + price);
            buyy1 = Math.max(buyy1, -price);
        }
        return sell2;
    }
}

188. 买卖股票的最佳时机 IV

  1. 定义状态:

    • $dp[i][j][0]$ 表示第 $i$ 天未持有股票且前 $i$ 天进行过 $j$ 次购买的最大利润。
    • $dp[i][j][1]$ 表示第 $i$ 天持有股票且前 $i$ 天进行过 $j$ 次购买的最大利润。
  2. 状态转移:

    • 未持有->未持有。
    • 未持有->持有。(购买,交易数+1)
    • 持有->未持有。(出售)
    • 持有->持有。

$$ \begin{aligned} dp[i][0][0] &= dp[i-1][0][0] \newline dp[i][j][0] &= \max(dp[i-1][j][0],dp[i-1][j][1] + prices[i]) \ (j \gt 0) \newline dp[i][0][1] &= -\infty \newline dp[i][j][1] &= \max(dp[i-1][j][1],dp[i-1][j-1][0] - prices[i]) \ (j \gt 0) \newline \end{aligned} $$

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Solution {
    public int maxProfit(int k, int[] prices) {
        int n = prices.length;
        if (n == 0) return 0;
        int[][][] dp = new int[n][k + 1][2];
        dp[0][0][1] = -100000; // 防止溢出
        for (int i = 1; i <= k; i++) dp[0][i][1] = -prices[0];
        for (int i = 1; i < n; i++) {
            dp[i][0][0] = dp[i - 1][0][0];
            dp[i][0][1] = -100000; // 防止溢出
            for (int j = 1; j <= k; j++) {
                dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i]);
                dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i]);
            }
        }
        return dp[n - 1][k][0];
    }
}
  1. 空间优化:每一天的状态只与前一天的状态有关。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Solution {
    public int maxProfit(int k, int[] prices) {
        int n = prices.length;
        if (n == 0) return 0;
        int[][] dp = new int[k + 1][2];
        for (int i = 0; i <= k; i++)
            dp[i][1] = -100000; // 防止溢出
        for (int price : prices) {
            for (int j = 1; j <= k; j++) {
                dp[j][0] = Math.max(dp[j][0], dp[j][1] + price);
                dp[j][1] = Math.max(dp[j][1], dp[j - 1][0] - price);
            }
        }
        return dp[k][0];
    }
}

309. 最佳买卖股票时机含冷冻期

  1. 定义状态:

    • $dp[i][j][0]$ 表示第 $i$ 天未持有股票且前 $i$ 天进行过 $j$ 次购买的最大利润。
    • $dp[i][j][1]$ 表示第 $i$ 天持有股票且前 $i$ 天进行过 $j$ 次购买的最大利润。
  2. 状态转移:

    • 未持有->未持有。
    • 未持有->持有。(购买,交易数+1)
    • 持有->未持有。(出售)
    • 持有->持有。

$$ \begin{aligned} dp[i][0][0] &= dp[i-1][0][0] \newline dp[i][j][0] &= \max(dp[i-1][j][0],dp[i-1][j][1] + prices[i]) \ (j \gt 0) \newline dp[i][0][1] &= -\infty \newline dp[i][j][1] &= \max(dp[i-1][j][1],dp[i-1][j-1][0] - prices[i]) \ (j \gt 0) \newline \end{aligned} $$

  1. 空间优化:每一天的状态只与前一天的状态有关。

714. 买卖股票的最佳时机含手续费

  1. 定义状态:

    • $dp[i][j][0]$ 表示第 $i$ 天未持有股票且前 $i$ 天进行过 $j$ 次购买的最大利润。
    • $dp[i][j][1]$ 表示第 $i$ 天持有股票且前 $i$ 天进行过 $j$ 次购买的最大利润。
  2. 状态转移:

    • 未持有->未持有。
    • 未持有->持有。(购买,交易数+1)
    • 持有->未持有。(出售)
    • 持有->持有。

$$ \begin{aligned} dp[i][0][0] &= dp[i-1][0][0] \newline dp[i][j][0] &= \max(dp[i-1][j][0],dp[i-1][j][1] + prices[i]) \ (j \gt 0) \newline dp[i][0][1] &= -\infty \newline dp[i][j][1] &= \max(dp[i-1][j][1],dp[i-1][j-1][0] - prices[i]) \ (j \gt 0) \newline \end{aligned} $$

  1. 空间优化:每一天的状态只与前一天的状态有关。

Longest Increasing Subsequence

动态规划

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
int lengthOfLIS(int[] nums) {
    int n = nums.length;
    int[] dp = new int[n];
    int ans = 1;
    for (int i = 0; i < n; i++) {
        dp[i] = 1;
        for (int j = 0; j < i; j++) {
            if (nums[i] > nums[j]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
                ans = Math.max(ans, dp[i]);
            }
        }
    }
    return ans;
}
  • $O(n^2)$

贪心 + 二分查找

 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
/**
 * list 中每个链表都是降序排列,只能将更小的值加入末尾
 * 若均无法添加,则作为一个新链表加入 list
 * 二分查找找到能够添加的下标最小的链表,然后加入
 * 最后保证了每个链表的尾结点单调递增
 * list 的大小即为最长子序列的长度
 */
int lengthOfLIS(int[] nums) {
    int[] top = new int[nums.length];
    int size = 0;
    for (int num : nums) {
        int left = 0;
        int right = size;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (top[mid] < num) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        top[size] = num;
        if (left == size) {
            size++;
        }
    }
    return size;
}

二分查找

 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
/**
 * 数组 d[i],表示长度为 i 的最长上升子序列的末尾元素的最小值
 * 用 len 记录目前最长上升子序列的长度
 */
int lengthOfLIS(int[] nums) {
    int[] dis = new int[nums.length];
    int len = 0;
    for (int num : nums) {
        int left = 0;
        int right = len;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (dis[mid] < num) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        dis[left] = num;
        if (left == len) {
            len++;
        }
    }
    return len;
}

Longest Common Subsequence

1143. 最长公共子序列

583. 两个字符串的删除操作

516. 最长回文子序列

1092. 最短公共超序列

Longest Common Increasing Subsequence

力扣-0730-统计不同回文子序列

1035. 不相交的线

64. 最小路径和

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Solution {
    public int minPathSum(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        for (int i = 1; i < m; i++) grid[i][0] += grid[i - 1][0];
        for (int j = 1; j < n; j++) grid[0][j] += grid[0][j - 1];
        for (int i = 1; i < m; i++)
            for (int j = 1; j < n; j++)
                grid[i][j] += Math.min(grid[i - 1][j], grid[i][j - 1]);
        return grid[m - 1][n - 1];
    }
}

473. 火柴拼正方形

 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
class Solution {
    public boolean makesquare(int[] matchsticks) {
        int totalLen = Arrays.stream(matchsticks).sum();
        if (totalLen % 4 != 0) {
            return false;
        }
        int len = totalLen / 4, n = matchsticks.length;
        int[] dp = new int[1 << n];
        Arrays.fill(dp, -1);
        dp[0] = 0;
        for (int s = 1; s < (1 << n); s++) {
            for (int k = 0; k < n; k++) {
                if ((s & (1 << k)) == 0) {
                    continue;
                }
                int s1 = s & ~(1 << k);
                if (dp[s1] >= 0 && dp[s1] + matchsticks[k] <= len) {
                    dp[s] = (dp[s1] + matchsticks[k]) % len;
                    break;
                }
            }
        }
        return dp[(1 << n) - 1] == 0;
    }
}