系列文章目录
算法及数据结构系列 - 二分查找
算法及数据结构系列 - BFS算法
框架思路
动态规划问题的一般形式就是求最值
。动态规划其实是运筹学的一种最优化方法,只不过在计算机问题上应用比较多,比如说求最长递增子序列,最小编辑距离等等。
既然是要求最值,核心问题是什么呢?求解动态规划的核心问题是穷举
。因为要求最值,肯定要把所有可行的答案穷举出来,然后在其中找最值呗。
首先,动态规划的穷举有点特别,因为这类问题存在「重叠子问题」
,如果暴力穷举的话效率会极其低下,所以需要「备忘录」
或者「DP table」
来优化穷举过程,避免不必要的计算。
而且,动态规划问题一定会具备「最优子结构」
,才能通过子问题的最值得到原问题的最值。
另外,虽然动态规划的核心思想就是穷举求最值,但是问题可以千变万化,穷举所有可行解其实并不是一件容易的事,只有列出正确的「状态转移方程」
才能正确地穷举。
以上提到的重叠子问题、最优子结构、状态转移方程就是动态规划三要素。我来提供我研究出来的一个思维框架,辅助你思考状态转移方程:
明确「状态」
-> 定义 dp 数组/函数的含义 -> 明确「选择」
-> 明确 base case
。
子序列问题解题模板
一维dp数组
时间复杂 O(n^2)
int n = array.length;
int[] dp = new int[n];
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
dp[i] = 最值(dp[i], dp[j] + ...)
}
}
举例:
300.最长上升子序列
二维dp数组
int n = arr.length;
int[][] dp = new dp[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (arr[i] == arr[j])
dp[i][j] = dp[i][j] + ...
else
dp[i][j] = 最值(...)
}
}
2.1 涉及两个字符串/数组时(比如最长公共子序列),dp 数组的含义如下:
在子数组 arr1[0…i] 和子数组 arr2[0…j] 中,我们要求的子序列(最长公共子序列)长度为 dp[i][j]。
举例:
1143.最长公共子序列
72.编辑距离
2.2 只涉及一个字符串/数组时(比如最长回文子序列),dp 数组的含义如下:
在子数组 array[i…j] 中,我们要求的子序列(最长回文子序列)的长度为 dp[i][j]。
举例:
经典题型
322. 零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
暴力递归
class Solution {
public int coinChange(int[] coins, int amount) {
return dp(coins, amount);
}
public int dp(int[] coins, int amount){
// Base Case
if(amount == 0){
return 0;
}
if(amount < 0){
return -1;
}
int res = Integer.MAX_VALUE;
for(int coin: coins){
int sub = dp(coins, amount - coin);
if(sub == -1){
// 不符合要求
continue;
}
res = Math.min(res, sub + 1);
}
res = (res == Integer.MAX_VALUE? -1:res);
return res;
}
}
带备忘录的暴力递归
class Solution {
public int coinChange(int[] coins, int amount) {
Map<Integer, Integer> memo = new HashMap<Integer, Integer>();
return dp(coins, amount, memo);
}
public int dp(int[] coins, int amount, Map<Integer, Integer> memo){
if(amount == 0){
return 0;
}
if(amount < 0){
return -1;
}
if(memo.containsKey(amount)){
return memo.get(amount);
}
int res = Integer.MAX_VALUE;
for(int coin: coins){
int sub = dp(coins, amount - coin, memo);
if(sub == -1){
continue;
}
res = Math.min(res, sub + 1);
}
res = (res == Integer.MAX_VALUE? -1:res);
memo.put(amount, res);
return res;
}
}
动态规划
class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount+1];
for(int i = 0; i < amount + 1; i++){
dp[i] = amount + 1; // 初始化为 amount+1 相当于是正无穷
}
dp[0] = 0; // Base Case
for(int i = 0; i < amount + 1; i++){
for(int coin: coins){
if(i - coin >= 0){
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] == amount + 1? -1:dp[amount];
}
}
300. 最长上升子序列
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
提示:dp[i] 表示以i结尾的最长子序列长度
class Solution {
public int lengthOfLIS(int[] nums) {
if(nums.length == 0){
return 0;
}
int[] dp = new int[nums.length];
for(int i = 0 ; i < nums.length; i++){
dp[i] = 1;
}
dp[0] = 1;
int maxLen = 1;
for(int i = 1; i < nums.length; i ++){
for(int j = 0; j <i; j++){
if(nums[j] < nums[i]){
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
maxLen = Math.max(maxLen, dp[i]);
}
return maxLen;
}
}
1143. 最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。
示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace",它的长度为 3。
第一步,一定要明确 dp 数组的含义。对于两个字符串的动态规划问题,套路是通用的。
比如说对于字符串 s1 和 s2,一般来说都要构造一个这样的 DP table:
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int[][] dp = new int[text1.length() + 1][text2.length() + 1];
for(int i = 0; i < text1.length() + 1; i ++){
dp[i][0] = 0;
}
for(int i = 0; i < text2.length() + 1; i++){
dp[0][i] = 0;
}
for(int i = 1; i < text1.length() + 1; i ++){
for(int j=1; j < text2.length() + 1; j ++){
if(text1.charAt(i - 1) == text2.charAt(j - 1)){
dp[i][j] = 1 + dp[i-1][j-1];
}else{
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
}
}
}
return dp[text1.length()][text2.length()];
}
}
72. 编辑距离
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
示例 1:
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')