DP学习记录Ⅱ

前言

状态定义,转移方程,边界处理,这三部分想好了,就问题不大了。重点在状态定义,转移方程是基于状态定义的,边界处理是方便转移方程的开始的。因此最好先在纸上写出自己状态的意义,越详细越好(如至少/恰好,包含/不包含XXX)

DP题通常码量不大,但是非常考验码力,因为细节非常多,比如边界包含不包含0/n?转移顺序是正着转移还是倒着转移?

通常情况下,边界设为 0~n 最为保险,但是要保证不出负数,并且保证0/n+1的状态合法(inf OR -inf OR 0) 等这么写完后发现会越界再改也可以。

至于转移顺序,就要具体分析了。主要看我们想要不想要之前已经转移过来的状态再多次转移 (如01背包就不能一件物品选多次,因此要干净的之前状态;恰好 -> 至少 的常用方法就要用一种类似前/后缀和的思想,就要不干净的/包含所有之前信息的状态来转移。

Continued...


线性dp

通常体现为序列上的dp。应该算dp的基础部分了(尽管也有难题)

P3646 [APIO2015]巴厘岛的雕塑

主要考查的是按位贪心的想法,以及可行性dp转化为最优性dp的方法。妙处在于设置“模板”来确定转移的合法性。坑点在 define int long long 并不能改 \(1 << i\) 为 \(1ll << i\),可能爆负数。

可行性dp -> 最优性dp

当我们的一开始想出的状态为 \(f[i][j][k]\) 表示前 \(i\) 个中某特征为 \(j\),某特征为 \(k\) 的状态是否合法的时候,如果 \(k\) 越大越好(或者越小越好之类的),可以转化为 \(f[i][j]\) 表示前 \(i\) 个中某特征为 \(j\),最大的合法的 \(k\) 是多少


背包

常见背包

\(f[i]\) 表示花费 \(i\) 的代价所能获得的最大收益。

  • 01背包
  • 完全背包
  • 多重背包(二进制拆分,单调队列)

特殊背包

  • 多限制背包

如除了体积外,还有大小,花费等限制。

\(f[i][x][y][z][...]\) 表示考虑前 \(i\) 个物品,体积为 \(x\),大小为 \(y\),花费为 \(z\)...的最大收益。第一位可以压掉。

  • 大容量小价值背包

体积的规模巨大,但是能保证价值之和足够小。

\(f[i][v]\) 表示考虑前 \(i\) 件物品,获得 \(v\) 的价值最小要用多大容量的背包。第一位可以压掉,答案可以二分。

背包合并

有 \(O(n^2)\) 的做法:

把一个背包看作 \(n\) 件物品,即 \(f[w] = v\) 看作一个体积为 \(w\),价值为 \(v\) 的物品。并且要求只能选择一件物品。这就要求我们在更新 \(F[W]\) 之前,不能更新 \(F[W - w]\)。(具体间转移方程)

转移方程:(\(f\) 合并到 \(F\) 里)

\[MAX(F[W], F[W-w] + f[w])
\]

需要保证 \(F[W-w]\) 是干净的,不带任何有关所有 \(f[w]\) 的。因此需要把 \(W\) 放在外层枚举,且倒序; \(w\) 放内层,顺序任意

for (i = K -> 0)
for (j = 0 -> i)
MAX(F[i], F[i - j] + f[j]);

背包与自然数的划分

把 \(n\) 划分为若干不同的正整数的方案数:可以看做对\(1\) ~ \(n\) 这 \(n\) 个物品做01背包。

把 \(n\) 划分为若干可重的正整数的方案数:可以看做对\(1\) ~ \(n\) 这 \(n\) 个物品做多重背包。


树形dp

树形dp做多少题也不算多

  • 最大独立集

  • 最小点覆盖

  • 最小支配(点或邻点被选,谓之“支配”)(三种状态)

  • 最大匹配

  • 换根dp

  • 基环树dp

  • 树形背包

换根dp

首先\(O(n)\) 的时间内搞出以1为根的信息;再尝试把根换为相邻点,需要 \(O(1)\) (或 \(O(logn)\))的时间内换完,然后更新答案,递归;回溯时还原信息。

树形背包

树上转移式子为 \(f[fa][i + j] = f[fa][i] + f[son][j]\) 的dp题。通常可以通过限制dp上界,保证复杂度不超过 \(O(n^2)\)

这部分细节比较多,对分类讨论能力要求较高。一定要细心啊!!

P3354 [IOI2005]Riv 河流

非常考察费用提前计算思想(没想到就无法dp)。

\(f[i][j][k]\) 表示 \(i\) 的伐木场建在 \(j\),且 \(i\) 及其子树内共建立了 \(k\) 个伐木场的最小费用。

子树向父亲转移类似两背包合并。

基本转移方程:

\[MIN(f[cur][anc][k],f[to][anc][k']+f[cur][anc][k-k'])
\]

\[f[cur][anc][k] += (dep[]-dep[]) * w[]
\]

小技巧1:分类与合并

我们发现 \(i\) 点建与不建伐木场是有一些区别的。因为如果单纯用 \(f[i][i][ * ]\) 来表示在 \(i\) 点建立伐木场的话,那么 \(i\) 的祖先将无法统计上 \(i\) 点。

那么我们需要一个 \(f[i][anc][ * ]\) 来表示 \(i\) 已经建了伐木场了,但是它还是想要在 \(anc\) 这一祖先上建伐木场。这样 \(anc\) 才能识别并拾取 \(i\) 点信息。

因此,我们需要新开一个状态 \(g[cur][anc][ * ]\) 表示 \(cur\) 点建了伐木场且希望在 \(anc\) 处再建一个伐木场。对 \(g\) 差别对待,最后再把它合并到 \(f\) 里头。

小技巧2:无脑赋值初始化

不知道怎么初始化怎么办?直接拿一绝对合法的转移做初始化即可。

小技巧3:费用最后一块算

如果不愿意在转移里面写那么多 \(+w[cur]\) 之类的东西还怕出错算重的话,可以尝试在最后同一加上 \(w[cur]\)。

P3267 [JLOI2016/SHOI2016]侦察守卫

消防局的设立的超级加强版。

\(f[cur][d]\) 表示从 \(cur\) 开始(含)向下 \(d\) 层需要被覆盖(有可能并不需要,或参差不齐,但是保证d层往下绝对不需要)的状态,的最小代价。

\(g[cur][d]\) 表示 \(cur\) 可以向上(不含)覆盖 \(d\) 层的状态(当然也包含可以向上盖d + XX层的情况),的最小代价。

转移方程:

将新子树的信息逐个加入至原信息中

一、g的转移。

  1. 初始化(一进dfs,未加入子树时就进行):\(g[cur][d(<= ~ D)] = w[cur]\);\((f[cur][0] =) g[cur][0] = w[cur](if ~ cur ~ is ~ important) ~ OR ~ 0(else)\)

  2. 子盖外:\(g[cur][d] <- f[cur][d + 1] + g[to][d + 1]\)

  3. 外(它子)盖子:\(g[cur][d] <- g[cur][d] + f[to][d]\)

  4. 维护“可以”的性质(后缀和):\(g[cur][d] <- g[cur][d + 1]\)

二、f的转移。

  1. 初始化(未加入子树时进行):\(f[cur][d] = 0\);\(f[cur][0] = w[cur]~ (if ~ cur ~ is ~ important)\)

  2. 需要子树全部被盖:\(f[cur][0] = g[cur][0]\)

  3. 父子同时等待被盖:\(f[cur][d] += f[to][d-1]\)

  4. 维护“d层内随意”性质(前缀和):\(f[cur][d] <- f[cur][d - 1]\)

想清楚所有情况后,转移顺序之类的小细节就有了依据。因此要码这种恶心的DP题,最好先想好所有的状态及转移。

T135128 树

给定 \(n\) (<=1e4) 个点的点带权树,要求选择一些点,使得其两两之间距离大于 \(K\) (<=1e2),最大化点权和。

收到上一道题的毒害,我这道题还是想\(f\),\(g\),然后写了五六个式子,最后连定义都弄不清了,发现 \(f\) 和 \(g\) 有重叠的地方,也就是说,我可以根据 \(f\) 来推导出 \(g\)。到这里我基本可以确定我又一次掉坑里了。

还是不要思维固化啊

实际上这道题还是听简单的,就只用设计一个状态就好了。毕竟不是覆盖问题。

设 \(f[cur][j]\) 表示处理好了 \(cur\) 及其子树(目前的),并且子树内(含cur)距离 \(cur\) 最近的那个点的距离不小于 \(j\) 的...

然后转移方程什么的就很显然了:

\[f[cur][0] = val[cur]
\]

\[f[cur][min(j, k + 1)] <- f[cur][j] + f[to][k],(j+k+1>K)
\]

\[f[cur][j] <- f[cur][j + 1]
\]

P6223 [COCI 2009] PODJELA

树形背包。

考查状态的灵活改变。使用 \(f[cur][i]\) 表示处理完 \(cur\) 节点的子树,使用 \(i\) 次交换机会的最大 val[cur] 值(贪心)

这里有一个有关dp顺序的小技巧:如果实在不知道怎么安排dp顺序,可以新开一个数组 \(g\),存储 \(f\) 的值,然后再清空 \(f\),再用 \(g\) 去更新 \(f\),就能保证 \(g\) 全部是干净的。

P3177 [HAOI2015]树上染色

树形背包。

考查状态的灵活改变。使用 \(f[cur][i]\) 表示处理完 \(cur\) 节点的子树,使用了 \(i\) 个黑点,子树中的边对答案的贡献的最大值。

注意到,如果存的是子树中的答案的最大值的话,没有“最优子结构”的性质(局部最优 \(\not=\) 全局最优).但是如果考虑边的贡献的话,搞完子树里面的边后,只要确定子树中有 \(i\) 个黑点,就和外面的边的贡献没关系了。

剩余的和上一道题类似。

其实这种把“两两之间的距离之和”转化为一条条边的贡献还是很常见的一种套路。

P4037 [JSOI2008]魔兽地图

状态不是很好想: \(f[cur][j][c]\) 表示 \(cur\) 子树中,有恰好 \(j\) 个 \(cur\) 用来上贡,使用恰好 \(c\) 个金币所能获得的最大剩余价值。

然后先算出 \(j\) 个 \(cur\),恰好花 \(c\) 个金币的最大剩余价值,这是个树形背包:

\[g[cur][j][c+c'] <- f[cur][j][c] + f[to][j * need[to]][c']
\]

然后再把 \(g\) 处理成 \(f\):

\[f[cur][j][c] <- f[cur][j'][c] + (j'-j) * val[cur]
\]

叶子的 \(f\) 可以直接特判掉:

\[f[cur][j][j * w[cur]] <- (j'-j) * val[cur]
\]

看似复杂度达到 \(1e10\),但是是可以跑过的。

然后细节较多,各种边界,dp顺序之类的东西要格外注意。

(然而最终竟然挂在了数组大小上...)

P4201 [NOI2008]设计路线

题目要求树上选择一些链,并且要求叶子节点到根节点的“轻边”的数量的最大值最小,还要求方案数。

如果只求最小值,那么这题可以开到 1e6,我们直接贪心DP取最小值即可。但是由于要求方案数,一些“局部不优”的方案可能会被一些不得不做的“更劣解”所“掩盖”掉,使其合法化,因此记录子树最大轻边数并做一些看似不必要的转移显得十分必要。

幸运的是,根据树链剖分的知识可知,最大轻边数是 \(log\) 级别的。因此直接计入状态DP即可。

我们可以设 \(f[cur][k][0/1/2]\) 表示 \(cur\) 子树里轻边数都小于等于 \(k\) 的方案数。

为了方便DP,我们可以设 \(g[cur][k][0/1]\) 表示 \(cur\) 的父边为轻/重边,对父亲关于 \(k\) 的转移的贡献

然后分类讨论:

\[g[cur][k][0]<-f[cur][k-1][0/1/2]
\]

\[g[cur][k][1]<-f[cur][k][0/1]
\]

\[f[cur][k][0]<-f[cur][k][0]* g[to][k][0]
\]

\[f[cur][k][1]<-f[cur][k][0]* g[to][k][1]+f[cur][k][1]* g[to][k][0]
\]

\[f[cur][k][2]<-f[cur][k][1]* g[to][k][1]+f[cur][k][2]* g[to][k][0]
\]

然后随便DP即可。


区间DP

通常是给定一个区间以及某些特征后,该区间内的最优价值/代价就可以通过两个子区间来确定,那么可以考虑通过区间DP来做。

常见的转移方程形式主要为:单点扩展;枚举划分点。

CF1312E Array Shrinking

板子题,不多说。状态:\(f[l][r]\) 表示合并 \([l, r]\) 后的那个数是什么。

P3205 [HNOI2010]合唱队

倒过来模拟,发现原前缀体现为一段区间。然后记录 \(f[l][r][0/1]\) 表示 \([l,r]\) (最后加在左/右边)的方案数。

注意,如果 \(f[i][i][0] = f[i][i][1] = 1\) 的话会算重。可以去掉一个,或者直接将长度为二的状态作为边界。

P1864 [NOI2009]二叉查找树

由于key值一定,可以确定中序序列。

发现区间DP类似BST的构建。一个区间类似一个子树。

如果我们设 \(f[l][r][rt]\) 的话,将无法体现 \(rt\) 的权值是什么,因为可能已经被修改了。因此,可以设 \(f[l][r][m]\) 表示区间的根的权值不低于 \(m\)。这样,就可以分两种情况(改 OR 不改)讨论转移。

由于权值可以取实数,避免了很多讨论。


状压DP

有时我们为了完整地表示出状态,不得不用一堆数来表示一个状态。状态压缩是一种常用的方法。

一般状态为01串最方便。如果状态为 \(k\) 进制数的话,最好从0开始数数,并且手写几个函数,来支持一系列操作,这样会方便很多。

旅行商(哈密顿路径)

过于经典的状压例题。

逐行转移 & 逐格转移(轮廓线DP)

例题:互不侵犯,炮兵阵地,玉米田

卡逐行转移的例题:P2435 染色

设上一行的状态为 \(S\),枚举这一行的状态为 \(T\)。判断 \(S\) 能否转移到 \(T\) 即可。复杂度: \(O(nm2^{2m})\)(一般达不到这个上限,因为有一些不合法的状态;并且那个 \(m\) 是位运算复杂度,可以认为是略小于 \(O(1)\) 的)

如果 \(m\) 比较大,比如 15 或者 20,怎么办?

逐格转移(轮廓线DP)!

(似乎感受到了插头DP的气息)

设 \(f[state]\) 为轮廓线上的状态。这样,在转移第 \(i\) 行第 \(j\) 列的格子转移的时候,只用判断轮廓线上与那个格子相邻的几个格子是否合法即可。

有的时候需要在一行的开头和结尾做一些特判。

复杂度:\(O(nm2^m)\)

//P2435
for (register int i = 1; i <= n; ++i)
for (register int j = 0; j < m; ++j) {
memcpy(g, f, sizeof(f));
memset(f, 0, sizeof(f));
for (register int s = 0; s < All; ++s) {
if (!g[s]) continue;
int l = j ? Find(s, j - 1) : -1;
int u = Find(s, j);
for (register int t = 0; t < k; ++t)
if (l != t && u != t)
MOD_ADD(f[Add(Del(s, j), j, t)], g[s]);
}
}

【经典问题】骨牌覆盖问题:初级 高级 终极

即:给定 \(n\) 行 \(m\) 列的棋盘,要求使用 1 × 2 的骨牌恰好完全覆盖整个棋盘。求方案数。

感觉hihoCoder上面讲得非常棒

Part1 : n = 2, m <= 1e9

状态 \(f[n]\) 表示到 \(n\) 列且恰好铺满的方案数。

发现只有最后横铺两个或者竖着铺一个这两种转移。即:\(f[n] = f[n - 1] + f[n - 2]\)。矩阵加速求斐波那契数列的第 \(m\) 项即可。

Part2 : n <= 7, m <= 1e9

考虑“按行转移”,关键在查询哪些状态的转移是合法的。不难发现,当前行确定后,上一行的可行状态唯一。

如果当前行横铺一个,那么要求上一行对应的那两列为1.

如果当前行竖着来一个,那么要求上一行对应列为0.

如果当前行空着一个格子,那么要求上一行对应列为1.

DFS判定即可。

得到矩阵后,以此作为转移矩阵,随便矩乘几下即可。

复杂度:\(O((2^n)^3logm)\)

Part3 : n,m <= 20

数组完全存不下。但是发现每一行对应的只有一种状态,直接存那种状态即可。

复杂度: \(O(2^nm)\)

Part4 : 不要求恰好完全覆盖,n,m <= 16

可以考虑按格转移。维护“一行”轮廓线。然后分三种情况转移即可。

Part5 : 不要求恰好完全覆盖,n <= 8, m <= 1e9

这时一种状态就可能会转移出多种状态了。需要 \(O(2^{2n})\) 枚举,\(O(n)\) 逐一判断。

据说可以“去掉⽆⽤的状态,即可通过”,然而并不知道哪些状态“无用”。

所以我这种方法只能通过 n <= 7 的点, n = 8 的时候计算量达到了 5e8。不过矩乘常数不大,或许可过? 可能需要更宽的时限。

P3959 宝藏

由于 \(n\) 非常小,因此可以考虑状压。

又因为代价由两个参数决定,如果控制住了其中一个,另一个就可以贪心解决了。

发现 \(L\) 没法控制,只好控制 \(K\)。即:一层一层地DP。

\[f[i][S] <= f[i - 1][s] + i * trans[s][S ~ xor ~ s]
\]

需要枚举 \(S\) 和其子集 \(s\)。

一个常识是:枚举一个集合 \(S\) 的所有子集的所有子集的复杂度是 \(O(3^{|S|})\)。这个可以用每一个数在各集合中的三种不同状态来证明。同理可证,枚举一个集合 \(S\) 的所有子集的所有子集的所有子集的复杂度是 \(O(4^{|S|})\).

然后预处理 \(trans[s][s']\) 后按照层数 DP 即可。预处理 \(trans[][]\) 可以贪心。思想与斯坦纳树类似。

复杂度: \(O(n^23^n)\)

P4297 [NOI2006]网络收费

DP综合题:需要用到树形DP,背包,状压DP,以及“做承诺”(费用提前计算)思想。

设\(f[cur][k][s]\) 表示DP到了\(cur\),子树中共有 \(k\) 个 \(B\) 型点,且从 \(fa[cur]\) 到根的点的状态为 \(s\) 的最优代价。

叶子的所有状态可以直接处理出来。这里包含了所有代价,剩下我们要做的只是把子树拼一拼,看看合不合法。

根据 \(A\) 型点的子树 \(A\) 严格少于 \(B\) 型点,我们控制枚举的 \(k\) 的范围。对于“拼子树”,做树形背包即可。

然后发现需要 \(O(2^{3n})\) 的空间复杂度。考虑到深度变浅一点, \(|s|\) 会变小一点,\(k\) 会增大一点。这样,我们直接把后两维压成一维即可做到空间 \(O(2^{2n})\)。

细节非常多。我谨慎小心地写代码,还出了六个错。还是太菜了。

DP学习记录Ⅰ的更多相关文章

  1. DP学习记录Ⅱ

    DP学习记录Ⅰ 以下为 DP 的优化. 人脑优化DP P5664 Emiya 家今天的饭 正难则反.考虑计算不合法方案.一个方案不合法一定存在一个主食,使得该主食在多于一半的方法中出现. 枚举这个&q ...

  2. 概率dp学习记录

    论文参考 汤可因<浅谈一类数学期望问题的解决方法> 反正是很神奇的东西吧..我脑子不好不是很能想得到. bzoj 1415 1415: [Noi2005]聪聪和可可 Time Limit: ...

  3. 数位DP学习笔记

    数位DP学习笔记 什么是数位DP? 数位DP比较经典的题目是在数字Li和Ri之间求有多少个满足X性质的数,显然对于所有的题目都可以这样得到一些暴力的分数 我们称之为朴素算法: for(int i=l_ ...

  4. 斜率优化DP学习笔记

    先摆上学习的文章: orzzz:斜率优化dp学习 Accept:斜率优化DP 感谢dalao们的讲解,还是十分清晰的 斜率优化$DP$的本质是,通过转移的一些性质,避免枚举地得到最优转移 经典题:HD ...

  5. 插头$DP$学习小结

    插头\(DP\)学习小结 这种辣鸡毒瘤东西也能叫算法... 很优秀的一个算法. 最基本的适用范围主要是数据范围极小的网格图路径计数问题. 如果是像\(Noi2018\)那种的话建议考生在其他两道题难度 ...

  6. Quartz 学习记录1

    原因 公司有一些批量定时任务可能需要在夜间执行,用的是quartz和spring batch两个框架.quartz是个定时任务框架,spring batch是个批处理框架. 虽然我自己的小玩意儿平时不 ...

  7. Java 静态内部类与非静态内部类 学习记录.

    目的 为什么会有这篇文章呢,是因为我在学习各种框架的时候发现很多框架都用到了这些内部类的小技巧,虽然我平时写代码的时候基本不用,但是看别人代码的话至少要了解基本知识吧,另外到底内部类应该应用在哪些场合 ...

  8. Apache Shiro 学习记录4

    今天看了教程的第三章...是关于授权的......和以前一样.....自己也研究了下....我觉得看那篇教程怎么说呢.....总体上是为数不多的精品教程了吧....但是有些地方确实是讲的太少了.... ...

  9. UWP学习记录12-应用到应用的通信

    UWP学习记录12-应用到应用的通信 1.应用间通信 “共享”合约是用户可以在应用之间快速交换数据的一种方式. 例如,用户可能希望使用社交网络应用与其好友共享网页,或者将链接保存在笔记应用中以供日后参 ...

随机推荐

  1. RabbitMQ:一、入门

    消息中间件 使用消息中间件的作用 解耦 削峰 异步 顺序保证 冗余(存储) RabbitMQ的特点 可靠性 灵活的路由 扩展性 高可用 多语言客户端 插件机制 多协议(主要还是AMQP) 相关概念 P ...

  2. 暑假集训Day 4 P4163 [SCOI2007]排列 (状压dp)

    状压dp (看到s的长度不超过10就很容易想到是状压dp了 但是这个题的状态转移方程比较特殊) 题目大意 给一个数字串 s 和正整数 d, 统计 s 有多少种不同的排列能被 d 整除(可以有前导 0) ...

  3. Oracle数据库中,误删除或者修改数据恢复方法

    在我们实际工作中,误删除或者修改Oracle数据库中的数据,怎么办呢?这里给大家分享一种解决办法.假如你误操作的时间不超过30分钟(数据库默认的回滚保持段里的数据时间,可以在pl/sql执行窗口按ct ...

  4. 调用微信内置的方法及wx.config的配置问题

    首先请看网址: https://www.w3cschool.cn/weixinkaifawendang/h8ap1qe5.html 重点说下怎么配置wx.config(为了安全,所有的参数都在服务端获 ...

  5. 超简单集成ML kit 实现听写单词播报

    背景   相信我们大家在刚开始学习一门语言的时候都有过听写,现在的小学生学语文的时候一项重要的课后作业就是听写课文中的生词,很多家长们都有这方面的经历.不过一方面这种读单词的动作相对简单,另一方面家长 ...

  6. 一个ACE 架构的 C++ Timer

    .h #ifndef _Timer_Task_ #define _Timer_Task_ #pragma once #include <ace/Task.h> #include <a ...

  7. Java多线程可重入锁例子解析

    “可重入锁”的概念是:自己可以再次获得自己的内部锁.比如有一条线程获得了某个对象的锁,此时这个对象还没有释放,当其再次想获得这个对象的锁的时候还是可以获得的,如果不可锁重入的话,就会造成死锁. cla ...

  8. Rancher1.6 部署prometheus

    一.rancher基础配置 镜像:prom/prometheus:latest 映射端口:9090:9090 服务连接: blackbox-exporter cadvisor node-exporte ...

  9. Python-自动用0补取长度

    描述 Python zfill() 方法返回指定长度的字符串,原字符串右对齐,前面填充0. 语法 zfill()方法语法: str.zfill(width) 参数 width -- 指定字符串的长度. ...

  10. [开源][示例更新]eCharts配置简化包OptionCreator[typescript版]

    前言 eCharts作为国内优秀的开源图表工具,功能强大,但是使用中也存在一定的问题. 文档更新较慢,文档说明不详细. 前端使用的弱类型语言,数据结构在灵活的同时,也容易造成一些问题.例如某些属性到底 ...