文章目录
参考资料
动态规划的基本特征:
- 求最值
- 重复子问题
- 最优子结构
动态规划的基本步骤:
- 确定状态
- 确定选择
- 确定动态转移方程
- 定义dp数组,确定初始化的值
- 确定base case
- 确定遍历顺序
10.确定最终结果的定义(可以直接是dp数组的定义,也可以是dp数组的加工)
动态规划的技巧:
- 状态压缩
例题
322. 零钱兑换
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
思路一
状态:
选择:
dp定义:
遍历顺序:
代码
递归解法,时间复杂度高。
class Solution {
public int coinChange(int[] coins, int amount) {
return dp(coins,amount);
}
public int dp(int[] coins,int amount){
if(amount<0){
return -1;
}
if(amount==0){
return 0;
}
int result=Integer.MAX_VALUE;
for(int coin:coins){
int subproblem = dp(coins,amount-coin);
if(subproblem<0){
continue;
}
result = Math.min(result,subproblem+1);
}
return result==Integer.MAX_VALUE?-1:result;
}
}
备忘录优化。自顶向下。
class Solution {
public int coinChange(int[] coins, int amount) {
int[] memo = new int[amount+1];
return dp(coins,amount,memo);
}
public int dp(int[] coins,int amount,int[] memo){
if(amount<0){
return -1;
}
if(amount==0){
return 0;
}
if(memo[amount]!=0){
return memo[amount];
}
int result=Integer.MAX_VALUE;
for(int coin:coins){
int subproblem = dp(coins,amount-coin,memo);
if(subproblem<0){
continue;
}
result = Math.min(result,subproblem+1);
}
memo[amount]=result==Integer.MAX_VALUE?-1:result;
return memo[amount];
}
}
自底向上的动态规划。
class Solution {
public int coinChange(int[] coins, int amount) {
if(amount==0){
return 0;
}
int[] dp = new int[amount+1];
Arrays.fill(dp,amount+1);// 细节,由于下面是比较最小值,这里需要初始化为amout+1
dp[0]=0;
for(int i=1;i<=amount;i++){
for(int coin:coins){
if(i-coin<0){
continue;
}
dp[i]=Math.min(dp[i],dp[i-coin]+1);
}
}
return dp[amount]==amount+1?-1:dp[amount];
}
}
两个字符串,二维数组的动态规划问题
编辑距离
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
思路
这道题一看就不会,以下是题解的总结。
- 首先,我们需要建立模型,将这道题转换为代码形式。
解决两个字符串的动态规划问题,一般都是用两个指针i,j
分别指向两个字符串的最后,然后一步步往前走,缩小问题的规模。 - 定义dp数组为
dp[i][j]
,表示s1[0...i-1]
和s2[0...j-1]
之间的最小编辑距离 - 状态:编辑距离
- 选择:不变,插入,删除,替换,不同的选择改变了状态编辑距离
- base case
如果其中一个字符串的指针i
指向了尽头,那么需要另外一个字符串剩余的长度j
个步骤,才可以使得两个字符串相同,比如增加或删除。所以base case就是
dp[0][j]=j
dp[i][0]=i
- 状态转移方程
对应字符串dp[i][j]
dp[i][j-1]+1,直接在s[i]插入一个跟s2[j]相同的字符,那么s2[j]就被匹配了,前移j,继续与i对比
dp[i-1][j]+1,删除s[i],前移i,继续与s2[j]匹配
dp[i-1][j-1]+1,直接把 s1[i] 替换成 s2[j],这样它俩就匹配了
代码
class Solution {
public int minDistance(String word1, String word2) {
int n = word1.length();
int m = word2.length();
int[][] dp = new int[n+1][m+1];
for(int i=0;i<=n;i++){
dp[i][0]=i;
}
for(int j=0;j<=m;j++){
dp[0][j]=j;
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(word1.charAt(i-1)==word2.charAt(j-1)){ // 自底向上,判断i-1
dp[i][j]=dp[i-1][j-1];
}else{
dp[i][j]=Math.min(dp[i-1][j]+1,Math.min(dp[i-1][j-1]+1,dp[i][j-1]+1));
}
}
}
return dp[n][m];
}
}
递归解法
class Solution {
public int minDistance(String word1, String word2) {
return dp(word1.length()-1, word2.length()-1, word1, word2);
}
private int dp(int i, int j, String word1, String word2) {
if (i < 0) {
return j+1;
}
if (j < 0) {
return i+1;
}
if (word1.charAt(i) == word2.charAt(j)) { // 细节
return dp(i - 1, j - 1, word1, word2);
} else {
int min = Math.min(dp(i - 1, j, word1, word2) + 1, dp(i - 1, j - 1, word1, word2)+1);
min = Math.min(dp(i, j - 1, word1, word2) + 1, min);
return min;
}
}
}
一维数组、一个字符串的动态规划问题
单词拆分
给你一个字符串 s 和一个字符串列表 wordDict 作为字典,判定 s 是否可以由空格拆分为一个或多个在字典中出现的单词。
说明:拆分时可以重复使用字典中的单词。
示例 1:
输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以被拆分成 “leet code”。
示例 2:
输入: s = “applepenapple”, wordDict = [“apple”, “pen”]
输出: true
解释: 返回 true 因为 “applepenapple” 可以被拆分成 “apple pen apple”。
注意你可以重复使用字典中的单词。
示例 3:
输入: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
输出: false
思路
爬楼梯问题,凑零钱问题是一样的。
状态
dp[i] 表示 s[0…i]能否由空格拆分为一个或多个在字典中出现的单词
选择
字典中的单词就是选择
base case
dp[0]=true,如果是空串,为true
遍历顺序
从小到大
代码
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
boolean[] dp = new boolean[s.length() + 1];
Arrays.fill(dp, false);
dp[0] = true;// "关键"对于边界条件,我们定义 \textit{dp}[0]=truedp[0]=true 表示空串且合法
for (int i = 1; i < dp.length; i++) {
for (String word : wordDict) {
if (i - word.length() >= 0) {
if (dp[i - word.length()] && s.startsWith(word, i - word.length())) {
dp[i] = true;
break;
}
}
}
}
return dp[s.length()];
}
}
53. 最大子数组和
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
思路
定义dp为:以i结尾的最大连续和
代码
class Solution {
public int maxSubArray(int[] nums) {
int max = Integer.MIN_VALUE;
int[] dp = new int[nums.length];
dp[0] = nums[0];
for (int i = 1; i < dp.length; i++) {
dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
}
for (int i = 0; i < dp.length; i++) {
max = Math.max(max, dp[i]);
}
return max;
}
}
状态压缩
class Solution {
public int maxSubArray(int[] nums) {
int pre = 0, maxAns = nums[0];
for (int x : nums) {
pre = Math.max(pre + x, x);
maxAns = Math.max(maxAns, pre);
}
return maxAns;
}
}
152. 乘积最大子数组
给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
示例 1:
输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
示例 2:
输入: [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
思路
本题是最大子数组和的变形。
我们仍然可以设置状态数组为:
dp:以i结尾的最大乘积。
但是需要考虑正负号问题,如果存在两个负数相乘,那么负负得正为整数,有可能为最大值。
如果仅仅考虑前面一个数的最大乘积,无法得出本题的所有情况。
所以我们添加一个状态数组dpMin
dpMIn:以i结尾的最小乘积
对于遍历到的数num,
- 如果num为负数,那么与dpMIn[i-1]相乘,如果dpMin也为负数,那么负负得正,num*dpMIn[i-1]有可能是最大值
- 如果num为正数,那么如果dpMax也为正数,那么num*dpMax[i-1]也有可能是最大值。
- num本身也可能是最大值
所以,
dpMax[i] = Math.max(nums[i], Math.max(dpMax[i - 1] * nums[i], dpMin[i - 1] * nums[i]));
同理:
dpMin[i] = Math.min(nums[i], Math.min(dpMax[i - 1] * nums[i], dpMin[i - 1] * nums[i]));
代码
public int maxProduct(int[] nums) {
int[] dpMax = new int[nums.length];
int[] dpMin = new int[nums.length];
dpMax[0] = nums[0];
dpMin[0] = nums[0];
int max = dpMax[0];
for (int i = 1; i < nums.length; i++) {
dpMax[i] = Math.max(nums[i], Math.max(dpMax[i - 1] * nums[i], dpMin[i - 1] * nums[i]));
dpMin[i] = Math.min(nums[i], Math.min(dpMax[i - 1] * nums[i], dpMin[i - 1] * nums[i]));
max = Math.max(dpMax[i], max);
}
return max;
}
状态压缩:
class Solution {
public int maxProduct(int[] nums) {
int maxF = nums[0], minF = nums[0], ans = nums[0];
int length = nums.length;
for (int i = 1; i < length; ++i) {
int mx = maxF, mn = minF;
maxF = Math.max(mx * nums[i], Math.max(nums[i], mn * nums[i]));
minF = Math.min(mn * nums[i], Math.min(nums[i], mx * nums[i]));
ans = Math.max(maxF, ans);
}
return ans;
}
}
70. 爬楼梯
// 版本一
class Solution {
public:
int climbStairs(int n) {
if (n <= 1) return n; // 因为下面直接对dp[2]操作了,防止空指针
vector<int> dp(n + 1);
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) { // 注意i是从3开始的
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
};
拓展
这道题目还可以继续深化,就是一步一个台阶,两个台阶,三个台阶,直到 m个台阶,有多少种方法爬到n阶楼顶。
class Solution {
public:
int climbStairs(int n) {
vector<int> dp(n + 1, 0);
dp[0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) { // 把m换成2,就可以AC爬楼梯这道题
if (i - j >= 0) dp[i] += dp[i - j];
}
}
return dp[n];
}
};
746. 使用最小花费爬楼梯
代码
class Solution {
/**
* 状态:第i级楼梯
* 选择:向上爬一级或者两级楼梯
*/
public int minCostClimbingStairs(int[] cost) {
if(cost.length<2){
return cost[cost.length-1];
}
int[] dp = new int[cost.length];
dp[0]=cost[0];
dp[1]=cost[1];
for(int i=2;i<cost.length;i++){
dp[i]=Math.min(dp[i-1]+cost[i],dp[i-2]+cost[i]));
}
return Math.min(dp[cost.length-1],dp[cost.length-2]);// 最后结果是dp的加工
}
}