AcWing 353 雨天的尾巴
写在前面
居然没有树剖的题解……
我来水一发
题目描述
深绘里一直很讨厌雨天。
灼热的天气穿透了前半个夏天,后来一场大雨和随之而来的洪水,浇灭了一切。
虽然深绘里家乡的小村落对洪水有着顽固的抵抗力,但也倒了几座老房子,几棵老树被连根拔起,以及田地里的粮食被弄得一片狼藉。
无奈的深绘里和村民们只好等待救济粮来维生。
不过救济粮的发放方式很特别。
有 n 个点,形成一个树状结构。
有 m 次发放操作,每次选择两个点 x,y,对 x 到 y 的路径上(包括 x,y)的每个点发放一袋 z 类型的物品。
求完成所有发放操作后,每个点存放最多的是哪种类型的物品。
输入格式
第一行两个正整数n,m,含义如题目所示。
接下来n-1行,每行两个数(a,b),表示(a,b)间有一条边。
再接下来m行,每行三个数(x,y,z),含义如题目所示。
输出格式
共n行,第i行一个整数,表示第i座房屋里存放的最多的是哪种救济粮,如果有多种救济粮存放次数一样,输出编号最小的。
如果某座房屋里没有救济粮,则对应一行输出0。
数据范围
\(1≤n,m≤100000\),
\(1≤z≤10^9\)
样例
输入样例
5 3
1 2
3 1
3 4
5 3
2 3 3
1 5 2
3 3 3
输出样例
2
3
3
0
2
算法1
线段树合并
有大佬已经写了,我就懒得赘述
#include <bits/stdc++.h>
#define lson l,mid,tree[now].l
#define rson mid + 1,r,tree[now].r
using namespace std;
const int maxn = 1e5 + 5;
const int maxm = 6e6 + 5;//数组还是 开大点
int n,m,size,first[maxn],tot,cntz;
int root[maxn],tmp[maxn],top[maxn],cnt[maxn],dep[maxn],father[maxn],ans[maxn];
struct Query{int x,y,z;}ask[maxn];
struct Edge{int v,nt;}edge[maxn << 1];
struct SegMentTree{int l,r,val,id;}tree[maxm];
//----------输入输出优化
char *TT,*mo,but[(1 << 18) + 2];
#define getchar() ((TT == mo && (mo = ((TT = but) + fread(but, 1, 1 << 18, stdin)),TT == mo)) ? -1 : *TT++)
template<class T>inline void read(T &x){
x = 0;bool flag = 0;char ch = getchar();
while(!isdigit(ch)) flag |= ch == '-',ch = getchar();
while(isdigit(ch)) x = (x << 1) + (x << 3) + (ch ^ 48),ch = getchar();
if(flag) x = -x;
}
template<class T>void putch(const T x){if(x > 9) putch(x / 10);putchar(x % 10 | 48);}
template<class T>void put(const T x){if(x < 0) putchar('-'),putch(-x);else putch(x);}
//---------读入数据
void eadd(int u,int v){edge[++size].v = v;edge[size].nt = first[u];first[u] = size;}
void readdata(){
read(n);read(m);
for(int i = 1;i < n; ++ i){int u,v;read(u);read(v);eadd(u,v);eadd(v,u);}
for(int i = 1;i <= m; ++ i){read(ask[i].x);read(ask[i].y);read(ask[i].z);tmp[i] = ask[i].z;}
}
//----------LCA
void dfs(int u,int f,int d){
top[u] = u,dep[u] = d,father[u] = f;cnt[u] = 1;int son = 0,mcnt = 0;
for(int i = first[u];i;i = edge[i].nt){
int v = edge[i].v;if(v == f) continue;
dfs(v,u,d + 1);cnt[u] += cnt[v];
if(cnt[v]>mcnt) mcnt = cnt[v],son = v;
}
if(son) top[son] = u;
}
int find(int x){return top[x] == x ? x : top[x] = find(top[x]);};
int LCA(int x,int y){
if(find(x) == find(y)) return dep[x] < dep[y] ? x : y;
else return dep[top[x]] < dep[top[y]] ? LCA(x,father[top[y]]) : LCA(father[top[x]],y);
}
//----------线段树的修改 & 合并
void pushup(int k){
int ls = tree[k].l,rs = tree[k].r;
if(tree[ls].val >= tree[rs].val) tree[k].val = tree[ls].val,tree[k].id = tree[ls].id;
else tree[k].val = tree[rs].val,tree[k].id = tree[rs].id;
}
void modify(int l,int r,int &now,int pos,int val){
if(!now) now = ++tot;
if(l == r && l == pos){tree[now].val += val;tree[now].id = l;return;}
int mid = (l + r) >> 1;
if(pos <= mid) modify(lson,pos,val);
else modify(rson,pos,val);
pushup(now);//记得pushup
}
void merge(int &x,int &y,int l,int r){
if(!x) return;if(!y) {y = x;return;}
if(l == r){tree[y].val += tree[x].val;return;}
int mid = (l + r) >> 1;
merge(tree[x].l,tree[y].l,l,mid);merge(tree[x].r,tree[y].r,mid + 1,r);
pushup(y);
}
void merge_dfs(int u,int f){
for(int i = first[u];i;i = edge[i].nt){
int v = edge[i].v;if(v == f) continue;//v = edge[i].v
merge_dfs(v,u);merge(root[v],root[u],1,cntz);
}
if(tree[root[u]].val == 0) ans[u] = 0;//当没有救济粮时输出0
else ans[u] = tmp[tree[root[u]].id];
}
//---------主模块
void work(){
//----------初始化
dfs(1,0,1);
//----------离散化 + 修改
sort(tmp + 1,tmp + m + 1);
cntz = unique(tmp + 1,tmp + m + 1) - (tmp + 1);
for(int i = 1;i <= m; ++ i){
ask[i].z = lower_bound(tmp + 1,tmp + cntz+ 1,ask[i].z) - tmp;
int lca = LCA(ask[i].x,ask[i].y);
modify(1,cntz,root[ask[i].x],ask[i].z,1);
modify(1,cntz,root[ask[i].y],ask[i].z,1);
modify(1,cntz,root[lca],ask[i].z,-1);
if(father[lca])modify(1,cntz,root[father[lca]],ask[i].z,-1);
}
//----------合并
merge_dfs(1,0);
for(int i = 1;i <= n; ++ i){
put(ans[i]);putchar('\n');
}
}
int main(){
readdata();
work();
return 0;
}
算法2
树链剖分 \(O(n(log n)^2)\)
虽然看起来要多一个log,但是由于常数小,跑起来比线段树合并还要快。
我们先回忆一下树剖做树上路径修改时的思想:
把路径拆分成若干区间,在线段树上修改
这道题也可以这么做
我们可以把每一次修改拆成若干区间来做。由于我们需要记录救济粮的种类与数量,那么就需要离散化,建一棵权值线段树维护最值及对应的编号。
将一条路经拆分后,由于这道题的空间卡的有些紧,不可能每个点建一棵线段树,所以我们考虑优化空间。
除了动态开点,我们应该还可以想到:树剖时,每个点对应的编号是一定的,而询问拆分过后,每一个区间的编号都是连续的,而我们要求的最终答案是在所有操作之后,可以离线求出每个点的答案。
这个编号在一般的树剖里是对应的线段树的编号,在这里由于建的是权值线段树,这些编号的意义不如就理解为一个时间序编号。
对于拆分后的每个连续区间的修改,若是按左端点排序,那么,一个点后面的区间的修改,是不会影响到已经修改完的点的,这样我们便可以差分处理,把编号理解为时间,直接在一颗线段树上求解。
对于每个区间修改\([a_i,b_i]\),可以进一步拆分成在时间\(a_i\)处对应的救济粮+1,在时间\(b_i+1\)处对应的救济粮-1,便于优化与求解,我们可以把每个时间的询问用类似链表的方式存储,每个时间求解完毕后,就可以算出对应的节点的答案。
这样的差分修改,前缀和求解只需开一颗线段树即可。而询问拆分大约拆为nlogn个。
整理一下思路:
-
开始时,将路径的修改拆分成一个个编号连续的区间\([a_i,b_i]\),对于每个区间有用差分思想拆为\(a_i\)处val的个数+1,\(b_i\)处val的个数-1,用类似链表的方式存下每个编号(时间)所对应的的各个修改。
-
然后,从1开始遍历所有编号的修改,在一颗权值线段树上执行,每个编号的修改遍历完之后,将编号对应的节点的答案记录下来(因为差分操作,这里相当于求前缀和)
C++ 代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 5;
int n,m,size,first[maxn],tot,head[maxn],num,rev[maxn],son[maxn],cntz;//head存询问的“头边”
int top[maxn],seg[maxn],father[maxn],dep[maxn],cnt[maxn],ans[maxn],tmp[maxn];
struct Edge{int v,nt;}edge[maxn << 1];
struct Query{int x,y,z;}a[maxn];//拆分前的询问
struct Split_Query{int val,nt;}ask[maxn * 20];//拆分后的询问
struct SegmentTree{int val,id;}tree[maxn << 2];
//----------输入输出优化
//char *TT,*mo,but[(1 << 18) + 2];
//#define getchar() ((TT == mo && (mo = ((TT = but) + fread(but, 1, 1 << 18, stdin)),TT == mo)) ? -1 : *TT++)
template<class T>inline void read(T &x){
x = 0;
bool flag = 0;
char ch = getchar();
while(!isdigit(ch)) flag |= ch == '-',ch = getchar();
while(isdigit(ch)) x = (x << 1) + (x << 3) + (ch ^ 48),ch = getchar();
if(flag) x = -x;
}
template<class T>void putch(const T x){if(x > 9) putch(x / 10);putchar(x % 10 | 48);}
template<class T>void put(const T x){if(x < 0) putchar('-'),putch(-x);else putch(x);}
//----------读入数据 & 初始化树剖
void eadd(int u,int v){edge[++size].v = v;edge[size].nt = first[u];first[u] = size;}
void dfs1(int u,int f){
father[u] = f;cnt[u] = 1;int mcnt = 0;
for(int i = first[u];i;i = edge[i].nt){
int v = edge[i].v;if(v == f) continue;
dep[v] = dep[u] + 1;dfs1(v,u);cnt[u] += cnt[v];//注意累加
if(cnt[v] > mcnt) mcnt = cnt[v],son[u] = v;
}
}
void dfs2(int u,int f,int tp){
seg[u] = ++tot;top[u] = tp;rev[seg[u]] = u;
if(son[u]) dfs2(son[u],u,tp);
for(int i = first[u];i;i = edge[i].nt){
int v = edge[i].v;if(v == f || v == son[u]) continue;
dfs2(v,u,v);
}
}
void readdata(){
read(n);read(m);
for(int i = 1;i < n; ++ i){int u,v;read(u);read(v);eadd(u,v);eadd(v,u);}
dfs1(1,0);dfs2(1,0,1);
//离散化
for(int i = 1;i <= m; ++ i) read(a[i].x),read(a[i].y),read(a[i].z),tmp[i] = a[i].z;
sort(tmp + 1,tmp + m + 1);
cntz = unique(tmp + 1,tmp + m + 1) - (tmp + 1);
}
//----------拆分修改
void add_query(int id,int val){ask[++num].val = val;ask[num].nt = head[id];head[id] = num;}
void Split(int x,int y,int z){
while(top[x] != top[y]){
if(dep[top[x]] < dep[top[y]]) swap(x,y);
add_query(seg[top[x]],z);add_query(seg[x] + 1,-z);//top[u]的编号小于u
x = father[top[x]];
}
if(dep[x] < dep[y]) swap(x,y);
add_query(seg[y],z);add_query(seg[x] + 1,-z);
}
//----------线段树模块
void pushup(int k){
int ls = k << 1,rs = k << 1 | 1;
if(tree[ls].val >= tree[rs].val) tree[k].val = tree[ls].val,tree[k].id = tree[ls].id;//>=
else tree[k].val = tree[rs].val,tree[k].id = tree[rs].id;
}
void modify(int l,int r,int k,int pos,int val){
if(l == r){tree[k].val += val;tree[k].id = l;return;}
int mid = (l + r) >> 1;
if(pos <= mid) modify(l,mid,k<<1,pos,val);
else modify(mid + 1,r,k<<1|1,pos,val);
pushup(k);
}
//----------主模块
void work(){
//----------拆分修改
for(int i = 1;i <= m; ++ i) {
a[i].z = lower_bound(tmp + 1,tmp + 1 + cntz,a[i].z) - tmp;
Split(a[i].x,a[i].y,a[i].z);
}//luogu上似乎不用离散化
//----------修改
for(int i = 1;i <= n; ++ i){//这里是seg值,也就是时间
for(int j = head[i];j;j = ask[j].nt){
if(ask[j].val > 0) modify(1,cntz,1,ask[j].val,1);
else modify(1,cntz,1,-ask[j].val,-1);
}
ans[rev[i]] = tree[1].val ? tmp[tree[1].id] : 0;
}
for(int i = 1;i <= n; ++ i){
put(ans[i]);putchar('\n');
}
}
int main(){
// freopen("testdata.in","r",stdin);
// freopen("testdata.out","w",stdout);
readdata();
work();
return 0;
}