树形DP 树的直径 树的中心 树的重心

树的直径(树的最长路径)

洛谷 U81904

简单来说 树的直径就是树中最长的一条链

求法如下:

任取一点作为起点 找到距离该点最远的一个点u(DFS)
再找到距离u最远的一点v
u v之间的路径就是一条直径

细节如下: 

对于每一个节点 我们关注挂在该点上的最长链

最长链的值的等于

从该点往下延伸至叶节点

最长路径的值 加 次长路径的值

在DFS中 对于位置较深的点

挂在该点的最长链会被计算

对于一层(高度更高)的点

只需要对(每一个子节点的最长链值+该点到对应子节点权值)求最大值即可

然后返回到更高层

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 5e5 + 10, M = N * 2;//无向边要乘2
int h[N], e[M], w[M], ne[M], idx;
int n, ans;

//任取一点作为起点 找到距离该点最远的一个点u(DFS)
//再找到距离u最远的一点v
//u v之间的路径就是一条直径
//对于有边权的树 直径是 权值和最大的一条链
//对于无边权的树(相当于每条边权重都为1) 直径是 经过点数最多的一条链子

void add(int a, int b, int c)
{
	e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

//记录father确保dfs只会从上层往下层搜索
int dfs(int u, int father)
{
	int dist = 0;//从当前点往下走的最大长度
	int d1 = 0, d2 = 0;//分别表示最长距离和第二长距离
    //初始化为0覆盖了负数的情况 对答案无影响
	//d1+d2即为"挂在当前点上"的最长一条链

	for (int i = h[u]; ~i; i = ne[i])
	{
		//对于每一个子节点继续进行搜索
		//当前点记录为子节点的父亲
		//防止后续的搜索搜回当前点
		int j = e[i];
		if (j == father)continue;
		//w[i]意为当前点到编号为i的子节点的距离
		int d = dfs(j, u) + w[i];
		dist = max(dist, d);

		//次大变最大 最大更新成更大的
		if (d >= d1)d2 = d1, d1 = d;
		else if (d > d2)d2 = d;//或者仅更新次大值
	}
	//直径是所有d1+d2的最大值
	ans = max(d1 + d2, ans);//d1+d2即为"挂在当前点上"的最长一条链

	return dist;
}

int main()
{
	cin >> n;

	//初始化邻接表表头
	memset(h, -1, sizeof h);

	for (int i = 1; i <= n - 1; i++)
	{
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		add(a, b, c), add(b, a, c);
	}

	//不妨取1号开始计算 把1号点"拎起来"成为最高的点
	//没有父节点 记作-1
	//从上往下遍历的时候 要确保只能往下走不能往上走
	//不然会重复搜索死循环(因为建立的是双向边)
	dfs(1, -1);

	cout << ans << endl;

	return 0;
}

树的中心

在树中找到一个点A

使得点A到树中相对点A的最远点距离最小

如果模仿上面 树的直径 的做法 把每一个点拎起来作为树根

求出向下走的最远距离 相当于n轮DFS 很容易TLE

于是产生新的解法如下:

随机选取一个u点 分为两类情况

第一类 从u点向下走的最远距离 用d1[u]表示

第二类 从u点向上走的最远距离 用up[u]表示 

这样 从u点到相对最远点的距离就是 max(d1[u],up[u])

把树上每一个点依次看做u 

最终得到答案 min(max(d1[i],up[i]))

对于第一类 自下而上递推 得到答案 和之前 树的直径 做法一致(DFS)

对于第二类 通过u点实现对j(u的子节点)的up[j]计算

如果j处于down1[u]的路径上

那么对于up[j]的计算只能使用down2[u]

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1e4 + 10;
const int M = N * 2;
const int INF = 1e9;
int h[N], e[M], w[M], ne[M], idx;
//从该点往下走的最长路径与次长路径
int down1[N], down2[N];
//记录最长路径要从哪一个点(倒数第二个点)上来
int p1[N];
//从该点往上走的最长路径
int up[N];
int n;

void add(int a, int b, int c)
{
	e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

int dfs_d(int u, int father)
{
	down1[u] = down2[u] = -INF;
	for (int i = h[u]; ~i; i = ne[i])
	{
		int j = e[i];
		if (j == father)continue;//不允许往上(往回)搜索
		int d = dfs_d(j, u) + w[i];
		if (d > down1[u])
		{
			down2[u] = down1[u], down1[u] = d;
			p1[u] = j;
		}
		else if (d > down2[u])down2[u] = d;
	}
	
	//如果值没有被更新 说明这是叶子节点
	if (down1[u] == -INF)down1[u] = down2[u] = 0;
	return down1[u];
}

void dfs_u(int u, int father)
{
	for (int i = h[u]; ~i; i = ne[i])
	{
		int j = e[i];
		if (j == father)continue;
		//通过u点实现对j(u的子节点)的up[j]计算
		//如果j处于down1[u]的路径上
		//那么对于up[j]的计算只能使用down2[u]
		if (p1[u] == j)up[j] = max(up[u], down2[u]) + w[i];
		else up[j] = max(up[u], down1[u]) + w[i];

		dfs_u(j, u);
	}
}

int main()
{
	cin >> n;
	memset(h, -1, sizeof h);
	for (int i = 0; i < n - 1; i++)
	{
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		add(a, b, c), add(b, a, c);
	}

	dfs_d(1, -1);//第一类 向下走 和 树的直径 求法一样
	dfs_u(1, -1);//第二类 再求往上走的最长路径

	//枚举每个点
	int res = INF;
	for (int i = 1; i <= n; i++)res = min(res, max(down1[i], up[i]));

	cout << res << endl;

	return 0;
}

树的重心

树的重心相对简单

在树中找到一个结点

如果将这个点删除

剩余的 各个连通块的点数的 最大值最小

也有相关定义

对于树上的每一个点 计算其所有子树中最大的子树节点数

这个值最小的点就是这棵树的重心。

这里以及下文中的“子树”都是指无根树的子树

即包括“向上”的那棵子树 并且不包括整棵树自身

思路如下: 

对于每一个点 求其所有子树的最大值(结点个数)

再对每一个点产生的最大值求最小值即为答案

任取树上一点u

对于分析所有子树的结点数 分为两类情况

第一类 是u的子树(在u的下部分)

第二类 是u的上部分(全部)

用DFS遍历树上的每一个点作为u

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1e5 + 10;
const int M = 2 * N;
int h[N], e[M], ne[M], idx;
bool st[N];//经典DFS判重 防止重复搜索
int ans = N;
int n;

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

//返回以u为根的子树大小
int dfs(int u)
{
    //标记当前这个点已经被搜过
    st[u] = true;

    //sum:以u为根的子树结点数
    //size:u的最大子树的结点数
    int sum = 1;//从1开始 u点算在内
    int size = 0;//去掉这个点后各个联通块的最大值

    //遍历u的所有出边
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j])
        {
            //s是以j为根的子树结点数
            int s = dfs(j);
            //size记录u的最大子树结点数
            size = max(size, s);
            //累加u的各个子树结点数
            sum += s;
        }
    }

    //求点u的最大子树
    //size是u的下部分
    //n-sum是u的上部分
    size = max(size, n - sum);

    ans = min(ans, size);

    //返回以u为根的子树结点数
    return sum;
}

int main()
{
    cin >> n;
    memset(h, -1, sizeof h);

    for (int i = 0; i < n; i++)
    {
        int a, b;
        cin >> a >> b;
        add(a, b), add(b, a);
    }

    //DFS遍历了树上的每一个点
    dfs(1);

    cout << ans << endl;

    return 0;
}

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

没伞的男孩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值