树形DP 状压DP

接上一篇DP
DP的本质就是 化零为整, 化整为零 : 把一堆方案按照一定因素划分成为较少类数的方案, 同一类方案有共同特点使得可以一起转移, 一般来说同一类方案我们记录的是最优的那个
状态的转移则是化整为零, 考虑当前状态从何而来, 分类讨论
DP的状态设计是难点, 只要设计出状态并且确保他是对的, 那么转移是平凡的.
所以关键在于如何设计正确的状态呢?

1. 积累

2. 把阶段划分一下, 考虑前半部分的决策究竟对后半部分的影响是什么, 比如背包, 我们只在乎已经选取的体积和价值并不在乎选的顺序, 个数.

  所以我们只需要记录**已经选取的体积和价值** 即可转移, 这样一定是对的, 因为只有这两个因素影响着决策

锻炼状态设计 : 高维DP, 当你发现不能转移时 不妨强迫自己说出究竟差的是什么因素, 思考前半部分的决策影响后半部分的东西到底都有哪些
树形DP就是看子树内部对外部的影响, 还有外部影响内部的情况 总之就是缺什么能转移就加什么
转移分为主动转移(向后, 刷表法)和被动转移(一般的转移方程), 一般主动好写又好想

高维DP

树形DP

树形DP的状态主要考虑子树内的决策对外部的影响

对于root来讲, 他只关心每个儿子的快乐指数, 以及每个子树的根节点选没选(因为选了的话root就不能选了)
所以我们设f[i][0/1]表示i子树内选i(不选i)时的最大快乐指数
f[u][0] = Σmax(f[v][0], f[v][1])
f[u][1] = ∑(f[v][0]) + u的快乐指数

影响: 每颗子树用了几条树枝, 苹果数量是多少
所以可以直接设计状态 f[i][j] 表示i子树用了j条树枝并且最大苹果数量是f[i][j]
x->y要想要转移必须保证x也选了, 所以调整状态为必须选i
f[u][j] = max(f[v][p] + f[u][j - p - 1]) 枚举每个子树用几个树枝来更新, 注意每选一个子树就要耗费一个树枝最终再加上u节点的苹果数量

书上背包例题
影响就是选了几节课, 学分是多少(体积, 价值)
有两种做法
一种比较直观的就是, 这是一个分组背包, (每个子树最多可以选一件物品), 只需要对于子树跑一遍分组背包求出 花费i结课获得的最大学分, 在转移给root即可
还有就是如上一题, 直接枚举这颗子树选多少给root转移, 但是注意为了保证这个子树只选了一件物品, 体积要从大到小枚举. 而第一种不在乎枚举顺序
for(子树) for(体积 大到小) for(决策 选的物品)

优化 : 有一个显然的优化就是枚举体积显然不需要到m, 而是子树大小(root是到当前子树的和), 然而这样就是n^2, 因为吗每两种物品都只会在lca处被枚举到, 而两两配对的方案树就是n^2

每个士兵可以看守与这个点相连的所有边, 要求最少放置士兵数量使得所有边都被看守
子树内对外部的影响: 放置的士兵个数, 看守的边数
由于是最优化问题, 求解的也是看守所有边, 所以看守的边数必须是子树内所有的边
思考还差了一些东西, 就是root这个点需不需要放士兵, 若儿子节点都放了那就不用放, 一旦有一个没放那root必须放
所以考虑加一维状态 f[i][0/1] 表示i子树内所有边都被看好i放了士兵(i没放)的最小代价(放置个数)
f[u][1] = ∑min(f[v][1], f[v][0])
f[u][0] = ∑(f[v][1])

加强版 这次看边变成了看点 (1 0 0 1)
思考有什么变化, 此时若root不放那么子树有一个放了就行, 并不需要都放
其实就是分裂讨论的过程
你考虑若子节点都不放那么root一定要放吗, 其实不一定root可以被父亲看守, 子节点可以被他们的子节点看守所以我们的状态就是
f[i][0/1/2] i子树内都被看守, 且i被父节点看着, i被子节点看着, i自己放了一个骑士
考虑转移
f[u][2] = ∑min(f[v][0/1/2]) + a[u]
f[u][0] = ∑min(f[v][1/2])
f[u][1] 比较特殊, 他需要子树内至少一个是2状态, 其余可以为0 / 2
可以记录∑f[v][0] 再枚举用哪个子树的2
还有一个就是记录最小的0和2的差, 然后如果有一个2比0小那就不管否则加上最小的差
都一样其实

[HDU5290]Bombing plan

再加强一下 这次每个点i可以看住距离他不超过a[i]距离的点了
我们设状态f[i][j] 表示i点向上看住距离不超过j的所有点 的最小代价(最少放置士兵的数目)
j为负数代表 向下
其实就是最低的看住的点就是 i + j, 状态很神奇加上j=-1 那么最低的没看住的点就是i i往下都被看住了
总之就是i + j以下的点都被看好了
转移
选i
那么 f[u][a[i]] 就等于 ∑ f[v][-a[i]] // i将上下距离他a[i]的点都看了, 那他的孩子得看-a[i]以下的点
不选i
j >= 0
f[u][j] = ∑ f[v1][j + 1] + f[v_else][-j]
思考为什么是-j而不是0
image

如图,某一个儿子能到达j + 1的话, 那么他也能到达其他子树的-j
我们记录∑f[v][-j] 然后枚举每一个儿子当那个申上去的即可

j < 0
f[i][j] = ∑f[v][j + 1] 这是显然的

Caves
树上两条路径

状压DP

枚举子集的转移很像区间DP(从小的转移到大的)

技巧
1.判断一个数字x二进制下第i位是不是等于1。(最低第1位)

方法:if(((1<<(i−1))&x)>0) 将1左移i-1位,相当于制造了一个只有第i位 上是1,其他位上都是0的二进制数。然后与x做与运算,如果结果>0, 说明x第i位上是1,反之则是0。

2.将一个数字x二进制下第i位更改成1。

方法:x=x|(1<<(i−1)) 证明方法与1类似。

3.将一个数字x二进制下第i位更改成0。

方法:x=x&~(1<<(i−1))

4.把一个数字二进制下最靠右的第一个1去掉。

5.枚举子集 暴力 \(4^n\) 优化 \(3^n\)
x = state
while(x) x = state & (x - 1)

6.数一个数的二进制表示下1的个数__builtin_popcount() O(1)

状压DP一般分为两大类: 集合式DP 棋盘式DP(基于连通性)

转化: 合法的横着摆放小矩形的方案数
考虑状态设计, 若一列一列考虑, 那么划分一下阶段, 前面放置的东西对当前有何影响呢?
我当前这一列哪些能摆, 取决于上一列伸到当前列的状态
所以设f[i][j]: 摆放好前i列, 第i列伸出到i+1列的状态为j的方案数
假设枚举当前列摆放的状态j 那么上一列是什么状态可以转移给我呢?
假设K可以转移那么 K需要满足1. 我要摆的行K没有摆 (j&k==0) 2.这两列摆放好后竖着的得可以放好也就是 连续空的行数得为偶数

状态设计: 划分一下阶段发现, 影响当前决策的只有两件事情, 1. 已经走过的路 2. 上一步停在哪里了
所以设f[i][j]: 停留在i, 已经走过的集合状态是j的 min花费
转移: f[i][j] = min(f[k][j中除掉i] + cost[k][i]) // 枚举k∈j 枚举顺序0~(1<<n)-1 显然计算j时j的子集都已经算好了

image
状态设计: 影响决策的因素: 已经买了哪些商品
转移就是枚举每个状态集合 再枚举要买哪个(背包)
本题的特点: 如果你想要在i买东西, 必须付一笔到i的钱
更新完每次的状态后, 再f[j] = min(f[j] + d[i], g[j]) 即可, g为上一层状态

状态设计: 划分一下阶段 发现影响因素有 已经安排的牛的集合(状压) f[s] 表示安排集合s的最大收获
已经进行到的比赛就是__builtin_popcount(state), 考虑转移
枚举一个集合中的牛k让它在这场比赛出场, f[state除掉k] 这个状态显然已经被计算过
记得加上奖励, (奖励排下序, 不然可能少拿)

最坏需要交换几次? n - 1
因为如果有解, 那么每一次至少可以满足一个🦌的需求, n-1个被满足了 由于有解所以最后一个也被满足了
我们考虑集合划分,定义一个合法的集合为,一个集合内部可以交换使得这个集合的🦌都被满足
若能分出k个集合,则答案一定可以为n - k. 所以我们要最大化k, 也就是求出最多的划分方案.
f[s]: s这个集合最少的交换次数
这个可以用状压DP实现, 首先考虑一个任意一个集合(大小为S)如果是合法的那么他最多交换S-1次
那么我们先预处理所有状态的最多交换次数(也有可能不合法 -1).
(合法)S=2的集合显然答案就是S-1, 那么我们就可以用来更新元素个数更多的状态(集合)答案
具体地 f[s] = min(f[k] + f[s除去k], f[s]) // k是枚举s的子集 可以用3^n实现

和上一题类似地, 我们考虑答案最差是多少, 显然是n1 + n2 - 2, 先都合并(n1-1)然后再分裂(n2-1)
但是在合并的过程中,有一些东西不需要再合并了(直接和分裂的一样了).
我们考虑枚举所有这种情况, 上面挑x个元素, 下面挑y个, 如果sum(X) == sun(Y)那么显然答案可以减2 (少一次合并少一次分裂)
那么和上一题类似地, 定义集合划分, 任意划分然后如果有上面的某个划分的和等于下面的某个划分的和, 答案减2
显然我们希望划分出的集合数量最多 因为答案 = n1 + n2 - 2 * 划分数量
考虑状压DP, 定义f[s][k]为上面选s状态的元素, 下面选k状态的元素的 最多划分集合数
考虑转移, 枚举x∈n1(并且再s里), y∈n2(并且再y里) f[s][k] = max(f[s除去x][k]) f[s][k] = max(f[s][k除去y])
如果s == k 那么还要f[s][k] ++ (这里可以思考一下)

当你理解了蒙德里安的梦想之后 这就题非常简单了
套路地我们一列一列考虑, f[i][j] 表示放好前i-1列后,第i列的状态为j时的方案数
首先不太一样的是要满足放恰好k个国王, 所以加一维表示当前放了几个国王即可
转移是平凡的, 考虑什么样的k可以转到j, 显然8联通内不可以有两个国王
f[i][j][p + j中1的个数] += f[i-1][k][p]
代码精华

rep(i, 0, (1 << n) - 1) if(((i >> 1) & i) == 0 && ((i << 1) & i) == 0)
    rep(j, 0, (1 << n) - 1)
    {
    	if(i & j) continue;
    	if((i << 1) & j) continue;
    	if((i >> 1) & j) continue;
    	if((j >> 1) & j) continue;
    	if((j << 1) & j) continue;
    	v[i].push_back(j);
	}
	f[0][0][0] = 1;
    rep(i, 1, n)
	rep(j, 0, (1 << n) - 1)   // 这一列放的个数
	{
		int p = __builtin_popcount(j);
		for(auto x : v[j])
		rep(o, __builtin_popcount(x), k - p)
		f[i][j][p + o] += f[i - 1][x][o];
	}
	int ans = 0;
	rep(i, 0, (1 << n) - 1) ans += f[n][i][k];

在上一题基础上 加上了限制, 有些贫瘠有些肥沃 可以暴力枚举然后判断, 也可以直接枚举子集

前面几个题揉起来, 有时间再打一遍

愤怒的小鸟

区间DP

先计算好小的区间,使小区间的答案是正确的, 用小区间合并出大区间 所以长度为1的区间必须有解
石子合并

考虑设计状态f[i, j] 表示将i~j合并的最(小)大代价
初始化f[i, i] = 0
转移 f[i, j] = min(f[i, k] + f[k + 1, j] + cost(i, j))

加分二叉树

非常适合把区间dp写成记忆话搜索
考虑解决l~r的问题
先枚举一个根,算左边最大值,右边最大值,再拼起来

需要二刷

直接设f[i][j] 为将i ~ j消除的最小代价是没办法转移的
我们考虑j与谁消除, 无非就两种情况 和前面消,和后面消(通过添加视作和后面)
我们考虑和后面的x个一起消除,那么就要先把这些和j相等的地方中间都消掉, 这是一个子区间
f[i][j][x] 消除i~j-1并且j后面x个已经存在的一起消除
那么有 f[i][j][x] = f[i][j−1][0] + max(0, k−x−1)
若有y ∈ [i~j-1] 满足 a[y] == a[j] 那么还有将[iy-1]先合并然后再合并[y+1j-1]这样y与j就能碰上了
f[i][j][x] = f[i][y][x+1] + f[y+1][j-1][0]
关于第三维 >=K个不需要额外添加所以答案都和K个的时候一样

首先发现一个性质最的数应该放在距离他最远的地方 因为他的贡献大
所以有个贪心就是每次让最大值去离他最远的地方,但是如果左右一样远那就没办法了。 所以考虑DP
重点在状态设计 延续贪心思想先将A升序排序
f[i][j]表示将1~len(j-i+1)这些数放在i-j位置上的最大收益
考虑转移f[i][j] = max( f[i][j-1] + a[len]放在j的贡献, f[i][j-1] + a[len]放在i的贡献 ) //原因就是考虑贪心, a[len]是当前最大值它放置在i或者j一定是离他最远的地方

考虑一个序列能否一次操作变成k,那么需要序列中所有的数都不等于k
所以设 f[i][j][k][0/1] 表示i~j都变成k(不是k)的最小操作次数
f[i][j][k][0] = min(f[i][j][枚举P(P!=k)][0] + 1) 先变成都不为P, 然后加一步变成都为P
还有区间DP经典转移 = min(f[i][g][k][0] + f[g+1][j][k][0])
类似地有
f[i][j][k][1] = min(f[i][j][k][0] + 1)
还有 = min(f[i][g][k][1] + f[g+1][j][k][1])
注意到f[][][][0]的转移有环但是只有最小的那个可以更新别人,所以把最小值领出来更新别人即可

posted @ 2025-09-02 10:34  闫柏军  阅读(16)  评论(0)    收藏  举报