树形dp之覆盖理论

INTRODUCTION:

这篇博客是针对树形dp中最小支配集的一个通用解法(own perspective),也是本人在做题中总结出的一个规律,如若之前尚未接触过支配集问题,建议先思索下面两个题然后再回来看本篇博客。

题目一:Cell Phone Network G

题目二:消防局的设立

MAIN BODY

我们先对于思维的逻辑理解,之后再套用到实际题目

一个点可以覆盖与它距离为 m 的点,现在给你一棵树(该树有 n 个节点),树间相连两点间的距离均为 1, 问最少可以将这棵树全部覆盖

假设上面这个图是一个n个点的树

这个时候我们分析其中一个点的状态

我们要去表达他的状态,不难发现它每个状态之间是有关联的,我们便可以把这些状态定义为:

  1. f[i][0]表示可以从 i 覆盖到向上 m 层的最小放置点的个数

  2. f[i][1]表示可以从$ i m-1 $层的最小放置点的个数

.

.

.

m+1. f[i][m] 表示可以从 i 覆盖到向上 0 层的最小放置点的个数

m+2. f[i][m+1] 表示可以从 i 覆盖到向上 1 层的最小放置点的个数

m+3. f[i][m+2] 表示可以从 i 覆盖到向上 2 层的最小放置点的个数

.

.

.

2m. f[i][2m+1] 表示可以从 i 覆盖到向上m层的最小放置点的个数

为什么我起名为覆盖理论呢,下面便是我的理论

覆盖到某层则一定覆盖了这棵子树中这一层和这层一下的所有子树

e.g.

“从 i 覆盖到向上1 层”表示我这个 i 节点的父亲节点和我的所有子孙们都被覆盖(包括我自身)

“从 i 覆盖到向上 1 层” 表示我这个 i 节点的儿子节点和我的所有子孙们都被覆盖(不包括我自己,因为1<0

注:为什么没有覆盖到m+1层因为我自身放了最多也只能管到向上m层,不可能再往上。 为什么没有覆盖到 -m-1 层因为这是必须的,如果没有将我向下这么多的子孙覆盖住,就不能够继续向上递归,因为我下面的情况并没有所有考虑完

此时,我们出现了一个重要的不等式,将是我们下面进行减缩代码的关键

f[i][0]f[i][1]f[i][2]f[i][2m+1]

状态转移

先分析上两层:

si 的儿子节点

f[i][0]=1+sf[s][[2m+1]

因为 i 可以向上覆盖 m 层,则自身必须放置节点。此时儿子的状态为f[s][0 2m+1]中的任意一个,然后结合上面的不等式我们就选择其中最小的f[s][2m+1]

si 的儿子,ti 的儿子

f[i][1]=min(min(f[s][1]+stf[i][2m]),f[i][0])

因为 i 可以向上覆盖 m 层所以存在 2 种情况:恰好覆盖 i 向上的m1层, 要么向上覆盖m层其中包括了这一层

如果是恰好覆盖,那么 i 的至少一个儿子要放置节点。其他的节点可以是f[i][0 2m]中的任意一个,取到最小。便是f[i][2m]

然后可以总结出来规律:

向上覆盖到第k(k>0) 那么

st 均为 i 的儿子

f[i][k]=min(min(f[s][k1]+tsf[t][k],f[i][0k1]

向上覆盖0层,其下m层至少有一个节点,其他下m同层的可以是状态f[i][0 m]层中的任意一种, 取最小即f[i][m].

si的儿子节点

f[i][m+1]=min(f[s][m],f[i][0m]

如果恰好可以覆盖i的向上1层,那么就是它的所有儿子都可以被覆盖,于是取f[s][m], 其余情况和上面取最小值。

再看向下k层的一般规律:

si的节点

f[i][k+m+1]=min(f[s][k+m],f[i][0m+k+1])

终极目标

我们希望找到整棵树都被覆盖的时候的最少节点数,那么需要覆盖到我们树的根部,结果就是f[1][m].

如何维护

我们上面说过了m层的状态表示,接下来便是我们对于这m层的维护。

首先我们需要对每一个只能覆盖到自己的子孙的部分初始化为0, 而覆盖到向上m的位置则要初始化为1

之后便是去遍历每一个子节点,并且向下递归整棵树。
然后便是将能够覆盖到自身节点及其以下的状态进行转移

之后我们要分情况进行讨论

  1. 如果我这个当前节点是叶子节点,那么我向上覆盖k(k>0)就只能是自身放置,所以赋值为1

  2. 如果我当前节点不是叶子节点,那么先将向上覆盖k层的状态初始化成 + 然后求它是哪一层叶子节点中的哪一个放置可以得到花费最小。然后进行转移

最后,也是最关键的一步 :便是维护我们上面的不等式。

这个时候我们就可以考虑将这个思维方式套用到我们的题目中了。

实战演练一:P2899

这个题就是一个标准的 m=1的一个最小支配集问题,用我们的覆盖理论来思考,那么便有了下面的几个状态

f[i][0] //表示向上可以覆盖1层 f[i][1] //表示向上可以覆盖0层 f[i][2] //表示向上可以覆盖-1层

注意这个题双向建边,开二倍空间。

再套用我们上面的思路就行,下面是 AC 代码(建议先写后看)

#include<bits/stdc++.h> using namespace std; #define ll long long #define rl register ll const ll N = 1e4+10, M = N * 2, INF = 1e12; ll n, f[N][3];// 0 --- +1 , 1 -- 0, 2 -- -1 ll tot, ne[M], e[M], h[N]; inline void dfs(ll u, ll fa) { f[u][0] = 1; f[u][2] = 0; for(rl i=h[u]; i; i=ne[i]) { ll v = e[i]; if(v == fa) continue; dfs(v, u); f[u][0] += f[v][2]; f[u][2] += f[v][1]; } if(!h[u]) { f[u][1] = 1; } else { f[u][1] = INF; for(rl i=h[u]; i; i=ne[i]) { ll v = e[i]; ll cnt1 = f[v][0]; for(rl j=h[u]; j; j=ne[j]) { if(i == j) continue; ll vv = e[j]; cnt1 += f[vv][1]; } f[u][1] = min(f[u][1], cnt1); } } for(rl i=1;i <= 2; ++ i) f[u][i] = min(f[u][i], f[u][i-1]); } inline void add(ll a, ll b) { ne[++tot] = h[a], h[a] = tot, e[tot] = b; } int main() { std::ios::sync_with_stdio(false); std::cin.tie(nullptr); cin >> n; for(rl i=1; i < n; ++ i) { ll a, b; cin >> a >> b; add(a, b), add(b, a); } dfs(1, -1); cout << f[1][1]; return 0; }

实战演练二:P2279

这个题目中不难看出是一个 m=2的最小支配集。直接按照上面分析的思路走,解决这个题 (这里就不过多赘述了)

#include<bits/stdc++.h> using namespace std; #define ll long long #define rl register ll #define wl putchar('\n') #define ws putchar(' ') template <class T> inline void read(T &res ){ char ch; bool flag = 0; while((ch = getchar()) < '0' || ch > '9'){ if(ch == '-') flag = 1; } res = (ch ^ 48); while((ch = getchar()) <= '9' && ch >= '0'){ res = (res << 1) + (res << 3) + (ch ^ 48); } if(flag) res = ~res + 1; } inline void ww(ll x){ if(x < 0 ) x = ~x + 1, putchar('-'); if(x > 9) ww(x / 10); putchar(x % 10 + '0'); } const ll N = 1010, INF = 1e12; ll n, f[N][5]; ll tot, h[N], e[N], ne[N], w[N]; inline void add(ll u, ll v){ ne[++tot] = h[u], h[u] = tot, e[tot] = v; } inline void dfs(ll u){ f[u][3] = f[u][4] = 0; f[u][0] = 1; for(rl i=h[u];i;i=ne[i]){ ll v = e[i]; dfs(v); f[u][0] += f[v][4]; f[u][3] += f[v][2]; f[u][4] += f[v][3]; } if(!h[u]){ f[u][1] = f[u][2] = 1; }else{ f[u][1] = f[u][2] = INF; for(rl i=h[u];i;i = ne[i]){ ll v = e[i]; ll cnt1 = f[v][0]; ll cnt2 = f[v][1]; for(rl j=h[u]; j; j = ne[j]){ if(i == j) continue; ll vv = e[j]; cnt1 += f[vv][3]; cnt2 += f[vv][2]; } f[u][1] = min(f[u][1], cnt1); f[u][2] = min(f[u][2], cnt2); } } for(rl i=1;i<=4;++i){ f[u][i] = min(f[u][i], f[u][i-1]); } } int main(){ read(n); for(rl i=2;i<=n;++i){ ll v; read(v); add(v, i); } dfs(1); ww(f[1][2]); return 0; }

感谢收看~


__EOF__

本文作者carp
本文链接https://www.cnblogs.com/carp-oier/p/17653001.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   carp_oier  阅读(142)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示