35、647. 回文子串
①状态表示:dp[i][j]表示:s字符串[i,j]的子串,是否是回文串。(i<=j)
②状态转移方程:
if(s[i]!=s[j]) dp[i][j]=false;
else if(s[i]==s[j])
{
if(i==j||i+1==j)dp[i][j]=true;
else dp[i+1][j-1];
}
③初始化:无需初始化。
④填表顺序:从下往上。
⑤返回值:dp表中true的个数。
class Solution
{
public:
int countSubstrings(string s)
{
int n = s.size();
vector<vector<bool>> dp(n, vector<bool>(n));
int ret = 0;
for(int i = n - 1; i >= 0; i--)
{
for(int j = i; j < n; j++)
{
if(s[i] == s[j])
dp[i][j] = i + 1 < j ? dp[i + 1][j - 1] : true;
if(dp[i][j])
ret++;
}
}
return ret;
}
};
36、5. 最长回文子串
①状态表示:dp[i][j]表示:s字符串[i,j]的子串,是否是回文串。(i<=j)
②状态转移方程:
if(s[i]!=s[j]) dp[i][j]=false;
else if(s[i]==s[j])
{
if(i==j||i+1==j)dp[i][j]=true;
else dp[i+1][j-1];
}
③初始化:无需初始化。
④填表顺序:从下往上。
⑤返回值:dp表中true的情况下,长度最大的子串的起始位置以及长度。
class Solution
{
public:
string longestPalindrome(string s)
{
int n = s.size();
vector<vector<bool>> dp(n, vector<bool>(n));
int len = 1, begin = 0;
for(int i = n - 1; i >= 0; i--)
{
for(int j = i; j < n; j++)
{
if(s[i] == s[j])
dp[i][j] = i + 1 < j ? dp[i + 1][j - 1] : true;
if(dp[i][j] && j - i + 1 > len)
{
len = j - i + 1 , begin = i;
}
}
}
return s.substr(begin, len);
}
};
37、1745. 分割回文串 IV
class Solution
{
public:
bool checkPartitioning(string s)
{
// 1、用dp把所有子串是否是回文预处理一下
int n = s.size();
vector<vector<bool>> dp(n, vector<bool>(n));
int len = 1, begin = 0;
for(int i = n - 1; i >= 0; i--)
for(int j = i; j < n; j++)
if(s[i] == s[j])
dp[i][j] = i + 1 < j ? dp[i + 1][j - 1] : true;
// 2、枚举所有的第二个字符串的起始位置以及结束位置
for(int i = 1; i < n - 1; i++)
for(int j = i; j < n - 1; j++)
if(dp[0][i - 1] && dp[i][j] && dp[j + 1][n - 1])
return true;
return false;
}
};
38、132. 分割回文串 II
①状态表示:dp[i]表示:s[0,i]区间上的的子串,最少分割次数。
②状态转移方程:
0~i是回文串:0
0~i不是回文串:(0<j<=i)j~i是否回文:
若是:dp[i]=min(dp[j-1]+1) 若否:×
优化:二维dp表,将所有的子串是否是回文的信息,保存在dp表里面。
③初始化:dp表内所有值都初始为无穷大。
④填表顺序:从左往右填表。
⑤返回值:dp[n-1]。
class Solution
{
public:
int minCut(string s)
{
// 1、用dp把所有子串是否是回文预处理一下
int n = s.size();
vector<vector<bool>> isPal(n, vector<bool>(n));
for(int i = n - 1; i >= 0; i--)
for(int j = i; j < n; j++)
if(s[i] == s[j])
isPal[i][j] = i + 1 < j ? isPal[i + 1][j - 1] : true;
// 2、
vector<int> dp(n, INT_MAX);
for(int i = 0; i < n; i++)
{
if(isPal[0][i]) dp[i] = 0;
else
{
for(int j = 1; j <= i; j++)
{
if(isPal[j][i]) dp[i]=min(dp[i], dp[j-1]+1);
}
}
}
return dp[n - 1];
}
};
39、516. 最长回文子序列
①状态表示:
dp[i]表示:以i位置为结尾的所有子序列中,最长的回文子序列的长度。
dp[i][j]表示:s字符串[i,j]区间内的所有子序列中,最长的回文子序列的长度。
②状态转移方程:
s[i]==s[j]:if(i==j) dp[i][j]=1
else if(i+1==j) dp[i][j]=2
else dp[i][j]=dp[i+1][j-1]+2
s[i]!=s[j]:dp[i][j]=max(dp[i][j-1],dp[i+1][j])
③初始化:无需初始化。
④填表顺序:从下往上填写,每一行从左往右填表。
⑤返回值:dp[0][n-1]。
class Solution
{
public:
int longestPalindromeSubseq(string s)
{
int n = s.size();
vector<vector<int>> dp(n, vector<int>(n));
for(int i = n - 1; i >= 0; i--)
{
dp[i][i] = 1;
for(int j = i + 1; j < n; j++)
{
if(s[i] == s[j]) dp[i][j] = dp[i + 1][j - 1] + 2;
else dp[i][j] = max(dp[i][j - 1], dp[i + 1][j]);
}
}
return dp[0][n - 1];
}
};
40、1312. 让字符串成为回文串的最少插入次数
①状态表示:
dp[i][j]表示:s字符串[i,j]区间内的所有子序列中,使它成为回文串的最小插入次数。
②状态转移方程:
s[i]==s[j]:if(i==j) dp[i][j]=0
else if(i+1==j) dp[i][j]=0
else dp[i][j]=dp[i+1][j-1]
s[i]!=s[j]:dp[i][j]=min(dp[i][j-1],dp[i+1][j])+1
③初始化:无需初始化。
④填表顺序:从下往上填写,每一行从左往右填表。
⑤返回值:dp[0][n-1]。
class Solution
{
public:
int minInsertions(string s)
{
int n = s.size();
vector<vector<int>> dp(n, vector<int>(n));
for(int i = n - 1; i >= 0; i--)
{
for(int j = i + 1; j < n; j++)
{
if(s[i] == s[j]) dp[i][j] = dp[i + 1][j - 1];
else dp[i][j] = min(dp[i][j - 1], dp[i + 1][j]) + 1;
}
}
return dp[0][n - 1];
}
};
41、1143. 最长公共子序列
①状态表示:
dp[i][j]表示:s1的[0,i]区间以及s2的[0,j]区间内的所有子序列中,最长公共子序列的长度。
②状态转移方程:根据最后一个位置的状况,分情况讨论。
s[i]==s[j]:dp[i][j]=dp[i-1][j-1]+1
s[i]!=s[j]:dp[i][j]=max(dp[i][j-1],dp[i-1][j])
③初始化:关于字符串的dp问题:空串是有研究意义的。引入空串的概念后,会方便我们初始化。
(1)里面的值要保证我们后续的填表是正确的·。
(2)下标的映射关系:下标减1或s1=“ ”+s1 s2=“ ”+s2
④填表顺序:从上往下,每一行从左往右填表。
⑤返回值:dp[m][n]。
class Solution
{
public:
int longestCommonSubsequence(string s1, string s2)
{
int m = s1.size(), n = s2.size();
s1 = " " + s1, s2 = " " + s2;
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
for(int i = 1; i <= m; i++)
{
for(int j = 1; j <= n; j++)
{
if(s1[i] == s2[j]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = max(dp[i][j - 1],dp[i - 1][j]);
}
}
return dp[m][n];
}
};
43、115. 不同的子序列
①状态表示:
dp[i][j]表示:s字符串[0,j]区间内所有的子序列,有多少个t字符串[0,i]区间内的子串。
②状态转移方程:根据s的子序列的最后一个位置包不包含s[j]
包含s[j]:t[i]==s[j]:dp[i][j]=dp[i-1][j-1]
不包含s[j]:dp[i][j]=dp[i][j-1]
dp[i][j]=dp[i][j-1]+dp[i-1][j-1]
③初始化:引入空串。
(1)里面的值要保证我们后续的填表是正确的。
(2)下标的映射关系:下标减1或s=“ ”+s
④填表顺序:从上往下,每一行从左往右填表。
⑤返回值:dp[m][n]。
class Solution
{
public:
int numDistinct(string s, string t)
{
int m = t.size(), n = s.size();
vector<vector<double>> dp(m + 1, vector<double>(n + 1));
for(int j = 0; j <= n; j++) dp[0][j] = 1; // 初始化
for(int i = 1; i <= m; i++)
for(int j = 1; j <= n; j++)
{
dp[i][j] += dp[i][j - 1];
if(t[i - 1] == s[j - 1]) dp[i][j] += dp[i - 1][j - 1];
}
return dp[m][n];
}
};
44、115. 不同的子序列
①状态表示:
dp[i][j]表示:p[0,j]区间内的子串能否匹配s[0,i]区间内的子串。
②状态转移方程:根据最后一个位置的状况,分情况讨论。
p[j]是普通字符→if(s[i]==p[i]&&dp[i-1][j-1]==true) dp[i][j]=true
p[j]=='?'→if(dp[i-1][j-1]==true) dp[i][j]=true
p[j]=='*'→代替n个字符:dp[i][j]=dp[i-n][j-1]
优化:
法1:数学:
dp[i][j]=dp[i][j-1]||dp[i-1][j-1]||dp[i-2][j-1]......=dp[i][j-1]||dp[i-1][j]
法2:根据状态表示以及实际情况,优化状态转移方程
空串→dp[i][j-1]
匹配一个,但不舍去→dp[i][j]=dp[i-1][j]→dp[i-2][j]→dp[i-3][j]......
dp[i][j]=dp[i][j-1]||dp[i-1][j]
③初始化:引入空串。
(1)里面的值要保证我们后续的填表是正确的。
(2)下标的映射关系:下标减1或s=“ ”+s
④填表顺序:从上往下,每一行从左往右填表。
⑤返回值:dp[m][n]。
class Solution
{
public:
bool isMatch(string s, string p)
{
int m = s.size(), n = p.size();
s = " " + s, p = " " + p;
vector<vector<bool>> dp(m + 1, vector<bool>(n + 1));
// 初始化
dp[0][0] = true;
for(int j = 1; j <= n; j++)
{
if(p[j] == '*') dp[0][j] = true;
else break;
}
for(int i = 1; i <= m; i++)
{
for(int j = 1; j <= n; j++)
{
if(p[j] == '*') dp[i][j] = dp[i][j - 1] || dp[i - 1][j];
else dp[i][j] = (p[j] == '?' || s[i] == p[j]) && dp[i - 1][j - 1];
}
}
return dp[m][n];
}
};
45、10. 正则表达式匹配
①状态表示:
dp[i][j]表示:p[0,j]区间内的子串能否匹配s[0,i]区间内的子串。
②状态转移方程:根据最后一个位置的状况,分情况讨论。
p[j]是普通字符→if(s[i]==p[i]&&dp[i-1][j-1]==true) dp[i][j]=true
p[j]=='.'→if(dp[i-1][j-1]==true) dp[i][j]=true
p[j]=='*'→p[j-1]=='.'→代替n个'.'→dp[i][j]=dp[i-n][j-2]
→p[j-1]是普通字符→空串→dp[i][j-2]
→匹配一个,然后保留→p[j-1]=s[i]&&dp[i-1][j]
总结:
dp[i][j]=(s[i]==p[i]||p[j]=='.')&&dp[i-1][j-1]
dp[i][j]=dp[i][j-2]||(p[j-1]=='.'||p[j-1]==s[i])&&dp[i-1][j]
优化:
法1:数学:
p[j]=='*'→dp[i][j]=dp[i][j-2]||dp[i-1][j-2]||dp[i-2][j-2]......=dp[i][j-2]||dp[i-1][j]
法2:根据状态表示以及实际情况,优化状态转移方程
p[j]=='*'→".*"干掉一个之后保留→dp[i][j]=dp[i-1][j]
→".*"舍去→dp[i][j-2]
③初始化:引入空串。
(1)里面的值要保证我们后续的填表是正确的。
(2)下标的映射关系:下标减1或s=“ ”+s
④填表顺序:从上往下,每一行从左往右填表。
⑤返回值:dp[m][n]。
class Solution
{
public:
bool isMatch(string s, string p)
{
int m = s.size(), n = p.size();
s = " " + s, p = " " + p;
vector<vector<bool>> dp(m + 1, vector<bool>(n + 1));
// 初始化
dp[0][0] = true;
for(int j = 2; j <= n; j+=2)
{
if(p[j] == '*') dp[0][j] = true;
else break;
}
for(int i = 1; i <= m; i++)
{
for(int j = 1; j <= n; j++)
{
if(p[j] == '*') dp[i][j] = dp[i][j-2] || (p[j - 1] == '.' || p[j - 1] == s[i]) && dp[i - 1][j];
else dp[i][j] = (s[i] == p[j] || p[j] == '.') && dp[i - 1][j - 1];
}
}
return dp[m][n];
}
};
46、97. 交错字符串
①状态表示:
dp[i][j]表示:s1中[1,i]内的字符串以及[1,j]区间内的字符串,能否拼凑成s3[1,i+j]区间内的字符串。
②状态转移方程:根据最后一个位置的状况,分情况讨论。
if(s1[i]==s3[i+j]&&dp[i-1][j]==true) dp[i][j]=true
if(s2[j]==s3[i+j]&&dp[i][j-1]==true) dp[i][j]=true
③初始化:略。
④填表顺序:从上往下,每一行从左往右填表。
⑤返回值:dp[m][n]。
class Solution
{
public:
bool isInterleave(string s1, string s2, string s3)
{
int m = s1.size(), n = s2.size();
if(m + n != s3.size()) return false;
s1 = " " + s1, s2 = " " + s2, s3 = " " + s3;
vector<vector<bool>> dp(m + 1, vector<bool>(n + 1));
// 初始化
dp[0][0] = true;
for(int j = 1; j <= n; j++) // 初始化第一行
{
if(s2[j] == s3[j]) dp[0][j] = true;
else break;
}
for(int i = 1; i <= m; i++) // 初始化第一列
{
if(s1[i] == s3[i]) dp[i][0] = true;
else break;
}
for(int i = 1; i <= m; i++)
{
for(int j = 1; j <= n; j++)
{
dp[i][j] = (s1[i] == s3[i + j] && dp[i - 1][j] == true)
|| (s2[j] == s3[i + j] && dp[i][j - 1]);
}
}
return dp[m][n];
}
};
47、712. 两个字符串的最小ASCII删除和
①状态表示:求两个字符串里面所欲的公共子序列里面,ASCLL的最大和。
dp[i][j]表示:s1中[0,i]区间以及s2的[0,j]区间内的所有子序列里,公共子序列的ASCll最大和。
②状态转移方程:根据最后一个位置的状况,分情况讨论。
有s1[i],有s2[j]→s1[i]==s2[j]→dp[i][j]=dp[i-1][j-1]+s1[i]
有s1[i],没有s2[j]→dp[i][j]=dp[i][j-1]
没有s1[i],有s2[j]→dp[i][j]=dp[i-1][j]
没有s1[i],没有s2[j]→dp[i][j]=dp[i-1][j-1]×
③初始化:一行一列虚拟节点。
④填表顺序:从上往下,每一行从左往右填表。
⑤返回值:sum-2*dp[m][n]。
class Solution
{
public:
int minimumDeleteSum(string s1, string s2)
{
int m = s1.size(), n = s2.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
for(int i = 1; i <= m; i++)
for(int j = 1; j <= n; j++)
{
dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);
if(s1[i - 1] == s2[j - 1])
dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + s1[i - 1]);
}
int sum = 0;
for(auto s : s1) sum += s;
for(auto s : s2) sum += s;
return sum - 2 * dp[m][n];
}
};
49、牛客 DP41 【模板】01背包
①状态表示:
dp[i]表示:从前i个物品中选,所有的选法中,能挑选出来的最大价值。×
dp[i][j]表示:从前i个物品中选,总体积不超过j,所有的选法中,能挑选出来的最大价值。
②状态转移方程:根据最后一步的状况,分情况讨论。
不选i物品→dp[i][j]=dp[i-1][j-1]
选i物品→dp[i][j]=dp[i-1][j-v[i]]+w[i]
③初始化:一行一列虚拟节点。
④填表顺序:从上往下。
⑤返回值:dp[n][v]。
#include <iostream>
#include <string.h>
using namespace std;
const int N = 1001;
int n, V, v[N], w[N];
int dp[N][N];
int main()
{
// 读入数据
cin >> n >> V;
for(int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
// 解决第一问
for(int i = 1; i <= n; i++)
for(int j = 1; j <= V; j++)
{
dp[i][j] = dp[i - 1][j];
if(j >= v[i]) dp[i][j] = max(dp[i][j],dp[i - 1][j - v[i]] + w[i]);
}
cout << dp[n][V] << endl;
// 解决第二问
memset(dp, 0, sizeof dp);
for(int j = 1; j <= V; j++) dp[0][j] = -1;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= V; j++)
{
dp[i][j] = dp[i - 1][j];
if(j >= v[i] && dp[i -1][j - v[i]] != -1)
dp[i][j] = max(dp[i][j],dp[i - 1][j - v[i]] + w[i]);
}
cout << (dp[n][V] == -1 ? 0 : dp[n][V]) << endl;
return 0;
}
优化:
①利用滚动数组做空间上的优化
②直接在原始的代码上稍加修改即可:删除所有的横坐标并修改j的遍历顺序。
不选i物品→dp[j]=dp[j]
选i物品→dp[i][j]=dp[j-v[i]](遍历顺序→从右往左)
#include <iostream>
#include <string.h>
using namespace std;
const int N = 1001;
int n, V, v[N], w[N];
int dp[N];
int main()
{
// 读入数据
cin >> n >> V;
for(int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
// 解决第一问
for(int i = 1; i <= n; i++)
for(int j = V; j >= v[i]; j--) // 修改遍历顺序
dp[j] = max(dp[j],dp[j - v[i]] + w[i]);
cout << dp[V] << endl;
// 解决第二问
memset(dp, 0, sizeof dp);
for(int j = 1; j <= V; j++) dp[j] = -1;
for(int i = 1; i <= n; i++)
for(int j = V; j >= v[i]; j--)
if(dp[j - v[i]] != -1)
dp[j] = max(dp[j],dp[j - v[i]] + w[i]);
cout << (dp[V] == -1 ? 0 : dp[V]) << endl;
return 0;
}
50、416.分割等和子集
①状态表示:(在数组中选择一些数出来,让这些数的和等于sum/2)
dp[i][j]表示:从前i个数中选,所有的选法中,能否凑成j这个数。
②状态转移方程:根据最后一步的状况,分情况讨论。
不选i→dp[i][j]=dp[i-1][j]
选i(j>=nums[i])→dp[i][j]=dp[i-1][j-nums[i]]
dp[i][j]=dp[i-1][j]||dp[i-1][j-nums[i]]
③初始化:一行一列虚拟节点。
④填表顺序:从上往下。
⑤返回值:dp[n][sum/2]。
class Solution
{
public:
bool canPartition(vector<int>& nums)
{
int n = nums.size(), sum = 0;
for(auto x : nums) sum += x;
if(sum % 2) return false;
int aim = sum / 2;
vector<vector<bool>> dp(n + 1, vector<bool>(aim + 1));
for(int i = 0; i <= n; i++) dp[i][0] = true;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= aim; j++)
{
dp[i][j] = dp[i-1][j];
if(j >= nums[i - 1]) dp[i][j] = dp[i][j] || dp[i - 1][j - nums[i - 1]];
}
return dp[n][aim];
}
};
优化:
class Solution
{
public:
bool canPartition(vector<int>& nums)
{
int n = nums.size(), sum = 0;
for(auto x : nums) sum += x;
if(sum % 2) return false;
int aim = sum / 2;
vector<bool> dp(aim + 1);
dp[0] = true;
for(int i = 1; i <= n; i++)
for(int j = aim; j >= nums[i - 1]; j--)
dp[j] = dp[j] || dp[j - nums[i - 1]];
return dp[aim];
}
};
51、494. 目标和
①状态表示:(转化:在数组中选择一些数,让这些数的和等于(target+sum)/2,问有多少种选法)
dp[i][j]表示:从前i个数中选,总和正好等于j,一共有多少种选法。
②状态转移方程:根据最后一步的状况,分情况讨论。
不选i→dp[i][j]=dp[i-1][j]
选i(j>=nums[i])→dp[i][j]=dp[i-1][j-nums[i]]
dp[i][j]=dp[i-1][j]+dp[i-1][j-nums[i]]
③初始化:一行一列虚拟节点,且初始化第一行即可。
④填表顺序:从上往下。
⑤返回值:dp[n][target+sum)/2]。
class Solution
{
int aim, ret;
public:
int findTargetSumWays(vector<int>& nums, int target)
{
int n = nums.size(), sum = 0;
for(auto x : nums) sum += x;
int aim = (target + sum) / 2;
if(aim < 0 || (sum + target) % 2) return 0;
vector<vector<int>> dp(n + 1, vector<int>(aim + 1));
dp[0][0] = 1;
for(int i = 1; i <= n; i++)
for(int j = 0; j <= aim; j++)
{
dp[i][j]=dp[i - 1][j];
if(j >= nums[i - 1]) dp[i][j] += dp[i - 1][j - nums[i - 1]];
}
return dp[n][aim];
}
};
⑥优化
class Solution
{
int aim, ret;
public:
int findTargetSumWays(vector<int>& nums, int target)
{
int n = nums.size(), sum = 0;
for(auto x : nums) sum += x;
int aim = (target + sum) / 2;
if(aim < 0 || (sum + target) % 2) return 0;
vector<int> dp(aim + 1);
dp[0] = 1;
for(int i = 1; i <= n; i++)
for(int j = aim; j >= nums[i - 1] && j >= 0; j--)
dp[j] += dp[j - nums[i - 1]];
return dp[aim];
}
};
52、1049. 最后一块石头的重量 II
①状态表示:(转化:在数组中选择一些数,让这些数的和等于尽可能接近sum/2,问有多少种选法)
dp[i][j]表示:从前i个数中选,总和不超过i,此时的最大和。
②状态转移方程:根据最后一步的状况,分情况讨论。
不选i→dp[i][j]=dp[i-1][j]
选i(j>=nums[i])→dp[i][j]=dp[i-1][j-nums[i]]+nums[i]
dp[i][j]=max(dp[i-1][j],dp[i-1][j-nums[i]]+nums[i])
③初始化:一行一列虚拟节点,且初始化第一行即可。
④填表顺序:从上往下填写每一行。
⑤返回值:sum-2*dp[n][sum/2]。
class Solution
{
public:
int lastStoneWeightII(vector<int>& stones)
{
int sum = 0;
for(auto x : stones) sum += x;
int n = stones.size(), m = sum / 2;
vector<vector<int>> dp(n + 1, vector<int>(m + 1));
for(int i = 1; i <= n; i++)
for(int j = 0; j <= m; j++)
{
dp[i][j]=dp[i - 1][j];
if(j >= stones[i - 1]) dp[i][j] = max(dp[i][j], dp[i - 1][j - stones[i - 1]] + stones[i - 1]);
}
return sum - 2 * dp[n][sum / 2];
}
};
优化:
class Solution
{
public:
int lastStoneWeightII(vector<int>& stones)
{
int sum = 0;
for(auto x : stones) sum += x;
int n = stones.size(), m = sum / 2;
vector<int> dp(m + 1);
for(int i = 1; i <= n; i++)
for(int j = m; j >= 0; j--)
if(j >= stones[i - 1]) dp[j] = max(dp[j], dp[j - stones[i - 1]] + stones[i - 1]);
return sum - 2 * dp[m];
}
};
53、牛客.DP42 【模板】完全背包
(1)①状态表示:
dp[i][j]表示:从前i个物品中选,总体积不超过j,所有的选法中,最大的价值。
②状态转移方程:根据最后一步的状况,分情况讨论。
不选dp[i][j]=dp[i-1][j]
选1个dp[i][j]=dp[i-1][j-v[i]]+w[i]
选2个dp[i][j]=dp[i-1][j-2*v[i]]+2*w[i]
......
优化:
dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i],dp[i-1][j-2*v[i]]+2*w[i]......dp[i-1][j-kv[i]]+kw[i])
dp[i][j-v[i]]=max(dp[i-1][j-v[i]],dp[i-1][j-2*v[i]]+w[i]......dp[i-1][j-kv[i]]+xw[i])+(x-1)w[i]
(其中k=x)
故dp[i][j]=dp[i][j-v[i]]+w[i]
综上:dp[i][j]=max(dp[i-1][j],dp[i][j-v[i]] +w[i])
③初始化:一行一列虚拟节点,且初始化第一行即可。
④填表顺序:从上往下,每一行从左往右填写每一行。
⑤返回值:dp[n][v]。
(2)①状态表示:
dp[i][j]表示:从前i个物品中选,总体积等于j,所有的选法中,最大的价值。
②状态转移方程:用-1表示没有这个状态
dp[i][j]=max(dp[i-1][j],dp[i][j-v[i]] +w[i]) (dp[i][j-v[i]]!=-1)
③初始化:一行一列虚拟节点,且初始化第一行即可。(除第一个位置,都初始为-1)
④填表顺序:从上往下,每一行从左往右填写每一行。
⑤返回值:dp[n][v]==-1?0:dp[n][v]。
#include <iostream>
#include <string.h>
using namespace std;
const int N = 1010;
int n, V, v[N], w[N];
int dp[N][N];
int main()
{
// 读入数据
cin >> n >> V;
for (int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
for (int i = 1; i <= n; i++)
for (int j = 0; j <= V; j++)
{
dp[i][j] = dp[i - 1][j];
if (j >= v[i])
dp[i][j] = max(dp[i][j], dp[i][j - v[i]] + w[i]);
}
cout << dp[n][V] << endl;
memset(dp, 0, sizeof dp);
for (int j = 1; j <= V; j++) dp[0][j] = -1;
for (int i = 1; i <= n; i++)
for (int j = 0; j <= V; j++)
{
dp[i][j] = dp[i - 1][j];
if (j >= v[i] && dp[i][j - v[i]] != -1)
dp[i][j] = max(dp[i][j], dp[i][j - v[i]] + w[i]);
}
cout << (dp[n][V] == -1 ? 0 : dp[n][V]) << endl;
return 0;
}
优化:利用滚动数组,做空间上的优化。
状态转移方程:dp[j]=max(dp[j],dp[j-v[i]]+w[i])
填表顺序:从左往右遍历(和01背包进行对比)
#include <iostream>
#include <string.h>
using namespace std;
const int N = 1010;
int n, V, v[N], w[N];
int dp[N];
int main()
{
// 读入数据
cin >> n >> V;
for (int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
for (int i = 1; i <= n; i++)
for (int j = 0; j <= V; j++)
if (j >= v[i])
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
cout << dp[V] << endl;
memset(dp, 0, sizeof dp);
for (int j = 1; j <= V; j++) dp[j] = -1;
for (int i = 1; i <= n; i++)
for (int j = v[i]; j <= V; j++)
if (dp[j - v[i]] != -1)
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
cout << (dp[V] == -1 ? 0 : dp[V]) << endl;
return 0;
}
54、322. 零钱兑换
①状态表示:
dp[i][j]表示:从前i个硬币中选,总和正好等于j,所有的选法中,最少的硬币个数。
②状态转移方程:根据最后一步的状况,分情况讨论。
不选i→dp[i][j]=dp[i-1][j]
选1个i→dp[i][j]=dp[i-1][j-coins[i]]+1
选2个i→dp[i][j]=dp[i-1][j-2*coins[i]]+2
......
dp[i][j]=min(dp[i-1][j],dp[i-1][j-coins[i]]+1) (j-coins[i]>=0)
③初始化:一行一列虚拟节点,且初始化第一行,除了第一个位置,都初始化为0x3f3f3f3f。
④填表顺序:从上往下,每一行从左往右填写。
⑤返回值:dp[n][amount]>=INF?-1:dp[n][amount]。
class Solution
{
public:
int coinChange(vector<int>& coins, int amount)
{
const int INF = 0x3f3f3f3f;
int n = coins.size();
vector<vector<int>> dp(n + 1, vector<int>(amount + 1));
for(int j = 1; j <= amount; j++)
dp[0][j] = INF;
for(int i = 1; i <= n; i++)
for(int j = 0; j <= amount; j++)
{
dp[i][j] = dp[i-1][j];
if(j >= coins[i - 1])
dp[i][j] = min(dp[i][j], dp[i][j - coins[i - 1]] + 1);
}
return dp[n][amount] >= INF ? -1 : dp[n][amount];
}
};
优化:
class Solution
{
public:
int coinChange(vector<int>& coins, int amount)
{
const int INF = 0x3f3f3f3f;
int n = coins.size();
vector<int> dp(amount + 1, INF);
dp[0] = 0;
for(int i = 1; i <= n; i++)
for(int j = coins[i - 1]; j <= amount; j++)
dp[j] = min(dp[j], dp[j - coins[i - 1]] + 1);
return dp[amount] >= INF ? -1 : dp[amount];
}
};
55、518. 零钱兑换 II
①状态表示:
dp[i][j]表示:从前i个硬币中选,总和正好等于j,一共有多少种选法。
②状态转移方程:根据最后一步的状况,分情况讨论。
不选i→dp[i][j]=dp[i-1][j]
选1个i→dp[i][j]=dp[i-1][j-coins[i]]
选2个i→dp[i][j]=dp[i-1][j-2*coins[i]]
......
dp[i][j]=min(dp[i-1][j],dp[i-1][j-coins[i]]) (j-coins[i]>=0)
③初始化:一行一列虚拟节点,且初始化第一行,除了第一个位置初始化为1,都初始化为0。
④填表顺序:从上往下,每一行从左往右填写。
⑤返回值:dp[n][amount]]。
class Solution
{
public:
int change(int amount, vector<int>& coins)
{
int n = coins.size();
vector<int> dp(amount + 1);
dp[0] = 1;
for(int i = 1; i <= n; i++)
for(int j = 0; j <= amount; j++)
if(j >= coins[i - 1])
dp[j] += dp[j - coins[i - 1]];
return dp[amount];
}
};
56、279. 完全平方数
①状态表示:
dp[i][j]表示:从前i个完全平方数中挑选,总和正好等于j,所有的选法中,最小的数量。
②状态转移方程:根据最后一步的状况,分情况讨论。
不选i方→dp[i][j]=dp[i-1][j]
选1个i方→dp[i][j]=dp[i-1][j-i方]+1
选2个i方→dp[i][j]=dp[i-1][j-2*i方]+2
......
dp[i][j]=min(dp[i-1][j],dp[i][j-i方]+1)
③初始化:一行一列虚拟节点,且初始化第一行,除了第一个位置初始化为0,都初始化为无穷大。
④填表顺序:从上往下,每一行从左往右填写。
⑤返回值:dp[n开方][n]。
class Solution
{
public:
int numSquares(int n)
{
int m = sqrt(n);
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
for(int j = 1; j <= n; j++) dp[0][j] = 0x3f3f3f3f;
for(int i = 1; i <= m; i++)
for(int j = 0; j <= n; j++)
{
dp[i][j] = dp[i - 1][j];
if(j >= i * i)
dp[i][j] = min(dp[i][j], dp[i][j - i * i] + 1);
}
return dp[m][n];
}
};
优化:
class Solution
{
public:
int numSquares(int n)
{
int m = sqrt(n);
vector<int> dp(n + 1);
for(int j = 1; j <= n; j++) dp[j] = 0x3f3f3f3f;
for(int i = 1; i <= m; i++)
for(int j = i * i; j <= n; j++)
dp[j] = min(dp[j], dp[j - i * i] + 1);
return dp[n];
}
};
57、474. 一和零
①状态表示:(二维费用)
dp[i][j][k]表示:从前i个完全平方数中挑选,字符0的个数不超过j,字符1的个数不超过k,所有的选法中,最大的长度。
②状态转移方程:根据最后一步的状况,分情况讨论。
不选str[i]→dp[i][j][k]=dp[i-1][j][k]
选str[i]→dp[i][j][k]=dp[i-1][j-a][k-b]+1 (j>=a&&k>=b)
dp[i][j]=max(dp[i-1][j][k],dp[i-1][j-a][k-b]+1)
③初始化:
④填表顺序:i从小到大。
⑤返回值:dp[len][m][n]。
class Solution
{
public:
int findMaxForm(vector<string>& strs, int m, int n)
{
int len = strs.size();
vector<vector<vector<int>>> dp(len + 1, vector<vector<int>>(m + 1, vector<int>(n + 1)));
for(int i = 1; i <= len; i++)
{
int a = 0, b = 0;
for(auto ch : strs[i - 1])
if(ch == '0') a++;
else b++;
for(int j = 0; j <= m; j++)
for(int k = 0; k <= n; k++)
{
dp[i][j][k] = dp[i - 1][j][k];
if(j >= a && k >= b)
dp[i][j][k] = max(dp[i][j][k], dp[i - 1][j - a][k - b] + 1);
}
}
return dp[len][m][n];
}
};
优化:
class Solution
{
public:
int findMaxForm(vector<string>& strs, int m, int n)
{
int len = strs.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
for(int i = 1; i <= len; i++)
{
int a = 0, b = 0;
for(auto ch : strs[i - 1])
if(ch == '0') a++;
else b++;
// 从大到小优化
for(int j = m; j >= a; j--)
for(int k = n; k >= b; k--)
dp[j][k] = max(dp[j][k], dp[j - a][k - b] + 1);
}
return dp[m][n];
}
};
58、879. 盈利计划
①状态表示:(二维费用)
dp[i][j][k]表示:从前i个计划中挑选,总人数不超过j,总利润至少为k,所有的选法中,一共有多少种选法。
②状态转移方程:根据最后一步的状况,分情况讨论。
不选i→dp[i][j][k]=dp[i-1][j][k]
选i→dp[i][j][k]=dp[i-1][j-g[i]][max(0,k-p[i])] j>=g[i]
dp[i][j][k]=dp[i-1][j][k]+dp[i-1][j-g[i]][max(0,k-p[i])]
dp[i][j][k]%=1e9+7
③初始化:dp[0][j][0]=1
④填表顺序:i从小到大。
⑤返回值:dp[len][m][n]。
class Solution
{
public:
int profitableSchemes(int n, int m, vector<int>& g, vector<int>& p)
{
const int MOD = 1e9 + 7;
int len = g.size();
vector<vector<vector<int>>> dp(len + 1, vector<vector<int>>(n + 1, vector<int>(m + 1)));
for(int j = 0; j <= n; j++) dp[0][j][0] = 1;
for(int i = 1; i <= len; i++)
for(int j = 0; j <= n; j++)
for(int k = 0; k <= m; k++)
{
dp[i][j][k] = dp[i-1][j][k];
if(j >= g[i - 1]) dp[i][j][k] += dp[i - 1][j - g[i - 1]][max(0, k - p[i - 1])];
dp[i][j][k] %= MOD;
}
return dp[len][n][m];
}
};
优化:
class Solution
{
public:
int profitableSchemes(int n, int m, vector<int>& g, vector<int>& p)
{
const int MOD = 1e9 + 7;
int len = g.size();
vector<vector<int>> dp(n + 1, vector<int>(m + 1));
for(int j = 0; j <= n; j++) dp[j][0] = 1;
for(int i = 1; i <= len; i++)
for(int j = n; j >= g[i - 1]; j--)
for(int k = m; k >= 0; k--)
{
dp[j][k] += dp[j - g[i - 1]][max(0, k - p[i - 1])];
dp[j][k] %= MOD;
}
return dp[n][m];
}
};
59、377. 组合总和 Ⅳ
①状态表示:根据分析问题的过程中,发现重复子问题,抽象出来一个状态表示。
dp[i]表示:凑成总和i,一共有多少种排列数。
②状态转移方程:根据最后一步的状况,分情况讨论。
if(i>=nums[j]) dp[i]=dp[i-nums[j]]
dp[i]+=[i-nums[j]]
③初始化:dp[0]=1
④填表顺序:从左到右。
⑤返回值:dp[target]。
class Solution
{
public:
int combinationSum4(vector<int>& nums, int target)
{
vector<double> dp(target + 1);
dp[0] = 1;
for(int i = 1; i <= target; i++)
for(auto x : nums)
if(i >= x)
dp[i] += dp[i - x];
return dp[target];
}
};
60、96. 不同的二叉搜索树(左子树<根<右子树)
①状态表示:根据分析问题的过程中,发现重复子问题,抽象出来一个状态表示。
dp[i]表示:结点个数为i时,一共有多少种二叉搜索树。
②状态转移方程:根据最后一步的状况,分情况讨论。
卡特兰数:dp[i]+=dp[j-1]*dp[i-j] (1<=j<=i)
③初始化:dp[0]=1
④填表顺序:从左到右。
⑤返回值:dp[n]。
class Solution
{
public:
int numTrees(int n)
{
vector<int> dp(n + 1);
dp[0] = 1;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= i; j++)
dp[i] += dp[i - j] * dp[j - 1];
return dp[n];
}
};