题目描述
深绘里一直很讨厌雨天。
灼热的天气穿透了前半个夏天,后来一场大雨和随之而来的洪水,浇灭了一切。
虽然深绘里家乡的小村落对洪水有着顽固的抵抗力,但也倒了几座老房子,几棵老树被连根拔起,以及田地里的粮食被弄得一片狼藉。
无奈的深绘里和村民们只好等待救济粮来维生。
不过救济粮的发放方式很特别。
首先村落里的一共有 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。全部操作完成后再通过线段树的启发式合并来进行还原)。
线段树的启发式合并我还不太会讲……看下面的代码把。
补:给树上的每个节点都开一棵权值线段树,如果将所有节点都建出来的话,将会用掉大量的空间(会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;
}