引入概念
支配集:
给定无向图G = <V,E>
其中V是大小为n的点集,E是边集。S是无向图G = <V,E>
的支配集当且仅当S为V的子集,且对于V - S
,任意v ∈ V
,都有u ∈ S
,使得<u,v> ∈ E
。
最小支配集:
S’为最小支配集当且仅当S’为支配集,且任意除去点u,u ∈ S'
,S’都不再是支配集。
树型DP
思路:
由题目意思我们可以观察出,一个点被影响只有三种可能:
1.自己被自己影响。
2.自己被子节点影响。
3.自己被父节点影响。
由此可以设计出DP方程f[n][3]
f[i][0]
表示i点为支配集中一点,且以i为根的子树都被覆盖了的最小支配集合大小。f[i][1]
表示i点不为支配集中一点,但是以i为根至少有一个子节点在支配集合中,且以i为根的子树都被覆盖了的最小支配集合大小。f[i][2]
表示i点不为支配集中一点,i为根的子树都被覆盖,但是i不被覆盖的最小支配集合大小。i被父节点覆盖。
动态方程推导:
设u为v的父节点
f[u][0]
的意思是u点位支配集中元素。我的理解是,u这个点可以自己影响自己,那么它就可以由下面的三个式子转移而得到。
f
[
u
]
[
0
]
+
=
m
i
n
(
f
[
v
]
[
0
]
,
f
[
v
]
[
1
]
,
f
[
v
]
[
2
]
)
f[u][0] += min(f[v][0],f[v][1],f[v][2])
f[u][0]+=min(f[v][0],f[v][1],f[v][2])
f[u][2]
的意思是u不为支配集合中的元素,而且它只受到父亲节点的影响。那么它由以下两个式子得到。
f
[
u
]
[
2
]
+
=
m
i
n
(
f
[
v
]
[
0
]
,
f
[
v
]
[
1
]
)
f[u][2]+=min(f[v][0],f[v][1])
f[u][2]+=min(f[v][0],f[v][1])
值得注意的是:这个无法由f[v][2]
转移而来。因为u为v的父节点,而f[v][2]
的含义是v被父节点覆盖,但是u并不是支配集合中的一个·元素,所以u无法覆盖它的子节点v。
f[u][1]
的意思是它会受到它的子节点的影响。如果我们简单推理一下会发现一个大问题。f[u][1]+=min(f[v][0],f[v][1])
它的式子和f[u][2]
的推导式子是一样的。但是我们仔细思考以下,如果十分不幸地f[u][1]
全由f[v][1]
得到,那么就有问题了,递推后发现所有的受到影响的节点都由子节点影响,那么哪个是子节点?是不是发现n个点都受到字节点影响结果没有可以影响父节点的子节点了!
所以,得出结论我们至少要在n个节点里选取一个为f[v][0]
,v在支配集合中,不然支配集合就没有元素了!
那么如何选取这个元素呢?我们可以用一个bool flag = 0
和一个int cnt = min(cnt,f[v][0]-f[v][1])
。flag用来记录n个点里面是否存在一个点有f[v][0]<=f[v][1]
有的话就为1,没有为0。当没有时我们就需要计算找到一个f[v][0]
替换f[v][1]
,同时使f[u][1]
变化最小的点。这个时候就需要cnt
出手了。
例题: 看这里
源代码:
#include<iostream>
#include<stdio.h>
#include<cstring>
#include<string>
#include<iostream>
#include<vector>
#include<cmath>
using namespace std;
const int N = 1e5 + 10;
int vis[N] = {};
vector<int>g[N];//这里其实是个无向的邻接表
int f[N][3]{};
void dp(int u)//其实和一个dfs没太大区别
{
f[u][0] = 1;
f[u][1] = 0;
f[u][2] = 0;
int flag = 0;
vis[u] = 1;
int cnt = 1e9 + 10;
for (int i = 0; i < g[u].size(); i++)
{
int v = g[u][i];
if (vis[v] == 0)
{
dp(v);
f[u][0] += min(min(f[v][0], f[v][1]), f[v][2]);
f[u][2] += min(f[v][0], f[v][1]);
if (f[v][1] >= f[v][0])
{
flag = 1;
f[u][1] += f[v][0];
}
else
{
cnt = min(cnt, f[v][0] - f[v][1]);
f[u][1] += f[v][1];
}
}
}
if (flag == 0)
f[u][1] += cnt;
return;
}
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n - 1; i++)
{
int x, y;
cin >> x >> y;
g[x].push_back(y);
g[y].push_back(x);
}
dp(1);
cout << min(f[1][0], f[1][1]) << '\n';
return 0;
}
变式题:2023杭电多校1
源代码:
#include<iostream>
#include<stdio.h>
#include<cstring>
#include<string>
#include<iostream>
#include<vector>
#include<cmath>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
int vis[N] = {};
vector<int>g[N];
ll f[N][3]{};
ll a[N]{};
void dp(int u)
{
f[u][0] = a[u];
f[u][1] = 0;
f[u][2] = 0;
bool flag = 0;
vis[u] = 1;
ll cnt = 2e9;
for (int i = 0; i < g[u].size(); i++)
{
int v = g[u][i];
if (vis[v] == 0)
{
dp(v);
f[u][0] += min(min(f[v][0], f[v][1]), f[v][2]);
f[u][2] += min(f[v][1], f[v][0]);
if (f[v][0] <= f[v][1])
{
flag = 1;
f[u][1] += f[v][0];
}
else
{
cnt = min(cnt, f[v][0] - f[v][1]);
f[u][1] += f[v][1];
}
}
}
if (flag == 0) f[u][1] += cnt;
return;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int t;
cin >> t;
while (t--)
{//这里清空内存的方法好笨比
memset(a, 0, sizeof(a));
memset(vis, 0, sizeof(vis));
memset(f, 0, sizeof(f));
for (int i = 1; i < N; i++)
{
g[i].clear();
}
int n;
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n - 1; i++)
{
int x, y;
cin >> x >> y;
g[x].push_back(y);
g[y].push_back(x);
}
dp(1);
cout << min(f[1][0], f[1][1]) << '\n';
}
return 0;
}