• LeetCode 动态规划专题


    LeetCode 动态规划专题

    53. 最大子序和

    集合+属性:所有以i结尾的子数组 的最大值

    状态计算: 1.最后一个不同点 2.子集划分

    class Solution {
    public:
        int maxSubArray(vector<int>& nums) {
            int n = nums.size();
            vector<int> f(n+1);
            if(n == 0) return 0;
            f[0] = max(0,nums[0]);
            int ans = nums[0];
            bool flag = false;
            for(int i=0;i<n;i++) 
                if(nums[i] >= 0) flag = true;
            int maxAns = nums[0];
            for(int i=1;i<n;i++){
                maxAns = max(nums[i],maxAns);
                f[i] = max(f[i-1]+nums[i],0);
                ans = max(ans,f[i]);
            }
            if(!flag) return maxAns;
            return ans;
        }
    };
    

    代码逻辑优化:滚动数组(这里一个变量),因为只用到了f[i-1]

    class Solution {
    public:
        int maxSubArray(vector<int>& nums) {
            int ans = INT_MIN,last = 0;
            for(int i=0;i<nums.size();i++){
                int now = max(last,0) + nums[i];
                ans = max(ans,now);
                last = now;
            }
            return ans;
        }
    };
    

    120. 三角形最小路径和

    集合+属性:f(i,j) 所有坐标以i,j为终点的路径的集合 中的路径和最小值

    状态计算:f(i,j) = 以正下方或斜下方为终点的路径的和的较小值 + 当前a(i,j)的值

    边界判断:第0行肯定要先独立的初始化,因为要用到i-1嘛;

    正下方,当j<i时才能用(比如每行最后1个j==i就不能用了)

    斜下方,当j>=1时才能用(比如每行第一个j==0时就不能由左斜下方推过来了)

    最后的答案:由集合属性知,应输出第n-行也就是最后一行中的各个路径终点,所对应的最小值

    class Solution {
    public:
        int minimumTotal(vector<vector<int>>& triangle) {
            if(triangle.size() == 0) return 0;
            int n = triangle.size();
            int m = triangle[n-1].size();
            int f[n][m];
            f[0][0] = triangle[0][0];
            for(int i=1;i<n;i++){
                for(int j=0;j<triangle[i].size();j++){
                    f[i][j] = INT_MAX;
                    if(j < i)
                        f[i][j] = f[i-1][j] + triangle[i][j];
                    if(j>=1) 
                        f[i][j] = min(f[i][j],
                        f[i-1][j-1]+triangle[i][j]);
                }
            }
            int ans = INT_MAX;
            for(int i = 0; i < m;i++){
                // cout<<f[n-1][i]<<" ";
                ans = min(ans,f[n-1][i]);
            }
            return ans;
        }
    };
    

    因为f[i] 只需要用到 上一层f[-1] 的结果 所以可以用滚动数组来优化空间

    滚动数组的版本:只需要开2个空间f[2],用&1来滚动 比如00变成01 01 变成 00

    10变成11 11变成 10 100变成 101 101 变成 100

    class Solution {
    public:
        int minimumTotal(vector<vector<int>>& triangle) {
            if(triangle.size() == 0) return 0;
            int n = triangle.size();
            int m = triangle[n-1].size();
            int f[2][m];
            f[0][0] = triangle[0][0];
            for(int i=1;i<n;i++){
                for(int j=0;j<triangle[i].size();j++){
                    f[i & 1][j] = INT_MAX;
                    if(j < i)
                        f[i & 1][j] = 
                        f[i-1 & 1][j] + triangle[i][j];
                    if(j>=1) 
                        f[i & 1][j] = min(f[i & 1][j],
                        f[i-1 & 1][j-1]+triangle[i][j]);
                }
            }
            int ans = INT_MAX;
            for(int i = 0; i < m;i++){
                ans = min(ans,f[n-1 & 1][i]);
            }
            return ans;
        }
    };
    

    63. 不同路径 II

    集合+属性:到达i,j的所有路径方案 的总个数

    状态计算:不重复、不漏;

    最后一步往下走:f(i,j) += f(i-1)(j) 当i>=1 并且f(i-1,j)能走到

    最后一步往右走:f(I,j) += f(i,j-1) 当j>=1 并且f(i,j-1)能走到

    class Solution {
    public:
        int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
            int n = obstacleGrid.size();
            int m = obstacleGrid[n-1].size();
            vector<vector<int>> f(n,vector<int>(m,0));
            if(obstacleGrid[n-1][m-1] == 1) return 0;
            f[0][0] = 1;
            for(int i=0;i<n;i++){
                for(int j=0;j<m;j++){
                    if(obstacleGrid[i][j] == 1) f[i][j] = 0;
                    if(i>=1 && obstacleGrid[i-1][j] == 0){
                        f[i][j] += f[i-1][j];
                    }
                    if(j>=1 && obstacleGrid[i][j-1] == 0){
                        f[i][j] += f[i][j-1];
                    }
                }
            }
            return f[n-1][m-1];
        }
    };
    

    91. 解码方法

    class Solution {
    public:
        int numDecodings(string s) {
            int n = s.size();
            vector<int> f(n+1);
            f[0] = 1; //初始化
            //注意从1开始算 因为是表示前多少个字母
            for(int i=1;i<=n;i++){
                if(s[i-1] != '0' ) f[i] += f[i-1];
                if(i>=2){
                    int num = (s[i-2] - '0') * 10 
                    + (s[i-1]-'0');
                    if(num>=10 && num <= 26) f[i] += f[i-2];
                }
            }
            return f[n];
        }
    };
    

    198. 打家劫舍

    假设偷盗经过了第i个房间时,那么有两种可能,偷第i个房间,或不偷第i个房间。如果偷得话,那么第i-1的房间一定是不偷的,所以经过第I个房间的最大值DP(i)=DP(I-2) +nums[i];如果经过第i房间不偷的话,那么经过第i房间时,偷取的最大值就是偷取前i-1房价的最大值。
    这两种方案分别是dp[i-2]+nums[i]和 dp[i-1],取最大值就是经过第i房间的最大值

    集合+属性:dp[i] 表示 到前i个房间为止所偷的方案 中的最大值

    状态计算:考虑最后一个不同点,由两种子集推导过来

    1. 选第i个,dp[i] = dp[i-2] + nums[i];
    2. 不选第i个,dp[i] = dp[i-1];
    class Solution {
    public:
        int rob(vector<int>& nums) {
            int n = nums.size();
            vector<int> dp(n);
            int ans = 0;
            if(n == 0) return 0;
            if(n == 1) return nums[0];
            //初始化边界
            dp[0] = nums[0];
            dp[1] = max(dp[0],nums[1]);
            for(int i=2;i<n;i++){
                //两种子集转移过来
                dp[i] = max(dp[i-2] + nums[i],dp[i-1]);
            }
            for(int i=0;i<n;i++) ans = max(dp[i],ans);
            return ans;
        }
    };
    

    另一种思路:分成两个状态,选或者不选

    最后结果为:max(f[n-1],g[n-1])

    300. 最长上升子序列

    class Solution {
    public:
        int lengthOfLIS(vector<int>& nums) {
            int n = nums.size();
            if(n == 0) return 0;
            if(n == 1) return 1;
            vector<int> f(n);
            for(int i=0;i<n;i++) f[i] = 1;
            for(int i=0;i<n;i++){
                for(int j=0;j<i;j++){
                    if(nums[i] > nums[j]){
                        f[i] = max(f[i],f[j] + 1);
                    }
                }
            }
            int ans = f[0];
            for(int i=0;i<n;i++) ans = max(ans,f[i]);
            return ans;
        }
    };
    

    72. 编辑距离

    初始化:

    ​ 1.把a前i个变成b前0个字母,需要删除i个;

    ​ 2.把a前0个变成b前i个字母,需要插入i次;

    class Solution {
    public:
        int minDistance(string word1, string word2) {
            int n = word1.size();
            int m = word2.size();
            vector<vector<int>> f(n+1,vector<int>(m+1));
            //初始化边界
            for(int i=0;i<=n;i++) f[i][0] = i;
            for(int i=0;i<=m;i++) f[0][i] = i;
            //让i从1开始 表示前i个 前j个
            for(int i=1;i<=n;i++){
                for(int j=1;j<=m;j++){
                    //插入 和 删除
                    f[i][j] = min(f[i-1][j],f[i][j-1]) + 1;
                    //空 和 替换
                    if(word1[i-1] == word2[j-1]){
                        f[i][j] = min(f[i][j],f[i-1][j-1]);
                    }else f[i][j] = min(f[i][j],f[i-1][j-1] + 1);
                }
            }
            return f[n][m];
        }
    };
    

    518. 零钱兑换 II

    完全背包问题

    三层循环

    找状态之间的关系,优化到两层循环

    优化成滚动数组,因为只会用到上一层和这一层前面的推导(正序从小到大推导)

    边界f[0] = 1 凑0元也是一种方案

    class Solution {
    public:
        int change(int amount, vector<int>& coins) {
            int n = coins.size();
            vector<int> f(amount+1);
            f[0] = 1;
            for(int i=0;i<n;i++){
                for(int j=coins[i];j<=amount;j++){
                    f[j] += f[j-coins[i]];
                }
            }
            return f[amount];
        }
    };
    

    664. 奇怪的打印机

    区间dp

    状态计算:

    1. 之前只染色了左端点,
    2. 之前左半边染了LK个,并且能然LK个,那么S[k]必须和S[L]的颜色相同才会去染色,再加上右半边的染色最小值F[K+1,R]
    class Solution {
    public:
        int strangePrinter(string s) {
            if(s.empty()) return 0;
            int n = s.size();
            vector<vector<int>> f(n+1, vector<int>(n+1));
            //枚举区间长度
            for(int len = 1; len <= n ; len++){
                //左端点
                for(int l = 0;l + len - 1 < n ;l ++){
                    //右端点
                    int r = l + len - 1;
                    //1. 前一次只染色左端点
                    f[l][r] = f[l + 1][r] + 1;
                    //2. 前一次染色了 l~k 
                    // 能染色的条件是因为s[k] = s[左端点]
                    for(int k = l + 1;k <= r; k ++){
                        if(s[k] == s[l]){
                            f[l][r] = min(f[l][r],f[l][k-1] + f[k+1][r]);
                        }
                    }
                }
            }
            return f[0][n-1];
        }
    };
    

    另一种子集划分方式,更清晰的题解

    10. 正则表达式匹配

    上面推导方案,需要枚举一遍 前面匹配的个数 O(n^3)

    推导与f[i-1,j]的关系

    可以看出,f[i,j]与f[i-1,j]比 多了一项判断条件:即s[i] 与 p[j-1]匹配

    类似完全背包优化,寻找不同项之间的关系,相邻两项非常像,可以由前一项经过结合律来推导出

    class Solution {
    public:
        bool isMatch(string s, string p) {
            int n = s.length(), m = p.length();
            vector<vector<bool>> f(n + 1, vector<bool>(m + 1, false));
            s = " " + s;
            p = " " + p;
            f[0][0] = true;
            for (int i = 0; i <= n; i++)
                for (int j = 1; j <= m; j++) {
                    //1.当s[i]==p[j] || p[j] ==.时可从f[i-1][j-1]转移过来
                    if (i > 0 && (s[i] == p[j] || p[j] == '.'))
                        f[i][j] = f[i][j] | f[i - 1][j - 1];
    
                    //2.当P[j]时'*'
                    if (p[j] == '*') {
                        //当*表示匹配0个p[j-1] 
                        //则可从f[i][j-2]即(p[j-2]字符)匹配过来
                        if (j >= 2)
                            f[i][j] = f[i][j] | f[i][j - 2];
                        //当*表示匹配了1个以上且最后1字符相等或.匹配
                        //则可以由f[i-1][j]匹配过来
                        //因为此时f[i-1][j]表示前i-1个s字符与前j个p字符能否匹配
                        //这个地方不太好看出来,就用推导相邻状态的方法推导处理
                        if (i > 0 && (s[i] == p[j - 1] || p[j - 1] == '.'))
                            f[i][j] = f[i][j] | f[i - 1][j];
                    }
                }
            return f[n][m];
        }
    };
    

    dp分析模型

    01背包问题

    #include<bits/stdc++.h>
    using namespace std;
    
    const int MAXN = 1010;
    int w[MAXN],v[MAXN];
    int n,m;
    int f[MAXN];
    
    /*
    集合+属性: 所有只考虑前i个物品的选法的体积不超过j集合的最大价值 
    状态计算: f[i][j] = max (f[i-1][j], f[i-1][j-v[i]] + w[i] )
    
    代码逻辑上的转移优化 一维: 
    	f[j] = max(f[j], f[j-v[i]] + w[i] ) j从m到v[i]倒推
    	f[j-v[i]]就相当于 f[i-1][j-v[i]]  
    	因为此时j>j-v[i] 此时推到了j 还没推到j-v[i]
    */
    
    int main(){
    	cin>>n>>m;
    	for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
    	for(int i=1;i<=n;i++){
    		for(int j=m;j>=v[i];j--){
    			f[j] = max(f[j],f[j-v[i]] + w[i]);
    		}
    	}
    	cout<<f[m]<<endl;
    	return 0;
    } 
    

    完全背包问题

    根据相邻状态来优化dp转移方程

    #include<bits/stdc++.h>
    using namespace std;
    
    const int MAXN = 1010;
    int w[MAXN],v[MAXN];
    int n,m;
    int f[MAXN];
    
    /*
    集合+属性: 所有只拿前i个物品任意个数下的集合选法  的最大总价值
    状态计算: f[i][j] = max(f[i-1][j] , f[i-1][j-v[i] + wi, f[i-1][j-v[i]*2]+2*w[i]...
    
    状态优化: 因f[i][j-v[i]] = max(f[i-1][j-v[i]],f[i-1][j-v[i]*2] + w[i],f[i-1][j-v[i]*3] + w[i]*2
    		所以: f[i][j] = max(f[i-1][j] , f[i][j-v[i]] + w[i];
    
    逻辑优化: 一维滚动
    		f[j] = max(f[j](上一轮), f[j-v[i]] + w[i] )  
    	   其中f[j-v[i]] 相当于 f[i][j-v[i]] 这一轮的j-v[i] 即j需要正序推导	
    */
    
    int main(){
    	cin>>n>>m;
    	for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
    	for(int i=1;i<=n;i++){
    		for(int j=v[i];j<=m;j++){
    			f[j] = max(f[j],f[j-v[i]] + w[i]);
    		}
    	}
    	cout<<f[m]<<endl;
    	return 0;
    }
    

    石子合并-区间dp模型

    #include<bits/stdc++.h>
    using namespace std;
    
    const int MAXN = 305;
    int n;
    int sum[MAXN],f[MAXN][MAXN];
    
    /*
    集合+属性: f[i][j] 所有i~j合并成一堆的方案的集合   的最小值 
    状态计算: 1.寻找最后一个不同点(每一堆石子都可由,左右两堆必须连续的推出)  
    		  2.找子集 f[i][j] = min(f[i][i] + f[i+1][j],  f[i][i+2] + f[i+3][j])
    		  f[i][j] = min(f[i][j], f[i][k] + f[k+1][j])
    */
    
    int main(){
    	cin>>n;
    	for(int i=1;i<=n;i++) cin>>sum[i],sum[i] = sum[i] + sum[i-1];
    	for(int i=1;i<=n;i++) f[i][i] = 0;
    	for(int len=2;len<=n;len++){
    		for(int i=1;i+len-1<=n;i++){
    			int j = i+len-1;
    			f[i][j] = 1e8;
    			for(int k=i;k<j;k++){
    				f[i][j] = min(f[i][j],f[i][k] + f[k+1][j] + sum[j] - sum[i-1]);
    			}
    		}
    	}
    	cout<<f[1][n]<<endl;
    	return 0;
    }
    

    最长公共子序列-字符串序列模型

    #include<bits/stdc++.h>
    using namespace std;
    
    const int MAXN = 1010;
    char a[MAXN],b[MAXN];
    int f[MAXN][MAXN];
    int n,m;
    
    /*
    集合+属性: f[i][j]  a前i个子序列与b前j个子序列的所有集合  中序列最长的长度 
    状态计算:  1.最后一个不同点:  2.找子集:  不重(求最值时可重) 不漏 
    	a前i-1个 和 b前i-1个
    	a前i-1个 和 b前i个   ->  包含 a前i-1个 和 b前i-1个
    	a前i个   和 b前i-1个  ->  包含 a前i-1个 和 b前i-1个
    	a前i个   和 b前i个   (当a[i] = b[j] 时 = max(f[i][j],f[i-1][j-1] + 1)
    	f[i][j] = max(f[i][j], f[i-1][j], f[i][j-1], f[i-1][j-1] + 1)
    */
    
    int main(){
    	cin>>n>>m>>a+1>>b+1;
    	f[0][0] = 0,f[1][0] = 0,f[0][1] = 0;
    	for(int i=1;i<=n;i++){
    		for(int j=1;j<=m;j++){
    			f[i][j] = max(f[i][j-1],f[i-1][j]);
    			if(a[i] == b[j]) f[i][j] = max(f[i][j], f[i-1][j-1] + 1);
    		}
    	}
    	cout<<f[n][m]<<endl;
    	return 0;
    } 
    

    小结

    把dp问题看成 集合 变化(增大)转移的问题
    集合 + 属性:最大/最小/总方案数/真假
    状态计算:相当于当前集合的状态,是由几种集合状态推导而来;怎么找到由哪些推导出来的?寻找最后一个不同点,子集要求1.不重(最值可重方案不可重)、2.不漏;

  • 相关阅读:
    基于Karma和Jasmine的angular自动化单元测试
    【转】使用SVG中的Symbol元素制作Icon
    【转】整理分析:Before 和 :After及其实例
    【转载】CSS中强大的EM
    【转】提升说服力!UI设计的心理学
    解决IE8不支持数组的indexOf方法
    KISSY Slide 组件应用遇到的一个放大缩小问题
    jQuery.extend 函数详解(转载)
    事件冒泡分析及return false、preventDefault、stopPropagation的区别
    jquery学习(一)-选择器
  • 原文地址:https://www.cnblogs.com/fisherss/p/12997238.html
Copyright © 2020-2023  润新知