我们借鉴树链剖分的思想,先对树进行树链剖分,处理出重儿子、轻儿子等数组。
而后我们就需要用这些数组对暴力进行优化(就是启发式合并!)
对于常规的套路,我们枚举到每一颗子树时,用桶暴力查询,查询完之后再暴力撤销,但是这样的时间复杂度显然不够优秀。
考虑优化。
我们处理到一棵子树以后,优先遍历轻儿子,然后暴力撤销,以免对其他兄弟产生影响。
最后,我们来处理重儿子,由于重儿子是最后处理的,所以我们做完之后就不需要再进行暴力撤销了,也不会对其他兄弟产生影响。
由于我们最后累加答案的时候,只需要再遍历轻链,将轻链的答案与重链合并。
也就是说对于每一个答案,都是将轻链与重链合并得来。
根据树链剖分的思想,一棵树最多有 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)
考虑树上启发式合并。
每次根据上面讲述的思想用桶来统计答案即可。
#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;
}
优势:思路简单,代码实现简单
劣势:限制较大,无法修改,较难的灵活运用
想要分治,首先的一点就是需要满足单调性。
那么这道题是否满足单调性呢?
设:
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序越早的点一定是越上层的点
那么知道单调性之后,如何分治呢?
如果我们递归到了区间 ( 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,那么就将这部分答案累加。
可以用一个指针来实现上述操作
#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个区间分块然后用序列莫队的方法做即可。
但是进行撤销的时候是否难以处理?
将当前答案撤销,颜色个数简单,但是最大值如何维护?
我们发现,正常情况下,我们无法维护最大值。
其实,这个问题是单点修改,区间查询,所以我们要用线段树之类的数据结构维护。
代码较长,思路简单,这里就不贴代码了。
对于上述的问题,我们发现删点难以维护。
所以,我们做莫队的时候,能否做到只加不删呢?
这就是回滚莫队
只加不删回滚莫队的实现方法:
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,这样每次做就能保证都是只加不删的了。
这道题也是这种思路
按照上述方法做即可
#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]);
}
优势:较模板,实现方式比较死板,且能够在很多题目上进行运用
劣势:速度不够优秀