文章目录

  • 介绍
  • 动态规划入门:从记忆化搜索到递推
    • 打家劫舍
      • 递归
      • 记忆化递归
      • 递推
      • 滚动变量
  • 背包
    • 0-1 背包
      • 递归写法
      • 记忆化递归
    • 目标和
      • 记忆化搜索
      • 递推
      • 两个数组
      • 一个数组
    • 完全背包
      • 记忆化递归搜索
    • 零钱兑换
      • 记忆化递归
      • 递推
    • 背包问题变形[至多|恰好|至少]
  • 最长公共子序列
    • 记忆化搜索
    • 递推
    • 两个一维数组
    • 一维数组
  • 编辑距离
    • 记忆化搜索
    • 递推
    • 一个数组

介绍

本篇文章主要是观看”灵茶山艾府”动态规划篇视频后,做出的笔记。
视频链接如下
[动态规划入门:从记忆化搜索到递推]
[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数组时,无法从单个元素中获取结果,而是从某一些元素中获取结果。

递归

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(2n)
  • 仔细观察上述的递归树就会发现有很多重复计算的地方,所以可以用一个数组用来存储计算过的值,这样下次直接拿来用即可。

记忆化递归

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->数组
  • 递归->循环
  • 递归边界->数组初始值
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)

滚动变量

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 背包

递归写法

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]);}

记忆化递归

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)的值一定要是偶数才可以。

记忆化搜索

class Solution {public:int findTargetSumWays(vector<int>& nums, int target) {//p个正值,s-p个负值//p-(s-p)=t p=(t+s)/2for(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];} };

递推

class Solution {public:int findTargetSumWays(vector<int>& nums, int target) {//p个正值,s-p个负值//p-(s-p)=t p=(t+s)/2for(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(n2)。
  • 空间复杂度为O((n+1)*(target+1))
    时间复杂度是不能再优化了,那空间复杂度可以优化吗” />两个数组
    class Solution {public:int findTargetSumWays(vector<int>& nums, int target) {//p个正值,s-p个负值//p-(s-p)=t p=(t+s)/2for(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 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];}};

    一个数组

    class Solution {public:int findTargetSumWays(vector<int>& nums, int target) {//p个正值,s-p个负值//p-(s-p)=t p=(t+s)/2for(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;
    会发现这样计算不会出现错误。

    完全背包

    记忆化递归搜索

    int dfs(const vector<int>&w,const vector<int> &v,int i,int c,vector<vector<int>>&cache){if (i<0){return0;}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背包类似,唯一不同的地方就是选中当前物品之后,下一个子问题还可以选取当前物品

    零钱兑换

    记忆化递归

    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];}};

    递推

    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> cache(n,vector(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];}

    背包问题变形[至多|恰好|至少]


    这三种变形的不同在于它们的递归终止条件或者说动态规划的初始化条件
    至多

    恰好

    至少

    最长公共子序列


    记忆化搜索

    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];}};

    递推

    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]; }};

    两个一维数组

    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]的状态在使用的时候已经被更新了,所以需要先保存之前的状态。

    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]; }};

    编辑距离

    记忆化搜索

    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];}};

    递推

    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)

    一个数组

    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)