P4556 [Vani有约会]雨天的尾巴(树上的差分+线段树的启发式合并)

题目描述

深绘里一直很讨厌雨天。
灼热的天气穿透了前半个夏天,后来一场大雨和随之而来的洪水,浇灭了一切。
虽然深绘里家乡的小村落对洪水有着顽固的抵抗力,但也倒了几座老房子,几棵老树被连根拔起,以及田地里的粮食被弄得一片狼藉。
无奈的深绘里和村民们只好等待救济粮来维生。
不过救济粮的发放方式很特别。
首先村落里的一共有 n 座房屋,并形成一个树状结构。然后救济粮分 m 次发放,每次选择两个房屋 (x, y),然后对于 x 到 y 的路径上(含 x 和 y)每座房子里发放一袋 z 类型的救济粮。
然后深绘里想知道,当所有的救济粮发放完毕后,每座房子里存放的最多的是哪种救济粮。

输入格式

输入的第一行是两个用空格隔开的正整数,分别代表房屋的个数 n 和救济粮发放的次数 m。
第 2 到 第 n 行,每行有两个用空格隔开的整数 a, b,代表存在一条连接房屋 a 和 b 的边。
第 (n+1) 到第 (n+m) 行,每行有三个用空格隔开的整数 x, y, z,代表一次救济粮的发放是从 x 到 y 路径上的每栋房子发放了一袋 z 类型的救济粮。

输出格式

输出 n 行,每行一个整数,第 i 行的整数代表 i 号房屋存放最多的救济粮的种类,如果有多种救济粮都是存放最多的,输出种类编号最小的一种。
如果某座房屋没有救济粮,则输出 0。

输入输出

输入
5 3
1 2
3 1
3 4
5 3
2 3 3
1 5 2
3 3 3
输出
2
3
3
0
2

说明/提示

对于 20% 的数据,保证 n,m≤100。
对于 50% 的数据,保证 n,m≤2×10^3 。
对于 100% 测试数据,保证 1≤n,m≤105,1≤a,b,x,y≤n,1≤z≤105

题目分析

这道题我们可以对于每一个节点都开一棵权值线段树,而每一棵权值线段树来存该节点上每种粮食的个数(每一种粮食对于一个位置,该位置上的数对应该种粮食的个数),而权值线段树内只需要维护此段上的最大值即可。

但这样我会发现一个问题:每次暴力给一条路径上所有点添加一个数太费时间了,会TLE。那么有什么方法能够快速给一条路径上所有点添加一个数呢:树上的差分用树上的差分给u到v的路径上所有节点添加一个数的方法是:给u节点的权值线段树对应位置上+1,给v节点的权值线段树对应位置上+1,给p(u和v的最近公共祖先)节点的权值线段树对应位置上-1,给p的父节点的权值线段树对应位置上-1。全部操作完成后再通过线段树的启发式合并来进行还原)。

扫描二维码关注公众号,回复: 12447394 查看本文章

线段树的启发式合并我还不太会讲……看下面的代码把。
补:给树上的每个节点都开一棵权值线段树,如果将所有节点都建出来的话,将会用掉大量的空间(会MLE),而且非常多的节点也确实不会被用到,因此我们可以用动态开点线段树来写,这样可以节省很多不必要的空间浪费

代码如下
#include <iostream>
#include <cstdio>
#include <cmath>
#include <string>
#include <cstring>
#include <map>
#include <queue>
#include <vector>
#include <set>
#include <algorithm>
#define LL long long
#define PII pair<int,int>
#define x first
#define y second
using namespace std;
const int N=1e5+5,R=1e5,INF=0x3f3f3f3f;
struct Node{
    
    			//权值动态开点线段树
	int l,r;			//记录左右儿子的节点信息
	int max=0;			//记录此段中的最大值
}tr[N*94];
int root[N*2],cnt;		//给每个树上的节点都开一棵线段树
int h[N],e[N*2],ne[N*2],idx;	//邻接表存图
int fa[N][19],depth[N];			//求lca需要的数组
int q[N];						//bfs所需的队列
void add(int a,int b)			//加边函数
{
    
    
	e[idx]=b;
	ne[idx]=h[a];
	h[a]=idx++;
}
void bfs()						//bfs预处理出fa[][]和depth[]数组的信息
{
    
    
	memset(depth,0x3f,sizeof depth);
	int head=0,tail=0;
	depth[0]=0,depth[1]=1;
	q[0]=1;
	while(tail>=head)
	{
    
    
		int u=q[head++];
		for(int i=h[u];~i;i=ne[i])
		{
    
    
			int v=e[i];
			if(depth[v]>depth[u]+1)
			{
    
    
				depth[v]=depth[u]+1;
				q[++tail]=v;
				fa[v][0]=u;
				for(int k=1;k<19;k++)
					fa[v][k]=fa[fa[v][k-1]][k-1];
			}
		}
	}
}
int lca(int a,int b)			//求a和b两节点的最近公共祖先
{
    
    
	if(depth[a]<depth[b]) swap(a,b);
	for(int k=18;k>=0;k--)		//让a和b跳到同一层上
		if(depth[fa[a][k]]>=depth[b]) a=fa[a][k];
		
	if(a==b) return a;
	for(int k=18;k>=0;k--)		//a和b同时向上跳,跳到lca节点的下面一层上
		if(fa[a][k]!=fa[b][k])
		{
    
    
			a=fa[a][k];
			b=fa[b][k];
		}
	return fa[a][0];
}
void pushup(int u)
{
    
    
	tr[u].max=max(tr[tr[u].l].max,tr[tr[u].r].max);
}
void update(int &u,int l,int r,int z,int c)		//在线段树z位置上加c
{
    
    
	if(!u) u=++cnt;
	if(l==r) tr[u].max+=c;
	else {
    
    
		int mid=l+r>>1;
		if(mid>=z) update(tr[u].l,l,mid,z,c);
		else update(tr[u].r,mid+1,r,z,c);
		pushup(u);
	}
}
int query(int &u,int l,int r)			//查询该线段树上的最大值的位置
{
    
    
	if(!u) return 0;
	if(l==r)						//找到最大值所在的位置
	{
    
    
		if(tr[u].max) return l;		//如果最大值存在,则返回该位置
		return 0;					//最大值不存在返回0
	}
	int mid=l+r>>1;
	//因为如果存在多个最大值,取编号最小的粮食,因此这里是>=号
	if(tr[tr[u].l].max>=tr[tr[u].r].max) return query(tr[u].l,l,mid);
	return query(tr[u].r,mid+1,r);
}
void merge(int &u,int x,int y,int l,int r)		//线段树的启发式合并(将x和y合并到u(x)上)
{
    
    
	if(!x||!y) {
    
     u=x+y; return; }			//当x或y为空时,u直接等于另一个	
	u=++cnt;								//建立一个新节点
	if(l==r) tr[u].max=tr[x].max+tr[y].max;	//将x和y进行合并
	else {
    
    
		int mid=l+r>>1;
		tr[u].l=tr[x].l;							//让u的左右儿子回归x
		tr[u].r=tr[x].r;
		merge(tr[u].l,tr[x].l,tr[y].l,l,mid);		//递归合并左右儿子
		merge(tr[u].r,tr[x].r,tr[y].r,mid+1,r);
		pushup(u);
	}
}
void dfs(int u)
{
    
    
	for(int i=h[u];~i;i=ne[i])
	{
    
    
		int v=e[i];
		if(v==fa[u][0]) continue;
		dfs(v);					//向下递归
		merge(root[u],root[u],root[v],1,R);		//线段树u和v合并到u上
	}
}
int main()
{
    
    
	memset(h,-1,sizeof h);
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1;i<n;i++)			//建图
	{
    
    
		int u,v;
		scanf("%d%d",&u,&v);
		add(u,v),add(v,u);
	}
	bfs();							//预处理求lca的相关信息
	while(m--)
	{
    
    
		int u,v,z;
		scanf("%d%d%d",&u,&v,&z);
		int p=lca(u,v);				//求u和v的lca
		update(root[u],1,R,z,1),update(root[v],1,R,z,1);	//树上的差分操作
		update(root[p],1,R,z,-1);
		if(fa[p][0]) update(root[fa[p][0]],1,R,z,-1);
	}
	dfs(1);				//将树进行前缀和合并
	for(int i=1;i<=n;i++) printf("%d\n",query(root[i],1,R));	//查询每个节点的答案
	return 0;
}

猜你喜欢

转载自blog.csdn.net/li_wen_zhuo/article/details/113063877