直观的DP
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
正向思维,把每一层的所有状态全都算出来,再给下一层。题目是不难,但这个图是比较直观的反映了 DP ,解决问题时如果可以把层次关系描述出来可以有效帮助做题。
其中只有根是已知的,每个状态的转移方式也是已知的。
从根到叶是正向思维。顺次可以得到所有的儿子的状态。当所有状态数量是有限的时候,正向的规模被限制,可以尝试求解。
但有时只需要求其中一个儿子的状态,如果还是从根节点出发一一求出状态则显得有点浪费。所以此时应该逆向思维。
当上一层对下一层状态如树状(类似于分治),则非常理想,从儿子看,他只需要知道父亲的状态,而不需知道伯伯的状态,依次到达根节点。但当状态有交叉时,则要求出相关联的伯伯的状态。逆向求解有助于减少运算的规模。
根据情况初始化。有时可能只能以角落(如第一行第一列的第一个格子)为第一层,有时可能以边界为第一层(比如第一行所有格子)。在层与层的转换时,注意剪枝,即本层要达到状态 s,则前一层必须达到 s’, 达不到 s’ 的状态可以不考虑。
一个人要从左上角往右下角走,每次只能向左或向下走一格;另一个人要从左下角往右上角走,每次只能向右或向上走一格。两条路径交叉于一点。求两条路径上的点数之和(不包括交叉点)。
显示解答
![](http://ww1.sinaimg.cn/mw690/006aIx0Cgw1f673xu77irj30bp062wez.jpg)1 | dp[i][j-1][0] + dp[i-1][j][1] + dp[i][j+1][2] + dp[i+1][j][3] |
给一个 N * M (N, M < 31)的矩阵,从 (1,1) 到 (N,M) 经过的格点分值分别为 Ai,(0 <= Ai < 31) 路径只能向右或向下走,共N+M-1步)。求各种路径中方差最小的路径。
显示解答
其中$A_{avg}={1\over {N+M-1}}\sum A_i$, 则$\sum 2A_iA_{avg}=2(N+M-1)A_{avg}^2$ $$原式=(N+M−1)[(\sum A_i^2)-(N+M-1)A_{avg}^2]$$$$=(N+M−1)\sum A_i^2-(\sum A_i)^2$$1 | dp[i][j][s] |
背包——DP中的经典
背包系列问题是DP入门的经典。一言以蔽之:n
件物品,第i
件体积为v[i]
, 价值为w[i]
, 件数为k[i]
$F_i(j):前i个物品放进容积为j的容器里的最大价值(逐步达到最大)$
0-1背包
所有k[i]均为1
显示公式
$$F_i(j)= \begin{cases} F_{i-1}(j) &&\text{$V_i > j >= 0$}\\ max\{F_{i-1}(j),F_\color{red}{i-1}(j-V_i)+W_i\} &&\text{$C>=j>=V_i$} \end{cases} $$显示代码
1 | for(int i = 0;i < n;i++) |
滚动数组
1 | for(int i = 0;i < n;i++) { |
显示代码
1 | for(int i = 0;i < n;i++) |
完全背包
所有k[i]=inf
显示公式
$$F_i(j)= \begin{cases} F_{i-1}(j) &&\text{$V_i > j >= 0$}\\ max\{F_{i-1}(j),F_\color{red}{i}(j-V_i)+W_i\} &&\text{$C>=j>=V_i$} \end{cases} $$显示代码
1 | for(int i = 0;i < n;i++) |
多重背包
k[i]>=1
显示解答
1. 化为0-1背包,则共有 $\sum k_i$ 件物品,当物品件数多时可能会超时。 2. 二进制拆分。将k拆为 $1,2,4,...,2^p, k-2^{p+1}+1$, p 为最大的满足该式的值。可知它们可以组合出在 1 到 k 范围内的所有数字。则只要将一件物品拆分成这样的 p+2 件物品,化为 0-1 背包。**注意每件物品重量、价值等比例扩大。**1 | for(int i = 0;i < n;i++) |
变形背包
有 n 个瓶子,各有水量和瓶体积。把水从一个瓶倒到另一个瓶。首先要使得最后不空的瓶子数最少,其次要倒水量最少。求瓶子数和倒水量。
分析
1. 确定瓶子数。 对瓶子的体积排序,前 km 个瓶子体积 V 恰好不小于总水量之和 wt ,则 km 即为最少的瓶子数。 2. 确定倒水量 `dp[i][j][k]`表示前 i 个瓶子选取 k 个(且第 i 个为所选第 k 个),使得 k 个瓶子体积和为 j ,可以容纳的最大水量。 先求出在 `dp[n-1][wt~V][km]` 的 `max`,再用 `wt-max` 即答案。 由于轮换,可降维到 `dp[j][k]`。 另外通过 `reach[j][k]` 表示是否可到达该状态。 `j`, `k` 的两层循环位置可调换,答案不变。但是一种比另一种速度快一倍,这个问题组原课有解释。 由于是0-1背包,须注意 `j`, `k` 是循环递减来遍历,否则就是完全背包了。状态压缩
当状态的内部顺序对于结果没有影响时,可以将多个状态进行状态压缩简化推理。
例题:在一个 n * n (n < 17)的矩阵里选择 n 个数字(两两不同行、列),求数字和最大是多少。
分析
逐行考虑。在考虑第 i 行的第 j 列 是否要取时,先观察第 j 列 上是否已经取了数字。至于第 j 列上取的数字是第几行的,则不需要考虑。所有关于第 j 列上放了数字的状态,都可以压缩到一起。压缩
定义:逐行选择,并且用一个n位的二进制数表示各列的选择情况。比如00101表示已经选择了两行,第三列、第五列被选择了。 a[i][j] 表示第i行、第j列的数值; F[s] 表示状态 s (用二进制表示)选取的最大值。 递推关系:每个二进制状态从前几个相关状态转换而来。比如01101由00101、01001、01100转化过来,即:1 | F[01101] = max(F[00101]+a[3][2], F[01001]+a[3][3], F[01100]+a[3][5]) |
给 n(n<13)条边,每条边只能用一次,拼成多个三角形,求三角形面积和的最大值。
分析
1~2^12 只有 4096,对每个数二进制分解,第 i 位为 1 则用这条边。 预处理出所有的数位和为 3 的倍数的状态。区间DP
求取一个区间内的有效信息,可以借助于子区间得到。分割子区间的方式,既可以是多次分割取其中一种,也可以是就分割一次。类似于最前面提到的,有树状的,也有非树状的。
例如区间 [1,5] 上的数的最大公约数,只需要分割一次,求 gcd[gcd[1, 3], gcd[4, 5]] 即可。分割的子区间没有交集。
多分割
求最多的括号匹配数目。
显示解答
枚举每种区间长度。 `dp[i][j]`代表`str[i...j]`区间内最多的合法括号数 状态转移方程:1 | if((str[i]=='(' && str[j]==')') || (str[i]=='[' && str[j]==']')) |
补全最少的括号使得所有括号匹配。
显示解答
`v[i][j]`记录`dp[i][j]`取最大值时的情况: -1表示`str[i]`与`str[j]`相互匹配;其余表示`dp[i][j]`取最大值时的k值,`str[i]`与`str[k]`相互匹配。 打印过程递归:1 | void draw(int x, int y){ |
单一分割
线段树
略略略
RMQ
查询O(1)
给N($N\le 100000$)个数,Q($Q\le 100000$)个询问,每次查询输出区间的最大公约数,以及最大公约数为这个数的区间数目。
显示解答
查询次数很多,要做预处理,用map<最大公约数,区间数>存下来,实现O(1)的查询。 预处理发现对于同一左端点的区间而言,右端点越靠右,区间gcd单调递减。因此可以固定左端点,二分右端点,找到gcd突变的右端点,确定对于同一gcd的区间数目。为了查询得更快,用ST表(RMQ)存下区间gcd,只要O(1)即可查询得到区间gcd。 对于这题还不够,需要继续进行优化。- 建表
$dp[i][j]$表示长度为$2^i$的区间$[i,i+2^j-1]$范围内的$gcd$
即区间$[i,i+2^{j-1}-1]与[i+2^{j-1},i+2^j-1]$共同确定了区间$[i,i+2^j-1]$的$gcd$
1 | for(int i = 0;i < n;i++){ |
- 询问
$[l,r]$由$[l,…]与[…,r]$共同决定。两者必须共同覆盖了整个区间。区间长度为$2^k$,其中$k=log_2(r-l+1)$
1 | int ask(int l, int r){ |
树状DP
树已经构造好了,如何利用父亲节点的已知信息进行DP。遍历更新深度时即是一种应用:1
dp[i] = dp[parent[i]] + 1
与贪心的关系
贪心需要论证,保证一步优,步步优。如果要证明贪心是有问题的,举一个 DP 的反例即可。
如果 DP 的过程中发现了每次都选了局部最优,则很有可能该问题是可以贪心解决的。