4 5
1 2
2 4
3 4
4 5
输出
8
[版本一] 0-1背包问题的二维状态定义
f[i][j] : 只选前i个物品, 总体积 <= j 的 Max
状态分析
(1) 当前背包容量不够时(j < v[i]),选不了第 i 个物品,因此前i个物品的最优解和前 i - 1一样
f[i][j] = f[i - 1][j]
(2) 当前背包容量够第 i 个时(j ≥ v[i]),可选第 i 个物品,因此需要继续决策 选 or 不选 第i个物品
f[i][j] = f[i - 1][j - v[i] + w[i]
f[i][j] = f[i - 1][j]
状态转移方程:
f[i][j] = max(f[i-1][j], f[i-1][j-v[i]] + w[i]
C++ 代码如下
#include
#include
#include
using namespace std;
const int N = 1010;
int n, m;
int v[N]; // 体积
int w[N]; // weight
int f[N][N]; // DP数组
// f[i][j] : 只选前i个物品, 总体积 <= j
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
// 二维f[i][j]
for(int i = 1; i <= n; i++){
for (int j = 0; j <= m; j ++ ){
f[i][j] = f[i-1][j]; // 对应情况1
if(j >= v[i]) // j-v[i] >= 0 对应情况2, 并决策
f[i][j] = max(f[i][j], f[i-1][j-v[i]] + w[i]);
} // !!! f[i][j] = max(f[i-1][j], f[i-1][j-v[i]] + w[i])
}
cout << f[n][m] <<endl;
return 0;
}
[版本二——最终版本] 0-1背包问题的一维状态
举个栗子:
假如枚举到:i = 3, j = 8, v[3] = 5, w[3] = 1
二维:dp[3][8] = max(dp[2][8], dp[2][3] + w[3]) 此时的dp[2][8]和dp[2][3]都是上一轮的状态值
一维:dp[8] = max(dp[8], dp[3] + w[3]) 优化一维, 需要保证dp[8]和dp[3]都是上一轮的状态值
如果顺序枚举j:(第i轮) 当j = 8时, j = 3 已经枚举过了,
dp[3] 已经计算过了, 因此 dp[3] 是第i轮的状态, 而不是第i-1轮的状态 ×
如果逆序枚举j: (第i轮) 当j = 8时, j = 3 还未枚举
dp[3] 还未计算, 因此 dp[3] 是第i-1轮的状态 √
C++ 代码如下
#include
#include
#include
using namespace std;
const int N = 1010;
int n, m;
int v[N]; // 体积
int w[N]; // weight
int f[N]; // DP数组
// f[j] : 总体积 <= j
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
// 优化: 一维f[j] v[i] <= j <= V
for(int i = 1; i <= n; i++){
for(int j = m; j >= v[i]; j--){ // [注] j需要逆序
f[j] = max(f[j], f[j-v[i]] + w[i]);
} //f[i][j] = max(f[i-1][j], f[i-1][j-v[i]] + w[i])
}
cout << f[m] <<endl;
return 0;
}
4 5
1 2
2 4
3 4
4 5
输出
10
版本一、暴力 (TLE)
for(int i = 1; i <= n; i++)
for(int j = 0; j <= m; j++)
for(int k = 0; k * v[i] <= j; k++)
f[i][j] = max(f[i][j], f[i-1][j - k*v[i]] + k*v[i]);
cout<< f[n][m] <<endl;
因此需要优化状态转移方程
原 : f[i , j ] = max(f[i-1,j], f[i-1,j-v]+w, f[i-1,j-2*v]+2*w, f[i-1,j-3*v]+3*w , .....)
又∵f[i , j-v]= max(-----------f[i-1,j-v] , f[i-1,j-2*v] + w , f[i-1,j-3*v]+2*w , .....)
由上两式,可得出如下递推关系:
f[i][j] = max(f[i - 1][j], f[i][j - v] + w)
对比 0-1 背包的二维转移方程, 只有下标不同
f[i][j] = max(f[i - 1][j], f[i - 1][j - v] + w)
版本二、优化后的二维状态方程
代码如下:
for(int i = 1; i <= n; i++){
for(int j = 0; j <= m; j++){
// f[i][j] = max(f[i-1][j], f[i-1][j-v] + w) 0-1背包
// f[i][j] = max(f[i-1][j], f[i][j-v] + w) 完全背包
f[i][j] = f[i-1][j];
if(j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
}
}
cout<< f[n][m] <<endl;
版本三——最终版:一维状态方程
完整代码如下
#include
#include
#include
using namespace std;
const int N = 1010;
int v[N], w[N];
int f[N];
int n, m;
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;
}
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出
10
状态分析
版本一、暴力法
f[i][j] = max(f[i-1][j-v[i] * k] + w[i] * k)
,k 为选的个数,范围从 0 到 s[i]
for(int i = 1; i <= n; i++)
for (int j = 0; j <= m; j ++ )
for (int k = 0; k <= s[i] && k * v[i] <= j; k ++ )
f[i][j] = max(f[i][j], f[i-1][j-v[i] * k] + w[i] * k);
cout << f[n][m] << endl;
版本二、利用分组二进制的思想优化
1, 2, 4,...,2^k, c 共 log(s) + 1 组
s == 1 + 2 + 4 + ... + 2^k + c
代码如下
#include
#include
#include
using namespace std;
const int N = 24000, M = 2010;
int n, m;
int v[N], w[N], s[N];
int f[M];
int cnt = 0;
int main(){
cin >> n >> m;
int cnt = 0; //分组数
for(int i = 1; i <= n;i ++) {
int a, b, s;
cin >> a >> b >> s;
int k = 1; // 组别里的个数
while(k <= s) {
cnt ++ ;
v[cnt] = a * k ; // V
w[cnt] = b * k; // W
s -= k; // s - 2^k
k *= 2; // k = k * 2
}
//剩余不满足2^k的一组
if(s > 0) {
cnt ++ ;
v[cnt] = a * s;
w[cnt] = b * s;
}
}
// for(int i = 1; i <= cnt; i++){
// cout << v[i] <<" "<< w[i] <
// }
// 分完组后, 每个组相当于0-1背包中的一个物品
// 0-1背包 一维优化
for(int i = 1; i <= cnt; 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;
}
3 5
2
1 2
2 4
1
3 4
1
4 5
输出
8
状态分析
从第 i 组选一个 , 总体积不超过 j 从第 i 组选一个, 总体积不超过 j 从第i组选一个,总体积不超过j
状态转移方程 :
f[i][j] = max(f[i-1][j], f[i-1][j-v[i][k]] + w[i][k])
[注]
代码如下
#include
#include
#include
using namespace std;
const int N = 105;
int f[N]; // f[i][j] : 从第 i 组选1个, 体积不超过 j
int v[N][N], w[N][N];
int s[N]; // 组数
int n, m;
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++){ // n : 总组数
cin >> s[i]; // s[i] : 组内个数
for(int j = 0; j < s[i]; j++){
cin >> v[i][j] >> w[i][j]; // [第i组] [第j个]
}
}
for(int i = 1; i <= n; i++)
for(int j = m; j >= 0; j--)
for(int k = 0; k < s[i]; k++) // 遍历组数
if(v[i][k] <= j)
f[j] = max(f[j], f[j-v[i][k]] + w[i][k]);
cout << f[m] <<endl;
return 0;
}
状态分析
f [ i ] [ j ] 表示从 ( 1 , 1 ) 到 ( i , j ) 所有方案的集合 f[i][j]表示从(1,1)到(i,j)所有方案的集合 f[i][j]表示从(1,1)到(i,j)所有方案的集合
状态转移方程
f[i][j] = max(f[i-1][j-1], f[i-1][j]) + a[i][j]
代码
#include
#include
#include
#include
using namespace std;
const int N = 510;
int f[N][N];
int a[N][N];
int main(){
int n;
cin>>n;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= i; j++){
cin >> a[i][j];
}
}
for(int i = 0; i <= n; i++){
for(int j = 0; j <= i + 1; j++){
f[i][j] = INT_MIN; // 初始化f数组,见图
}
}
f[1][1] = a[1][1];
for(int i = 2; i <= n; i++){
for(int j = 1; j <= i; j++){
f[i][j] = max(f[i-1][j-1], f[i-1][j]) + a[i][j];
}
}
int ans = INT_MIN;
for(int j = 1; j <= n; j++){
ans = max(ans, f[n][j]);
}
cout << ans << endl;
return 0;
}
状态分析
f [ i ] 表示以第 i 个数结尾的上升子序列的 m a x 长度 f[i]表示以第i个数结尾的上升子序列的max长度 f[i]表示以第i个数结尾的上升子序列的max长度
状态转移方程
f[i] = max(f[i], f[j] + 1) , j ∈ [0, i-1]
#include
#include
#include
using namespace std;
const int N = 1010;
int n;
int a[N], f[N];
int main(){
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i <= n; i++){
f[i] = 1;
for(int j = 1; j < i; j++){
if(a[j] < a[i]) f[i] = max(f[i], f[j] + 1);
}
}
int ans = 0;
for(int i = 1; i <= n; i++){
ans = max(ans, f[i]);
}
cout << ans << endl;
return 0;
}
LeetCode代码:LeetCode 300. 最长递增子序列
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
int f[n+1];
for(int i = 0; i < n; i++){
f[i] = 1;
for(int j = 0; j < i; j++){
if(nums[j] < nums[i]) f[i] = max(f[i], f[j] + 1);
}
}
int ans = 0;
for(int i = 0; i < n; i++){
ans = max(ans, f[i]);
// cout << f[i] << " " << ans << endl;
}
return ans;
}
};
推荐一个讲得比较好的视频 最长公共子序列LCS
状态分析
f [ i ] [ j ] 表示 : s 1 [ 0 − i ] 和 s 2 [ 0 − j ] 的 m a x 公共子序列长度 f[i][j]表示: s_1[0-i] 和s_2[0-j]的max公共子序列长度 f[i][j]表示:s1[0−i]和s2[0−j]的max公共子序列长度
图源上述视频
状态转移方程
s1[i] == s2[j], dp[i][j] = dp[i-1][j-1] + 1
s1[i] != s2[j], dp[i][j] = max(dp[i-1][j], dp[i][j-1])
代码:
#include
#include
#include
using namespace std;
const int N = 1050;
int n, m;
char a[N], b[N];
int f[N][N];
int main(){
cin >> n >> m;
cin >> a + 1 >> b + 1;
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]);
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;
}
LeetCode代码:1143. 最长公共子序列
注意:下标从0开始
class Solution {
public:
int longestCommonSubsequence(string t1, string t2) {
int n = t1.size(), m = t2.size();
int f[n+1][m+1];
memset(f, 0, sizeof(f));
for(int i = 0; i < n; i++){
for(int j = 0; j < m; j++){
if(t1[i] == t2[j]){
f[i+1][j+1] = f[i][j] + 1;
}else
f[i+1][j+1] = max(f[i][j+1], f[i+1][j]);
}
}
return f[n][m];
}
};
空间优化:滚动数组(2个一维)
class Solution {
public:
int longestCommonSubsequence(string t1, string t2) {
int n = t1.size(), m = t2.size();
int f[2][m+1];
memset(f, 0, sizeof(f));
for(int i = 0; i < n; i++){
for(int j = 0; j < m; j++){
if(t1[i] == t2[j]){
f[(i+1)%2][j+1] = f[i%2][j] + 1;
}else
f[(i+1)%2][j+1] = max(f[i%2][j+1], f[(i+1)%2][j]);
}
}
return f[n%2][m];
}
};
空间优化:一维数组
【注意】在使用一维数组的情况下,由于f[i+1][j+1
]是由f[i][j]
、f[i+1][j]
、f[i][j+1]
(左上、左、上)转移过来的,所以需要用临时变量t来保存左上状态(f[i][j]
),否则f[i][j]
会被f[i+1][j]
覆盖。
class Solution {
public:
int longestCommonSubsequence(string t1, string t2) {
int n = t1.size(), m = t2.size();
int f[m+1];
memset(f, 0, sizeof(f));
for(int i = 0; i < n; i++){
int pre_i_j = 0;
for(int j = 0; j < m; j++){
int t = f[j+1];
if(t1[i] == t2[j]){
f[j+1] = pre_i_j + 1;
}else
f[j+1] = max(f[j+1], f[j]);
pre_i_j = t;
}
}
return f[m];
}
};\
状态转移方程
s[i] == t[j], dp[i][j] = dp[i-1][j-1]
s[i] != t[j], dp[i][j] = min(dp[i][j-1], dp[i-1][j], dp[i-1][j-1]) + 1
代码
class Solution {
public:
int minDistance(string s, string t) {
int n1 = s.size(), n2 = t.size();
int dp[n1+1][n2+1];
memset(dp, 0, sizeof(dp));
// dp[i][j]:从 [长度为i的字符串s] 转换为 [长度为j的字符串t] 的最小操作次数
// 边界条件1:当 len(s) = 0, len(t) = j时, dp[0][j] = j;
// 边界条件2:当 len(s) = i, len(t) = 0时, dp[i][0] = i;
for(int j = 0; j <= n2; j++) dp[0][j] = j;
for(int i = 0; i <= n1; i++) dp[i][0] = i;
for(int i = 0; i < n1; i++){
for(int j = 0; j < n2; j++){
if(s[i] == t[j]){
dp[i+1][j+1] = dp[i][j]; // 无需操作
}else{ // 插入 || 删除 || 更改 + 1 (s插入一个字符 等价于 t删除一个字符)
dp[i+1][j+1] = min({dp[i+1][j], dp[i][j+1], dp[i][j]}) + 1;
}
}
}
return dp[n1][n2];
}
};