球球速刷LC之DP问题

目录

    • 经典
          • 三角形最小路径和
    • 网格递推
          • 到达路径数目
          • 到达路径数目2
          • 最大正方形
          • 骑士游戏
    • 序列DP
    • 股票系列
          • 只能交易一次
          • 交易任意次数
          • 只能交易2次
          • 只能交易K次
          • 交易之间需要间隔一天
          • 交易收取交易费
    • 字符串匹配
          • 最长公共子串
          • 编辑距离
          • 通配符匹配
          • 正则匹配
          • 相间字符串
          • 不同字符串子序列
    • 区间DP
          • 最长回文子串
          • 最长回文子序列
          • 打气球
    • 背包DP
    • 博弈DP
          • 预测胜利者

DP的本质是巧妙定义状态,并找到状态的递推关系

经典

三角形最小路径和

状态:对于第i行Row[i], 假设以该行j元素Row[i][j]结尾的路径最小和为sum[i][j].
则sum[i][j]=Row[i][j]+min{sum[i-1][j-1],sum[i-1][j]};(注意j的边界)
最终求得最后一行里sum的最小值。

class Solution {
    
public:
    int minimumTotal(vector<vector<int>>& triangle) {
        if(triangle.size() == 0) return 0;
               
        if(triangle.size() == 1){            
            return *min_element(triangle[0].begin(),triangle[0].end());            
        }
        //第0行的各个元素的最小路径和就是元素本身
        auto lastRow = triangle[0];
        for(int i = 1; i<triangle.size();++i){
            //取得当前行元素
            auto currRow = triangle[i];
            //对当前行元素遍历,计算以每个元素为路径结尾的最小和
            for(int j = 0;j<triangle[i].size();++j){                
                if(j==0){
                     currRow[j] = triangle[i][j]+lastRow[0];
                }else if(j==triangle[i].size()-1){
                     currRow[j] = triangle[i][j]+lastRow[j-1];
                }else{
                     currRow[j] = std::min(triangle[i][j]+lastRow[j],triangle[i][j]+lastRow[j-1]);
                }   
            }
            lastRow = currRow;
        }        
        return *min_element(lastRow.begin(),lastRow.end());        
    }
};

网格递推

到达路径数目
class Solution {
public:
    int uniquePaths(int m, int n) {
        int dp[m][n];
        
        for(int i =0 ; i<m;++i){
            for(int j =0; j<n;++j){
                if(i == 0 || j==0){
                    dp[i][j]=1;
                }else{
                //依赖与上方与左边的方案数目
                    dp[i][j] = dp[i-1][j] + dp[i][j-1];
                }
            }
        }       
        return dp[m-1][n-1];
    }
};
到达路径数目2

相对于上一题,增加对障碍物判断

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
       long m=obstacleGrid.size();
       long n=obstacleGrid.at(0).size();        
       long dp[m][n];
        
        for(long i =0;i<m;++i){
            for(long j=0;j<n;++j){
               if(obstacleGrid.at(i).at(j) == 1){
                   dp[i][j]=0;
               }else{
                   if(i==0&&j==0) dp[i][j]=1;
                   else{
                       dp[i][j] =0;
                   }
                   if((i-1) >=0){
                       dp[i][j] += dp[i-1][j];
                   }
                   
                   if((j-1)>=0){
                       dp[i][j] += dp[i][j-1];
                   }
               }  
            }
        }        
        return dp[m-1][n-1];        
    }
};
最大正方形

dp[i][j]代表以(i,j)为右下角顶点所能形成的最大正方形的边长

class Solution {
public:
    int maximalSquare(vector<vector<char>>& matrix) {        
        int m=matrix.size();
        if(m==0) return 0;        
        int n=matrix[0].size();
        int dp[m][n];
        
        int max_square=0;
        for(int i = 0 ; i<m;++i){
            for(int j=0;j<n;++j){
                if(i==0 || j==0) dp[i][j] = (matrix[i][j]=='1'?1:0);
                else {
                   if(matrix[i][j]=='1') dp[i][j] = std::min(dp[i-1][j],min(dp[i-1][j-1],dp[i][j-1]))+1;
                   else{
                       dp[i][j]=0;
                   }                    
                }
                if(dp[i][j] > max_square){
                    max_square = dp[i][j];
                }
            }
        }
      return max_square*max_square;        
    }
};
骑士游戏
class Solution {
 public:
    int calculateMinimumHP(vector<vector<int>>& dungeon) {
        int m=dungeon.size();
        int n=dungeon[0].size();
        //dp[m][n]表示进入房间[m][n]所需要的最小血量HP
        int dp[m][n];                
        //从终点出发,先假设到达终点时所需要的最小血量,再依次求得为了继续前进到下一步所需的最小HP
        for(int i= m-1;i>=0;--i){
            for(int j=n-1;j>=0;--j){
                //dp[i][j] 代表从i,j处出发到达终点需要的最少血量                
                if(i==m-1 && j==n-1){
                    //本身就在终点处,则进入时的血量 HP+dungeon[i][j] >= 1 && HP>=1则只要剩余1即可
                    //即: HP>=1-dungeon[i][j] && HP>=1
                    //满足以上不等式的HP的最小解为 max(1-dungeon[i][j],1);
                    dp[i][j] = max(1-dungeon[i][j],1);
                }else if(i== m-1 && j<n-1){
                    //只能向右边走
                    //进入本格子血量 HP+dungeon[i][j] >= DP[i][j+1] && HP >= 1 
                    //               -->> 满足以上的不等式的最小的HP是 max(DP[i][j+1]-dungeon[i][j],1)
                    dp[i][j] =max(dp[i][j+1]-dungeon[i][j],1); 
                }else if(i<m-1 && j==n-1){
                    //只能向下走
                    dp[i][j] =max(dp[i+1][j]-dungeon[i][j],1); 
                }else{
                    //此时既可以向下走,也可以向右边走,根据以上两种情况
                    //向右走进入当前位置至少需要血量: 
                    int r  =max(dp[i][j+1]-dungeon[i][j],1); 
                    //向下走为:
                    int d =max(dp[i+1][j]-dungeon[i][j],1); 
                    
                    //从两个方向中选择对血量要求低的
                    dp[i][j] = min(r,d);
                }
            }
        }
                return dp[0][0];
    }
        
};

序列DP

股票系列

所有套路一致: 参照文章 团灭股票买卖问题

只能交易一次
交易任意次数
只能交易2次
只能交易K次
交易之间需要间隔一天
交易收取交易费

字符串匹配

基本套路
字符串匹配的套路基本是建立一个二维DP,分别代表S1到达位置i 和S2到达位置j时的匹配状况。
再根据末尾元素是否相等,是否是特殊字符等分类递推。

最长公共子串

状态定义:dp[i][j] 代表s1 s2 中到第i-1结尾和第j-1结尾的子串的最长公共子串。i == 0或 j == 0代表空串。
递推公式:

dp[i+1][j+1]= dp[i][j]+1 (当s[i]==s[j])
dp[i+1][j+1]=max{dp[i][j],dp[i][j+1],dp[i+1][j]} (当s[i]!=s[j])
class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        if(text1.empty()||text2.empty()) return 0;
        
        vector<vector<int>>dp(text1.size()+1,vector<int>(text2.size()+1,0));        
        for(int i=0;i<text1.size();++i){
            for(int j=0;j<text2.size();++j){              
                if(text1[i]==text2[j]){
                    dp[i+1][j+1]=dp[i][j]+1;
                }else{
                    dp[i+1][j+1]=max(dp[i][j],dp[i+1][j]);
                    dp[i+1][j+1]=max(dp[i+1][j+1],dp[i][j+1]);
                }
            }
        }
        return dp[text1.size()][text2.size()];
    }
};
编辑距离

思路类似最长公共子序列,dp[i][j]表示字符串s1 s2的i+1,j+1结尾的子串的最小编辑距离.i == 0 || j==0 表示空串。再判断s1[i] 与s[j]是否相同。

class Solution {
public:
    int minDistance(string word1, string word2) {
        if(word1.empty()||word2.empty()) return max(word1.size(),word2.size());
        
        int dp[word1.size()+1][word2.size()+1];
        
        for(int i=0;i<=word1.size();++i){
            for(int j=0;j<=word2.size();++j){
                if(i==0 && j==0) dp[i][j]=0;
                else if(i==0){
                    dp[i][j]=dp[i][j-1]+1;
                }else if(j==0){
                    dp[i][j]=dp[i-1][j]+1;
                }else{
                    if(word1[i-1]==word2[j-1]){
                        dp[i][j]=min(dp[i-1][j-1],min(dp[i-1][j]+1,dp[i][j-1]+1));
                    }else{
                        dp[i][j]=min(dp[i-1][j-1]+1,min(dp[i-1][j]+1,dp[i][j-1]+1));
                    }
                }
            }
        }
        return dp[word1.size()][word2.size()];
    }
};
通配符匹配

类似于72,以,dp[i][j]表示字符串s1 s2的i+1,j+1结尾的子串的匹配状态。
注意边界条件的初始化

class Solution {
public:
    bool isMatch(string s, string p) {        
       if(p.size() == 0){
          return s.empty();
       }
       
       //注意dp的第0行与第0列代表s或p为空串的边界情况 
        vector<vector<bool>>dp(s.size()+1,vector<bool>(p.size()+1,false));             
        //初始化都为空串的边界条件
        dp[0][0]=true;
             
        //初始化s为空串状态的边界条件   
        for(int j=0;j<p.size();++j){
            //注意数组索引!!!
            if(p[j] == '*'){
                dp[0][j+1] = dp[0][j];
            }else{
                dp[0][j+1] =false;
            }
        }
         
         //更新状态方程       
        for(int i=0;i<s.size();++i){
            for(int j=0;j<p.size();++j){
                if(p[j] == '*'){
                    dp[i+1][j+1] = (dp[i][j]||dp[i+1][j]||dp[i][j+1]);
                }else if((p[j] =='?') || (p[j]==s[i])){
                    dp[i+1][j+1] = dp[i][j];
                }
            }
        }
         return dp[s.size()][p.size()];        
    }
};
正则匹配

类似72,注意以* 符号进行分类,当前字符为是 * 时,分为匹配0次与1次的情况。
其中匹配1次时包括s[i-1]与p[j-1]已经匹配以及 s[i-1]与p[j]已经匹配两种情况。

class Solution {    
public:
    bool isMatch(string s, string p) {        
        int m=s.size();
        int n=p.size();
        //dp[i][j]代表s0-i 与p0-j是否匹配
        vector<vector<bool>> dp(m+1,vector<bool>(n+1,false));
        dp[0][0]=true;

        //初始化s为空串的边界情况
        for(int i=0;i<n;++i){
            if(p[i]=='*' && i-1>=0)dp[0][i+1] = dp[0][i-1];
            else dp[0][i+1]=false;
        }
        
        for(int i=0;i<m;++i){
            for(int j=0;j<n;++j){
                //当前模式字符不是*
                if(p[j]!='*'){
                   dp[i+1][j+1] = dp[i][j] && (s[i]==p[j] || p[j]=='.'); 
                }else{
                   //1.当前*前字符重复0次
                   bool r1 = j-1>=0 && dp[i+1][j-1];
                   //2.当前*前字符可匹配一次
                   bool r2= j-1>=0 && (dp[i][j] || dp[i][j+1])&&(s[i]==p[j-1] || p[j-1]=='.');
                   dp[i+1][j+1] =r1||r2;
                }
            }
        }
        return dp[m][n];        
    }
};
相间字符串
class Solution {    
public:
    bool isInterleave(string s1, string s2, string s3) {      
        if(s1.size()+s2.size() != s3.size()) return false;

        int m=s1.size();
        int n=s2.size();
        int dp[m+1][n+1];        
        //初始化边缘条件 s1为空
        for(int i = 0 ; i<n+1;++i){
            if(s2.substr(0,i) == s3.substr(0,i)){
                dp[0][i] = 1;
            }else{
                dp[0][i]=0;
            }
        }
        //初始化边缘条件  s2为空           
        for(int i = 0 ; i<m+1;++i){
            if(s1.substr(0,i) == s3.substr(0,i)){
                dp[i][0] = 1;
            }else{
                dp[i][0]=0;
            }
        }
       
        for(int i=1; i<m+1;++i){
            for(int j=1;j<n+1;++j){
                bool cmp1=false,cmp2=false;
                if(s1[i-1] == s3[i+j-1]){
                   cmp1 = dp[i-1][j]; 
                }
                if(s2[j-1] == s3[i+j-1]){
                   cmp2 = dp[i][j-1]; 
                }                
                dp[i][j] = cmp1||cmp2;          
            }
        }
        return dp[m][n];
    }
};
不同字符串子序列
class Solution {
public:
    int numDistinct(string s, string t) {
        if(t.empty()) return 1;
        
        if(s.size() < t.size()) return 0;
                
        int m=s.size();
        int n=t.size();
        unsigned int dp[m+1][n+1];
        
        for(int i=0 ;i<n+1;++i){
            dp[0][i]=0;
        }
        
        for(int i=0;i<m+1;++i){
           dp[i][0] = 1;
        }
        
        for(int i=1;i<m+1;++i){
            for(int j=1;j<n+1;++j){
                if(i<j){
                    dp[i][j] = 0;
                }else{
                   if(s[i-1] == t[j-1]){
                       dp[i][j] = dp[i-1][j-1] + dp[i-1][j];
                   }else{
                       dp[i][j] = dp[i-1][j];
                   }   
                }
            }
        }        
        return dp[m][n];        
    }
};

区间DP

最长回文子串

注意子串需要判断两端之间的子串是否是回文串

class Solution {
public:
    string longestPalindrome(string s) {
        if(s.size() <=1) return s;
        if(s.size() == 2 && s[0]==s[1]) return s;
        if(s.size()==3 &&s[0]==s[2]) return s;
        
        int m=s.size();
        bool dp[m][m];
        for(int i=0;i<m;++i){
            for(int j=0;j<m;++j){
                dp[i][j] = false;
            }
        }
        
        int max_len =0;
        int max_s_start =0;
        for(int i=0;i<m;++i){
            for(int j=i;j>=0;--j){
                if(i==j) {
                    dp[j][i]=true;
                }else if(s[i] == s[j]){
                    if(j+1 == i) dp[j][i] =true;
                    else if(j+1<=i-1 && dp[j+1][i-1]) dp[j][i]=true;
                    else dp[j][i]=false;
                }else{
                    dp[j][i]=false;
                }
                if(dp[j][i]){
                    if(i-j+1 > max_len){
                        max_len =i-j+1;
                        max_s_start =j;
                    }
                }
                
            }
        }
        return s.substr(max_s_start,max_len);
    }
};
最长回文子序列

dp[i][j]表示s[i]开头 s[j]结尾的子串
从而通过判断s[i]与s[j]是否相等,找到dp[i][j] 与dp[i+1][j-1]关系或与dp[j+1][i],dp[j][i-1]关系

class Solution {
public:
    int longestPalindromeSubseq(string s) {
        if(s.size()<=1) return s.size();        
        int dp[s.size()][s.size()];        
         for(int i=0;i<s.size();++i){
            for(int j=i;j>=0;--j){
                if(i==j){
                    dp[j][i]=1;
                }else{
                    //两端字符相同,从里侧子串推导,注意j+1==i的情况
                    if(s[i]==s[j]){
                        if(j+1<=i-1) dp[j][i]=dp[j+1][i-1]+2;
                        else{
                            dp[j][i]=2;
                        }
                    }else{
                      //两端字符不同,则各取一端,取最大值
                        dp[j][i]=max(dp[j+1][i],dp[j][i-1]);
                    }
                }
            }
        }
        return dp[0][s.size()-1];        
    }
};
打气球

此题的解题关键是每个气球得分是与向邻两侧气球分数有关。所以可以假设气球i为最后一个射破气球。再将气球i分割为左右两个子区间L,R。因此依赖于L R的解。

class Solution {
public:
       //<1>核心要点:
    //想到把某个气球作为最后一个被射击的,从而可以得到一个状态。但是该气球把整个序列化为左右子区间
    //此时应意识到应该以区间作为迭代元素,从小区间逐渐迭代到大区间,从而在以气球划分子区间时,可将子区间结果作为已知量。   
    int maxCoins(vector<int>& nums) {
        
        if(nums.size()==0) return 0;
        if(nums.size()== 1)return nums[0];
        
        int N=nums.size();
        
        //两侧加1,作为边界
        vector<int>temp={1};
        for(auto i:nums){
            temp.push_back(i);
        }
        temp.push_back(1);
        
        //dp[i][j]代表只射击区间[i,j]的最大score
        int dp[N+1][N+1];
        //依次扫描长度为1 2 ...N的区间
        for(int len=1;len<=N;++len){
            //对每种区间长度,生成区间的首尾元素
            for(int start=1;start<=N;++start){
                int end = start+len-1;
                if(end>N) break;                
                //区间只有一个元素,直接射破
                if(start == end){
                    dp[start][end] = temp[start-1]*temp[start]*temp[end+1];
                }else{
                    //依次假设区间某个元素为最后一个被射击,先求其左右子区间
                    //最终得到当前区间最大值
                    dp[start][end]=0;
                    for(int last = start;last<=end;++last){
                        int left_sub_range_score =0;
                        int right_sub_range_score =0;
                        if(last-1>=start) left_sub_range_score=dp[start][last-1];
                        if(last+1<=end) right_sub_range_score=dp[last+1][end];                        
                        //射爆最后一个气球时,由于左右子区间已经被射掉,因此乘以当前区间两边外侧的第一个元素
                        int last_burst_score=temp[start-1]*temp[last]*temp[end+1];
                        dp[start][end] = max(dp[start][end],left_sub_range_score+last_burst_score+right_sub_range_score);
                    }
                }
            }
        }
        
        return dp[1][N];
    }
};

背包DP

博弈DP

参考文章:动态规划之博弈问题

预测胜利者
//采用博弈问题的状态方程解题框架
class Solution{
    public:
     bool PredictTheWinner(vector<int>& nums) {
         if(nums.size()<=1)return true;
                  
         pair<int,int>dp[nums.size()][nums.size()];
         
        for(int j=0;j<nums.size();++j){ 
          for(int i=j;i>=0;--i){ 
            if(i==j){
                dp[i][j].first=nums[i];
                dp[i][j].second=0;
            }else{
                int l=dp[i+1][j].second+nums[i];
                int r=dp[i][j-1].second+nums[j];
                dp[i][j].first=max(r,l);
         
                if(l>r){
                  dp[i][j].second=dp[i+1][j].first;
                }else{
                  dp[i][j].second=dp[i][j-1].first;
                }
            }
          }
        }
         return dp[0][nums.size()-1].first>= dp[0][nums.size()-1].second;
         
     }
};

你可能感兴趣的:(leetcode)