1.线性dp基本方法
每个问题暴力做法可以是o(n2)级别甚至指数级别,但是dp其实是由于不同状态间具有转移关系,每次转移其实就是借助已经计算了的东西(存在数组f中的),从而减少很多计算量。
注意:
1.申请空间f[n]的下标取值是[0,n-1],下标都是从0开始,但有意义的i需要判断它和下标的关系,也就导致n或者n+1等等。
2.遍历的起点i常从1或2开始,取决于状态转移方程的下标[i-1]或[i-2],因此[0]对应的值常不具有可理解的意思,但是需要根据题意初始化。(起码有时候我理解不了[0])
2.最简单的dp
70. 爬楼梯(剑指 Offer 10- II. 青蛙跳台阶问题)
class Solution {
public:
/*
1.状态表示:f[n],输入台阶数量[0,n],对应下标[0-n],需要n+1空间
2.状态属性:求值
3.状态转移:f[i] = f[i-1]+f[i-2]
*/
int climbStairs(int n) {
vector<int> f(n + 1);
f[0] = 1, f[1] = 1;
for(int i = 2; i <= n; i ++)
{
f[i] = f[i - 1] + f[i - 2];
}
return f[n];
}
};
剑指 Offer 10- I. 斐波那契数列
class Solution {
public:
/*
1.状态表示:f[n],输入项数范围[0,n],对应下标[0,n],需要n+1空间
2.状态属性:求值
3.状态转移:f[i] = f[i-1]+f[i-2]
*/
int fib(int n) {
int N = 1e9 + 7;
if(n == 0 || n == 1) return n;//n==0不存在下标,n==1时不存在[1]
vector<int> f(n + 1);
f[0] = 0, f[1] = 1;
for(int i = 2;i <= n;i ++)
{
f[i] = (f[i - 1] + f[i - 2]) % N;
}
return f[n];
}
};
3.常规的一纬dp
用一纬就能直接简单的优化计算:找准状态表示的意义,状态转移采用什么关系式
53. 最大子序和
class Solution {
public:
/*
1.状态表示:f[n],输入nums下标[0,n-1]对应[1,n],需要n+1空间(注意nums的[i]和f[i]不一样!)
2.状态属性:max
3.状态转移:以i结尾的子数组最大值;由于子数组连续,只用与[i-1]比较:f[i] = max(f[i-1],0)+nums[i]
*/
int maxSubArray(vector<int>& nums) {
int n = nums.size();
vector<int> f(n + 1);
f[0] = 0;
int res = INT_MIN;
for(int i = 1; i <= n;i ++)
{
f[i] = max(f[i - 1], 0) + nums[i - 1];
res = max(res, f[i]);
}
return res;
}
};
91. 解码方法
class Solution {
public:
/*
1.状态表示:f[n],输入nums下标[0,n-1]对应[1,n],需要n+1空间(注意nums的[i]和f[i]不一样!)
2.状态属性:数值
3.状态转移:枚举结尾字符,分[当前字符自己解码]和[与前一个字符一起解码]两种情况:f[i] = if([1,9])f[i-1]+if(在[10,26])f[i-2],注意两位数必须>10,01不算1
*/
int numDecodings(string s) {
int n = s.size();//string s = to_string(num);如果输入是int类型需要转换
vector<int> f(n + 1);
f[0] = 1;
for(int i = 1; i <= n; i ++)
{
int m = s[i - 1] - '0';
if(m > 0) f[i] = f[i - 1];
if(i >= 2)
{
int tmp = (s[i - 2] - '0') * 10 + m;
if(tmp >= 10 && tmp <= 26) f[i] += f[i - 2];
}
}
return f[n];
}
};
300. 最长上升子序列
class Solution {
public:
/*
1.状态表示:f[n],输入nums下标[0,n-1]对应[1,n],需要n+1空间(注意nums的[i]和f[i]不一样!)
2.状态属性:max
3.状态转移:i表示包含这个点之前的最长上升子序列,子序列没有要求连续,因此枚举每个结尾位置时,前面需要多一个遍历,在[j]<[i]的f[j]中判断大小~res也直接在循环里判断
*/
int lengthOfLIS(vector<int>& nums) {
if(nums.empty()) return 0;
int n = nums.size();
vector<int> f(n + 1);
f[0] = 0;
int res = INT_MIN;
for(int i = 1; i <= n; i ++)
{
f[i] = 1;
for(int j = 1; j < i; j ++)
{
if(nums[j - 1] < nums[i - 1])
f[i] = max(f[j] + 1, f[i]);
}
res = max(res, f[i]);
}
return res;
}
};
139. 单词拆分
class Solution {
public:
/*
1.状态表示:f[n],输入nums下标[0,n-1]对应[1,n],需要n+1空间(注意nums的[i]和f[i]不一样!)
2.状态属性:bool
3.状态转移:i表当前点结尾的连续序列能否被拆分,例如[j,i]是个单词,只要[j-1]=true就可以拆分,所以需要遍历j,找到一个使[i]为true的
*/
bool wordBreak(string s, vector<string>& wordDict) {
unordered_map<string, int> map;
for(auto x: wordDict) map[x] ++;//利用map判断是否存在某个单词
int n = s.size();
vector<int> f(n+1, false);
f[0] = true;
for(int i = 1; i <= n; i ++)
{
for(int j = 1; j <= i; j ++)//一个字母可以是一个单词,所以可以j==i
{
if(f[j - 1] && map.count(s.substr(j - 1, i - j + 1)))
{
f[i] = true;
break;
}
}
}
return f[n];
}
};
198. 打家劫舍
class Solution {
public:
/*
1.状态表示:f[n],nums数组输入[0,n-1]对应下标[1,n]
2.状态属性:max
3.状态转移:表示前i家房屋能偷到的最大值,f[i] = max(f[i-2]+nums[i],f[i-1])
*/
int rob(vector<int>& nums) {
if(nums.empty()) return 0;
int n = nums.size();
if(n == 1) return nums[0];
vector<int> f(n + 1);
f[0] = 0;
f[1] = nums[0];
for(int i = 2; i <= n; i ++)
{
f[i] = max(f[i - 2]+nums[i - 1],f[i - 1]);
}
return f[n];
}
};
4.常规的二维dp
一纬不好直接表示或者表示不出来所有状态,需要借助二维空间
5. 最长回文子串
class Solution {
public:
/*
1.状态表示:f[n][n],字符串输入[0,n-1]对应[1,n]
2.状态属性:bool
3.状态转移:i表示子串起点,j表示终点,f[i][j]就是[i,j]是否为回文串:f[i][j]=f[i+1][j-1]&&s[i-1]==s[j-1],但需要判断边界情况
*/
string longestPalindrome(string s) {
int n = s.size();
vector<vector<bool>> f(n+1, vector<bool> (n+1, false));
//初始化对角线
for(int i = 0; i<=n; i ++) f[i][i] = true;
int max_len = INT_MIN, l = 0;
for(int j = 1; j <= n; j ++)
{
for(int i = 1; i <= j; i ++)
{
if(j - i == 1) f[i][j] = s[i - 1] == s[j - 1];
if(j - i >= 2) f[i][j] = (s[i - 1] == s[j - 1]) && f[i + 1][j - 1];
if(f[i][j] && j - i + 1 > max_len) //出现了新的max
{
max_len = j - i + 1;//记录长度
l = i - 1;//记录s的起始下标
}
}
}
return s.substr(l,max_len);
}
};
62. 不同路径
class Solution {
public:
/*
1.状态表示:f[n][n],网格输入[0,n-1]对应[1,n],[0,m-1]对应[1,m]
2.状态属性:数值
3.状态转移:f[i][j]表示网格位置,来源于f[i-1][j]和f[i][j-1]
*/
int uniquePaths(int m, int n) {
vector<vector<int>> f(m + 1, vector<int> (n + 1, 0));//直接全部初始化就好
//初始化
//for(int i = 0; i <= m; i ++) f[i][0] = 0;
//for(int i = 0; i <= n; i ++) f[0][i] = 0;
for(int i = 1; i <= m; i ++)
for(int j = 1; j <= n; j ++)
{
if(i == 1 && j == 1) f[i][j] = 1;
else f[i][j] = f[i - 1][j] + f[i][j - 1];
}
return f[m][n];
}
};
63. 不同路径 II
class Solution {
public:
/*
1.状态表示:f[n][n],网格输入[0,n-1]对应[1,n],[0,m-1]对应[1,m]
2.状态属性:数值
3.状态转移:f[i][j]表示网格位置,来源于f[i-1][j]和f[i][j-1]
*/
int uniquePathsWithObstacles(vector<vector<int>>& g) {
//从起点走到ij的路径数量
int m = g.size(), n = g[0].size();
vector<vector<int>> f(m + 1, vector<int> (n + 1, 0));//初始化为0
//for(int i = 0; i <= m; i ++) f[i][0] = 0;
//for(int i = 0; i <= n; i ++) f[0][i] = 0;
for(int i = 1; i <= m; i ++)
for(int j = 1; j <= n; j ++)
{
if(g[i - 1][j - 1]) continue;//与没有障碍物相比就 只多了这一个
if(i ==1 && j == 1) f[i][j] = 1;
else f[i][j] = f[i - 1][j] + f[i][j - 1];
}
return f[m][n];
}
};
64. 最小路径和(剑指 Offer 47. 礼物的最大价值)
class Solution {
public:
/*
1.状态表示:f[n][n],网格输入[0,n-1]对应[1,n],[0,m-1]对应[1,m]
2.状态属性:min
3.状态转移:f[i][j]表示网格位置,来源于f[i-1][j]和f[i][j-1]
*/
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size(),n = grid[0].size();
vector<vector<int>> f(m + 1,vector<int>(n + 1, INT_MAX));
//for(int i = 0;i<=m;i++) f[i][0] = INT_MAX;
//for(int j = 0;j<=n;j++) f[0][j] = INT_MAX;
for(int i = 1;i <= m;i ++)
for(int j = 1;j <= n;j ++)
{
if(i == 1 && j == 1) f[i][j] = grid[i - 1][j - 1];
else {f[i][j] = min(f[i - 1][j], f[i][j - 1])+grid[i - 1][j - 1];}
}
return f[m][n];
}
};
120. 三角形最小路径和
class Solution {
public:
/*
1.状态表示:f[n][n],三角形层数(横坐标)[1,n]对应[1,n]
2.状态属性:min
3.状态转移:f[i][j]表示网格位置,来源于min(f[i-1][j],f[i-1][j-1])
*/
int minimumTotal(vector<vector<int>>& triangle) {
int n = triangle.size();
vector<vector<int>> f(n + 1,vector<int> (n + 1, INT_MAX));
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= i; j ++)
{
if(i == 1 && j == 1) f[i][j] = triangle[i - 1][j - 1];
else {f[i][j] = min(f[i - 1][j], f[i - 1][j - 1])+triangle[i - 1][j - 1];}
}
int res = INT_MAX;
for(int i = 0; i <= n; i ++) res = min(res, f[n][i]);
return res;
}
};
72. 编辑距离
class Solution {
public:
/*
1.状态表示:f[n][n],单词输入[0,n-1]对应下标[1,n]
2.状态属性:min
3.状态转移:f[i][j]表示word1的前i个字母转化成word2的前j个字母需要的最小次数:
删除i:f[i-1][j]+1,
插入i:f[i][j-1]+1,
修改或者不管:f[i-1][j-1]+0/1
*/
int minDistance(string word1, string word2) {
int n = word1.size(), 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;
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;//删、插入
f[i][j] = min(f[i][j], f[i - 1][j - 1]+(word1[i - 1] != word2[j - 1]));//替换、不用改
}
return f[n][m];
}
};
1143. 最长公共子序列
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int n = text1.size();
int m = text2.size();
vector<vector<int>> f(n + 1, vector<int>(m + 1, 0));
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++)
{
f[i][j] = max(f[i - 1][j], f[i][j - 1]);//忽略s1或者s2
if(text1[i - 1] == text2[j - 1])
f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
}
return f[n][m];
}
};
10. 正则表达式匹配
class Solution {
public:
bool isMatch(string s, string p) {
s = " "+ s;
p = " " + p;
int m = s.size(), n = p.size();
vector<vector<bool>> f(m + 1, vector<bool> (n+1,false));
f[0][0] = true;
for(int i = 1; i <= m; i ++)
for(int j = 1; j <= n; j ++)
{
if(p[j-1] != '*') f[i][j] = f[i-1][j-1] && (s[i-1] == p[j-1] || p[j-1] == '.');
else f[i][j] = f[i][j-2] || f[i-1][j] && (s[i-1] == p[j-2] || p[j-2] == '.');
}
return f[m][n];
}
};
部分内容来自y总的教学,致谢!