算法学习——树型dp求树的最小支配集

文章目录

引入概念

支配集:

给定无向图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;
}
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值