【题解 && 树上启发式合并】算法杂交详解 Lomsat gelral

题目传送门

题目描述:

在这里插入图片描述


方法一、树上启发式合并

前言

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
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
    若存在点 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
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
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]);
}

优劣势分析:

优势:较模板,实现方式比较死板,且能够在很多题目上进行运用
劣势:速度不够优秀

你可能感兴趣的:(树上启发式合并,题解)