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 }
View Code

  如果你觉着这样逆着推导有点不舒服的话, 我们可以简单的对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
", dp[N][W]);
36     return 0;
37 }
View Code

   最长公共子序列问题:

  那么我们完全可以把看看成追赶问题, 而且联想 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
", dp[len0][len1]);
32     return 0;
33 }
34 
35 /*
36 abcd
37 becd
38 
39 
40 */
41  
View Code

终于到了兄弟们最喜欢的完全背包问题了 :

    那么对于这个类似于 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
", 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
", dp[0][W]);

    return 0;
}
/*
3 7
3 4
4 5
2 3


THE RESULT SHOULD BE 10

*/
View Code

 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
", 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
", 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
", 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
", res);
    return 0;
}
/*
4 5
2 3
1 2
3 4
2 2


THE RESULT SHOULD BE 7

*/
View Code

多重部分和问题

最长上升子序列问题    

 最简单的第一种算法, 就是把它抽象为追赶问题, 而且

  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 :
");
45         printf("%d
", res);
46     }
47     return 0;
48 }
View Code

  下一个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 :
");
39         printf("%d
", lower_bound(dp, dp + n, INF) - dp);
40     }
41     return 0;
42 }
View Code
原文地址:https://www.cnblogs.com/lucky-light/p/11504534.html