直接刷题链接直达
动态规划(Dynamic Programming, DP)是一种通过拆解子问题并利用子问题的最优解来构建整体问题的最优解的方法,通常用于具有重叠子问题和最优子结构的优化问题。
- 最优子结构:一个问题的最优解包含其子问题的最优解。例如,最短路径问题,求从A到B的最短路径,其中A到C和C到B的最短路径也必须是最优的。
- 重叠子问题:相同的子问题在递归过程中会被多次计算。例如,斐波那契数列
F(n) = F(n-1) + F(n-2)
,计算F(n-2)
时也需要计算F(n-3)
。
如果问题满足这两个特性,通常可以考虑使用动态规划。
(1)状态的定义是解决动态规划的关键。通常可以通过以下方式思考:
- 分析问题,找出“规模较小”的子问题。
- 确定状态变量,这些变量应该能唯一标识一个子问题的情况。
- 一般来说,状态变量的选取往往与问题的输入参数有关。
- 例如,背包问题中
dp[i][j]
表示前i
个物品,在总容量为j
的背包中的最优解。 - 在最长公共子序列问题中
dp[i][j]
表示s1
的前i
个字符和s2
的前j
个字符的最长公共子序列长度。
(2)状态转移方程用于描述如何从较小的子问题递推到较大的子问题。常见的方法:
- 考虑决策过程:在某个状态下,可以有哪些选择?
- 根据子问题的解构建当前问题的解。
例如:
- 斐波那契数列:
dp[n] = dp[n-1] + dp[n-2]
- 0/1 背包问题:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i])
(取或者不取当前物品)
(3)初始状态是动态规划表 dp
的起点,通常根据问题的边界条件来确定。例如:
- 斐波那契数列:
dp[0] = 0, dp[1] = 1
- 背包问题:
dp[0][j] = 0
(没有物品可选时,价值均为0)
(4)动态规划可以通过**自顶向下(带记忆化的递归)或自底向上(迭代)**来实现:
- 自顶向下:递归 + 记忆化搜索(Memoization),避免重复计算
- 自底向上:直接从小规模问题开始填表,通常是
O(n)
或O(n^2)
- 最佳折扣问题
- double calculateMinPrice(int [] counts, double [] prices, Map<int[], Double> promotions)
- counts表示一个要买的商品数量列表(如0011表示第3件和第4件都买一个),prices表示单价,promotions表示折扣表 比如 0011->9 表示第3件和第4件一起打包买打9折,输出一个最低价格
- 实现类似 动态规划_最小费用购物问题
- 设计一个模糊匹配算法,给定一个字符串列表和一个字符串,输出列表中最匹配的那个(若都不匹配可输出空串)
- 类似于 72. 编辑距离,基于字符长度配置好阈值
- 最长回文子串
- 完全平方数
- 最长递增子序列
- 正则表达式匹配
- 零钱兑换
- 鸡蛋掉落
- dp[K] [N] = 1 + max(dp[K - 1] [i - 1] + dp[K] [N - i]) + 1 (i~(1,N)) (K蛋N层,最直观的DP,还有其他解法)
- 887. 鸡蛋掉落
- Egg Dropping Dynamic Programming (画状态转移表)
- 单词拆分
编辑距离
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
class Solution {
// dp[i][j] : word1[0:i]转换成 word2[0:j] 所使用的最少操作数
// dp[i][j] = (s[i] == s[j] && dp[i-1][j-1]) or (Math.min(插删替换))
public int minDistance(String word1, String word2) {
int m = word1.length(), n = word2.length();
int[][] dp = new int[m+1][n+1];
// 初始 dp[0][j] = j; dp[i][0] = i;
for (int i = 0; i <= m; i++) {
dp[i][0] = i;
}
for (int j = 0; j <= n; j++) {
dp[0][j] = j;
}
//
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(Math.min(dp[i][j-1], dp[i-1][j]),dp[i-1][j-1])+1;
}
}
}
return dp[m][n];
}
}
最长回文子串
给你一个字符串 s,找到 s 中最长的 回文 子串。
class Solution {
// dp[i][j] s[i-j] 是否为回文串
public String longestPalindrome(String s) {
if (s.length() <= 1) return s;
boolean[][] dp = new boolean[s.length()][s.length()];
for (int i = 0; i < s.length(); i++) {
dp[i][i] = true;
}
int start = 0, maxlen = 1;
for (int len = 2; len <= s.length(); len++) {
for (int i = 0; i <= s.length() - len; i++) {
int j = i + len - 1;
if (len == 2 && s.charAt(i) == s.charAt(j)) {
dp[i][j] = true;
}else {
dp[i][j] = s.charAt(i) == s.charAt(j) && dp[i+1][j-1];
}
if (dp[i][j] && len > maxlen) {
start = i;
maxlen = len;
}
}
}
return s.substring(start, start+maxlen);
}
}
完全平方数
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
class Solution {
// dp[i] = dp[i-完全平方数]+1
public int numSquares(int n) {
int[] dp = new int[n+1];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
for (int i = 1; i <= n; i++){
for (int j = 1; j*j <= i; j++) {
dp[i] = Math.min(dp[i], dp[i-j*j]+1);
}
}
return dp[n];
}
}
最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
class Solution {
public int lengthOfLIS(int[] nums) {
// dp[i] 以i结尾 的 最长严格递增子序列的长度
int[] dp = new int[nums.length];
Arrays.fill(dp, 1);
int max = 1;
for (int i = 1; i < nums.length; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
max = Math.max(max, dp[i]);
}
return max;
}
}
正则表达式匹配
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。
‘.’ 匹配任意单个字符
‘*’ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s 的,而不是部分字符串。
class Solution {
public boolean isMatch(String s, String p) {
int n = s.length();
int m = p.length();
boolean[][] dp = new boolean[n + 1][m + 1];
// 空字符串匹配空模式
dp[0][0] = true;
// 处理 p[j-1] 是 '*' 的情况 (匹配 0 次)
for (int j = 2; j <= m; j++) {
if (p.charAt(j - 1) == '*') {
dp[0][j] = dp[0][j - 2];
}
}
// 填充 DP 数组
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (p.charAt(j - 1) == '.' || p.charAt(j - 1) == s.charAt(i - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else if (p.charAt(j - 1) == '*') {
dp[i][j] = dp[i][j - 2]; // `*` 匹配 0 次
if (p.charAt(j - 2) == s.charAt(i - 1) || p.charAt(j - 2) == '.') {
dp[i][j] = dp[i][j] || dp[i - 1][j]; // `*` 匹配多次
}
}
}
}
return dp[n][m];
}
}
零钱兑换
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
class Solution {
public int coinChange(int[] coins, int amount) {
// dp[i] :可以凑成 i 所需的 最少的硬币个数
int[] dp = new int[amount+1];
// dp[i] = dp[i-coins] + 1
Arrays.fill(dp, amount + 1);
dp[0] = 0;
Arrays.sort(coins);
for (int coin : coins) {
if (coin <= amount) {
dp[coin] = 1;
}else {
break;
}
}
for (int i = 1; i <= amount; i++) {
for (int coin : coins) {
if (coin > i) {
break;
}
dp[i] = Math.min(dp[i], dp[i-coin]+1);
}
}
return dp[amount] >= amount + 1 ? -1:dp[amount];
}
}
鸡蛋掉落
单词拆分
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
class Solution {
// dp[i] 表示 s[0:i] 是否可以拆分成 wordDict 里的单词组合。
// dp[i]=dp[j] 且 s[j:i] 在 wordDict
public boolean wordBreak(String s, List<String> wordDict) {
boolean[] dp = new boolean[s.length()+1];
dp[0] = true;
for(int i = 1; i <= s.length(); i++) {
for (int j = 0; j <= i; j++) {
if (dp[j] && wordDict.contains(s.substring(j, i))) {
dp[i] = true;
break;
}
}
}
return dp[s.length()];
}
}