P4556 [Vani有约会]雨天的尾巴 /【模板】线段树合并

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\)

输入输出样例

输入 #1

5 3
1 2
3 1
3 4
5 3
2 3 3
1 5 2
3 3 3

输出 #1

2
3
3
0
2

说明/提示

  • 对于 \(20\%\) 的数据,保证 \(n, m \leq 100\)
  • 对于 \(50\%\) 的数据,保证 \(n, m \leq 2 \times 10^3\)
  • 对于 \(100\%\) 测试数据,保证 \(1 \leq n, m \leq 10^5\)\(1 \leq a,b,x,y \leq n\)\(1 \leq z \leq 10^5\)

思路

若用\(a_{i,j}\)表示结点\(i\)上救济粮\(j\)的数量,则结点\(i\)的答案就是\(k\),满足\(a_{i,k}\)最大,\(k\)最小,即最值所在位置.

我们考虑建一个\(a\)的(树上)差分数组\(b\):

\[a_{u,i}=b_{u,i}+\sum_{v\in son(i)}b_{v,i} \]

但是一个\(i\)一个\(i\)求,还要维护最值,肯定是爆时空复杂度的.

在看看问题,一次发救济粮,就改了几个数字:\(b_{x,z}\ ,\ b_{y,z}\ ,\ b_{lca(x,y),z}\ ,\ b_{fa(lca(x,y)),z}\)(具体参考树上差分),也就是说\(b\)数组很多都是空的.所以,我们用 动态开点 + 线段树合并 来维护\(a,b\),具体看代码.

代码

#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
int read() {
	int re = 0;
	char c = getchar();
	bool negt = false;
	while(c < '0' || c > '9')
		negt |= (c == '-') , c = getchar();
	while(c >= '0' && c <= '9')
		re = (re << 1) + (re << 3) + c - '0' , c = getchar();
	return negt ? -re : re;
}

const int N = 100010;
const int maxZ = 100000;
const int M = 100010;
const int M_logZ = M * 20;

struct EDGE {
	int to , nxt;
} ed[N * 2];
int head[N];
void addedge(int u , int v) {
	static int cnt;
	++cnt;
	ed[cnt].to = v , ed[cnt].nxt = head[u] , head[u] = cnt;
}

int n , m , root;

int father[N];
namespace LCA {//RMQ-LCA,LCA就对了
	int cnt = 0;
	int dep[N * 2] , id[N * 2] , first[N];
	int st[N * 4][25];
	void dfs(int u , int nowdep) {
		++cnt;
		id[cnt] = u , dep[cnt] = nowdep , first[u] = cnt;
		for(int i = head[u] ; i ; i = ed[i].nxt) {
			int v = ed[i].to;
			if(v == father[u])
				continue;
			father[v] = u;
			dfs(v , nowdep + 1);
			++cnt;
			id[cnt] = u , dep[cnt] = nowdep;
		}
	}
	void init(int root) {
		dfs(root , 1);

		for(int i = 1 ; i <= cnt ; i++)
			st[i][0] = i;
		int k = log(n) / log(2) + 1;
		dep[0] = 0x3fffffff;
		for(int j = 1 ; j <= k ; j++)
			for(int i = 1 ; i <= cnt ; i++)
				st[i][j] = dep[st[i][j - 1]] < dep[st[i + (1 << j - 1)][j - 1] ] ? st[i][j - 1] : st[i + (1 << j - 1)][j - 1];

	}
	int lca(int u , int v) {
		u = first[u] , v = first[v];
		if(u > v) {
			int tmp;
			tmp = u , u = v , v = tmp;
		}
		int k = log(v - u + 1) / log(2);
		return id[
		           dep[st[u][k]] < dep[st[v - (1 << k) + 1][k]] ? st[u][k] : st[v - (1 << k) + 1][k]
		       ];
	}
}
inline int dep(int u) {
	return LCA::dep[LCA::first[u]];
}


namespace SegT {//我的一个代码习惯:函数中的p表示当前递归到的线段树结点的编号
	int root[N];
	struct NodeClass {
		int l , r , ls , rs , dat_num , dat_id;
//当前区间[l,r] , ls/rs:LeftSon,RightSon左右子结点编号 , dat_num:区间最值(存放最多的救济粮的数量) ,dat_id:区间最值出现的位置(该救济粮的种类)
	} node[4 * M_logZ];//其实我也不知道数组要开多大QAQ
	inline int newnode (int l , int r) {//开点
		static int cnt = 0;
		++cnt;
		node[cnt].l = l , node[cnt].r = r , node[cnt].dat_id = l;
		return cnt;
	}
	inline void push_up(int p) {//递归子结点后,更新结点p的数据
#define ls(p) node[p].ls
#define rs(p) node[p].rs
		int tmp;
		if(ls(p) == 0)
			tmp = rs(p);
		else if(rs(p) == 0)
			tmp = ls(p);
		else if(node[ls(p)].dat_num >= node[rs(p)].dat_num)//注意等号
			tmp = ls(p);
		else
			tmp = rs(p);
		node[p].dat_id = node[tmp].dat_id , node[p].dat_num = node[tmp].dat_num;
#undef ls
#undef rs
	}
	void change(int p , int pos , int dat) {
		if(node[p].l == node[p].r) {
			node[p].dat_num += dat;
			return;
		}

		int mid = (node[p].l + node[p].r) / 2;
		if(pos <= mid)//这里比较压行,其实就是先检查左/右子树是否为空,若为空,先开一个结点,然后递归
			change(node[p].ls = (node[p].ls == 0 ? newnode(node[p].l , mid) : node[p].ls) , pos , dat);
		else
			change(node[p].rs = (node[p].rs == 0 ? newnode(mid + 1 , node[p].r) : node[p].rs) , pos , dat);
		push_up(p);
	}
	int merge(int p1 , int p2 , int l , int r) {//合并以p1,p2为根的线段树
		if(p1 == 0 || p2 == 0)
			return p1 + p2;
		if(l == r) {
			node[p1].dat_num += node[p2].dat_num;
			return p1;
		}
		int mid = (l + r) / 2;
		node[p1].ls = merge(node[p1].ls , node[p2].ls , l , mid);
		node[p1].rs = merge(node[p1].rs , node[p2].rs , mid + 1 , r);
		push_up(p1);
		return p1;
	}
}


int ans[N];
void calc(int u) {
	for(int i = head[u] ; i ; i = ed[i].nxt) {
		int v = ed[i].to;
		if(v == father[u])
			continue;
		calc(v);
		SegT::root[u] = SegT::merge(SegT::root[u] , SegT::root[v] , 1 , maxZ);
	}
	ans[u] = SegT::node[SegT::root[u]].dat_num == 0 ? 0 : SegT::node[SegT::root[u]].dat_id;
}
int main() {
	n = read() , m = read() , root = 1;
	for(int i = 1 ; i < n ; i++) {
		int u = read() , v = read();
		addedge(u , v) , addedge(v , u);
	}
	LCA::init(root);

	for(int i = 1 ; i <= n ; i++)
		SegT::root[i] = SegT::newnode(1 , maxZ);
	for(int i = 1 ; i <= m ; i++) {
		int u = read() , v = read() , z = read();
		int lca = LCA::lca(u , v);
		SegT::change(SegT::root[u] , z , 1);
		SegT::change(SegT::root[v] , z , 1);
		SegT::change(SegT::root[lca] , z , -1);
		SegT::change(SegT::root[father[lca]] , z , -1);
	}
	calc(root);
	for(int i = 1 ; i <= n ; i++)
		printf("%d\n" , ans[i]);
	return 0;
}
posted @ 2021-08-19 19:27  追梦人1024  阅读(35)  评论(0编辑  收藏  举报