动态规划常见算法题讲解

爬楼梯问题

爬楼梯问题是经典的动态规划问题,通常问题描述如下:

问题描述:一个人站在楼梯的底部,需要到达楼梯的顶部。总共有 n 级台阶,每次可以向上走一阶或两阶。问有多少种不同的方法可以到达楼梯的顶部。

解决方案

可以用动态规划来解决这个问题。动态规划的核心思想是将复杂问题分解为更简单的子问题,利用子问题的解来构建原问题的解。

步骤解析

  1. 定义子问题:定义 dp[i] 为到达第 i 级台阶的方法总数。
  2. 递推关系:到达第 i 级台阶有两种方式:
    • 从第 i-1 级台阶迈一步到达第 i 级台阶。
    • 从第 i-2 级台阶迈两步到达第 i 级台阶。 因此,递推公式为:dp[i] = dp[i-1] + dp[i-2]
  3. 边界条件
    • dp[0] = 1:只有一种方法,即不动。
    • dp[1] = 1:只有一种方法,从第 0 级台阶迈一步到第 1 级台阶。
  4. 计算顺序:从底向上计算 dp 数组的值,直至 dp[n]
代码实现

以下是 Java 实现的代码:

 
public class ClimbingStairs {
    public static void main(String[] args) {
        int n = 5; // 总的台阶数
        System.out.println(climbStairs(n)); // 输出不同的爬楼梯方法数
    }

    public static int climbStairs(int n) {
        if (n <= 1) {
            return 1;
        }

        int[] dp = new int[n + 1]; // 创建 dp 数组
        dp[0] = 1;
        dp[1] = 1;

        for (int i = 2; i <= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }

        return dp[n]; // 返回到达第 n 级台阶的方法总数
    }
}

代码解析

  1. 边界条件
    • n <= 1 时,直接返回 1。
  2. 初始化
    • 创建一个大小为 n + 1dp 数组,其中 dp[0]dp[1] 初始化为 1。
  3. 递推关系
    • 从第 2 级台阶开始,依次计算每一级台阶的方法数,使用递推公式 dp[i] = dp[i - 1] + dp[i - 2]
  4. 返回结果
    • 最后返回 dp[n],即到达第 n 级台阶的方法总数。

优化方案

可以进一步优化空间复杂度,由于每次计算只需要前两级台阶的方法数,可以用两个变量来替代 dp 数组。

 
public class ClimbingStairsOptimized {
    public static void main(String[] args) {
        int n = 5; // 总的台阶数
        System.out.println(climbStairs(n)); // 输出不同的爬楼梯方法数
    }

    public static int climbStairs(int n) {
        if (n <= 1) {
            return 1;
        }

        int prev1 = 1;
        int prev2 = 1;

        for (int i = 2; i <= n; i++) {
            int current = prev1 + prev2;
            prev2 = prev1;
            prev1 = current;
        }

        return prev1; // 返回到达第 n 级台阶的方法总数
    }
}

代码解析

  1. 边界条件
    • n <= 1 时,直接返回 1。
  2. 初始化
    • 使用 prev1prev2 分别保存前两级台阶的方法数,初始值为 1。
  3. 递推关系
    • 依次计算每一级台阶的方法数,用 current 保存当前级台阶的方法数,并更新 prev1prev2
  4. 返回结果
    • 最后返回 prev1,即到达第 n 级台阶的方法总数。

通过上述优化,空间复杂度从 O(n) 降到了 O(1),但时间复杂度仍然保持为 O(n)。

通过动态规划,我们可以高效地解决爬楼梯问题。动态规划的核心在于定义子问题、找到递推关系、确定边界条件,并依此计算每个子问题的解。

0/1 背包问题

问题描述:给定一个容量为 W 的背包和 N 个物品,每个物品有一个重量和价值。要求在不超过背包容量的情况下,选择若干物品使得这些物品的总价值最大。

解决方案

动态规划是解决 0/1 背包问题的有效方法。我们通过构建一个二维数组 dp 来记录在不同的容量和物品选择情况下的最优解。

步骤解析

  1. 定义子问题:定义 dp[i][w] 为前 i 个物品在容量为 w 的情况下的最大价值。

  2. 递推关系

    • 如果不选择第 i 个物品,则 dp[i][w] = dp[i-1][w]
    • 如果选择第 i 个物品,则 dp[i][w] = dp[i-1][w - weight[i]] + value[i],其中 weight[i]value[i] 分别是第 i 个物品的重量和价值。
    • 因此,递推公式为:
      dp[i][w] = max(dp[i-1][w], dp[i-1][w - weight[i]] + value[i])
      

  3. 边界条件

    • i = 0w = 0 时,dp[i][w] = 0,因为没有物品或容量为 0 时的最大价值为 0。
  4. 计算顺序:从小到大计算所有子问题的解,最终得到 dp[N][W],即在容量为 W 的情况下前 N 个物品的最大价值。

代码实现

以下是用 Java 实现的 0/1 背包问题的代码:

public class Knapsack {
    public static void main(String[] args) {
        int[] weights = {2, 3, 4, 5}; // 物品重量
        int[] values = {3, 4, 5, 6};  // 物品价值
        int W = 8;                    // 背包容量
        System.out.println(knapsack(weights, values, W)); // 输出最大价值
    }

    public static int knapsack(int[] weights, int[] values, int W) {
        int N = weights.length;
        int[][] dp = new int[N + 1][W + 1];

        // 填充 dp 数组
        for (int i = 1; i <= N; i++) {
            for (int w = 1; w <= W; w++) {
                if (weights[i - 1] <= w) {
                    dp[i][w] = Math.max(dp[i - 1][w], dp[i - 1][w - weights[i - 1]] + values[i - 1]);
                } else {
                    dp[i][w] = dp[i - 1][w];
                }
            }
        }

        return dp[N][W]; // 返回背包容量为 W 时的最大价值
    }
}

代码解析

  1. 初始化

    • 创建一个二维数组 dp,大小为 [N+1][W+1]N 是物品数量,W 是背包容量。
    • dp[i][w] 表示前 i 个物品在容量为 w 时的最大价值。
  2. 填充 dp 数组

    • 逐个物品进行处理,从 1N
    • 对于每个物品,从容量 1W 依次计算。
    • 如果当前物品的重量小于或等于当前容量 w,则更新 dp[i][w] 为以下两者的最大值:
      • 不选择当前物品时的最大价值 dp[i-1][w]
      • 选择当前物品时的最大价值 dp[i-1][w-weights[i-1]] + values[i-1]
    • 如果当前物品的重量大于当前容量 w,则 dp[i][w] = dp[i-1][w]
  3. 返回结果

    • dp[N][W] 即为在容量为 W 的情况下,前 N 个物品的最大价值。

优化方案

可以优化空间复杂度为 O(W),通过使用一维数组代替二维数组,因为 dp[i][w] 只依赖于 dp[i-1][...]

public class KnapsackOptimized {
    public static void main(String[] args) {
        int[] weights = {2, 3, 4, 5}; // 物品重量
        int[] values = {3, 4, 5, 6};  // 物品价值
        int W = 8;                    // 背包容量
        System.out.println(knapsack(weights, values, W)); // 输出最大价值
    }

    public static int knapsack(int[] weights, int[] values, int W) {
        int N = weights.length;
        int[] dp = new int[W + 1];

        // 填充 dp 数组
        for (int i = 0; i < N; i++) {
            for (int w = W; w >= weights[i]; w--) {
                dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
            }
        }

        return dp[W]; // 返回背包容量为 W 时的最大价值
    }
}

代码解析

  1. 初始化

    • 创建一个一维数组 dp,大小为 [W+1]
  2. 填充 dp 数组

    • 逐个物品进行处理,从 0N-1
    • 对于每个物品,从容量 W 逆序计算到 weights[i],以确保每个物品只被考虑一次。
    • 更新 dp[w] 为以下两者的最大值:
      • 不选择当前物品时的最大价值 dp[w]
      • 选择当前物品时的最大价值 dp[w-weights[i]] + values[i]
  3. 返回结果

    • dp[W] 即为在容量为 W 的情况下,前 N 个物品的最大价值。

完全背包问题

完全背包问题是背包问题的一个变种,不同之处在于每种物品可以选择多次。也就是说,每种物品的数量是无限的。我们可以使用动态规划来解决这个问题,方法类似于 0/1 背包问题,但在状态转移时稍作调整。

问题描述

给定一个容量为 W 的背包和 N 个物品,每个物品有一个重量和价值,且每种物品可以选取多次。要求在不超过背包容量的情况下,选择若干物品使得这些物品的总价值最大。

解决方案

我们仍然使用动态规划来解决这个问题。与 0/1 背包问题的不同之处在于,对于每个物品,我们会在状态转移中考虑多次选取该物品的情况。

步骤解析

  1. 定义子问题:定义 dp[w] 为容量为 w 时的最大价值。

  2. 递推关系

    • 对于每个物品 i 和每个容量 w,我们考虑是否选择物品 i
    • 如果选择物品 i,则 dp[w] = dp[w - weight[i]] + value[i],这里 weight[i]value[i] 分别是物品 i 的重量和价值。
    • 我们取所有可能选择的情况中的最大值,即 dp[w] = max(dp[w], dp[w - weight[i]] + value[i])
  3. 边界条件

    • w = 0 时,dp[w] = 0,因为没有物品可以放入容量为 0 的背包中。
  4. 计算顺序:从小到大计算所有子问题的解,直至 dp[W],即在容量为 W 的情况下的最大价值。

代码实现

以下是用 Java 实现的完全背包问题的代码:

public class CompleteKnapsack {
    public static void main(String[] args) {
        int[] weights = {2, 3, 4, 5}; // 物品重量
        int[] values = {3, 4, 5, 6};  // 物品价值
        int W = 8;                    // 背包容量
        System.out.println(completeKnapsack(weights, values, W)); // 输出最大价值
    }

    public static int completeKnapsack(int[] weights, int[] values, int W) {
        int N = weights.length;
        int[] dp = new int[W + 1];

        // 填充 dp 数组
        for (int i = 0; i < N; i++) {
            for (int w = weights[i]; w <= W; w++) {
                dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
            }
        }

        return dp[W]; // 返回背包容量为 W 时的最大价值
    }
}

代码解析

  1. 初始化

    • 创建一个一维数组 dp,大小为 [W + 1]
  2. 填充 dp 数组

    • 对于每个物品,从 0N-1 进行处理。
    • 对于每个物品,从容量 weights[i] 开始,顺序计算到 W,以允许每个物品多次选取。
    • 更新 dp[w] 为以下两者的最大值:
      • 不选择当前物品时的最大价值 dp[w]
      • 选择当前物品时的最大价值 dp[w - weights[i]] + values[i]
  3. 返回结果

    • dp[W] 即为在容量为 W 的情况下,前 N 个物品的最大价值。

优化方案

与 0/1 背包问题类似,我们也可以优化空间复杂度为 O(W),通过使用一维数组代替二维数组。

示例解释

以输入 weights = {2, 3, 4, 5}values = {3, 4, 5, 6},背包容量 W = 8 为例:

  • 初始化 dp 数组为 [0, 0, 0, 0, 0, 0, 0, 0, 0]
  • 对于每个物品,更新 dp 数组:
    • 对于物品 1 (重量 2,价值 3):
      • w = 2 时,dp[2] = max(dp[2], dp[2-2] + 3) = 3
      • w = 3 时,dp[3] = max(dp[3], dp[3-2] + 3) = 3
      • w = 4 时,dp[4] = max(dp[4], dp[4-2] + 3) = 6
      • ...
    • 对于物品 2 (重量 3,价值 4):
      • w = 3 时,dp[3] = max(dp[3], dp[3-3] + 4) = 4
      • w = 4 时,dp[4] = max(dp[4], dp[4-3] + 4) = 6
      • w = 5 时,dp[5] = max(dp[5], dp[5-3] + 4) = 7
      • ...

最终计算得到 dp[8] 即为最大价值。

最长回文子序列

最长回文子序列(Longest Palindromic Subsequence, LPS)是一个经典的动态规划问题。给定一个字符串,要求找到其最长的回文子序列的长度。回文子序列是指一个子序列,其字符顺序正读和反读都相同。

问题描述

给定一个字符串 s,找到 s 的最长回文子序列的长度。

动态规划解决方案

我们可以通过动态规划来解决这个问题。动态规划的核心思想是将复杂问题分解为更简单的子问题,并利用子问题的解来构建原问题的解。

步骤解析

  1. 定义子问题:定义 dp[i][j] 表示字符串 s 从第 i 个字符到第 j 个字符的最长回文子序列的长度。

  2. 递推关系

    • 如果 s[i] == s[j],则 dp[i][j] = dp[i+1][j-1] + 2,因为首尾两个字符相等,可以构成一个回文子序列。
    • 如果 s[i] != s[j],则 dp[i][j] = max(dp[i+1][j], dp[i][j-1]),表示要么不考虑第 i 个字符,要么不考虑第 j 个字符,取两者的最大值。
  3. 边界条件

    • i == j 时,dp[i][j] = 1,因为单个字符本身就是一个回文子序列。
    • i > j 时,dp[i][j] = 0,因为这是无效的子序列。
  4. 计算顺序:我们需要从短的子序列开始,逐步计算更长的子序列,最终得到整个字符串的最长回文子序列的长度。

代码实现

以下是用 Java 实现的最长回文子序列的代码:

public class LongestPalindromicSubsequence {
    public static void main(String[] args) {
        String s = "bbbab";
        System.out.println(longestPalindromeSubseq(s)); // 输出4
    }

    public static int longestPalindromeSubseq(String s) {
        int n = s.length();
        int[][] dp = new int[n][n];

        // 单个字符的回文子序列长度为1
        for (int i = 0; i < n; i++) {
            dp[i][i] = 1;
        }

        // 逐步计算更长的子序列
        for (int len = 2; len <= n; len++) {
            for (int i = 0; i <= n - len; i++) {
                int j = i + len - 1;
                if (s.charAt(i) == s.charAt(j)) {
                    dp[i][j] = dp[i + 1][j - 1] + 2;
                } else {
                    dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
                }
            }
        }

        // 返回整个字符串的最长回文子序列长度
        return dp[0][n - 1];
    }
}

代码解析

  1. 初始化

    • 创建一个二维数组 dp,大小为 n x n,其中 n 是字符串 s 的长度。
    • 初始化单个字符的回文子序列长度为 1,即 dp[i][i] = 1
  2. 填充 dp 数组

    • 我们从长度为 2 的子序列开始,逐步计算更长的子序列。
    • 对于每个子序列,检查首尾字符是否相等:
      • 如果相等,表示首尾字符可以构成一个回文子序列,则 dp[i][j] = dp[i+1][j-1] + 2
      • 如果不相等,表示要么不考虑首字符,要么不考虑尾字符,则 dp[i][j] = max(dp[i+1][j], dp[i][j-1])
  3. 返回结果

    • 最后返回 dp[0][n-1],即整个字符串的最长回文子序列的长度。

示例解释

以输入字符串 s = "bbbab" 为例:

  1. 初始化

    • dp 数组初始化为:
      1 0 0 0 0
      0 1 0 0 0
      0 0 1 0 0
      0 0 0 1 0
      0 0 0 0 1
      

  2. 填充 dp 数组

    • 对于长度为 2 的子序列:
      • dp[0][1] = 2,因为 s[0] == s[1]
      • dp[1][2] = 2,因为 s[1] == s[2]
      • dp[2][3] = 1,因为 s[2] != s[3]
      • dp[3][4] = 1,因为 s[3] != s[4]
    • 对于长度为 3 的子序列:
      • dp[0][2] = 3,因为 s[0] == s[2]dp[1][1] = 1
      • dp[1][3] = 2,因为 s[1] != s[3]max(dp[2][3], dp[1][2]) = 2
      • dp[2][4] = 1,因为 s[2] != s[4]max(dp[3][4], dp[2][3]) = 1
    • 对于长度为 4 的子序列:
      • dp[0][3] = 3,因为 s[0] != s[3]max(dp[1][3], dp[0][2]) = 3
      • dp[1][4] = 3,因为 s[1] == s[4]dp[2][3] = 1
    • 对于长度为 5 的子序列:
      • dp[0][4] = 4,因为 s[0] == s[4]dp[1][3] = 2

最终 dp 数组填充完毕后,dp[0][4] = 4,表示最长回文子序列的长度为 4。

编辑距离问题

编辑距离(Edit Distance),也称为Levenshtein距离,是一个经典的动态规划问题。它衡量两个字符串之间的相似程度,定义为将一个字符串转换成另一个字符串所需的最少操作次数。操作包括插入一个字符、删除一个字符或替换一个字符。

问题描述

给定两个字符串 word1word2,计算将 word1 转换为 word2 所需的最少操作次数。

动态规划解决方案

我们可以通过动态规划来解决这个问题。动态规划的核心思想是将复杂问题分解为更简单的子问题,并利用子问题的解来构建原问题的解。

步骤解析

  1. 定义子问题:定义 dp[i][j] 为将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最少操作次数。

  2. 递推关系

    • 如果 word1[i-1] == word2[j-1],则 dp[i][j] = dp[i-1][j-1],因为最后一个字符已经相同,不需要额外操作。
    • 如果 word1[i-1] != word2[j-1],则考虑三种操作:
      • 插入:dp[i][j] = dp[i][j-1] + 1,表示在 word1 插入一个字符。
      • 删除:dp[i][j] = dp[i-1][j] + 1,表示在 word1 删除一个字符。
      • 替换:dp[i][j] = dp[i-1][j-1] + 1,表示将 word1[i-1] 替换为 word2[j-1]
    • 综合上述情况,递推公式为:
      dp[i][j] = {
          dp[i-1][j-1], if word1[i-1] == word2[j-1]
          min(dp[i-1][j-1], dp[i][j-1], dp[i-1][j]) + 1, if word1[i-1] != word2[j-1]
      }
      

  3. 边界条件

    • i = 0 时,dp[0][j] = j,因为将空字符串转换为 word2 的前 j 个字符需要插入 j 次。
    • j = 0 时,dp[i][0] = i,因为将 word1 的前 i 个字符转换为空字符串需要删除 i 次。
  4. 计算顺序:从小到大计算所有子问题的解,最终得到 dp[m][n],即将 word1 的前 m 个字符转换为 word2 的前 n 个字符的最少操作次数。

代码实现

以下是用 Java 实现的编辑距离的代码:

public class EditDistance {
    public static void main(String[] args) {
        String word1 = "horse";
        String word2 = "ros";
        System.out.println(minDistance(word1, word2)); // 输出 3
    }

    public static int minDistance(String word1, String word2) {
        int m = word1.length();
        int n = word2.length();
        int[][] dp = new int[m + 1][n + 1];

        // 初始化边界条件
        for (int i = 0; i <= m; i++) {
            dp[i][0] = i;
        }
        for (int j = 0; j <= n; j++) {
            dp[0][j] = j;
        }

        // 填充 dp 数组
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    dp[i][j] = Math.min(dp[i - 1][j - 1],
                                        Math.min(dp[i][j - 1], dp[i - 1][j])) + 1;
                }
            }
        }

        // 返回编辑距离
        return dp[m][n];
    }
}

代码解析

  1. 初始化

    • 创建一个二维数组 dp,大小为 [m+1][n+1],其中 mword1 的长度,nword2 的长度。
    • 初始化边界条件:
      • dp[i][0] = i 表示将 word1 的前 i 个字符转换为空字符串需要删除 i 次。
      • dp[0][j] = j 表示将空字符串转换为 word2 的前 j 个字符需要插入 j 次。
  2. 填充 dp 数组

    • 对于每个子问题,检查 word1word2 的当前字符是否相等:
      • 如果相等,则 dp[i][j] = dp[i-1][j-1]
      • 如果不相等,则 dp[i][j] = min(dp[i-1][j-1], dp[i][j-1], dp[i-1][j]) + 1
  3. 返回结果

    • 最后返回 dp[m][n],即将 word1 的前 m 个字符转换为 word2 的前 n 个字符的最少操作次数。

示例解释

以输入 word1 = "horse"word2 = "ros" 为例:

  1. 初始化 dp 数组

    0 1 2 3
    1 0 0 0
    2 0 0 0
    3 0 0 0
    4 0 0 0
    5 0 0 0
    

  2. 填充 dp 数组

    • 对于 i = 1j = 1word1[0] != word2[0]dp[1][1] = min(dp[0][0], dp[1][0], dp[0][1]) + 1 = 1
    • 对于 i = 1j = 2word1[0] != word2[1]dp[1][2] = min(dp[0][1], dp[1][1], dp[0][2]) + 1 = 2
    • 对于 i = 1j = 3word1[0] != word2[2]dp[1][3] = min(dp[0][2], dp[1][2], dp[0][3]) + 1 = 3
    • 对于 i = 2j = 1word1[1] != word2[0]dp[2][1] = min(dp[1][0], dp[2][0], dp[1][1]) + 1 = 2
    • 对于 i = 2j = 2word1[1] == word2[1]dp[2][2] = dp[1][1] = 1
    • 对于 i = 2j = 3word1[1] != word2[2]dp[2][3] = min(dp[1][2], dp[2][2], dp[1][3]) + 1 = 2
    • 依此类推,最终填充完 dp 数组,得到 dp[5][3] = 3

最长递增子序列

最长递增子序列(Longest Increasing Subsequence, LIS)问题是一个经典的动态规划问题。给定一个整数数组,要求找到其中最长的递增子序列的长度。子序列不要求是连续的,但必须保持原数组中的相对顺序。

问题描述

给定一个整数数组 nums,找到其中最长的递增子序列,并返回该序列的长度。

动态规划解决方案

我们可以通过动态规划来解决这个问题。动态规划的核心思想是将复杂问题分解为更简单的子问题,并利用子问题的解来构建原问题的解。

步骤解析

  1. 定义子问题:定义 dp[i] 为以 nums[i] 结尾的最长递增子序列的长度。

  2. 递推关系

    • 对于每个 i,我们需要检查 nums[0]nums[i-1] 的所有元素 nums[j],如果 nums[j] < nums[i],则 dp[i] 可以从 dp[j] 更新过来,即 dp[i] = max(dp[i], dp[j] + 1)
    • 因此,递推公式为:
      dp[i] = max(dp[i], dp[j] + 1) for all 0 <= j < i and nums[j] < nums[i]
      

  3. 边界条件

    • 每个位置的最小值都是1,因为每个元素自身可以成为一个递增子序列。
  4. 计算顺序:从前往后依次计算所有子问题的解,最终取 dp 数组中的最大值,即为最长递增子序列的长度。

代码实现

以下是用 Java 实现的最长递增子序列的代码:

public class LongestIncreasingSubsequence {
    public static void main(String[] args) {
        int[] nums = {10, 9, 2, 5, 3, 7, 101, 18};
        System.out.println(lengthOfLIS(nums)); // 输出 4
    }

    public static int lengthOfLIS(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }

        int n = nums.length;
        int[] dp = new int[n];
        int maxLength = 1;

        // 初始化 dp 数组
        for (int i = 0; i < n; i++) {
            dp[i] = 1;
        }

        // 填充 dp 数组
        for (int i = 1; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            maxLength = Math.max(maxLength, dp[i]);
        }

        return maxLength; // 返回最长递增子序列的长度
    }
}

代码解析

  1. 初始化

    • 创建一个 dp 数组,大小为 n,用于存储以每个元素结尾的最长递增子序列的长度。
    • 初始化 dp 数组中的每个元素为 1,因为每个元素自身可以成为一个递增子序列。
  2. 填充 dp 数组

    • 对于每个 i,检查 nums[0]nums[i-1] 的所有元素 nums[j],如果 nums[j] < nums[i],则 dp[i] 可以从 dp[j] 更新过来,即 dp[i] = max(dp[i], dp[j] + 1)
    • 在每次更新 dp[i] 之后,更新 maxLength 以记录当前找到的最长递增子序列的长度。
  3. 返回结果

    • 最后返回 maxLength,即最长递增子序列的长度。

示例解释

以输入 nums = {10, 9, 2, 5, 3, 7, 101, 18} 为例:

  1. 初始化 dp 数组

    dp = [1, 1, 1, 1, 1, 1, 1, 1]
    

  2. 填充 dp 数组

    • 对于 i = 1nums[0] > nums[1],所以 dp[1] 不变。
    • 对于 i = 2nums[0] > nums[2]nums[1] > nums[2],所以 dp[2] 不变。
    • 对于 i = 3nums[2] < nums[3],所以 dp[3] = dp[2] + 1 = 2
    • 对于 i = 4nums[2] < nums[4],所以 dp[4] = dp[2] + 1 = 2nums[3] > nums[4],所以 dp[4] 不变。
    • 对于 i = 5nums[2] < nums[5]nums[3] < nums[5]nums[4] < nums[5],所以 dp[5] = max(dp[2], dp[3], dp[4]) + 1 = 3
    • 对于 i = 6nums[2] < nums[6]nums[3] < nums[6]nums[4] < nums[6]nums[5] < nums[6],所以 dp[6] = max(dp[2], dp[3], dp[4], dp[5]) + 1 = 4
    • 对于 i = 7nums[2] < nums[7]nums[3] < nums[7]nums[4] < nums[7]nums[5] < nums[7],所以 dp[7] = max(dp[2], dp[3], dp[4], dp[5]) + 1 = 4

最终 dp 数组为:

dp = [1, 1, 1, 2, 2, 3, 4, 4]

最长递增子序列的长度为 4

股票买卖问题

股票买卖问题是一类经典的动态规划问题,通常有多个变种。以下是几个常见的变种及其解决方案:

1. 最佳买卖股票时机问题(一次交易)

问题描述

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

动态规划解决方案

我们可以通过一次遍历的方法来解决这个问题。在遍历的过程中,记录历史最低点,然后计算当前价格与历史最低点的差值作为当前的最大利润,并不断更新最大利润。

代码实现
public class MaxProfitOneTransaction {
    public static void main(String[] args) {
        int[] prices = {7, 1, 5, 3, 6, 4};
        System.out.println(maxProfit(prices)); // 输出 5
    }

    public static int maxProfit(int[] prices) {
        if (prices == null || prices.length == 0) {
            return 0;
        }

        int minPrice = prices[0];
        int maxProfit = 0;

        for (int i = 1; i < prices.length; i++) {
            minPrice = Math.min(minPrice, prices[i]);
            maxProfit = Math.max(maxProfit, prices[i] - minPrice);
        }

        return maxProfit;
    }
}

2. 最佳买卖股票时机问题(二次交易)

问题描述

你最多可以完成两笔交易(即买入和卖出两次股票)。但是,你必须在再次购买前出售掉之前的股票。

动态规划解决方案

我们可以用四个变量来跟踪两个买卖的过程:

  • firstBuy:第一次买入的最大花费。
  • firstSell:第一次卖出的最大利润。
  • secondBuy:第二次买入的最大花费(考虑第一次卖出的利润)。
  • secondSell:第二次卖出的最大利润。
代码实现
public class MaxProfitTwoTransactions {
    public static void main(String[] args) {
        int[] prices = {3, 3, 5, 0, 0, 3, 1, 4};
        System.out.println(maxProfit(prices)); // 输出 6
    }

    public static int maxProfit(int[] prices) {
        int firstBuy = Integer.MIN_VALUE, firstSell = 0;
        int secondBuy = Integer.MIN_VALUE, secondSell = 0;

        for (int price : prices) {
            firstBuy = Math.max(firstBuy, -price);
            firstSell = Math.max(firstSell, firstBuy + price);
            secondBuy = Math.max(secondBuy, firstSell - price);
            secondSell = Math.max(secondSell, secondBuy + price);
        }

        return secondSell;
    }
}

3. 最佳买卖股票时机问题(多次交易)

问题描述

你可以尽可能地完成多次交易(即买入和卖出多次股票)。但是,你不能同时参与多笔交易(即你必须在再次购买前出售掉之前的股票)。

动态规划解决方案

我们可以简单地在每次有利润的时候进行买卖。

代码实现
 
public class MaxProfitMultipleTransactions {
    public static void main(String[] args) {
        int[] prices = {7, 1, 5, 3, 6, 4};
        System.out.println(maxProfit(prices)); // 输出 7
    }

    public static int maxProfit(int[] prices) {
        int maxProfit = 0;

        for (int i = 1; i < prices.length; i++) {
            if (prices[i] > prices[i - 1]) {
                maxProfit += prices[i] - prices[i - 1];
            }
        }

        return maxProfit;
    }
}

4. 最佳买卖股票时机问题(含冷冻期)

问题描述

你可以尽可能地完成多次交易,但是你卖出股票后有一天的冷冻期(即你必须在卖出股票后的第二天才能买入股票)。

动态规划解决方案

我们可以用三个状态变量来解决这个问题:

  • hold:持有股票时的最大利润。
  • sold:当天卖出股票时的最大利润。
  • rest:当天是冷冻期或未交易时的最大利润。
代码实现
 
public class MaxProfitWithCooldown {
    public static void main(String[] args) {
        int[] prices = {1, 2, 3, 0, 2};
        System.out.println(maxProfit(prices)); // 输出 3
    }

    public static int maxProfit(int[] prices) {
        if (prices == null || prices.length == 0) {
            return 0;
        }

        int n = prices.length;
        int[] hold = new int[n];
        int[] sold = new int[n];
        int[] rest = new int[n];

        hold[0] = -prices[0];
        sold[0] = 0;
        rest[0] = 0;

        for (int i = 1; i < n; i++) {
            hold[i] = Math.max(hold[i - 1], rest[i - 1] - prices[i]);
            sold[i] = hold[i - 1] + prices[i];
            rest[i] = Math.max(rest[i - 1], sold[i - 1]);
        }

        return Math.max(sold[n - 1], rest[n - 1]);
    }
}

5. 最佳买卖股票时机问题(含手续费)

问题描述

你可以尽可能地完成多次交易,但是每次交易需要支付手续费。

动态规划解决方案

我们可以用两个状态变量来解决这个问题:

  • cash:不持有股票时的最大利润。
  • hold:持有股票时的最大利润。
代码实现
 
public class MaxProfitWithFee {
    public static void main(String[] args) {
        int[] prices = {1, 3, 2, 8, 4, 9};
        int fee = 2;
        System.out.println(maxProfit(prices, fee)); // 输出 8
    }

    public static int maxProfit(int[] prices, int fee) {
        int cash = 0;
        int hold = -prices[0];

        for (int price : prices) {
            cash = Math.max(cash, hold + price - fee);
            hold = Math.max(hold, cash - price);
        }

        return cash;
    }
}

 最短路径问题

最短路径问题是图论中的一个经典问题,通常用于在图中找到两个节点之间的最短路径。最常见的最短路径算法有 Dijkstra 算法、Bellman-Ford 算法和 Floyd-Warshall 算法。下面介绍这些算法的解决方案和代码实现。

1. Dijkstra 算法

Dijkstra 算法用于找到从单个源节点到图中所有其他节点的最短路径。它要求边的权重为非负数。

算法步骤
  1. 初始化:设置起点的距离为0,其余节点的距离为无穷大。将所有节点标记为未访问。
  2. 选择未访问节点中距离最小的节点作为当前节点。
  3. 更新当前节点的邻居节点的距离。
  4. 将当前节点标记为已访问。
  5. 重复步骤2-4,直到所有节点都已访问。
代码实现
 
import java.util.Arrays;
import java.util.PriorityQueue;

public class Dijkstra {
    static class Node implements Comparable<Node> {
        int vertex, distance;

        Node(int vertex, int distance) {
            this.vertex = vertex;
            this.distance = distance;
        }

        public int compareTo(Node other) {
            return Integer.compare(this.distance, other.distance);
        }
    }

    public static int[] dijkstra(int[][] graph, int src) {
        int V = graph.length;
        int[] dist = new int[V];
        boolean[] visited = new boolean[V];

        Arrays.fill(dist, Integer.MAX_VALUE);
        dist[src] = 0;

        PriorityQueue<Node> pq = new PriorityQueue<>();
        pq.offer(new Node(src, 0));

        while (!pq.isEmpty()) {
            Node current = pq.poll();
            int u = current.vertex;

            if (visited[u]) continue;
            visited[u] = true;

            for (int v = 0; v < V; v++) {
                if (!visited[v] && graph[u][v] != 0 && dist[u] + graph[u][v] < dist[v]) {
                    dist[v] = dist[u] + graph[u][v];
                    pq.offer(new Node(v, dist[v]));
                }
            }
        }

        return dist;
    }

    public static void main(String[] args) {
        int[][] graph = {
            {0, 10, 20, 0, 0, 0},
            {10, 0, 0, 50, 10, 0},
            {20, 0, 0, 20, 33, 0},
            {0, 50, 20, 0, 20, 2},
            {0, 10, 33, 20, 0, 1},
            {0, 0, 0, 2, 1, 0}
        };
        int src = 0;
        int[] distances = dijkstra(graph, src);

        System.out.println("Vertex\tDistance from Source");
        for (int i = 0; i < distances.length; i++) {
            System.out.println(i + "\t\t" + distances[i]);
        }
    }
}

2. Bellman-Ford 算法

Bellman-Ford 算法也用于找到从单个源节点到图中所有其他节点的最短路径。它能够处理包含负权重边的图,但不能处理负权重环。

算法步骤
  1. 初始化:设置起点的距离为0,其余节点的距离为无穷大。
  2. 迭代 V-1 次,对每条边进行松弛操作。
  3. 检查是否存在负权重环。
代码实现
 
import java.util.Arrays;

public class BellmanFord {
    static class Edge {
        int src, dest, weight;

        Edge(int src, int dest, int weight) {
            this.src = src;
            this.dest = dest;
            this.weight = weight;
        }
    }

    public static void bellmanFord(int V, int E, Edge[] edges, int src) {
        int[] dist = new int[V];
        Arrays.fill(dist, Integer.MAX_VALUE);
        dist[src] = 0;

        for (int i = 1; i < V; i++) {
            for (int j = 0; j < E; j++) {
                int u = edges[j].src;
                int v = edges[j].dest;
                int weight = edges[j].weight;
                if (dist[u] != Integer.MAX_VALUE && dist[u] + weight < dist[v]) {
                    dist[v] = dist[u] + weight;
                }
            }
        }

        for (int j = 0; j < E; j++) {
            int u = edges[j].src;
            int v = edges[j].dest;
            int weight = edges[j].weight;
            if (dist[u] != Integer.MAX_VALUE && dist[u] + weight < dist[v]) {
                System.out.println("Graph contains negative weight cycle");
                return;
            }
        }

        printSolution(dist);
    }

    public static void printSolution(int[] dist) {
        System.out.println("Vertex\tDistance from Source");
        for (int i = 0; i < dist.length; i++) {
            System.out.println(i + "\t\t" + dist[i]);
        }
    }

    public static void main(String[] args) {
        int V = 5;
        int E = 8;
        Edge[] edges = new Edge[E];
        edges[0] = new Edge(0, 1, -1);
        edges[1] = new Edge(0, 2, 4);
        edges[2] = new Edge(1, 2, 3);
        edges[3] = new Edge(1, 3, 2);
        edges[4] = new Edge(1, 4, 2);
        edges[5] = new Edge(3, 2, 5);
        edges[6] = new Edge(3, 1, 1);
        edges[7] = new Edge(4, 3, -3);

        bellmanFord(V, E, edges, 0);
    }
}

3. Floyd-Warshall 算法

Floyd-Warshall 算法用于找到所有节点对之间的最短路径。它使用动态规划来解决问题,可以处理负权重边,但不能处理负权重环。

算法步骤
  1. 初始化:创建一个距离矩阵 distdist[i][j] 表示从节点 i 到节点 j 的距离。
  2. 对于每对节点 (i, j),更新 dist[i][j]dist[i][k] + dist[k][j] 的最小值,遍历所有可能的中间节点 k
代码实现
 
public class FloydWarshall {
    final static int INF = 99999;

    public static void floydWarshall(int[][] graph) {
        int V = graph.length;
        int[][] dist = new int[V][V];

        for (int i = 0; i < V; i++) {
            for (int j = 0; j < V; j++) {
                dist[i][j] = graph[i][j];
            }
        }

        for (int k = 0; k < V; k++) {
            for (int i = 0; i < V; i++) {
                for (int j = 0; j < V; j++) {
                    if (dist[i][k] + dist[k][j] < dist[i][j]) {
                        dist[i][j] = dist[i][k] + dist[k][j];
                    }
                }
            }
        }

        printSolution(dist);
    }

    public static void printSolution(int[][] dist) {
        int V = dist.length;
        System.out.println("Shortest distances between every pair of vertices:");
        for (int i = 0; i < V; i++) {
            for (int j = 0; j < V; j++) {
                if (dist[i][j] == INF) {
                    System.out.print("INF ");
                } else {
                    System.out.print(dist[i][j] + " ");
                }
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
        int[][] graph = {
            {0, 5, INF, 10},
            {INF, 0, 3, INF},
            {INF, INF, 0, 1},
            {INF, INF, INF, 0}
        };
        floydWarshall(graph);
    }
}

结论

通过使用 Dijkstra、Bellman-Ford 和 Floyd-Warshall 算法,我们可以解决不同类型的最短路径问题。这些算法各有优劣,适用于不同的应用场景。

最大路径和问题

最大路径和问题可以出现在不同的上下文中,例如树形结构或网格。以下是两种常见的最大路径和问题及其解决方案:

1. 二叉树中的最大路径和
问题描述

给定一个二叉树中的节点,找出从树中任意节点到任意节点的路径上的最大路径和。路径必须至少包含一个节点,并且不一定经过根节点。

动态规划解决方案

我们可以使用递归的方式来解决这个问题。对于每一个节点,我们需要计算两种值:

  1. 单边最大路径和:从当前节点到叶节点的最大路径和。
  2. 全局最大路径和:可能包括当前节点的左子树、右子树以及当前节点本身的路径和。

我们使用全局变量来记录全局最大路径和,并在递归过程中不断更新这个值。

代码实现
 
class TreeNode {
    int val;
    TreeNode left, right;
    TreeNode(int x) { val = x; }
}

public class MaxPathSum {
    private int maxSum = Integer.MIN_VALUE;

    public int maxPathSum(TreeNode root) {
        maxGain(root);
        return maxSum;
    }

    private int maxGain(TreeNode node) {
        if (node == null) return 0;

        int leftGain = Math.max(maxGain(node.left), 0);
        int rightGain = Math.max(maxGain(node.right), 0);

        int priceNewPath = node.val + leftGain + rightGain;
        maxSum = Math.max(maxSum, priceNewPath);

        return node.val + Math.max(leftGain, rightGain);
    }

    public static void main(String[] args) {
        TreeNode root = new TreeNode(-10);
        root.left = new TreeNode(9);
        root.right = new TreeNode(20);
        root.right.left = new TreeNode(15);
        root.right.right = new TreeNode(7);

        MaxPathSum solution = new MaxPathSum();
        System.out.println(solution.maxPathSum(root)); // 输出 42
    }
}

2. 网格中的最大路径和
问题描述

给定一个包含非负整数的 m x n 网格,找到一条从左上角到右下角的路径,使得路径上的数字总和最大。每次只能向下或者向右移动一步。

动态规划解决方案

我们可以使用动态规划来解决这个问题。定义 dp[i][j] 为从左上角到位置 (i, j) 的路径上的最大数字和。dp[i][j] 的值取决于 dp[i-1][j]dp[i][j-1]

代码实现
 
public class MaxPathSumInGrid {
    public static int maxPathSum(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        int[][] dp = new int[m][n];

        dp[0][0] = grid[0][0];

        for (int i = 1; i < m; i++) {
            dp[i][0] = dp[i - 1][0] + grid[i][0];
        }

        for (int j = 1; j < n; j++) {
            dp[0][j] = dp[0][j - 1] + grid[0][j];
        }

        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
            }
        }

        return dp[m - 1][n - 1];
    }

    public static void main(String[] args) {
        int[][] grid = {
            {5, 3, 2, 1},
            {1, 2, 10, 1},
            {4, 3, 1, 7}
        };
        System.out.println(maxPathSum(grid)); // 输出 28
    }
}

代码解析

1. 二叉树中的最大路径和
  1. maxGain方法

    • 递归地计算左子树和右子树的最大路径和(如果为负数则取0)。
    • 计算通过当前节点的路径和,并更新全局最大路径和 maxSum
    • 返回从当前节点到叶节点的最大路径和,用于递归调用的上一层。
  2. maxPathSum方法

    • 调用 maxGain 方法,并返回全局最大路径和 maxSum
2. 网格中的最大路径和
  1. dp数组初始化

    • 初始化左上角的路径和为 grid[0][0]
    • 初始化第一列和第一行的路径和。
  2. 填充dp数组

    • 遍历网格,从左上角到右下角,计算每个位置的最大路径和。
  3. 返回结果

    • 返回右下角的路径和,即为从左上角到右下角的最大路径和。

通过这些方法,我们可以高效地解决二叉树和网格中的最大路径和问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值