DP的初级问题——01包、最长公共子序列、完全背包、01包value、多重部分和、最长上升子序列、划分数问题、多重集组合数
当初学者最开始学习 dp 的时候往往接触的是一大堆的 背包 dp 问题, 那么我们在这里就不妨讨论一下常见的几种背包的 dp 问题;
初级的时候背包 dp 就完全相当于BFS DFS 进行搜索之后的记忆化查找。
背包问题 一 、 0 ~ 1 背包问题
实现一、 return max ( rec ( i + 1, j ) , rec ( i + 1, j - cost[ i ]) + value[ i ]); //对第 i 件物品的选或者不选
记忆化、 这个是由于实现一产生的dp数组 : dp[ i ] [ j ] : 首先注意这个 i 表示的是第 i 件物品, j 表示的是剩余的价值, 那么这个整体的 dp 表示的就是 i 件物品到 最后一个物品 n - 1 他能够产生的最大价值;
同样我们利用那个递归关系,我们可以知道
dp[ i ] [ j ] = max ( dp[ i + 1] [ j ] , dp[ i + 1] [ j - w[ i ] ] + value [ i ]) ;
1 #include <iostream> 2 #include <cstdio> 3 #include <cstdlib> 4 #include <cstring> 5 #include <cmath> 6 #include <cctype> 7 #include <algorithm> 8 #include <vector> 9 #include <queue> 10 #include <list> 11 #include <map> 12 #include <stack> 13 #include <set> 14 #include <string> 15 16 using namespace std; 17 const int MAX_N = 1010; 18 const int MAX_W = 1010; 19 int dp[MAX_N][MAX_W]; 20 int N, W; 21 int w[MAX_N], value[MAX_N]; 22 int main() 23 { 24 cin>>N>>W; 25 for(int i = 0; i < N; i++) cin>>w[i]>>value[i]; 26 dp[N][0] = 0; 27 fill(dp[N], dp[N] + W, 0); 28 for(int i = N - 1; i >= 0; i--){ 29 for(int j = 0; j <= W; j++){ 30 if(j < w[i]) 31 dp[i][j] = dp[i+1][j]; 32 else 33 dp[i][j] = max(dp[i+1][j], dp[i+1][j-w[i]] + value[i]); 34 } 35 } 36 return 0; 37 }
如果你觉着这样逆着推导有点不舒服的话, 我们可以简单的对dp 数组换一下定义, 我们从原来的 dp [i] [j] 使用由 i 到最后; 同样dp[ i + 1 ] [ j ]我们可以设定为 从 0 到 i 这 i + 1 个物品, 但是你需要注意了;
1 #include <iostream> 2 #include <cstdio> 3 #include <cstdlib> 4 #include <cstring> 5 #include <cmath> 6 #include <cctype> 7 #include <algorithm> 8 #include <vector> 9 #include <queue> 10 #include <list> 11 #include <map> 12 #include <stack> 13 #include <set> 14 #include <string> 15 16 using namespace std; 17 const int MAX_N = 1010; 18 const int MAX_W = 1010; 19 int dp[MAX_N][MAX_W]; 20 int N, W; 21 int w[MAX_N], value[MAX_N]; 22 int main() 23 { 24 cin>>N>>W; 25 for(int i = 0; i < N; i++) cin>>w[i]>>value[i]; 26 fill(dp[0], dp[0] + W, 0); 27 for(int i = 0; i < N; i++){ 28 for(int j = 0; j <= W; j++){ 29 if(j < w[i]) 30 dp[i+1][j] = dp[i][j]; 31 else 32 dp[i+1][j] = max(dp[i][j], dp[i][j-w[i]] + value[i]); 33 } 34 } 35 printf("THE RESULT : %d\n", dp[N][W]); 36 return 0; 37 }
最长公共子序列问题:
那么我们完全可以把看看成追赶问题, 而且联想 dp 背包问题的 i + 1而不是 i 的巧妙设定, 这样防止了数组下标的越界!
dp[ i + 1] [ j + 1] : 这个指的是 s0[ i ] 与 s0[ j ] 到这两个字符串的这两个元素的时候, 他目前的最长公共子序列长度的问题, i 是表示长度为 i ; j 表示长度为 j ; 分别表示了两个子序列的长度。
那我们利用追赶问题的方法进行寻找上下级别的关系:
dp[ i + 1] [ j + 1] = dp[ i ] [ j ] + 1; 这个是当且仅当 s0[ i ] = s1[ j ] ;
当不满足这个等式的时候
dp [ i + 1] [ j + 1] = max ( dp[ i + 1] [ j ], dp[ i ] [ j +1]) //这个就是因为最后两个不相等, 不可能同时使用, 所以说我们删去一个 , 还可以减小问题的规模!
1 #include <iostream> 2 #include <cstring> 3 #include <bits/stdc++.h> 4 #include <algorithm> 5 #include <cstdio> 6 #include <cstdlib> 7 #include <string> 8 #include <cctype> 9 #include <cmath> 10 using namespace std; 11 const int MAX_S = 1010; 12 char s0[MAX_S], s1[MAX_S]; 13 int len1, len0; 14 int dp[MAX_S][MAX_S]; 15 int main() 16 { 17 cin>>s0>>s1; 18 len0 = strlen(s0); 19 len1 = strlen(s1); 20 fill(dp[0], dp[0] + MAX_S, 0); 21 for(int i = 0; i < len0; i++){ 22 for(int j = 0; j < len1; j++){ 23 if(s0[i] == s1[j]){ 24 dp[i+1][j+1] = dp[i][j] + 1; 25 } 26 else{ 27 dp[i+1][j+1] = max(dp[i+1][j], dp[i][j+1]); 28 } 29 } 30 } 31 printf("THE ANSWER IS THAT : %d\n", dp[len0][len1]); 32 return 0; 33 } 34 35 /* 36 abcd 37 becd 38 39 40 */ 41
终于到了兄弟们最喜欢的完全背包问题了 :
那么对于这个类似于 0 1背包, 但是数量无限的情况下, 我们如何进行递归呢?
最简单的、 最容易想的就是仿照我们的0 1 背包进行求解
他是由第 i 个物品买还是不买,转换成了买多少个!!
为了保持正向进行, 我们的 dp [ i + 1] [ j ] 表示的是在处理第 i 件产品, 结果是 0 - i 种物品合理选择后的最大价值量 !
因此我们有 dp[ i + 1] [ j ] = max(dp [ i ] [ j - k * w[ i ] ] + k * value[ i ] ) 注意这个是要保证下标索引是 大于零 的, 而且我们的 k 是自然数 ;
但是不得不说这样的算法复杂度太高, 那么我们怎么进一步化简??
dp[ i + 1] [ j ] = max(dp [ i ] [ j ], dp[ i + 1] [ j - w[ i ]] + value[ i ]);
注意了, 调用这个式子的时候一定要注意, 这个下标索引大于零, 而且初始化
dp[ i + 1] [ j ] = max(dp [ i ] [ j - k * w[ i ] ] + k * value[ i ] ) k >= 0;
= max(dp[ i ] [ j ] , dp [ i ] [ j - k * w[ i ] ] + k * value[ i ] ) k >= 1;
= max(dp[ i ] [ j ] , dp [ i ] [ j - w[ i ] - k * w[ i ] ] + k * value[ i ] + value[ i ]) k >= 0;
= max(dp[ i ] [ j ] , dp [ i + 1 ] [ j - w[ i ] ] + value[ i ]);
那么这样的证明就是好起来了。
#include <iostream> #include <cstdio> #include <cstdlib> #include <cstring> #include <string> #include <vector> #include <list> #include <map> #include <set> #include <stack> #include <cctype> #include <algorithm> #include <cmath> #include <queue> using namespace std; const int MAX = 105; int dp[MAX][MAX]; int w[MAX], v[MAX]; int W, N; //这个是对完全背包问题的解决 //分为了两种解法 int main() { int N; cin>>N>>W; for(int i = 0; i < N; i++){ scanf("%d%d", w + i, v + i); } memset(dp, 0, sizeof(dp)); //小小的初始化一手 //不节省空间的写法 for(int i = 0; i < N; i++){ for(int j = 0; j <= W; j++){ if(j-w[i] >= 0) dp[i+1][j] = max(dp[i][j], dp[i+1][j-w[i]]+v[i]); else dp[i+1][j] = dp[i][j]; } } printf("THE FIRST RESULT IS %d\n", dp[N][W]); //节省空间的写法 memset(dp, 0, sizeof(dp)); //再次进行初始化 for(int i = 0; i < N; i++){ for(int j = 0; j <= W; j++){ if(j-w[i] >= 0) dp[0][j] = max(dp[0][j], dp[0][j-w[i]]+v[i]); else dp[0][j] = dp[0][j]; } } printf("THE SECOND RESULT IS %d\n", dp[0][W]); return 0; } /* 3 7 3 4 4 5 2 3 THE RESULT SHOULD BE 10 */
0-1 背包问题二
注意初始化时候的灵魂加一
这个是适用于NV 复杂度, 还有一个普通的 NW 复杂度, 在下面的代码之中都有着提及
#include <iostream> #include <cstdio> #include <cstdlib> #include <cstring> #include <string> #include <vector> #include <list> #include <map> #include <set> #include <stack> #include <cctype> #include <algorithm> #include <cmath> #include <queue> using namespace std; const int MAX = 105; const int MAX_V = 100; const int INF = 1e6; int dp[MAX+1][MAX+1]; int w[MAX], v[MAX]; int W, N; int main() { int N; cin>>N>>W; for(int i = 0; i < N; i++){ scanf("%d%d", w + i, v + i); } memset(dp, 0, sizeof(dp)); //小小的初始化一手 //0 1 背包的多种解法, 直接暴力 NW, 后者是进一步优化后的节省空间的一维数组的直接求解,又或者是NV //FIRST 直接暴力 dp NW dp[i+1][j] 前 i 个物品的重量不超过 J 的最大价值 for(int i = 0; i < N; i++){ for(int j = 0; j <= W; j++){ if(j >= w[i]) dp[i+1][j] = max(dp[i][j], dp[i][j - w[i]] + v[i]); //买或者不买 else dp[i+1][j] = dp[i][j]; } } printf("FIRST RESULT IS %d\n", dp[N][W]); //SECOND 多次利用了同一个一维数组, 复杂度没有变,但是在所占存储大大优化 memset(dp, 0, sizeof(dp)); for(int i = 0; i < N; i++){ for(int j = W; j >=0 ; j--){ if(j >= w[i]) dp[0][j] = max(dp[0][j], dp[0][j - w[i]] + v[i]); //买或者不买 else dp[0][j] = dp[0][j]; } } printf("SECOND RESULT IS %d\n", dp[0][W]); //THIRD O(NV)这个是需要看你的数据要求然后进行选择 // dp[i+1][j] 表示的是, 对前 I 个物品进行选择,, 然后当前 J 价值的最小重量 for(int i = 0; i < N; i++) fill(dp[i], dp[i] + MAX_V + 1, INF), dp[i][0] = 0; //进行小小的初始化 for(int i = 0; i < N; i++){ for(int j = 0; j <= MAX_V; j++){ if(j - v[i] >= 0) dp[i+1][j] = min(dp[i][j], dp[i][j - v[i]] + w[i]); //当前获取价值所需要的最小重量 else dp[i+1][j] = dp[i][j]; } } int res = 0; for(int i = 0; i <= MAX_V; i++){ if(dp[N][i] <= W){ res = i; //我们需要找的是重量要求条件之内的最大价值 } } printf("THE THIRD RESULT : %d\n", res); //FOURTH O(NV)这个是需要看你的数据要求然后进行选择 // dp[i+1][j] 表示的是, 对前 I 个物品进行选择,, 然后当前 J 价值的最小重量 //这个是教科书上面最简短的代码 fill(dp[0], dp[0] + MAX_V + 1, INF); dp[0][0] = 0; for(int i = 0; i < N; i++){ for(int j = 0; j <= MAX_V; j++){ if(j - v[i] >= 0) dp[i+1][j] = min(dp[i][j], dp[i][j - v[i]] + w[i]); //当前获取价值所需要的最小重量 else dp[i+1][j] = dp[i][j]; } } res = 0; for(int i = 0; i <= MAX_V; i++){ if(dp[N][i] <= W){ res = i; //我们需要找的是重量要求条件之内的最大价值 } } printf("THE FOURTH RESULT : %d\n", res); return 0; } /* 4 5 2 3 1 2 3 4 2 2 THE RESULT SHOULD BE 7 */
多重部分和问题
最长上升子序列问题
最简单的第一种算法, 就是把它抽象为追赶问题, 而且
dp [ j ] = max ( dp [ j ] , dp [ i ] + 1 ); i < j 而且 a [ i ] < a [ j ]; 有些问题, 我们需要的是最长下降子序列的问题
代码
1 /* 2 将本题目抽象出来居然是 最长上升子序列问题 3 */ 4 #include <iostream> 5 #include <cstdio> 6 #include <cstdlib> 7 #include <cstring> 8 #include <cmath> 9 #include <cctype> 10 #include <algorithm> 11 #include <vector> 12 #include <queue> 13 #include <list> 14 #include <map> 15 #include <stack> 16 #include <set> 17 #include <string> 18 19 using namespace std; 20 const int MAX_N = 4e4+10; 21 int a[MAX_N]; 22 int dp[MAX_N]; 23 24 int main() 25 { 26 int T; cin>>T; 27 while(T--){ 28 int n; cin>>n; 29 for(int i = 0; i < n; i++){ 30 scanf("%d", &a[i]); 31 } 32 memset(dp, 0, sizeof(dp)); 33 int res = 0; 34 dp[0] = 1; 35 for(int i = 1; i < n; i++){ 36 dp[i] = 1; 37 for(int j = 0; j < i; j++){ 38 if(a[i] > a[j]){ 39 dp[i] = max(dp[i], dp[j] + 1); 40 res = max(res, dp[i]); 41 } 42 } 43 } 44 printf("THE RESULT :\n"); 45 printf("%d\n", res); 46 } 47 return 0; 48 }
下一个dp [i] 就更牛逼了, 甚至可以使用二分查找; 真的是好用!!
(注意下标存取的是长度, 然后dp [ i ] 存储是传说当中的当前长度的最小尾部元素)
lower_bound : ai >= k 的最小指针
upper_bound : ai >k 的最小指针
1 /* 2 将本题目抽象出来居然是 最长上升子序列问题 3 */ 4 #include <iostream> 5 #include <cstdio> 6 #include <cstdlib> 7 #include <cstring> 8 #include <cmath> 9 #include <cctype> 10 #include <algorithm> 11 #include <vector> 12 #include <queue> 13 #include <list> 14 #include <map> 15 #include <stack> 16 #include <set> 17 #include <string> 18 19 using namespace std; 20 const int MAX_N = 4e4+10; 21 const int INF = 1e5; 22 int a[MAX_N]; 23 int dp[MAX_N]; 24 //然后里面的dp换了含义, dp[i] 代表的是长度为 i 的最小尾部, 25 //那么我们就可以依靠这个进行不断地更新 26 27 int main(void){ 28 int T; cin>>T; 29 while(T--){ 30 int n; cin>>n; 31 for(int i = 0; i < n; i++){ 32 scanf("%d", &a[i]); 33 } 34 fill(dp, dp + n, INF); 35 for(int i = 0; i < n; i++){ 36 *lower_bound(dp, dp + n, a[i]) = a[i]; 37 } 38 // printf("THE RESULT IS :\n"); 39 printf("%d\n", lower_bound(dp, dp + n, INF) - dp); 40 } 41 return 0; 42 }