文章目录
介绍
本篇文章主要是观看"灵茶山艾府"动态规划篇视频后,做出的笔记。
视频链接如下
[动态规划入门:从记忆化搜索到递推]
[0-1背包,完全背包]
[最长公共子序列,编辑距离]
动态规划入门:从记忆化搜索到递推
打家劫舍
对于第i间房有两种抉择,选或者不选。选的话对应的子问题就是前i-2间房,不选的话对应的子问题就是前i-1间房。从第一间房子或者最后一间房子考虑受到的约束最小。假设从最后一间房子开始考虑,形成的递归树如下
上述思考过程可以抽象为:
- 当前操作?枚举第i个房子选或者不选。
- 子问题?从前i个房子中得到的最大金额和
- 下一个子问题:
不选,从前i-1个房子中得到的最大金额和
选,从前i-2个房子中得到的最大金额和
dfs(i) = max(dfs(i-1),dfs(i-1)+nums[i])
在定义dfs或者dp数组时,无法从单个元素中获取结果,而是从某一些元素中获取结果。
递归
cpp
class Solution {
public:
int rob(vector<int>& nums) {
//i表示第i件物品选还是不选
//dfs(i)表示偷取前i家获得的最大金额
int n=nums.size();
return dfs(nums,n-1);
}
private:
int dfs(const vector<int>&nums,int i){
if(i<0) return 0;
return max(dfs(nums,i-1),dfs(nums,i-2)+nums[i]);
}
};
- 这份代码会超出时间限制。时间复杂度是指数级别的为O(2^n^)
- 仔细观察上述的递归树就会发现有很多重复计算的地方,所以可以用一个数组用来存储计算过的值,这样下次直接拿来用即可。
记忆化递归
cpp
class Solution {
public:
int rob(vector<int>& nums) {
//i表示第i件物品选还是不选
//dfs(i)表示偷取前i家获得的最大金额
int n=nums.size();
vector<int>cache(n,-1);
return dfs(nums,n-1,cache);
}
private:
int dfs(const vector<int>&nums,int i,vector<int>&cache){
if(i<0) return 0;
//记忆化
if(cache[i]!=-1) return cache[i];
cache[i]=max(dfs(nums,i-1,cache),dfs(nums,i-2,cache)+nums[i]);
return cache[i];
}
};
- 时间复杂度=状态个数乘以单个状态花费的时间O(n*1)
- 空间复杂度O(n)
递推
通过递归树和代码可以发现cache[i]的实际计算发生在向上"归"的过程中,所以我们直接省去向下"递"的过程只留下向上归的过程,这被称之为递推。
- 自顶向下算=记忆化搜索
- 自低向上算=递推
递归与递推
- dfs->数组
- 递归->循环
- 递归边界->数组初始值
cpp
class Solution {
public:
int rob(vector<int>& nums) {
int n=nums.size();
vector<int>f(n+2,0);
for(int i=0;i<n;++i){
f[i+2]=max(f[i+1],f[i]+nums[i]);
}
return f[n+1];
}
};
- 空间复杂度为O(n)
- 时间复杂度为O(n)
滚动变量
cpp
class Solution {
public:
int rob(vector<int>& nums) {
int n=nums.size();
int f0=0,f1=0,newf;
for(int i=0;i<n;++i){
newf=max(f1,f0+nums[i]);
f0=f1;
f1=newf;
}
return f1;
}
};
背包
0-1 背包
递归写法
cpp
int zero_one(const vector<int>&w,const vector<int>&v,int i,int c){
if(i<0) return 0;
//不选
if(c<w[i]) return zero_one(w,v,i-1,c);
return max(zero_one(w,v,i-1,c),zero_one(w,i-1,c-w[i])+v[i]);
}
记忆化递归
cpp
int zero_one(const vector<int>&w,const vector<int>&v,int i,int c,vector<vector<int>>&cache){
if(i<0) return 0;
if(cache[i][c]!=-1) return cache[i][c];
//不选
if(c<w[i]) cache[i][c]=zero_one(w,v,i-1,c);
else cache[i][c]=max(zero_one(w,v,i-1,c),zero_one(w,v,i-1,c-w[i])+v[i]);
return cache[i][c];
}
目标和
假设所有元素中,正数的和为p,所有元素的总和是s,目标和为t,则所有负数的和就是(s-p),这样一来就可以得到一个等式p-(s-p)=t。移项之后可以得到p=(s+t)/2;由于p是正数之和故(s+t)的值一定大于0.要想一分为二(s+t)的值一定要是偶数才可以。
记忆化搜索
cpp
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
//p个正值,s-p个负值
//p-(s-p)=t p=(t+s)/2
for(auto&n:nums) target+=n;
if(target<0 || target&1) return 0;
target/=2;
vector<vector<int>> cache(nums.size(),vector<int>(target+1,-1));
return zero_one(nums,nums.size()-1,target,cache);
}
private:
int zero_one(const vector<int>&nums,int i,int target,vector<vector<int>>&cache){
if(i<0) {//所有的数都选过一遍
/*
还有一个条件就是目标和需要满足target。又由于target是倒着减的,所以target==0的时候就找到了一种目标方案,此时返回1.
*/
if(target==0) return 1;//找到一种方案
return 0;
}
if(cache[i][target]!=-1) return cache[i][target];
if(target<nums[i]) cache[i][target]=zero_one(nums,i-1,target,cache);
else cache[i][target]=zero_one(nums,i-1,target,cache)+zero_one(nums,i-1,target-nums[i],cache);
return cache[i][target];
}
};
递推
cpp
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
//p个正值,s-p个负值
//p-(s-p)=t p=(t+s)/2
for(auto&n:nums) target+=n;
if(target<0 || target&1) return 0;
target/=2;
//递推
int n=nums.size();
vector<vector<int>> dp(n+1,vector<int>(target+1,0));
dp[0][0]=1;
for(int i=1;i<=n;++i){
for(int j=0;j<=target;++j){
if(j<nums[i-1]) dp[i][j]=dp[i-1][j];
else dp[i][j]=dp[i-1][j]+dp[i-1][j-nums[i-1]];
}
}
return dp[n][target];
}
};
- 时间复杂度为O(n^2^)。
- 空间复杂度为O((n+1)*(target+1))
时间复杂度是不能再优化了,那空间复杂度可以优化吗?
两个数组
cpp
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
//p个正值,s-p个负值
//p-(s-p)=t p=(t+s)/2
for(auto&n:nums) target+=n;
if(target<0 || target&1) return 0;
target/=2;
//递推
int n=nums.size();
vector<vector<int>> dp(2,vector<int>(target+1,0));
//vector<int> dp(target+1,0);
dp[0][0]=1;//递归边界就是dp数组的初始值
for(int i=1;i<=n;++i){
//滚动数组是直接在原有的状态上进行覆盖,所以需要倒着覆盖
for(int j=0;j<=target;++j){
if(j<nums[i-1]) dp[i%2][j]=dp[(i-1)%2][j];
else dp[i%2][j]=dp[(i-1)%2][j]+dp[(i-1)%2][j-nums[i-1]];
}
}
return dp[n%2][target];
}
};
一个数组
cpp
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
//p个正值,s-p个负值
//p-(s-p)=t p=(t+s)/2
for(auto&n:nums) target+=n;
if(target<0 || target&1) return 0;
target/=2;
//递推
int n=nums.size();
vector<int> dp(target+1,0);
dp[0]=1;
for(int i=1;i<=n;++i){
//滚动数组是直接在原有的状态上进行覆盖,所以需要倒着覆盖
for(int j=target;j>=nums[i-1];--j){
dp[j]+=dp[j-nums[i-1]];
}
}
return dp[target];
}
};
这里解释一下,为什么第二层循环需要倒着遍历
如果是正着遍历,在i层的状态上修改i+1层的数据,
f[2] = f[2]+f[0]=3+1=4;
f[3] = f[3]+f[1]=5+2=7;
f[4] = f[4]+f[2]=6+4这里原本应该要加的是3,但是由于f[2]已经被覆盖了所以会变成加4,导致这里计算错误。
现在来看看倒着遍历会不会发生错误。
f[6] = f[6]+f[4] = 9+6=15;
f[5] = f[5]+f[3]= 7+5=12;
f[4] = f[4]+f[2] = 6+3=9;
f[3] = f[3]+f[1]= 5+2=7
f[2] = f[2]+f[0] = 3+1=4;
会发现这样计算不会出现错误。
完全背包
记忆化递归搜索
cpp
int dfs(const vector<int>&w,const vector<int> &v,int i,int c,vector<vector<int>>&cache){
if (i<0){
return 0;
}
if(cache[i][c]!=-1) return cache[i][c];
if(c<w[i]) cache[i][c]=dfs(w,v,i-1,c,cache);
else cache[i][c]=max(dfs(w,v,i-1,c,cache),dfs(w,v,i,c-w[i],cache)+v[i]);//i表示选过了还可以再选
return cache[i][c];
}
- 与01背包类似,唯一不同的地方就是选中当前物品之后,下一个子问题还可以选取当前物品
零钱兑换
记忆化递归
cpp
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int n=coins.size();
vector<vector<int>> cache(n,vector<int>(amount+1,-1));
int ans = dfs(coins,n-1,amount,cache);
return ans==INT_MAX-1?-1:ans;
}
private:
int dfs(const vector<int>&ws,int i,int c,vector<vector<int>>&cache){
if (i<0){
if(c==0) return 0;
return INT_MAX-1;
}
if(cache[i][c]!=-1) return cache[i][c];
if(c<ws[i]) cache[i][c]=dfs(ws,i-1,c,cache);
else cache[i][c]=min(dfs(ws,i-1,c,cache),dfs(ws,i,c-ws[i],cache)+1);
return cache[i][c];
}
};
递推
cpp
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int n=coins.size();
vector<vector<int>> dp(n+1,vector<int>(amount+1,INT_MAX-1));
dp[0][0]=0;
//vector<vector<int>> cache(n,vector<int>(amount+1,-1));
for(int i=1;i<=n;++i){
for(int j=0;j<=amount;++j){
if(j<coins[i-1]) dp[i][j]=dp[i-1][j];
else dp[i][j] = min(dp[i-1][j],dp[i][j-coins[i-1]]+1);
}
}
return dp[n][amount]==INT_MAX-1?-1:dp[n][amount];
}
背包问题变形[至多|恰好|至少]
这三种变形的不同在于它们的递归终止条件或者说动态规划的初始化条件
至多
恰好
至少
最长公共子序列
记忆化搜索
cpp
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int l1=text1.size(),l2=text2.size();
vector<vector<int>> cache(l1,vector<int>(l2,-1));
return dfs(text1,text2,l1-1,l2-1,cache);
}
private:
int dfs(const string&s,const string&t,int i,int j,vector<vector<int>>&cache){
if(i<0 || j<0) return 0;
if(cache[i][j]!=-1) return cache[i][j];
if(s[i]==t[j]) cache[i][j]=dfs(s,t,i-1,j-1,cache)+1;
else cache[i][j]=max(dfs(s,t,i-1,j,cache),dfs(s,t,i,j-1,cache));
return cache[i][j];
}
};
递推
cpp
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int l1=text1.size(),l2=text2.size();
vector<vector<int>> dp(l1+1,vector<int>(l2+1,0));
for(int i=1;i<=l1;++i){
for(int j=1;j<=l2;++j){
if(text1[i-1]==text2[j-1]) dp[i][j]=dp[i-1][j-1]+1;
else dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
return dp[l1][l2];
}
};
两个一维数组
cpp
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int l1=text1.size(),l2=text2.size();
vector<vector<int>> dp(2,vector<int>(l2+1,0));
for(int i=1;i<=l1;++i){
for(int j=1;j<=l2;++j){
if(text1[i-1]==text2[j-1]) dp[i%2][j]=dp[(i-1)%2][j-1]+1;
else dp[i%2][j]=max(dp[(i-1)%2][j],dp[i%2][j-1]);
}
}
return dp[l1%2][l2];
}
};
一维数组
由递推公式可以看出,f[i][j]由f[i-1][j-1]、f[i-1][j]、f[i][j-1]三个状态得出。先假设只用一个数组完成三个状态的转换,会发现f[i-1][j-1]的状态在使用的时候已经被更新了,所以需要先保存之前的状态。
cpp
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int l1=text1.size(),l2=text2.size();
vector<int> dp(l2+1,0);
for(int i=1;i<=l1;++i){
int pre=dp[0];
for(int j=1;j<=l2;++j){
int tmp=dp[j];
if(text1[i-1]==text2[j-1]) dp[j]=pre+1;
else dp[j]=max(dp[j],dp[j-1]);
pre=tmp;
}
}
return dp[l2];
}
};
编辑距离
记忆化搜索
cpp
class Solution {
public:
int minDistance(string word1, string word2) {
int l1=word1.size(),l2=word2.size();
vector<vector<int>> cache(l1,vector<int>(l2,-1));
return dfs(word1,word2,l1-1,l2-1,cache);
}
private:
int dfs(const string&w1,const string&w2,int i,int j,vector<vector<int>>&cache){
if(i<0) return j+1;
if(j<0) return i+1;
if(cache[i][j]!=-1) return cache[i][j];
if(w1[i]==w2[j]) cache[i][j]=dfs(w1,w2,i-1,j-1,cache);
else cache[i][j]=min(dfs(w1,w2,i-1,j,cache),min(dfs(w1,w2,i,j-1,cache),dfs(w1,w2,i-1,j-1,cache)))+1;
return cache[i][j];
}
};
递推
cpp
class Solution {
public:
int minDistance(string word1, string word2) {
int l1=word1.size(),l2=word2.size();
vector<vector<int>> dp(l1+1,vector<int>(l2+1,0));
for(int i=1;i<=l2;++i) dp[0][i]=i;
for(int i=1;i<=l1;++i) dp[i][0]=i;
for(int i=1;i<=l1;++i){
for(int j=1;j<=l2;++j){
if(word1[i-1]==word2[j-1]) dp[i][j]=dp[i-1][j-1];
else dp[i][j]=min(dp[i-1][j],min(dp[i][j-1],dp[i-1][j-1]))+1;
}
}
return dp[l1][l2];
}
};
- 时间复杂度为O(n*m)
- 空间复杂度为O(n*m)
一个数组
cpp
class Solution {
public:
int minDistance(string word1, string word2) {
int l1=word1.size(),l2=word2.size();
vector<int> dp(l2+1,0);
//for(int i=1;i<=l2;++i) dp[0][i]=i;
//for(int i=1;i<=l1;++i) dp[i][0]=i;
for(int i=1;i<=l2;++i) dp[i]=i;
for(int i=1;i<=l1;++i){
int pre=dp[0];
dp[0]+=1;//相当于二维数组中的dp[i][0]=i;
for(int j=1;j<=l2;++j){
int tmp=dp[j];
if(word1[i-1]==word2[j-1]) dp[j]=pre;
else dp[j]=min(dp[j],min(dp[j-1],pre))+1;
pre=tmp;
}
}
return dp[l2];
}
};
- 时间复杂度为O(n*m)
- 空间复杂度为O(m)