题目传送门
题目描述:
方法一、树上启发式合并
前言
1、什么是树上启发式合并?
- Dsu on tree(树上启发式合并) 用来解决这样一类问题:统计树上一个节点的子树中具有某种特征的节点数。 例如例题中的子树 x 中颜色为 c 的个数。
2、如何实现?
我们借鉴树链剖分的思想,先对树进行树链剖分,处理出重儿子、轻儿子等数组。
而后我们就需要用这些数组对暴力进行优化(就是启发式合并!)
对于常规的套路,我们枚举到每一颗子树时,用桶暴力查询,查询完之后再暴力撤销,但是这样的时间复杂度显然不够优秀。
考虑优化。
我们处理到一棵子树以后,优先遍历轻儿子,然后暴力撤销,以免对其他兄弟产生影响。
最后,我们来处理重儿子,由于重儿子是最后处理的,所以我们做完之后就不需要再进行暴力撤销了,也不会对其他兄弟产生影响。
3、复杂度如何证明?
由于我们最后累加答案的时候,只需要再遍历轻链,将轻链的答案与重链合并。
也就是说对于每一个答案,都是将轻链与重链合并得来。
根据树链剖分的思想,一棵树最多有
l
o
g
n
logn
logn条重链,对于轻链上的每一个点,就算每一次往上合并答案,也最多合并
l
o
g
n
logn
logn次,而对于一个点,修改合并的时间复杂度为
O
(
1
)
O(1)
O(1),因此时间复杂度
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
S o l u t i o n Solution Solution
考虑树上启发式合并。
每次根据上面讲述的思想用桶来统计答案即可。
C o d e Code Code
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 2e5+10;
int n;
struct Node{int y,Next;}e[2*N];
int len = 0 , linkk[N];
int sum , Max;
int co[N] , cnt[N];
int ans[N];
void Insert(int x,int y){
e[++len].Next = linkk[x]; linkk[x] = len; e[len].y = y;
}
int siz[N] , son[N];
void dfs1(int x,int faa){
siz[x] = 1;
int maxx = 0;
for (int i = linkk[x]; i; i = e[i].Next){
int y = e[i].y;
if (y == faa) continue;
dfs1(y,x);
siz[x]+=siz[y];
if (maxx < siz[y]) maxx = siz[son[x] = y];
}
}//套路预处理
void Back(int x,int faa){
cnt[co[x]]--;
for (int i = linkk[x]; i; i = e[i].Next){
int y = e[i].y;
if (y == faa) continue;
Back(y,x);
}
}//撤销操作
void Calc(int x,int faa,int hes){
cnt[co[x]]++;
if (cnt[co[x]] > Max) Max = cnt[co[x]] , sum = co[x];
else if (cnt[co[x]] == Max) sum+=co[x];//计算
for (int i = linkk[x]; i; i = e[i].Next)
if (e[i].y != faa && e[i].y != hes) Calc(e[i].y,x,hes);
//由于重链的答案目前还在,所以无需重新遍历
}
void dfs(int x,int faa){
int he = son[x];
for (int i = linkk[x]; i; i = e[i].Next){
int y = e[i].y;
if (y == faa || y == he) continue;//优先遍历轻儿子
dfs(y,x);
Back(y,x);
sum = Max = 0;//遍历完轻儿子之后撤销影响
}
if (he) dfs(he,x);//在遍历重儿子
Calc(x,faa,he);//将轻链的答案进行合并
ans[x] = sum;
}
signed main(){
scanf("%lld",&n);
for (int i = 1; i <= n; i++) scanf("%lld",&co[i]);
for (int i = 1,x,y; i < n; i++)
scanf("%lld %lld",&x,&y) , Insert(x,y) , Insert(y,x);
dfs1(1,0);//预处理出重儿子
dfs(1,0);//树上启发式合并主要部分
for (int i = 1; i <= n; i++) printf("%lld ",ans[i]);
return 0;
}
优劣分析:
优势:思路简单,代码实现简单
劣势:限制较大,无法修改,较难的灵活运用
方法二:dfs序 + 分治
想要分治,首先的一点就是需要满足单调性。
那么这道题是否满足单调性呢?
S o l u t i o n Solution Solution
设:
L
[
x
]
:
L[x]:
L[x]:最早到达
x
x
x的时间戳
R
[
x
]
R[x]
R[x]:最晚到达
x
x
x的时间戳
h
o
m
[
c
n
t
]
:
hom[cnt]:
hom[cnt]:
d
f
s
序
dfs序
dfs序为
c
n
t
cnt
cnt的点是哪个点
众所周知:
1、一棵子树里面的dfs序是连续的
2、dfs序越早的点一定是越上层的点
- 证明:
如果当前点的dfs序为 i i i
假设有dfs序 i ′ < i i'<i i′<i
若存在点 j j j, i < = j < = R [ h o m [ i ] ] i<=j<=R[hom[i]] i<=j<=R[hom[i]]且 i ′ < = j < = R [ h o m [ i ′ ] ] i'<=j<=R[hom[i']] i′<=j<=R[hom[i′]]
那么有 R [ i ′ ] > = R [ i ] R[i'] >= R[i] R[i′]>=R[i]
即 i i i包含在 i ′ i' i′中, i ′ i' i′的答案相当于 i i i的答案只优不劣,单调性得到保证
那么知道单调性之后,如何分治呢?
如果我们递归到了区间
(
l
,
r
)
(l,r)
(l,r),
m
i
d
=
l
+
r
>
>
1
mid = l+r>>1
mid=l+r>>1
如果求解答案呢?
我们在
[
l
,
m
i
d
]
[l,mid]
[l,mid]区间中倒叙枚举
i
i
i,如果
m
i
d
<
=
R
[
h
o
m
[
i
]
]
<
=
r
mid<=R[hom[i]]<=r
mid<=R[hom[i]]<=r,那么就将这部分答案累加。
可以用一个指针来实现上述操作
C o d e Code Code
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+10;
int n;
int co[N];
struct Node{
int y,Next;
}e[2*N];
int ans[N];
int len = 0 , linkk[N];
void Insert(int x,int y){
e[++len] = (Node){y,linkk[x]};
linkk[x] = len;
}
int L[N] , R[N] , cnt , hom[N];
void dfs(int x,int faa){
L[x] = ++cnt;
hom[cnt] = x;
for (int i = linkk[x]; i; i = e[i].Next)
if (e[i].y != faa) dfs(e[i].y,x);
R[x] = cnt;
}//预处理dfs序数组
int Cnt[N], Co[N] , tot = 0;
int Max = 0,sum = 0;
void Clear(){
while (tot) Cnt[Co[tot--]]--;
Max = sum = 0;
}
void In(int x){
++Cnt[Co[++tot] = co[x]];//加入答案
if (Max < Cnt[co[x]]) Max = Cnt[co[x]] , sum = co[x];
else if (Max == Cnt[co[x]]) sum+=co[x];//比较更新答案
}
void Solve(int l,int r){
if (l == r){
if (L[hom[l]] == R[hom[l]])ans[hom[l]] = co[hom[l]];return;
}//单个点
int mid = l + r >> 1;
Solve(l,mid); Solve(mid+1,r);//分别处理两个小区间的情况
Clear();//清空数组
int p = mid;
for (int i = mid,j; i >= l && (j = R[hom[i]])<=r; i--){
//倒叙是因为后面的区间被前面的区间覆盖
In(hom[i]);
if (j <= mid) continue;//不需要处理
while (p < j) In(hom[++p]);//扩展,将hom[i]为根的子树累加进答案
ans[hom[i]] = sum;
}
}
int main(){
scanf("%d",&n);
for (int i = 1; i <= n; i++) scanf("%d",&co[i]);
for (int i = 1,x,y; i < n; i++)
scanf("%d %d",&x,&y) , Insert(x,y) , Insert(y,x);
dfs(1,0);
Solve(1,n);
for (int i = 1; i <= n; i++) printf("%d ",ans[i]);
return 0;
}
优劣势分析:
优势:速度较快,代码清晰
劣势:一定的思维性,代码细节较多
方法三 树上莫队
一、暴力:树上莫队 + 数据结构维护
仍然是dfs序,将树上问题转化为数列问题。
对于一个节点
x
x
x,他的询问区间就是
(
L
[
x
]
,
R
[
x
]
)
(L[x],R[x])
(L[x],R[x])。
一共
n
n
n个点,因此一共对应
n
n
n个区间。
对这n个区间分块然后用序列莫队的方法做即可。
但是进行撤销的时候是否难以处理?
将当前答案撤销,颜色个数简单,但是最大值如何维护?
我们发现,正常情况下,我们无法维护最大值。
其实,这个问题是单点修改,区间查询,所以我们要用线段树之类的数据结构维护。
代码较长,思路简单,这里就不贴代码了。
二、100pts:回滚莫队。
对于上述的问题,我们发现删点难以维护。
所以,我们做莫队的时候,能否做到只加不删呢?
这就是回滚莫队
只加不删回滚莫队的实现方法:
-
1 1 1、对原序列进行分块
-
2 2 2、将询问离线,以左端点所在的块为第一关键字,右端点为第二关键字进行排序
-
3、对于每个询问,我们分两种情况讨论:
I I I、当前询问的左端点与右端点处于同一块中,直接暴力查询I I ( 1 ) II(1) II(1)、对于左端点全部在块 T T T内的询问,我们先初始化 l = R [ t ] + 1 l = R[t]+1 l=R[t]+1, r = R [ t ] r=R[t] r=R[t],对应一个空区间。
I I ( 2 ) II(2) II(2)、由于在同一块内的询问右端点是单调递增的,因此对于右端点,我们只需要进行单调的加点即可
I I ( 3 ) II(3) II(3)、但是对于左端点却是乱序的,因此为了保证单调递增,我们每次处理完当前问题时,需要将左端点撤回到 R [ t ] + 1 R[t]+1 R[t]+1,这样每次做就能保证都是只加不删的了。
S o l u t i o n Solution Solution
这道题也是这种思路
按照上述方法做即可
C o d e Code Code
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 2e5+10;
int n;
int coo[N] , co[N];
struct Node{
int y,Next;
}e[2*N];
int LL[N] , RR[N] , bel[N],L[N] , R[N],cntt[N];
int Cnt[N];
int maxx = 0 , maxn = 0 , sum = 0;
struct qujian{
int l,r,id;
}q[2*N];
int len , linkk[N];
int ans[N];
int op[N];
void Insert(int x,int y){
e[++len] = (Node){y,linkk[x]};
linkk[x] = len;
}
int cnt,hom[N];
void dfs(int x,int faa){
L[x] = ++cnt;
hom[L[x]] = x;
for (int i = linkk[x]; i; i = e[i].Next)
if (e[i].y != faa) dfs(e[i].y,x);
R[x] = cnt;
}
bool mycmp(qujian x,qujian y){
if (bel[x.l] == bel[y.l]) return x.r < y.r;
return bel[x.l] < bel[y.l];
}
void Add(int x,int &Maxx,int &Sum){
++Cnt[co[x]];
if (Cnt[co[x]] > Maxx) Maxx = Cnt[co[x]] , Sum = co[x];
else if (Cnt[co[x]] == Maxx) Sum+=co[x];
}
void Del(int x){
--Cnt[co[x]];
}
void work(int ll,int rr,int x){
for (int i = ll; i <= rr; i++) cntt[co[i]] = 0;
int Maxx = 0 ,Sum = 0;
for (int i = ll; i <= rr; i++){
++cntt[co[i]];
if (cntt[co[i]] > Maxx) Maxx = cntt[co[i]] , Sum = co[i];
else if (cntt[co[i]] == Maxx) Sum+=co[i];
}
ans[q[x].id] = Sum;
}
signed main(){
scanf("%lld",&n);
for (int i = 1; i <= n; i++) scanf("%lld",&coo[i]);
for (int i = 1,x,y; i < n; i++)
scanf("%lld %lld",&x,&y) , Insert(x,y) , Insert(y,x);
dfs(1,0);
for (int i = 1; i <= n; i++) co[L[i]] = coo[i];//dfs序之后的颜色
// **分块过程**
int lenn = sqrt(n);
int Siz = n/lenn;
for (int i = 1; i <= Siz; i++){
if (i*lenn > n) break;
LL[i] = RR[i-1] + 1 , RR[i] = i*lenn;
}
if (RR[Siz] < n) LL[++Siz] = RR[Siz-1] + 1 , RR[Siz] = n;
for (int i = 1; i <= Siz; i++)
for (int j = LL[i]; j <= RR[i]; j++) bel[j] = i;
// **End**
// **询问离线**
int Len = sqrt(n);
for (int i = 1; i <= n; i++){
q[i].l = L[i] , q[i].r = R[i];
q[i].id = i;
}
sort(q+1,q+n+1,mycmp);
// **End**
// **回滚莫队主程序**
int la = 0 , l = 1 , r = 0;
for (int i = 1; i <= n; i++){
if (bel[q[i].l] == bel[q[i].r]){
int Maxx = 0 , Sum = 0;
work(q[i].l,q[i].r,i);
continue;
}//处于同一块的问题直接暴力
if (la != bel[q[i].l]){//出现了新的块,进行初始化
la = bel[q[i].l];
while (r > RR[la]) Del(r--);
while (l < RR[la] + 1) Del(l++);
maxx = sum = 0;
}
while (r < q[i].r) Add(++r,maxx,sum);//右端点单调递增
int maxxx = maxx , summ = sum , ll = l;
//注意赋一个新的值,避免左端点对当前的值造成影响,不然回滚就失去了意义
while (ll > q[i].l) Add(--ll,maxxx,summ);//将左端点的答案加进去
while (ll < l) Del(ll++);//回滚
ans[q[i].id] = summ;
}
//**End**
for (int i = 1; i <= n; i++)
printf("%lld ",ans[i]);
}
优劣势分析:
优势:较模板,实现方式比较死板,且能够在很多题目上进行运用
劣势:速度不够优秀
具体的回滚莫队可参考胡队的博客
附
之前的DSU ON TREE 的写法可能不够好。。
主要是我照着模板抄然后有一道题就没过。
下面的可能稳一点
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+10;
int n,m,ty,la;
struct Node{
int y,Next;
}e[2*N];
int len , linkk[N];
struct node{
int x,y,v;
}E[N];
int V[N];
int c[N],fa[N*2];
int tot;
int cnt[N],ans[N];
int maxx = 0 , maxn = 1e9;
int logn;
bool mycmp(node x,node y){return x.v < y.v;}
int get(int x){return fa[x] == x?x:fa[x] = get(fa[x]);}
int siz[N] , Fa[N][30] , son[N];
void Dfs(int x,int faa){
int Max = 0;
siz[x] = 1; Fa[x][0] = faa;
for (int i = linkk[x]; i; i = e[i].Next){
int y = e[i].y; if (y == faa) continue;
Dfs(y,x);
siz[x]+=siz[y];
if (Max < siz[y]) Max = siz[son[x] = y];
}
}
void find_fa(){
for (int i = 1; i < 30; i++) for (int j = 1; j <= tot; j++) Fa[j][i] = Fa[Fa[j][i-1]][i-1];
}
void Calc(int x,int faa,int he){
if (c[x]!=0) cnt[c[x]]++;
if (cnt[c[x]] > maxx) maxx = cnt[c[x]] , maxn = c[x];
else if (cnt[c[x]] == maxx) maxn = min(maxn,c[x]);
for (int i = linkk[x]; i; i = e[i].Next){
int y = e[i].y; if (y == faa || y == he) continue;
Calc(y,x,he);
}
}
void Back(int x,int faa,int he){
cnt[c[x]]--;
for (int i = linkk[x]; i; i = e[i].Next){
int y = e[i].y; if (y == faa || y == he) continue;
Back(y,x,he);
}
}
void Dsu(int x,int faa,int op){
int he = son[x];
for (int i = linkk[x]; i; i = e[i].Next){
int y = e[i].y; if (y == faa || y == he) continue;
Dsu(y,x,0);
}
if (he) Dsu(he,x,1);
Calc(x,faa,he); /*cout<<"x = "<<x<<' '<<maxx<<' '<<maxn<<endl;*/
ans[x] = maxn;
if (op == 0) Back(x,faa,0) , maxx = maxn = 0;
}
int Find(int x,int y){
for (int i = 29; i >= 0; i--)
if (Fa[x][i] && V[Fa[x][i]] <= y) x = Fa[x][i];
return ans[x];
}
void Insert(int x,int y){
e[++len] = (Node){y,linkk[x]};
linkk[x] = len;
}
int main(){
freopen("garden.in","r",stdin);
freopen("garden.out","w",stdout);
scanf("%d %d %d",&n,&m,&ty);
for (int i = 1; i <= n; i++) scanf("%d",&c[i]);
for (int i = 1; i <= m; i++) scanf("%d %d %d",&E[i].x,&E[i].y,&E[i].v);
sort(E+1,E+m+1,mycmp);
for (int i = 1; i <= 2*n; i++) fa[i] = i; tot = n;
for (int i = 1; i <= m; i++){
int x = get(E[i].x) , y = get(E[i].y); if (x == y) continue;
fa[x] = fa[y] = ++tot; Insert(x,tot) , Insert(y,tot) , Insert(tot,x) , Insert(tot,y); V[tot] = E[i].v;
}
for (int i = 1; i <= tot; i++) if (fa[i] == i) Dfs(i,0); find_fa();
for (int i = 1; i <= tot; i++) if (fa[i] == i) Dsu(i,0,0);
int q; scanf("%d",&q);
while (q--){
int x,y; scanf("%d %d",&x,&y);
if (ty == 2) x^=la , y^=la;
la = Find(x,y); printf("%d\n",la);
}
}
//题目可能不是同一道。但是写法是类似的