.

二进制状态压缩动态规划

对于某些情况,如果题目中所给的限制数目比较小,我们可以尝试状态压缩动态规划。例如,题目中给出数据范围\(n<=20\),这个一般情况下是一个状压DP的提示。

状态压缩,顾名思义,要把每种状态压缩起来。一个经典的问题是洛谷P1171,也就是著名的货郎担问题,它是一个NPC难题,目前不存在多项式算法。当题目中\(n\)的范围比较小时,我们可以考虑使用状态压缩动态规划(状压DP)来解决。(注:本文出现的"状压DP"若无特殊说明,均指状态压缩动态规划)

我们用状压DP解决货郎担问题的时间复杂度是\(O(n^{2}2^{n})\),我们用\(dp[i][j]\)来表示目前处在第\(i\)个城市,集合\(j\)中的城市已经全部都经过一次所花费的最小代价。那么,由于\(j\)的范围是\(2^{n}\)(每个城市都有两种状态,共\(n\)个城市),而\(i\)的取值范围是\(n\),所以一共有\(2^{n}n\)种状态,每种状态可以出发去其他任何一个城市,所以有\(n\)种决策,所以总时间复杂度为\(O(n^{2}2^{n})\)。

通常,我们使用状压DP的时候,把集合用一个\(int\)型整数表示,它通常取值为\([0,2^{n}-1]\),用来表示每个元素的两种状态,从而表示出当前状态。我们怎么知道元素\(i\)是否在这个集合中呢?我们可以用1<<i-1来表示,这种表示方法可以查出得到元素\(i\)所代表的那一位,然后我们就可以用位运算符\(\&\)来于集合取一个交集,如果返回为真,那么说明元素\(i\)在原集合中,否则不在。

状态剪枝

普通的动态规划通常有很多个状态,而这些状态会占用大量内存以及消耗时间。有的时候,我们没必要真的去计算每一个状态,因为有的状态永远也无法转移到答案。

我们结合一道例题具体分析.



显然,我们有一个思路:用\(f[i][j]\)来表示我们在岛屿上\(i\)处,我们上一次跳跃距离是\(j\),那么我们就可以很方便的转移了,时间复杂度\(O(n^2)\),空间复杂度\(n^2\)。

现在,出题人想卡掉这种做法。这种做法的复杂之处在于:有一些状态,我们永远也无法访问,但我们还是记录并从他向外界转移了。这不是我们希望的,而我们观察到有用的\(j\)只存在于一定范围内。这个发现可以让我们减少自己的决策数,实际上,打表发现,有用的\(j\)的分布只存在于\([1,\sqrt{n}]\)中,所以我们可以进一步优化到\(O(n\sqrt{n})\).

这种优化实际上是一种直觉,我们看到\(n\)的范围是\(10000\),我们必须想办法优化,DP优化的一般思路是:打表->发现规律->利用规律->AC.

** 改变DP对象**

这道题,如果\(H,W<3000\),那么我们可以很方便的用一般的\(2D\)状态的DP来做。时间复杂度\(O(H*W)\) __ , 但是这道题的H,W太大了,我们只好考虑别的方法。

我们观察到,不能走的点很少,只有\(2000\)个,我们打算从他入手。

我们把所有的不能走的点,把\(x\)作为第一关键字,\(y\)作为第二关键字,进行排序,同时我们让点\((H,W)\)也加进来,显然他会排在最后一个。

现在,我们用\(dp[i]\)表示到达第\(i\)个不能走的点的路径条数(假设第i个格子可以走),注意到我们是根据横纵坐标递增排序的,所以对于两个数\(i,j\) 如果\(i<j\)那么一定无法从\(j\)到达\(i\),这确保了无后效性。

我们考虑两个点\((X_1,Y_1),(X_2,Y_2)\),如果中间不含任何无法走的方块,那么一共有\(\frac{(X_2-X_1+Y_2-Y_1-2)!}{(X_2-X_1+1)!(Y_2-Y_1+1)!}\)种方法。

我们用\(W[x][y]\)表示\(x->y\)的路径条数,也就是上面那个式子,我们可以通过预处理阶乘的方法,那么对于每个\(W\)我们可以O(1)求,但是\(W[0][n+1]\)并不是答案,因为经过了不能走的点。

考虑如何计算\(dp[i]\),对于没有任何限制,那么它为\(W[0][i]\),我们考虑,如果第一个遇到的不可走的点为\(j\),那么接下来走到\(i\)的方案数为\(dp[j]*W[j][i]\),我们把这个数从中减去就好啦!答案存在\(dp[n+1]\)中。

差分dp

对于这个题,我们不是太好设定状态呢。我们首先先给所有的物品排序,按照价格从小到大。那么,我们可以把他们放在数轴上,作为数轴上的点。

每个集合就是一条线段咯!而我们要求的值,就是每条线段的长度之和。因为,最左边的是价值最低的,最右边的是价值最高的。那么,我们可以这么设计状态。

\(dp[i][j][k]\)表示,当前已经放置了\(i\)个商品,也就是有\(i\)个点都已经属于某条线段了,还有\(j\)条线段只有一个端点的方案数,那么,我们每次更新一个点,都会对\(k\)产生一点贡献,这个贡献是多少呢?\(j*(a_i-a_{i-1})\).为什么是\(j\)乘呢?我们每次不是只放在了一组里吗?原因是,如果每次只更新单租的贡献,那么状态不好转移。我们不知道这一组之前上一个是谁。

也就是说,我们每次一个点更新以后,就把将来一定会用到的值更新。因为我们是已经排好序了,所以这么做是没问题的。

那么,每个状态\(dp[i][j][k]\)都有以下几个转移方式转移而来:

1.我们让商品\(i\)开一条线段,那么\(dp[i][j+1][k+j*(a_i-a_{i-1})]+=dp[i-1][j][k]\)

2.我们让\(i\)单独成一组,那么\(dp[i][j][k+j*(a_i-a_{i-1})]+=dp[i-1][j][k]\)

3.我们让\(i\)结束一条线段,此时须保证\(j!=0\),那么\(dp[i][j-1][k+j*(a_i-a_{i-1})]+=dp[i-1][j][k]*j\)

4.我们把\(i\)加入到某一条线段里(但不结束这条线段),那么\(dp[i][j][k+j*(a_i-a_{i-1})]+=dp[i-1][j][k]*j\)

最终答案存在\(\sum_{i=0}^{k}dp[n][0][i]\)

连通块dp

文文也不知道该怎么给这种dp取名,目前还没遇到过这样的题目。

例题:

给出\(n\)个数字\(a_1...a_n\),以及一个整数\(L\),\(n<=100,a_i<=1000,L<=1000\),求有多少种排列,满足\(|a_1-a_2|+|a_3-a_4|+....+|a_{n-1}-a_n|<=L\).

这道题我们很难直接设状态,我们先把他们排一遍,然后把他们一个一个加入到排列中。每加一次,都统计一下答案。例如:2,7,?,5,6,?,?,?,?,9

还没有填数的地方用?来表示,假设我们已经把前\(i-1\)个数全部填进去了,现在考虑第\(i\)个,由于是排好序的,第\(i\)个一定大于其中任意一个.

我们用\(dp[i][j][k][l]\)来表示当:

填入数字个数为\(i\),连通块个数为\(j\),当前的代价为\(k\),连通块结尾是否已经全部填充(l=0 没有, l=1一部分 l=2 全部填充)的方案总数

小细节:

1.每填入一个元素,都要更新答案的值为新造成的差值的绝对值乘上连通块的个数(因为连通块是等价的,可以调换位置)

2.每个元素,要么合并两个连通块,要么新建一个连通块,要么插在某个连通块开头或结尾

转移方式结合在代码中详细解释

  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. typedef long long ll;
  4. typedef pair<int,int> ii;
  5. typedef vector<int> vi;
  6. typedef vector<ii> vii;
  7. typedef long double ld;
  8. #define fi first
  9. #define se second
  10. #define pb push_back
  11. #define mp make_pair
  12. ll dp[101][101][1001][3];
  13. ll a[101];
  14. const ll MOD = 1e9 + 7;
  15. int main()
  16. {
  17. ios_base::sync_with_stdio(0); cin.tie(0);
  18. int n, l;
  19. cin>>n>>l;
  20. for(int i = 0; i < n; i++)
  21. {
  22. cin>>a[i];
  23. }
  24. sort(a, a + n);
  25. if(n == 1) //特殊情况
  26. {
  27. cout << 1;
  28. return 0;
  29. }
  30. a[n] = 10000; //无穷大
  31. if(a[1] - a[0] <= l) dp[1][1][a[1] - a[0]][1] = 2; //在其中一个终止点填入a[0],还有两个终止点等待填充
  32. if(2*(a[1] - a[0]) <= l) dp[1][1][2*(a[1] - a[0])][0] = 1;
  33. for(int i = 1; i < n; i++)
  34. {
  35. int diff = a[i + 1] - a[i]; //如果i=n-1 diff = inf
  36. for(int j = 1; j <= i; j++)
  37. {
  38. for(int k = 0; k <= l; k++)
  39. {
  40. for(int z = 0; z < 3; z++)
  41. {
  42. if(!dp[i][j][k][z]) continue; //值不存在
  43. //首先尝试填充其中一个端点
  44. if(z < 2 && k + diff*(2*j - z - 1) <= l) //有2j-z-1个位置想要更优(因为这些位置中的某一个将在这一步以后与一个终止点合并)
  45. {
  46. if(i == n - 1)
  47. {
  48. dp[i + 1][j][k + diff*(2*j - z - 1)][z + 1] = (dp[i + 1][j][k + diff*(2*j - z - 1)][z + 1] + dp[i][j][k][z]*(2-z)*j)%MOD;//我们有j个连通块可以合并
  49. }
  50. else if(z == 0 || j > 1) //i==n-1
  51. {
  52. dp[i + 1][j][k + diff*(2*j - z - 1)][z + 1] = (dp[i + 1][j][k + diff*(2*j - z - 1)][z + 1] + dp[i][j][k][z]*(2-z)*(j-z))%MOD;//没有连接到结尾
  53. }
  54. if(k + diff*(2*j - z + 1) <= l) //新建连通块
  55. {
  56. dp[i + 1][j + 1][k + diff*(2*j - z + 1)][z + 1] = (dp[i + 1][j + 1][k + diff*(2*j - z + 1)][z + 1] + dp[i][j][k][z]*(2-z))%MOD; //找一个结尾创建
  57. }
  58. }
  59. //接下来填充尾部
  60. //先创建一个新连通块
  61. if(k + diff*(2*j - z + 2) <= l) // 2个新位置可以更新
  62. {
  63. dp[i + 1][j + 1][k + diff*(2*j - z + 2)][z] = (dp[i + 1][j + 1][k + diff*(2*j - z + 2)][z] + dp[i][j][k][z])%MOD;
  64. }
  65. //合到一个连通块中
  66. if(k + diff*(2*j - z) <= l)
  67. {
  68. dp[i + 1][j][k + diff*(2*j - z)][z] = (dp[i + 1][j][k + diff*(2*j - z)][z] + dp[i][j][k][z]*(2*j - z))%MOD;
  69. }
  70. //然后把两个连通块合在一起
  71. if((k + diff*(2*j - z - 2) <= l) && (j >= 2) && (i == n - 1 || j > 2 || z < 2))
  72. {
  73. if(z == 0)
  74. {
  75. dp[i + 1][j - 1][k + diff*(2*j - z - 2)][z] = (dp[i + 1][j - 1][k + diff*(2*j - z - 2)][z] + dp[i][j][k][z]*j*(j-1))%MOD; //j*P2种可能的合并
  76. }
  77. if(z == 1)
  78. {
  79. dp[i + 1][j - 1][k + diff*(2*j - z - 2)][z] = (dp[i + 1][j - 1][k + diff*(2*j - z - 2)][z] + dp[i][j][k][z]*(j-1)*(j-1))%MOD; // (j-1)P2+(j-1) 种可能的合并
  80. }
  81. if(z == 2)
  82. {
  83. if(i == n - 1)
  84. {
  85. dp[i + 1][j - 1][k + diff*(2*j - z - 2)][z] = (dp[i + 1][j - 1][k + diff*(2*j - z - 2)][z] + dp[i][j][k][z])%MOD;//一种可能的合并,直接继承过来
  86. }
  87. else
  88. {
  89. dp[i + 1][j - 1][k + diff*(2*j - z - 2)][z] = (dp[i + 1][j - 1][k + diff*(2*j - z - 2)][z] + dp[i][j][k][z]*(j-2)*(j-1))%MOD;// (j-2)P2 + 2(j-2)种可能的合并
  90. }
  91. }
  92. }
  93. }
  94. }
  95. }
  96. }
  97. ll answer = 0;
  98. for(int i = 0; i <= l; i++)
  99. {
  100. answer = (answer + dp[n][1][i][2])%MOD;
  101. }
  102. cout << answer << '\n';
  103. return 0;
  104. }

常见的DP技巧还有很多,文文这里仅举5例,难度依次递增。

由于文文水平不足,难免存在错误或纰漏,欢迎指正。

有更多技巧想要和文文探讨,可以QQ(434935191)或邮箱(v@18sec.cn)联系文文。

[文文殿下]基本的DP技巧的更多相关文章

  1. 树形dp技巧,多叉树转二叉树

    今天复习树形dp时发现一道比较古老的题,叫选课,是树形dp的一道基础题,也是多叉树转二叉树应用的模版题 多叉树转二叉树的应用非常广泛,因为如果一个节点的儿子太多,一个一个存下来不方便去查询,并且会增加 ...

  2. Codeforces 900 E. Maximum Questions (DP,技巧)

    题目链接:900 E. Maximum Questions 题意: 给出一个长度为n只含有a和b还有'?'的串s,且'?'可以被任意替换为a或b.再给出一个字符串t (奇数位上为a,偶数位上为b,所以 ...

  3. Two Melodies CodeForces - 813D (DP,技巧)

    https://codeforces.com/problemset/problem/813/D dp[i][j] = 一条链以i结尾, 另一条链以j结尾的最大值 关键要保证转移时两条链不能相交 #in ...

  4. 【文文殿下】WC2019游记

    Day0 今天早上三点半才睡着,五点起床,前往省城郑州.与省实验常老师汇合,坐上高铁,下午三点半多才到广州二中. 下午随便找了一个教室进去敲一敲代码,发现自己越来越菜了. 和一大堆网上的dalao面基 ...

  5. 【文文殿下】NOIp2018游记

    Day-1 本段更新于 2018年11月8日23:26:44 今天还在机房里面,无所事事吧.上午睡了一上午,出去理了一下发,花了20块钱 QAQ. 下午来到机房,复习了一下exgcd的东西. 发现自己 ...

  6. 【文文殿下】[CEOI2004]锯木厂选址 题解

    题解 我们枚举建厂的位置,发现有个\(n^2\)的DP.随手搞个斜率优化到\(O(n)\). #include<bits/stdc++.h> using namespace std; ty ...

  7. 【文文殿下】【CF724C】Ray Tracing (中国剩余定理)

    题解 我们考虑将棋盘扩大一倍,这样相当于取膜.然后,我们只要对x,y,的位置分类讨论,做四次crt就行.具体细节看文文代码. #include<cstdio> #include<al ...

  8. 【文文殿下】[BZOJ4008] [HNOI2015] 亚瑟王

    题解 这是一个经典的概率DP模型 设\(f_{i,j}\)表示考虑到前\(i\)张牌,有\(j\)轮没打出牌的可能性,那么显然\(f_{0,r} = 1\). 考虑第\(i+1\)张牌,他可能在剩下的 ...

  9. 【文文殿下】【HAOI2008】硬币购物

    题目描述 硬币购物一共有4种硬币.面值分别为c1,c2,c3,c4.某人去商店买东西,去了tot次.每次带di枚ci硬币,买si的价值的东西.请问每次有多少种付款方法. 数据规模 di,s<=1 ...

随机推荐

  1. latex如何插入空白行

    1.~\\:一行空白2.\\[行距]:可加入任意间距的空白行 [xpt]

  2. PC上对限制在微信客户端访问的html页面进行调试

    PC上对微信的html5页面做测试,一般来说需要两个条件:浏览器UA改为微信客户端的UA(打开页面提示请在微信客户端登录就需要修改UA):增加满足html5验证条件的Cookie来进行微信OAUTH验 ...

  3. Shiro框架的简单应用

    一.概念 Shiro是一个安全框架,可以进行角色.权限管理. Shiro主要功能如下:Authentication(认证):用户身份识别,通常被称为用户“登录”Authorization(授权):访问 ...

  4. 78. Subsets (Back-Track, DP)

    Given a set of distinct integers, nums, return all possible subsets. Note: Elements in a subset must ...

  5. CentOS错误

    centos下yum lock的解决办法 Another app is currently holding the yum lock; waiting for it to exit... 解决办法:  ...

  6. Visual Studio 2010 常用快捷方式

    调试快捷键 F6:           生成解决方案 Ctrl+F6:   生成当前项目 F7:           查看代码 Shift+F7:  查看窗体设计器 F5:           启动调 ...

  7. [z]微信平台开发教程

    http://blog.csdn.net/lyq8479?viewmode=contents

  8. Halcon二维仿射变换实例探究

    二维仿射变换,顾名思义就是在二维平面内,对对象进行平移.旋转.缩放等变换的行为(当然还有其他的变换,这里仅论述这三种最常见的). Halcon中进行仿射变换的常见步骤如下: ① 通过hom_mat2d ...

  9. struts,hibernate,spring配置时问题汇总及解决办法

    1.java.lang.NoClassDefFoundError: org/objectweb/asm/ClassVisitor 缺少asm-3.3.jar 2.java.lang.NoClassDe ...

  10. 15 Independent Alleles

    Problem Figure 2. The probability of each outcome for the sum of the values on two rolled dice (blac ...