• 记录结果再利用的"动态规划"



    01背包问题

    • 问题描述:有n个重量和价值分别为wi、vi的物品,从这些物品中挑选出总重量不超过W的物品,求所有挑选方案中价值总和的最大值。
    • 限制条件:
      • 1≤n≤100
      • 1≤wi、vi≤100
      • 1≤W≤10000
    • 分析:
      • 不妨先用最朴素的方法,针对每个物品是否放入背包进行搜索试试看:
         1 #include <iostream>
         2 using namespace std;
         3 
         4 int n,W;
         5 int *w,*v;
         6 
         7 int max(int x, int y)
         8 {
         9     if (x>y) return x;
        10     return y;
        11 }
        12 
        13 int rec(int i, int j)//从数组下标为i的物品开始往后挑选总重小于j的物体 
        14 {
        15     int res;
        16     if (i==n) res=0;
        17     else if (j<w[i]) res=rec(i+1,j);
        18     else res=max(rec(i+1,j),rec(i+1,j-w[i])+v[i]);
        19     return res;
        20 }
        21 
        22 int main()
        23 {
        24     cin >> n >> W;
        25     w = new int[n];
        26     v = new int[n];
        27     for (int i=0; i<n; i++) cin >> w[i] >> v[i];
        28     cout << rec(0,W) << endl;
        29 }
        朴素搜索

         这种方法的搜索深度是n,而且每一层的搜索都需要两次分支,最坏就需要O(2n)的时间。

      • 通过分析ren递归调用的情况,我们可以发现rec()函数对于相同的参数进行了多次调用,因此进行了很多遍相同的计算过程,如果我们把第一次计算的结果记录下来,那么就可以省掉第二次及以后的重复计算,这种方法叫做记忆话搜索,对于同样的参数,只会在第一次被调用到时执行递归部分,第二次之后都会直接返回结果,参数的组合不过nW种,而函数内只调用2次递归,所以只需要O(nW)的复杂度:
         1 #include <iostream>
         2 #include <cstring>
         3 using namespace std;
         4 
         5 int n,W;
         6 int *w,*v;
         7 int **dp;
         8 
         9 int max(int x, int y)
        10 {
        11     if (x>y) return x;
        12     return y;
        13 }
        14 
        15 int rec(int i, int j)//从数组下标为i的物品开始往后挑选总重小于j的物体 
        16 {
        17     if (j[i[dp]]>=0) return j[i[dp]]; 
        18     int res;
        19     if (i==n) res=0;
        20     else if (j<w[i]) res=rec(i+1,j);
        21     else res=max(rec(i+1,j),rec(i+1,j-w[i])+v[i]);
        22     return j[i[dp]] = res;
        23 }
        24 
        25 int main()
        26 {
        27     cin >> n >> W;
        28     w = new int[n];
        29     v = new int[n];
        30     dp = new int*[n+1];
        31     for (int i=0; i<=n; i++)
        32     {
        33         dp[i] = new int[W+1];
        34         memset(dp[i],-1,sizeof(int)*(W+1));
        35     }
        36     for (int i=0; i<n; i++) cin >> w[i] >> v[i];
        37     cout << rec(0,W) << endl;
        38 }
        记忆化搜索

         其中memset()函数时按照1字节为单位对内存进行填充的,通过使用memset可以快速地对高维数组等进行初始化

      • 在需要剪枝的情况下,可能会把各种参数都写在函数上,但是在这种情况下会让记忆化搜索难以实现,需要注意
      • 研究一下记忆化数组,记dp[i][j]为从第i个物品(编号为i的物品)开始挑选总重小于j时,总价值的最大值。于是我们就有如下的递推式:
        dp[n][j]=0
                      /  dp[i+1][j]  (j<w[i]时)
        dp[i][j] =
                        max(dp[i+1][j],dp[i+1][j-w[i]]+v[i])  (其它情况下)
        如上所示,不同写递归函数,直接利用递推式将各项的值计算出来,简单地用二重循环也可以解决这一问题,复杂度为O(nW),与记忆化搜索是一样的,但是简洁了很多,这种方法叫做动态规划,即常说的DP:
         1 #include <iostream>
         2 #include <cstring>
         3 using namespace std;
         4 
         5 int n,W;
         6 int *w,*v;
         7 int **dp;
         8 
         9 int max(int x, int y)
        10 {
        11     if (x>y) return x;
        12     return y;
        13 }
        14 
        15 int main()
        16 {
        17     cin >> n >> W;
        18     w = new int[n];
        19     v = new int[n];
        20     dp = new int*[n+1];
        21     for (int i=0; i<=n; i++)
        22     {
        23         dp[i] = new int[W+1];
        24         memset(dp[i],0,sizeof(int)*(W+1));
        25     }
        26     for (int i=0; i<n; i++) cin >> w[i] >> v[i];
        27     for (int i=n-1; i>=0; i--)
        28     {
        29         for (int j=0; j<=W; j++)
        30         {
        31             if (j<w[i]) dp[i][j]=dp[i+1][j];
        32             else dp[i][j] = max(dp[i+1][j],dp[i+1][j-w[i]]+v[i]);
        33         }
        34     }
        35     cout << dp[0][W] << endl;
        36 }
        动态规划
      • 此外,我们还有各种各样的DP方式:
        1.刚刚的DP中关于i的循环是逆向进行的,那如果按照如下的方式定义递推关系的话,关于i的循环就可以正向进行:
        dp[i+1][j] := 从前i+1个物品(即从编号为0到i这i+1个物品)中选出总重量不超过j的物品时总价值的最大值
        dp[0][j] = 0
                         /  dp[i][j]  (j<w[i]时)
        dp[i+1][j] = 
                           max(dp[i][j],dp[i][j-w[i]]+v[i])  (其它情况下)
         1 #include <iostream>
         2 #include <cstring>
         3 using namespace std;
         4 
         5 int n,W;
         6 int *w,*v;
         7 int **dp;
         8 
         9 int max(int x, int y)
        10 {
        11     if (x>y) return x;
        12     return y;
        13 }
        14 
        15 int main()
        16 {
        17     cin >> n >> W;
        18     w = new int[n];
        19     v = new int[n];
        20     dp = new int*[n+1];
        21     for (int i=0; i<=n; i++)
        22     {
        23         dp[i] = new int[W+1];
        24         memset(dp[i],0,sizeof(int)*(W+1));
        25     }
        26     for (int i=0; i<n; i++) cin >> w[i] >> v[i];
        27     for (int i=0; i<n; i++)
        28     {
        29         for (int j=0; j<=W; j++)
        30         {
        31             if (j<w[i]) dp[i+1][j]=dp[i][j];
        32             else dp[i+1][j] = max(dp[i][j],dp[i][j-w[i]]+v[i]);
        33         }
        34     }
        35     cout << dp[n][W] << endl;
        36 }
        动态规划'

         2.除了运用递推方式逐项求解之外,还可以把状态转移想象成从"前i个物品中选取总重量不超过j时的状态"向"前i+1个物品中选取总重量不超过j"和"从前i+1个物品中选取总重量不超过j+w[i]时的状态"的转移:

         1 #include <iostream>
         2 #include <cstring>
         3 using namespace std;
         4 
         5 int n,W;
         6 int *w,*v;
         7 int **dp;
         8 
         9 int max(int x, int y)
        10 {
        11     if (x>y) return x;
        12     return y;
        13 }
        14 
        15 int main()
        16 {
        17     cin >> n >> W;
        18     w = new int[n];
        19     v = new int[n];
        20     dp = new int*[n+1];
        21     for (int i=0; i<=n; i++)
        22     {
        23         dp[i] = new int[W+1];
        24         memset(dp[i],0,sizeof(int)*(W+1));
        25     }
        26     for (int i=0; i<n; i++) cin >> w[i] >> v[i];
        27     for (int i=0; i<n; i++)
        28     {
        29         for (int j=0; j<=W; j++)
        30         {
        31             dp[i+1][j] = max(dp[i+1][j],dp[i][j]);
        32             if (j+w[i]<=W) dp[i+1][j+w[i]] = max(dp[i+1][j+w[i]],dp[i][j]+v[i]);
        33         }
        34     }
        35     cout << dp[n][W] << endl;
        36 }
        动态规划''

        如果像上面这样,把问题写成从当前状态转移成下一状态的形式的话,需要特别注意初项之外也需要初始化,在这个问题中,因为价值总和至少是0,所以初值设为0就可以了,不过根据问题也有可能需要初始化成无穷大。


    最长公共子序列问题

    • 问题描述:给定两个字符串s1s2…sn和t1t2…tn。求这两个字符串最长的公共子序列的长度。
    • 限制条件:1≤n,m≤1000
    • 分析:这个问题是被称为最长公共子序列问题(LCS,Longest Common Subsequence)的著名问题。不妨使用下面的定义:
      dp[i][j] :=s1…si和t1…tj对应的LCS的长度
      由此,s1…si+1和t1…tj+1对应的公共子列可能是
      ①当si+1=tj+1时,在s1…si和t1…tj的LCS末尾追加上si+1
      ②s1…si和t1…tj+1的LCS;
      ③s1…si+1和t1…tj和LCS;
      三者中的某一个,所以就有如下的递推关系成立:
                            /  max(dp[i][j]+1,dp[i][j+1],dp[i+1][j])  (si+1=tj+1)
      dp[i+1][j+1] = 
                              max(dp[i][j+1],dp[i+1][j])  (其它情况下)
      然而,稍微思考一下,就能发现当si+1=tj+1时,只需令dp[i+1][j+1]=dp[i][j]+1就可以了
      于是,总的递推式可写为:
                           /  dp[i][j]+1  (si+1=tj+1)
      dp[i+1][j+1] = 
                              max(dp[i][j+1],dp[i+1][j])  (其它情况下)
      复杂度为O(nm),dp[n][m]就是LCS的长度
    • 代码:
       1 #include <iostream>
       2 #include <cstring>
       3 using namespace std;
       4 
       5 int n,m;
       6 char * s;
       7 char * t;
       8 int **dp;
       9 
      10 int max(int x, int y)
      11 {
      12     if (x>y) return x;
      13     return y;
      14 }
      15 
      16 int main()
      17 {
      18     cin >> n >> m;
      19     s = new char[n+1];
      20     t = new char[m+1];
      21     for (int i=0; i<n; i++)
      22     {
      23         cin >> s[i];
      24     }
      25     for (int i=0; i<m; i++)
      26     {
      27         cin >> t[i];
      28     }
      29     dp = new int*[n+1];
      30     for (int i=0; i<=n; i++) 
      31     {
      32         dp[i] = new int[m+1];
      33         memset(dp[i],0,sizeof(int)*(m+1));
      34     }
      35     for (int i=0; i<n; i++)
      36     {
      37         for (int j=0; j<m; j++)
      38         {
      39             if (s[i]==t[j]) dp[i+1][j+1]=dp[i][j]+1;
      40             else dp[i+1][j+1]=max(dp[i+1][j],dp[i][j+1]);
      41         }
      42     }
      43     cout << dp[n][m] << endl;
      44 }
      LCS

    完全背包问题

    • 问题描述:有n种重量和价值分别为wi,vi的物品,从这些物品中挑选总重量不超过W的物品,求出挑选物品价值总和的最大值,在这里,每种物品可以挑选任意多件。
    • 限制条件:
      • 1≤n≤100
      • 1≤wi,vi≤100
      • 1≤W≤10000
    • 分析:
      • 这次同一种类的物品可以选择任意多件了,尝试着写出递推关系:
        dp[i+1][j] := 从前i+1种(编号)物品中挑选总重量不超过j时总价值的最大值.
        dp[0][j]=0
        dp[i+1][j]=max{dp[i][j-k*w[i]]+k*v[i]|k≥0}
         1 #include <iostream>
         2 #include <cstring>
         3 using namespace std;
         4 
         5 int n,W;
         6 int * w;
         7 int * v;
         8 int **dp;
         9 
        10 int max(int x, int y)
        11 {
        12     if (x>y) return x;
        13     return y;
        14 }
        15 
        16 int main()
        17 {
        18     cin >> n >> W;
        19     w = new int[n];
        20     v = new int[n];
        21     for (int i=0; i<n; i++)
        22     {
        23         cin >> w[i] >>v[i];
        24     }
        25     dp = new int*[n+1];
        26     for (int i=0; i<=n; i++) 
        27     {
        28         dp[i] = new int[W+1];
        29         memset(dp[i],0,sizeof(int)*(W+1));
        30     }
        31     for (int i=0; i<n; i++)
        32     {
        33         for (int j=0; j<=W; j++)
        34         {
        35             for (int k=0; k*w[i]<=j; k++)
        36             {
        37                 dp[i+1][j] = max(dp[i+1][j],dp[i][j-k*w[i]]+k*v[i]);
        38             }
        39         }
        40     }
        41     cout << dp[n][W] << endl;
        42 }
        完全背包
      • 上面的程序是三重循环的,关于k的循环最坏可能从0到W,所以这个算法的复杂度为O(nW2),这样并不够好
        我们来找一找这个算法中多余的计算(已经知道结果的计算),在dp[i+1][j]的计算中选择k(k≥1)个的情况,与在dp[i+1][j-w[i]]的计算中选择k-1个情况是相同的,所以dp[i+1][j]的递推中k≥1部分的计算已经在dp[i+1][j-w[i]]的计算中完成了:
        dp[i+1][j]
        = max{dp[i][j-k*w[i]]+k*v[i]|k≥0}
        = max(dp[i][j],max{dp[i][j-k*w[i]]+k*v[i]|k≥1})
        = max(dp[i][j],max{dp[i][(j-w[i])-k*w[i]]+k*v[i]|k≥0}+v[i])
        = max(dp[i][j],dp[i+1][j-w[i]]+v[i])
        即:dp[i+1][j] = max(dp[i][j],dp[i+1][j-w[i]]+v[i])
        这样处理之后,就不需要关于k的循环了,现在的复杂度为O(nW):
         1 #include <iostream>
         2 #include <cstring>
         3 using namespace std;
         4 
         5 int n,W;
         6 int * w;
         7 int * v;
         8 int **dp;
         9 
        10 int max(int x, int y)
        11 {
        12     if (x>y) return x;
        13     return y;
        14 }
        15 
        16 int main()
        17 {
        18     cin >> n >> W;
        19     w = new int[n];
        20     v = new int[n];
        21     for (int i=0; i<n; i++)
        22     {
        23         cin >> w[i] >>v[i];
        24     }
        25     dp = new int*[n+1];
        26     for (int i=0; i<=n; i++) 
        27     {
        28         dp[i] = new int[W+1];
        29         memset(dp[i],0,sizeof(int)*(W+1));
        30     }
        31     for (int i=0; i<n; i++)
        32     {
        33         for (int j=0; j<=W; j++)
        34         {
        35             if (j<w[i]) dp[i+1][j] = dp[i][j];
        36             else dp[i+1][j] = max(dp[i][j],dp[i+1][j-w[i]]+v[i]);
        37         }
        38     }
        39     cout << dp[n][W] << endl;
        40 }
        完全背包'
      • 此外,此前提到的01背包问题和这里的完全背包问题,可以利用一维数组来实现:
         1 #include <iostream>
         2 #include <cstring>
         3 using namespace std;
         4 
         5 int n,W;
         6 int * w;
         7 int * v;
         8 int *dp;
         9 
        10 int max(int x, int y)
        11 {
        12     if (x>y) return x;
        13     return y;
        14 }
        15 
        16 int main()
        17 {
        18     cin >> n >> W;
        19     w = new int[n];
        20     v = new int[n];
        21     for (int i=0; i<n; i++)
        22     {
        23         cin >> w[i] >>v[i];
        24     }
        25     dp = new int[W+1];
        26     memset(dp,0,sizeof(int)*(W+1));
        27     for (int i=0; i<n; i++)
        28     {
        29         for (int j=W; j>=w[i]; j--)
        30         {
        31             dp[j] = max(dp[j],dp[j-w[i]]+v[i]);
        32         }
        33     }
        34     cout << dp[W] << endl;
        35 }
        01背包(一维数组)
         1 #include <iostream>
         2 #include <cstring>
         3 using namespace std;
         4 
         5 int n,W;
         6 int * w;
         7 int * v;
         8 int *dp;
         9 
        10 int max(int x, int y)
        11 {
        12     if (x>y) return x;
        13     return y;
        14 }
        15 
        16 int main()
        17 {
        18     cin >> n >> W;
        19     w = new int[n];
        20     v = new int[n];
        21     for (int i=0; i<n; i++)
        22     {
        23         cin >> w[i] >>v[i];
        24     }
        25     dp = new int[W+1];
        26     memset(dp,0,sizeof(int)*(W+1));
        27     for (int i=0; i<n; i++)
        28     {
        29         for (int j=w[i]; j<=W; j++)
        30         {
        31             dp[j] = max(dp[j],dp[j-w[i]]+v[i]);
        32         }
        33     }
        34     cout << dp[W] << endl;
        35 }
        完全背包(一维数组)

        可以发现,两者只有关于j的循环方向不同,仔细想来,是非常有道理的

      • 除了上面的情况外,还有可能通过将两个数组滚动使用来实现重复利用,例如之前的
        dp[i+1][j] = max(dp[i][j], dp[i+1][j-w[i]]+v[i])
        这一递推式中,dp[i+1]计算时只需要dp[i]和dp[i+1],所以可以结合奇偶性写成:
         1 #include <iostream>
         2 #include <cstring>
         3 using namespace std;
         4 
         5 int n,W;
         6 int * w;
         7 int * v;
         8 int *dp[2];
         9 
        10 int max(int x, int y)
        11 {
        12     if (x>y) return x;
        13     return y;
        14 }
        15 
        16 int main()
        17 {
        18     cin >> n >> W;
        19     w = new int[n];
        20     v = new int[n];
        21     for (int i=0; i<n; i++)
        22     {
        23         cin >> w[i] >>v[i];
        24     }
        25     dp[0] = new int[W+1];
        26     dp[1] = new int[W+1];
        27     memset(dp[0],0,sizeof(int)*(W+1));
        28     memset(dp[1],0,sizeof(int)*(W+1));
        29     for (int i=0; i<n; i++)
        30     {
        31         for (int j=0; j<=W; j++)
        32         {
        33             if (j<w[i]) dp[(i+1) & 1][j] = dp[i & 1][j];
        34             else dp[(i+1) & 1][j] = max(dp[i & 1][j],dp[i & 1][j-w[i]]+v[i]); 
        35         }
        36     }
        37     cout << dp[n & 1][W] << endl;
        38 }
        01背包(滚动数组)

    01背包问题之2

    • 问题描述:有n个重量和价值分别为wi、vi的物品,从这些物品中挑选出总重量不超过W的物品,求所有挑选方案中价值总和的最大值。
    • 限制条件:
      • 1≤n≤100
      • 1≤wi≤107
      • 1≤vi≤100
      • 1≤W≤109
    • 这一问题与最初的01背包问题相比,只是修改了限制条件的大小。此前求解此问题的方法的复杂度是O(nW),对于这一问题的规模来讲就不够用了。在这个问题中,相比较重量而言,价值的范围比较小,所以可以试着改变DP的对象。之前的方法中,我们用DP针对不同的重量限制计算最大的价值,在这里,我们不妨尝试着用DP针对不同的价值计算最小的重量:
      dp[i+1][j] := 从前i+1个物品(编号从0到i)中挑选出价值总和为j时总重量的最小值(不存在时就是一个充分大的数值INF)
      由于前0个物品中什么都挑选不了,所以初始值为
      dp[0][0] = 0
      dp[0][j] = INF
      此外,从前i个物品中挑选出价值总和为j时,一定有
      ①前i-1个物品中挑选价值总和为j的部分
      ②前i-1个物品中挑选价值总和为j-v[i]的部分,然后再选中第i个物品
      这两种方法之一,所以就得到递推式:
      dp[i+1][j] = min(dp[i][j], dp[i][j-v[i]]+w[i])
      最终的答案就对应于令dp[n][j]≤W的最大的j,这样求解的时间复杂度为O(n∑vi),对此题限制条件下的输入就可以在时间限制内解出了。当然如果价值变大了的话,这里的算法也变得不可行了,我们需要依据问题的规模来改变算法
       1 #include <iostream>
       2 #include <cstring>
       3 using namespace std;
       4 
       5 const int INF = 0x3FFFFFF;
       6 int n,W;
       7 int * w;
       8 int * v;
       9 int **dp;
      10 
      11 int min(int x, int y)
      12 {
      13     if (x>y) return y;
      14     return x;
      15 }
      16 
      17 int main()
      18 {
      19     cin >> n >> W;
      20     w = new int[n];
      21     v = new int[n];
      22     int maxv=0;
      23     for (int i=0; i<n; i++)
      24     {
      25         cin >> w[i] >>v[i];
      26         if (v[i]>maxv) maxv=v[i];
      27     }
      28     dp= new int*[n+1];
      29     for (int i=0; i<=n; i++)
      30     {
      31         dp[i] = new int[n*maxv+1];
      32         for (int j=0; j<=n*maxv; j++) dp[i][j]=INF;
      33     }
      34     dp[0][0] = 0;
      35     for (int i=0; i<n; i++)
      36     {
      37         for (int j=0; j<=n*maxv; j++)
      38         {
      39             if (j<v[i]) dp[i+1][j] = dp[i][j];
      40             else dp[i+1][j] = min(dp[i][j],dp[i][j-v[i]]+w[i]); 
      41         }
      42     }
      43     for (int i=n*maxv; i>=0; i--)
      44         if (dp[n][i] <= W)
      45         {
      46             cout << i << endl;
      47             break;
      48         }
      49 }
      01背包2

    多重部分和问题

    • 问题描述:有n种不同大小的数字ai,每种各mi个,判断是否可以从这些数字之中选出若干使它们的和恰好为K。
    • 限制条件:
      • 1≤n≤100
      • 1≤ai,mi≤100000
      • 1≤K≤100000
    • 分析:
      • 这个问题可以用DP来做,不过如何定义递推关系会影响到最终的复杂度。先看这个定义:
        dp[i+1][j] := 用前i种数字是否能加和成j
        为了用前i种数字加和成j,也就需要能用前i-1种数字加和成j,j-ai,…,j-mi*ai中的某一种,由此可以得出如下递推关系:
        dp[i+1][j] = (0≤k≤mi且k*ai≤j时存在使dp[i][j-k*ai]为真的k)
        这个算法的时间复杂度是O(K*∑mi)
         1 #include <iostream>
         2 #include <cstring>
         3 
         4 using namespace std;
         5 
         6 int n,K;
         7 int * a;
         8 int * m;
         9 bool ** dp;
        10 
        11 int main()
        12 {
        13     cin >> n >> K;
        14     a = new int[n];
        15     m = new int[n];
        16     for (int i=0; i<n; i++)
        17         cin >> a[i] >> m[i];
        18     dp = new bool*[n+1];
        19     for (int i=0; i<=n; i++)
        20     {
        21         dp[i] = new bool[K+1];
        22         memset(dp[i],false,sizeof(bool)*(K+1));
        23     }
        24     dp[0][0] = true;
        25     for (int i=0; i<n; i++)
        26     {
        27         for (int j=0; j<=K; j++)
        28         {
        29             for (int k=0; j-k*a[i]>=0 && k<=m[i]; k++)
        30             {
        31                 dp[i+1][j] |= dp[i][j-k*a[i]];
        32             }
        33         }
        34     }
        35     if (dp[n][K]) cout << "Yes" << endl;
        36     else cout << "No" << endl;
        37 }
        多重部分和问题
      • 上面的做法并不够好,一般来讲,用DP来求取bool结果的话会有不少浪费,同样的复杂度通常能获得更多的信息,在这个问题中,我们不光求出能否得到目标的和数,同时把得到时ai这个数还剩下多少个计算出来,这样就可以减少复杂度:
        dp[i+1][j] := 用前i+1种数加和得到j时第i+1种数(编号为i)最多能剩余多少个(不能加和得到j的情况下为-1)
        按照如上所述定义递推关系,这样如果前i-1个数加和能得到j的话,第i个数就可以留下mi个。此外,前i+1种数加和出j-ai时第i+1种数还剩下k(k>0)的话,用这i+1种数加和j时第i+1种数就能剩下k-1个,由此可以得出下面的递推式:
        dp[i+1][j] = ① mi  (dp[i][j]>=0)
                          ② -1  (j<ai或者dp[i+1][j-ai]≤0)
                          ③dp[i+1][j-ai]-1  (其它情况下)
        这样,只要看dp[n][K]≥0是否成立,就可以知道答案了,时间复杂度是O(nK),如果使用一维数组还可以将空间压缩:

         1 #include <iostream>
         2 #include <cstring>
         3 
         4 using namespace std;
         5 
         6 int n,K;
         7 int * a;
         8 int * m;
         9 int * dp;
        10 
        11 int main()
        12 {
        13     cin >> n >> K;
        14     a = new int[n];
        15     m = new int[n];
        16     for (int i=0; i<n; i++)
        17         cin >> a[i] >> m[i];
        18     dp = new int[K+1];
        19     memset(dp,-1,sizeof(int)*(K+1));
        20     dp[0] = 0;
        21     for (int i=0; i<n; i++)
        22     {
        23         for (int j=0; j<=K; j++)
        24         {
        25             if (dp[j]>=0) dp[j]=m[i];
        26             else if(j<a[i] || dp[j-a[i]]<=0) dp[j]=-1;
        27             else dp[j]=dp[j-a[i]]-1;
        28         }
        29     }
        30     if (dp[K]>=0) cout << "Yes" << endl;
        31     else cout << "No" << endl;
        32 }
        多重部分和问题'

    最长上升子序列问题

    • 问题描述:有一个长为n的数列a0,a1,…,an-1.求出这个序列中最长的上升子序列的长度。
    • 限制条件:
      • 1≤n≤1000
      • 0≤ai≤1000000
    • 分析:这个问题是被称作最长上升子序列(LCS,Longest Increasing Subsequence)的著名问题。这一问题通过使用DP可以很有效率地解决。
      • 首先,先建立递推关系:
        定义dp[i] := 以ai为末尾的最长上升子序列的长度
        以ai结尾的上升子序列是
        ①只包含ai的子序列
        ②在满足j<i并且ai<aj的以aj为结尾的上升子序列末尾,追加上ai后得到的子序列
        这二者之一,这样不难得到递推关系:dp[i] = max{1,dp[j]+1|j<i且aj<ai}
        时间复杂度为O(n2)
         1 #include <iostream>
         2 
         3 using namespace std;
         4 
         5 int n;
         6 int * a;
         7 int * dp;
         8 int ans=1;
         9 
        10 int main()
        11 {
        12     cin >> n;
        13     a = new int[n];
        14     dp = new int[n];
        15     for (int i=0; i<n; i++)
        16     {
        17         cin >> a[i];
        18         dp[i] = 1;
        19     }
        20     for (int i=1; i<n; i++)
        21     {
        22         for (int j=0; j<i; j++)
        23         {
        24             if (a[j]<a[i] && (dp[j]+1)>dp[i]) dp[i]=dp[j]+1;
        25         }
        26         if (dp[i]>ans) ans = dp[i];
        27     }
        28     cout << ans << endl;
        29 }
        LIS
      • 那么,有没有效率更高效的方法呢?前面我们利用DP求取针对最末位的元素的LIS,如果子序列的长度相同,那么最末位的元素较小的在之后会更加有优势,所以我们再反过来用DP针对相同长度情况下最小的末尾元素进行求解:
        dp[i] := 长度为i+1的上升子序列中末尾元素的最小值(不存在的话就是INF)
        最开始的话dp[i]都初始化为INF,然后由前到后逐个考虑数组a的元素,对于每个aj,如果i=0或者dp[i-1]<aj的话,就用dp[i]=min(dp[i],aj)进行更新,最终找出使得dp[i]<INF的最大的i+1就是结果了。复杂度和之前一样为O(n2),但这一算法还可以进一步优化,显然dp数组中除INF之外是单调递增的,所以可以知道对于每个aj最多只需要1次更新,对于这次更新的位置,可二分搜索,这样就可以在O(nlogn)时间内出结果了
         1 #include <iostream>
         2 #include <algorithm>
         3 
         4 using namespace std;
         5 
         6 const int INF = 0x3FFFFFF;
         7 int n;
         8 int * a;
         9 int * dp;
        10 int ans=1;
        11 
        12 int main()
        13 {
        14     cin >> n;
        15     a = new int[n];
        16     dp = new int[n];
        17     for (int i=0; i<n; i++)
        18     {
        19         cin >> a[i];
        20         dp[i] = INF;
        21     }
        22     for (int i=0; i<n; i++)
        23     {
        24         *(lower_bound(dp,dp+n,a[i])) = a[i];
        25     }
        26     cout << lower_bound(dp,dp+n,INF)-dp << endl;
        27 }
        LIS'
        上面的代码使用了lower_bound这个STL函数,这个函数从已排好序的序列a中利用二分搜索找出指向满足ai≥k的ai的最小的指针
        类似的函数还有upper_bound,这一函数求出的是指向满足ai>k的ai的最小的指针
        有了它们,比如长度为n的有序数组a中的k的个数,可以这样方便地求出:upper_bound(a,a+n,k)-lower_bound(a,a+n,k)

    有关计数问题的DP


    划分数

    • 问题描述:有n个无区别的物品,将它们划分成不超过m组,求出划分方法数模M的余数
    • 限制条件 
      • 1≤m≤n≤1000
      • 2≤M≤10000
    • 这样的划分被称作n的m划分,特别地,m=n时称作n的划分数。(
      科普:将基数为n的集合划分为恰好k个非空集的方法的数目称为第二类Stirling数,而将基数为n的集合划分为任意个非空集的方法的数目称为Bell数)
      DP不仅对于求解最优问题有效,对于各种排列组合的个数、概率或者期望之类的计算同样很有用。
      分析:
      定义:dp[i][j] :=j的i划分的总数
      根据这一定义写出递推关系,将j个划分i份的话,可以先取k个,然后将剩下的j-k个分成i-1份,这个想法看起来很自然,但是错误的,因为有大量的重复计数!
      寻找别的递推关系,考虑n的m划分ai(∑ai=n),如果对于每个i都有ai>0,那么{ai-1}就对应了n-m的m划分,另外,如果存在ai=0,那么这就对应了n的m-1划分,综上,我们可以写出这样的递推关系:
      dp[i][j]=dp[i][j-i]+dp[i-1][j]
      这个递推式可以不重复地计算所有的划分,复杂度为O(nm),像这样需要在计数问题中解决重复计算问题时,需要特别小心
       1 #include <iostream>
       2 #include <cstring>
       3 
       4 using namespace std;
       5 
       6 int n,m,M;
       7 int * a;
       8 int **dp;
       9 
      10 int main()
      11 {
      12     cin >> n >> m >> M;
      13     a = new int[n];
      14     dp = new int*[m+1];
      15     for (int i=0; i<=m; i++)
      16     {
      17         dp[i]=new int[n+1];
      18         memset(dp[i],0,sizeof(int)*(n+1));
      19     }
      20     dp[0][0]=1;
      21     for (int i=1; i<=m; i++)
      22     {
      23         for (int j=0; j<=n; j++)
      24         {
      25             if (j>=i) dp[i][j]=(dp[i][j-i]+dp[i-1][j])%M;
      26             else dp[i][j]=dp[i-1][j];
      27         }
      28     }
      29     cout << dp[m][n] << endl;
      30 }
      划分数

    多重集组合数

    • 问题描述:有n种物品,第i种物品有ai个,不同种类的物品可以互相区分,但相同种类的无法区分,从这些物品中取出m个的话,有多少种取法?求出方案数模M的余数。
    • 限制条件:
      • 1≤n≤1000
      • 1≤m≤1000
      • 1≤ai≤1000
      • 2≤M≤10000
    • 分析:为了不重复计数,同一种类的物品最好一次性处理好。
      按照如下方式进行定义:
      dp[i+1][j] := 从前i+1种物品中取出j个的组合总数
      为了从前i种物品中取出j个,可以从前i-1种物品中取出j-k个,再从第i种物品中取出k个添加进来,所以递推关系为:dp[i+1][j] = ∑dp[i][j-k](0≤k≤min(j,a[i])),
      直接计算的话,复杂度是O(nm2),比较高,接下来一波常规操作,将递推式进行变形:
      由于  ∑dp[i][j-k](0≤k≤min(j,a[i]))=(∑dp[i][j-1-k])+dp[i][j]-dp[i][j-1-a[i]] (0≤k≤min(j-1,a[i])) (其中a[i]≤j-1)  
               ∑dp[i][j-k](0≤k≤min(j,a[i]))=(∑dp[i][j-1-k])+dp[i][j] (0≤k≤min(j-1,a[i])) (其中a[i]≥j)
      所以,可将递推式变形为:
      dp[i+1][j] = dp[i+1][j-1]+dp[i][j]-dp[i][j-1-a[i]] (其中a[i]≤j-1)
      dp[i+1][j] = dp[i+1][j-1]+dp[i][j] (其中a[i]≥j)
       1 #include <iostream>
       2 #include <cstring>
       3 
       4 using namespace std;
       5 
       6 int n,m,M;
       7 int * a;
       8 int **dp;
       9 
      10 int main()
      11 {
      12     cin >> n >> m >> M;
      13     a = new int[n];
      14     dp = new int*[n+1];
      15     for (int i=0; i<n; i++) cin >> a[i]; 
      16     for (int i=0; i<=n; i++)
      17     {
      18         dp[i]=new int[m+1];
      19         memset(dp[i],0,sizeof(int)*(m+1));
      20     }
      21     for (int i=0; i<=n; i++)
      22     {
      23         dp[i][0] = 1;
      24     }
      25     for (int i=0; i<n; i++)
      26     {
      27         for (int j=1; j<=m; j++)
      28         {
      29             if (j-1>=a[i]) dp[i+1][j]=(dp[i+1][j-1]+dp[i][j]-dp[i][j-1-a[i]]+M)%M;
      30             else dp[i+1][j]=(dp[i+1][j-1]+dp[i][j])%M;
      31         }
      32     }
      33     cout << dp[n][m] << endl;
      34 }
      多重集组合数
  • 相关阅读:
    数据结构之双向链表的插入
    循环链表之约瑟夫 环
    循环链表
    顺序表的A-A交B
    环境变量
    java之构造函数的简单应用
    java之覆盖
    数据结构之栈的进制转换(10进制-- > 8进制 )
    数据结构链表之 一元多次多项式
    排序之简单的快速排序
  • 原文地址:https://www.cnblogs.com/Ymir-TaoMee/p/9419377.html
Copyright © 2020-2023  润新知