状态:对于第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];
}
};
相对于上一题,增加对障碍物判断
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,分别代表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];
}
};
注意子串需要判断两端之间的子串是否是回文串
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];
}
};
参考文章:动态规划之博弈问题
//采用博弈问题的状态方程解题框架
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;
}
};