码迷,mamicode.com
首页 > 其他好文 > 详细

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

时间:2018-08-05 22:30:51      阅读:133      评论:0      收藏:0      [点我收藏+]

标签:nbsp   alt   情况   限制   技术   初始   ora   不同   ||   

 


 

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 }
    多重集组合数

     

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

标签:nbsp   alt   情况   限制   技术   初始   ora   不同   ||   

原文地址:https://www.cnblogs.com/Ymir-TaoMee/p/9419377.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!