蓝桥杯练习笔记(十)

蓝桥杯练习笔记(十)

一、医院设置

题目描述

设有一棵二叉树,如图:

其中,圈中的数字表示结点中居民的人口。圈边上数字表示结点编号,现在要求在某个结点上建立一个医院,使所有居民所走的路程之和为最小,同时约定,相邻接点之间的距离为 1 1 1。如上图中,若医院建在 1 1 1 处,则距离和 = 4 + 12 + 2 × 20 + 2 × 40 = 136 =4+12+2\times20+2\times40=136 =4+12+2×20+2×40=136;若医院建在 3 3 3 处,则距离和 = 4 × 2 + 13 + 20 + 40 = 81 =4\times2+13+20+40=81 =4×2+13+20+40=81

输入格式

第一行一个整数 n n n,表示树的结点数。

接下来的 n n n 行每行描述了一个结点的状况,包含三个整数 w , u , v w, u, v w,u,v,其中 w w w 为居民人口数, u u u 为左链接(为 0 0 0 表示无链接), v v v 为右链接(为 0 0 0 表示无链接)。

输出格式

一个整数,表示最小距离和。

样例 #1

样例输入 #1

5						
13 2 3
4 0 0
12 4 5
20 0 0
40 0 0

样例输出 #1

81

提示

数据规模与约定

对于 100 % 100\% 100% 的数据,保证 1 ≤ n ≤ 100 1 \leq n \leq 100 1n100 0 ≤ u , v ≤ n 0 \leq u, v \leq n 0u,vn 1 ≤ w ≤ 1 0 5 1 \leq w \leq 10^5 1w105

#include <cstdio>
#define rep(i, m, n) for(register int i = m; i <= n; ++i)
#define INF 2147483647
#define Open(s) freopen(s".in","r",stdin);freopen(s".out","w",stdout);
#define Close fclose(stdin);fclose(stdout);
using namespace std;
inline int read(){
	int s = 0, w = 1;
	char ch = getchar();
	while(ch < '0' || ch > '9') { if(ch == '-') w = -1; ch = getchar(); }
	while(ch >= '0' && ch <= '9') { s = s * 10 + ch - '0'; ch = getchar(); }
	return s * w;
}
const int MAXN = 10010;
struct Edge{
	int next, to;
}e[MAXN << 1];
int head[MAXN], num, w[MAXN], n, size[MAXN];
long long ans = INF, f[MAXN];
inline void Add(int from, int to){
	e[++num].to = to;
	e[num].next = head[from];
	head[from] = num;
}
void dfs(int u, int fa, int dep){ //预处理f[1]和size
    size[u] = w[u];
	for(int i = head[u]; i; i = e[i].next){
	   if(e[i].to != fa)
	     dfs(e[i].to, u, dep + 1), size[u] += size[e[i].to];
	}
	f[1] += w[u] * dep;
}
void dp(int u, int fa){  //转移
    for(int i = head[u]; i; i = e[i].next)
       if(e[i].to != fa)
         f[e[i].to] = f[u] + size[1] - size[e[i].to] * 2, dp(e[i].to, u);
    ans = min(ans, f[u]); //取最小值
}
int a, b;
int main(){
    //Open("hospital");
    ans *= ans;
    n = read();
    rep(i, 1, n){
       w[i] = read();
       a = read(); b = read();
       if(a) Add(i, a), Add(a, i);
       if(b) Add(i, b), Add(b, i);
    }
    dfs(1, 0, 0);
    dp(1, 0);
    printf("%lld\n", ans);
    //Close;
    return 0;
}

这个题解用了图论里的树上问题里有关树的重心问题的结论。具体解释在上面原作者的文章里,
值得学习的有:
如何找树的重心节点?
怎么构建一颗树的数据结构?
输入函数的模板

二、求LCA

最近在找有关树上问题的时候发现了一个总结了大部分算法的教学网站:oi-wiki
当然有关树的算法肯定比较复杂,包括数据结构,所以在看这部分问题的时候建议从开始看起,不然可能因为看不懂部分数组、数据结构半途而废。

下面是有关更系统的LCA解决办法:
在这里插入图片描述
待会要用到的top数组、son数组都是根据上面链接里的这篇文章的前半部分介绍的“重链”有关理论,对其进行两次遍历得到的,这些前置信息可以用来解决很多有关树的问题。比如下面即将看到的LCA问题
在这里插入图片描述

这里提出了“跳链”方法,其实就是将以前最朴素的将节点往上移动的方法进行了进一步抽象。
并且使用了top、dep、fa数组之后LCA算法看起来就整洁了很多,方便了我们的记忆。

三、树上启发式算法

在这里插入图片描述
在这里插入图片描述

  • 题解原文链接:there,原文如下

更加面向新手的颜色平衡树题解

对于很多新手,往往难以看懂其他题解中的一些变量命名,进而影响对解题思路的理解。这份题解尽可能详细地阐述了变量的作用,希望能对大家有所帮助。

我们使用树上启发式合并的方法。重儿子是指一个节点的儿子中规模最大的儿子(即拥有最多节点的儿子),轻儿子指一个节点的非重儿子。

对于本题,树上启发式合并的流程如下:

​ 对于每一个节点node

  1. 先遍历node的每一个轻儿子,计算轻儿子中颜色平衡树的数量,但不保留轻儿子的信息。
  2. 遍历node的重儿子,计算重儿子中颜色平衡树的数量,且保留重儿子的信息。
  3. 再次遍历node的每一个轻儿子,但这次只需要记录轻儿子的信息。

让轻儿子将信息贡献给重儿子,能优化时间复杂度。

至于如何计算颜色平衡树的数量,我们可以参考@farmerhost的方法:

“如果一棵树是颜色平衡树,那么这棵树中出现次数最多的颜色们的出现次数之和等于这棵树的节点数。”

例如:如果一棵树有4个节点,出现了2次红色,2次绿色,0次黄色,那么出现次数最多的颜色们就是红色和绿色,出现次数为4次,与这棵树的节点数相同,显然是颜色平衡树。

所以我们需要记录的信息即为出现次数最多的颜色们的出现次数,即为代码中的sum_of_most,我们还需要记录颜色c出现的次数(num_of[c])以及出现次数最多的颜色的出现次数(max_color_num)来辅助我们记录sum_of_most。


#include <iostream>
#include <vector>
using namespace std;
#define N 200010

int color_of[N];		//节点n的颜色
int heavy_son_of[N];	//节点n的重儿子
int size_of[N];			//节点n的大小
int num_of[200010];		//颜色c出现的次数
int max_color_num;		//出现次数最多的*颜色*的出现次数
int sum_of_most;		//出现次数最多的*颜色们*的出现次数
int res;				//当前颜色平衡树的数量
vector<int> child_of[N];//节点n的孩子们

void add(int node,bool option){
    /*!
     *@param node 当前节点
     *@param option 开关,首次调用时为TRUE,否则为FALSE
     *@discussion add函数将递归地记录一个节点及其轻儿子(包括轻儿子的重儿子)的信息到num_of[200010]中,并更新max_color_num和sum_of_most的值。
    */
    int this_color=color_of[node];
    num_of[this_color]++;
    if(num_of[this_color] > max_color_num){
        max_color_num=num_of[this_color];
        sum_of_most=num_of[this_color];
    }else if(num_of[this_color] == max_color_num){
        sum_of_most+=num_of[this_color];
    }
    for(int child:child_of[node]){
        ///首次调用时跳过重子树
        if(child==heavy_son_of[node]&&option) { continue; }
        add(child,false);
    }
}

void sub(int node){
    /*!
     *@param node 当前节点
     *@discussion sub函数将递归地从num_of[200010]中删除一个节点及其所有儿子的信息。
    */
    int this_color=color_of[node];
    num_of[this_color]--;
    for(int child:child_of[node]){
        sub(child);
    }
}

void init_dfs(int node){
    /*!
     *@discussion 计算每个子树的大小,记录在size_of[node]中;并且找到每个子树的重子树,记录在heavy_son_of[node]中。
    */
    size_of[node]=1;
    for(int child:child_of[node]){
        init_dfs(child);
        size_of[node]+=size_of[child];
        if(size_of[child]> size_of[heavy_son_of[node]]){
            heavy_son_of[node]=child;
        }
    }
}

void dfs(int node,bool option){
    /*!
     *@param node: 当前节点
     *@param option: 是否保留该次遍历的记录
     *@discussion 进行树上启发式合并。
    */
    for(int child:child_of[node]){
        /// 1:跳过重子树,遍历轻子树,计算出轻子树中的结果,但不保留记录
        if(child==heavy_son_of[node])
            continue;
        dfs(child,false);
    }
    if(heavy_son_of[node]){
        /// 2:如果重子树存在,遍历重子树,计算出重子树中的结果,且保留记录
        dfs(heavy_son_of[node],true);
    }
    /// 3:遍历轻子树,保留记录(将记录贡献到2中保留的记录上)
    add(node,true);
    /// 4:如果(出现次数最多的)颜色(们)的出现次数之和==子树的大小,则符合题意,结果+1
    if(sum_of_most==size_of[node]){ res++; }
    if(!option){
        /// 5:如果不保留该次记录,则对记录进行撤销
        sub(node);
        sum_of_most=0;
        max_color_num=0;
    }
}

int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        int parent;
        cin>>color_of[i]>>parent;
        child_of[parent].emplace_back(i);
    }
    init_dfs(1);
    dfs(1, true);
    cout<<res;
    return 0;
}

这篇题解的前置知识点在这里:树上问题中的树链剖分and树上启发式合并
只有明白了这些前置知识点才能看明白这里的这些数据结构以及函数的作用。

  • 32
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值