爬楼梯问题
爬楼梯问题是经典的动态规划问题,通常问题描述如下:
问题描述:一个人站在楼梯的底部,需要到达楼梯的顶部。总共有 n
级台阶,每次可以向上走一阶或两阶。问有多少种不同的方法可以到达楼梯的顶部。
解决方案
可以用动态规划来解决这个问题。动态规划的核心思想是将复杂问题分解为更简单的子问题,利用子问题的解来构建原问题的解。
步骤解析
- 定义子问题:定义
dp[i]
为到达第i
级台阶的方法总数。 - 递推关系:到达第
i
级台阶有两种方式:- 从第
i-1
级台阶迈一步到达第i
级台阶。 - 从第
i-2
级台阶迈两步到达第i
级台阶。 因此,递推公式为:dp[i] = dp[i-1] + dp[i-2]
- 从第
- 边界条件:
dp[0] = 1
:只有一种方法,即不动。dp[1] = 1
:只有一种方法,从第 0 级台阶迈一步到第 1 级台阶。
- 计算顺序:从底向上计算
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 级台阶的方法总数
}
}
代码解析
- 边界条件:
- 当
n <= 1
时,直接返回 1。
- 当
- 初始化:
- 创建一个大小为
n + 1
的dp
数组,其中dp[0]
和dp[1]
初始化为 1。
- 创建一个大小为
- 递推关系:
- 从第 2 级台阶开始,依次计算每一级台阶的方法数,使用递推公式
dp[i] = dp[i - 1] + dp[i - 2]
。
- 从第 2 级台阶开始,依次计算每一级台阶的方法数,使用递推公式
- 返回结果:
- 最后返回
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 级台阶的方法总数
}
}
代码解析
- 边界条件:
- 当
n <= 1
时,直接返回 1。
- 当
- 初始化:
- 使用
prev1
和prev2
分别保存前两级台阶的方法数,初始值为 1。
- 使用
- 递推关系:
- 依次计算每一级台阶的方法数,用
current
保存当前级台阶的方法数,并更新prev1
和prev2
。
- 依次计算每一级台阶的方法数,用
- 返回结果:
- 最后返回
prev1
,即到达第n
级台阶的方法总数。
- 最后返回
通过上述优化,空间复杂度从 O(n) 降到了 O(1),但时间复杂度仍然保持为 O(n)。
通过动态规划,我们可以高效地解决爬楼梯问题。动态规划的核心在于定义子问题、找到递推关系、确定边界条件,并依此计算每个子问题的解。
0/1 背包问题
问题描述:给定一个容量为 W
的背包和 N
个物品,每个物品有一个重量和价值。要求在不超过背包容量的情况下,选择若干物品使得这些物品的总价值最大。
解决方案
动态规划是解决 0/1 背包问题的有效方法。我们通过构建一个二维数组 dp
来记录在不同的容量和物品选择情况下的最优解。
步骤解析
-
定义子问题:定义
dp[i][w]
为前i
个物品在容量为w
的情况下的最大价值。 -
递推关系:
- 如果不选择第
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])
- 如果不选择第
-
边界条件:
- 当
i = 0
或w = 0
时,dp[i][w] = 0
,因为没有物品或容量为 0 时的最大价值为 0。
- 当
-
计算顺序:从小到大计算所有子问题的解,最终得到
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 时的最大价值
}
}
代码解析
-
初始化:
- 创建一个二维数组
dp
,大小为[N+1][W+1]
,N
是物品数量,W
是背包容量。 dp[i][w]
表示前i
个物品在容量为w
时的最大价值。
- 创建一个二维数组
-
填充
dp
数组:- 逐个物品进行处理,从
1
到N
。 - 对于每个物品,从容量
1
到W
依次计算。 - 如果当前物品的重量小于或等于当前容量
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]
。
- 逐个物品进行处理,从
-
返回结果:
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 时的最大价值
}
}
代码解析
-
初始化:
- 创建一个一维数组
dp
,大小为[W+1]
。
- 创建一个一维数组
-
填充
dp
数组:- 逐个物品进行处理,从
0
到N-1
。 - 对于每个物品,从容量
W
逆序计算到weights[i]
,以确保每个物品只被考虑一次。 - 更新
dp[w]
为以下两者的最大值:- 不选择当前物品时的最大价值
dp[w]
。 - 选择当前物品时的最大价值
dp[w-weights[i]] + values[i]
。
- 不选择当前物品时的最大价值
- 逐个物品进行处理,从
-
返回结果:
dp[W]
即为在容量为W
的情况下,前N
个物品的最大价值。
完全背包问题
完全背包问题是背包问题的一个变种,不同之处在于每种物品可以选择多次。也就是说,每种物品的数量是无限的。我们可以使用动态规划来解决这个问题,方法类似于 0/1 背包问题,但在状态转移时稍作调整。
问题描述
给定一个容量为 W
的背包和 N
个物品,每个物品有一个重量和价值,且每种物品可以选取多次。要求在不超过背包容量的情况下,选择若干物品使得这些物品的总价值最大。
解决方案
我们仍然使用动态规划来解决这个问题。与 0/1 背包问题的不同之处在于,对于每个物品,我们会在状态转移中考虑多次选取该物品的情况。
步骤解析
-
定义子问题:定义
dp[w]
为容量为w
时的最大价值。 -
递推关系:
- 对于每个物品
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])
。
- 对于每个物品
-
边界条件:
- 当
w = 0
时,dp[w] = 0
,因为没有物品可以放入容量为 0 的背包中。
- 当
-
计算顺序:从小到大计算所有子问题的解,直至
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 时的最大价值
}
}
代码解析
-
初始化:
- 创建一个一维数组
dp
,大小为[W + 1]
。
- 创建一个一维数组
-
填充
dp
数组:- 对于每个物品,从
0
到N-1
进行处理。 - 对于每个物品,从容量
weights[i]
开始,顺序计算到W
,以允许每个物品多次选取。 - 更新
dp[w]
为以下两者的最大值:- 不选择当前物品时的最大价值
dp[w]
。 - 选择当前物品时的最大价值
dp[w - weights[i]] + values[i]
。
- 不选择当前物品时的最大价值
- 对于每个物品,从
-
返回结果:
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
。- ...
- 对于物品 1 (重量 2,价值 3):
最终计算得到 dp[8]
即为最大价值。
最长回文子序列
最长回文子序列(Longest Palindromic Subsequence, LPS)是一个经典的动态规划问题。给定一个字符串,要求找到其最长的回文子序列的长度。回文子序列是指一个子序列,其字符顺序正读和反读都相同。
问题描述
给定一个字符串 s
,找到 s
的最长回文子序列的长度。
动态规划解决方案
我们可以通过动态规划来解决这个问题。动态规划的核心思想是将复杂问题分解为更简单的子问题,并利用子问题的解来构建原问题的解。
步骤解析
-
定义子问题:定义
dp[i][j]
表示字符串s
从第i
个字符到第j
个字符的最长回文子序列的长度。 -
递推关系:
- 如果
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
个字符,取两者的最大值。
- 如果
-
边界条件:
- 当
i == j
时,dp[i][j] = 1
,因为单个字符本身就是一个回文子序列。 - 当
i > j
时,dp[i][j] = 0
,因为这是无效的子序列。
- 当
-
计算顺序:我们需要从短的子序列开始,逐步计算更长的子序列,最终得到整个字符串的最长回文子序列的长度。
代码实现
以下是用 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];
}
}
代码解析
-
初始化:
- 创建一个二维数组
dp
,大小为n x n
,其中n
是字符串s
的长度。 - 初始化单个字符的回文子序列长度为
1
,即dp[i][i] = 1
。
- 创建一个二维数组
-
填充
dp
数组:- 我们从长度为
2
的子序列开始,逐步计算更长的子序列。 - 对于每个子序列,检查首尾字符是否相等:
- 如果相等,表示首尾字符可以构成一个回文子序列,则
dp[i][j] = dp[i+1][j-1] + 2
。 - 如果不相等,表示要么不考虑首字符,要么不考虑尾字符,则
dp[i][j] = max(dp[i+1][j], dp[i][j-1])
。
- 如果相等,表示首尾字符可以构成一个回文子序列,则
- 我们从长度为
-
返回结果:
- 最后返回
dp[0][n-1]
,即整个字符串的最长回文子序列的长度。
- 最后返回
示例解释
以输入字符串 s = "bbbab"
为例:
-
初始化:
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
-
填充
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
。
- 对于长度为 2 的子序列:
最终 dp
数组填充完毕后,dp[0][4] = 4
,表示最长回文子序列的长度为 4。
编辑距离问题
编辑距离(Edit Distance),也称为Levenshtein距离,是一个经典的动态规划问题。它衡量两个字符串之间的相似程度,定义为将一个字符串转换成另一个字符串所需的最少操作次数。操作包括插入一个字符、删除一个字符或替换一个字符。
问题描述
给定两个字符串 word1
和 word2
,计算将 word1
转换为 word2
所需的最少操作次数。
动态规划解决方案
我们可以通过动态规划来解决这个问题。动态规划的核心思想是将复杂问题分解为更简单的子问题,并利用子问题的解来构建原问题的解。
步骤解析
-
定义子问题:定义
dp[i][j]
为将word1
的前i
个字符转换为word2
的前j
个字符所需的最少操作次数。 -
递推关系:
- 如果
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] }
- 如果
-
边界条件:
- 当
i = 0
时,dp[0][j] = j
,因为将空字符串转换为word2
的前j
个字符需要插入j
次。 - 当
j = 0
时,dp[i][0] = i
,因为将word1
的前i
个字符转换为空字符串需要删除i
次。
- 当
-
计算顺序:从小到大计算所有子问题的解,最终得到
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];
}
}
代码解析
-
初始化:
- 创建一个二维数组
dp
,大小为[m+1][n+1]
,其中m
是word1
的长度,n
是word2
的长度。 - 初始化边界条件:
dp[i][0] = i
表示将word1
的前i
个字符转换为空字符串需要删除i
次。dp[0][j] = j
表示将空字符串转换为word2
的前j
个字符需要插入j
次。
- 创建一个二维数组
-
填充
dp
数组:- 对于每个子问题,检查
word1
和word2
的当前字符是否相等:- 如果相等,则
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
。
- 如果相等,则
- 对于每个子问题,检查
-
返回结果:
- 最后返回
dp[m][n]
,即将word1
的前m
个字符转换为word2
的前n
个字符的最少操作次数。
- 最后返回
示例解释
以输入 word1 = "horse"
和 word2 = "ros"
为例:
-
初始化
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
-
填充
dp
数组:- 对于
i = 1
,j = 1
,word1[0] != word2[0]
,dp[1][1] = min(dp[0][0], dp[1][0], dp[0][1]) + 1 = 1
。 - 对于
i = 1
,j = 2
,word1[0] != word2[1]
,dp[1][2] = min(dp[0][1], dp[1][1], dp[0][2]) + 1 = 2
。 - 对于
i = 1
,j = 3
,word1[0] != word2[2]
,dp[1][3] = min(dp[0][2], dp[1][2], dp[0][3]) + 1 = 3
。 - 对于
i = 2
,j = 1
,word1[1] != word2[0]
,dp[2][1] = min(dp[1][0], dp[2][0], dp[1][1]) + 1 = 2
。 - 对于
i = 2
,j = 2
,word1[1] == word2[1]
,dp[2][2] = dp[1][1] = 1
。 - 对于
i = 2
,j = 3
,word1[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
,找到其中最长的递增子序列,并返回该序列的长度。
动态规划解决方案
我们可以通过动态规划来解决这个问题。动态规划的核心思想是将复杂问题分解为更简单的子问题,并利用子问题的解来构建原问题的解。
步骤解析
-
定义子问题:定义
dp[i]
为以nums[i]
结尾的最长递增子序列的长度。 -
递推关系:
- 对于每个
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]
- 对于每个
-
边界条件:
- 每个位置的最小值都是1,因为每个元素自身可以成为一个递增子序列。
-
计算顺序:从前往后依次计算所有子问题的解,最终取
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; // 返回最长递增子序列的长度
}
}
代码解析
-
初始化:
- 创建一个
dp
数组,大小为n
,用于存储以每个元素结尾的最长递增子序列的长度。 - 初始化
dp
数组中的每个元素为1
,因为每个元素自身可以成为一个递增子序列。
- 创建一个
-
填充
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
以记录当前找到的最长递增子序列的长度。
- 对于每个
-
返回结果:
- 最后返回
maxLength
,即最长递增子序列的长度。
- 最后返回
示例解释
以输入 nums = {10, 9, 2, 5, 3, 7, 101, 18}
为例:
-
初始化
dp
数组:dp = [1, 1, 1, 1, 1, 1, 1, 1]
-
填充
dp
数组:- 对于
i = 1
,nums[0] > nums[1]
,所以dp[1]
不变。 - 对于
i = 2
,nums[0] > nums[2]
,nums[1] > nums[2]
,所以dp[2]
不变。 - 对于
i = 3
,nums[2] < nums[3]
,所以dp[3] = dp[2] + 1 = 2
。 - 对于
i = 4
,nums[2] < nums[4]
,所以dp[4] = dp[2] + 1 = 2
;nums[3] > nums[4]
,所以dp[4]
不变。 - 对于
i = 5
,nums[2] < nums[5]
,nums[3] < nums[5]
,nums[4] < nums[5]
,所以dp[5] = max(dp[2], dp[3], dp[4]) + 1 = 3
。 - 对于
i = 6
,nums[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 = 7
,nums[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 算法用于找到从单个源节点到图中所有其他节点的最短路径。它要求边的权重为非负数。
算法步骤
- 初始化:设置起点的距离为0,其余节点的距离为无穷大。将所有节点标记为未访问。
- 选择未访问节点中距离最小的节点作为当前节点。
- 更新当前节点的邻居节点的距离。
- 将当前节点标记为已访问。
- 重复步骤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 算法也用于找到从单个源节点到图中所有其他节点的最短路径。它能够处理包含负权重边的图,但不能处理负权重环。
算法步骤
- 初始化:设置起点的距离为0,其余节点的距离为无穷大。
- 迭代
V-1
次,对每条边进行松弛操作。 - 检查是否存在负权重环。
代码实现
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 算法用于找到所有节点对之间的最短路径。它使用动态规划来解决问题,可以处理负权重边,但不能处理负权重环。
算法步骤
- 初始化:创建一个距离矩阵
dist
,dist[i][j]
表示从节点i
到节点j
的距离。 - 对于每对节点
(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. 二叉树中的最大路径和
问题描述
给定一个二叉树中的节点,找出从树中任意节点到任意节点的路径上的最大路径和。路径必须至少包含一个节点,并且不一定经过根节点。
动态规划解决方案
我们可以使用递归的方式来解决这个问题。对于每一个节点,我们需要计算两种值:
- 单边最大路径和:从当前节点到叶节点的最大路径和。
- 全局最大路径和:可能包括当前节点的左子树、右子树以及当前节点本身的路径和。
我们使用全局变量来记录全局最大路径和,并在递归过程中不断更新这个值。
代码实现
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. 二叉树中的最大路径和
-
maxGain方法:
- 递归地计算左子树和右子树的最大路径和(如果为负数则取0)。
- 计算通过当前节点的路径和,并更新全局最大路径和
maxSum
。 - 返回从当前节点到叶节点的最大路径和,用于递归调用的上一层。
-
maxPathSum方法:
- 调用
maxGain
方法,并返回全局最大路径和maxSum
。
- 调用
2. 网格中的最大路径和
-
dp数组初始化:
- 初始化左上角的路径和为
grid[0][0]
。 - 初始化第一列和第一行的路径和。
- 初始化左上角的路径和为
-
填充dp数组:
- 遍历网格,从左上角到右下角,计算每个位置的最大路径和。
-
返回结果:
- 返回右下角的路径和,即为从左上角到右下角的最大路径和。
通过这些方法,我们可以高效地解决二叉树和网格中的最大路径和问题。