1. 程式人生 > 實用技巧 >淺談樹上啟發式合併

淺談樹上啟發式合併

\[\text{前言} \]

\(\quad\)\(51\) nod 上做了一道毒瘤題,順便學了樹上啟發式合併的演算法(又叫 Dcu on Tree、靜態鏈分治),接下來我會先概述樹上啟發式合併的基本知識,然後結合部分題目講解,可能會寫的很長,做好心理準備。

\[\text{前置知識} \]

\(\quad\)學這個之前需要對樹上操作、 \(dfs\) 序和輕重鏈剖分等知識有一定了解,最好已經掌握了樹鏈剖分。

\[\text{演算法思想} \]

\(\quad\)樹上啟發式合併 (Dsu on Tree),是一個在 \(O(n\log n)\) 時間內解決許多樹上問題的有力演算法,對於某些樹上離線問題可以速度大於等於大部分演算法且更易於理解和實現。

\(\quad\)雖然這個演算法不能完成修改操作,只能完成詢問操作,但還是很受歡迎,在樹上離線問題中獨佔鰲頭,畢竟其他演算法(例如樹上莫隊)時間複雜度都是 \(O(n\sqrt{n})\)

\[\text{模板題:CF600E Lomsat gelral} \]

\(\quad\)題目連結:CF600E Lomsat gelral(洛谷的連結)

題目描述

  • 有一棵 \(n\) 個結點的以 \(1\) 號結點為根的有根樹。

  • 每個結點都有一個顏色,顏色是以編號表示的, \(i\) 號結點的顏色編號為 \(c_i\)

  • 如果一種顏色在以 \(x\) 為根的子樹內出現次數最多,稱其在以 \(x\)

    為根的子樹中占主導地位。顯然,同一子樹中可能有多種顏色占主導地位。

  • 你的任務是對於每一個 \(i\in[1,n]\) ,求出以 \(i\) 為根的子樹中,占主導地位的顏色的編號和。

  • \(n\le 10^5,c_i\le n≤10^5,c_i≤n\)

\(\quad\)先想一下暴力演算法,對於每一次詢問都遍歷整棵子樹,然後統計答案,最後再清空cnt陣列,最壞情況是時間複雜度為 \(O(n^2)\) ,對於 \(10^5\) 的資料肯定是過不去的。

\(\quad\)現在考慮優化演算法,暴力演算法跑得慢的原因就是多次遍歷,多次清空陣列,一個顯然的優化就是將詢問同一個子樹的詢問放在一起處理,但這樣還是沒有處理到關鍵,最壞情況時間複雜度還是 \(O(n^2)\)

,考慮到詢問 \(x\) 節點時, \(x\) 的子樹對答案有貢獻,所以可以不用清空陣列,先統計 \(x\) 的子樹中的答案,再統計 \(x\) 的答案,這樣就需要提前處理好 \(dfs\) 序。

\(\quad\)然後我們可以考慮一個優化,遍歷到最後一個子樹時是不用清空的,因為它不會產生對其他節點影響了,根據貪心的思想我們當然要把節點數最多的子樹(即重兒子形成的子樹)放在最後,之後我們就有了一個看似比較快的演算法,先遍歷所有的輕兒子節點形成的子樹,統計答案但是不保留資料,然後遍歷重兒子,統計答案並且保留資料,最後再遍歷輕兒子以及父節點,合併重兒子統計過的答案。

\(\quad\)其實樹上啟發式合併的基本思路就是這樣,可以看一下程式碼理解。

il int check(int x)//統計答案
{
  int num=0,ret=0;
  for(re i=1;i<=n;i++)
    {
      if(cnt[i]==num){ret+=i;}
      else if(cnt[i]>num){num=cnt[i],ret=i;}
    }
  return ret;
}
il void add(int x){cnt[col[x]]++;}//單點增加
il void del(int x){cnt[col[x]]--;}//單點減少
il void raise(int x){for(re i=seg[x];i<=seg[x]+size[x]-1;i++)add(rev[i]);}//增加x子樹的貢獻
il void clear(int x){for(re i=seg[x];i<=seg[x]+size[x]-1;i++)del(rev[i]);}//清空x子樹的貢獻
il void dfs1(int x,int fa)
{
  dep[x]=dep[fa]+1;father[x]=fa;//處理深度,父親
  seg[x]=++seg[0];rev[seg[x]]=x;size[x]=1;//子樹大小,dfs序
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      if(y==fa)continue;dfs1(y,x);
      size[x]+=size[y];
      if(size[y]>size[son[x]])son[x]=y;//重兒子
    }
}
il void dfs2(int x,int flag)//flag表示是否為重兒子,1表示重兒子,0表示輕兒子
{
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      if(y==son[x]||y==father[x])continue;
      dfs2(y,0);//先遍歷輕兒子
    }
  if(son[x])dfs2(son[x],1);//再遍歷重兒子
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      if(y==son[x]||y==father[x])continue;
      raise(y);//更新輕兒子的貢獻
    }add(x);//加上x結點本身的貢獻
  ans[x]=check(x);//更新答案
  if(!flag)clear(x);//如果是輕兒子,就清空
}

\[\text{時間複雜度} \]

\(\quad\)這樣時間複雜度就可以變成 \(O(n\log n)\) 的,但還是要考慮嚴謹證明一下。

\(\quad\)我們像樹鏈剖分一樣定義重邊和輕邊(連向重兒子的為重邊,其餘為輕邊),對於一棵有 \(n\) 個節點的樹:

\(\quad\)根節點到樹上任意節點的輕邊數不超過 \(\log n\) 條。我們設根到該節點有 \(x\) 條輕邊該節點的子樹大小為 \(y\) ,顯然輕邊連線的子節點的子樹大小小於父親的一半(若大於一半就不是輕邊了),則 \(y<n/2^x\) ,顯然 \(n>2^x\) ,所以 \(x<\log n\)

\(\quad\)又因為如果一個節點是其父親的重兒子,則他的子樹必定在他的兄弟之中最多,所以任意節點到根的路徑上所有重邊連線的父節點在計算答案是必定不會遍歷到這個節點,所以一個節點的被遍歷的次數等於他到根節點路徑上的輕邊樹 \(+1\)(之所以要 \(+1\) 是因為他本身要被遍歷到),所以一個節點的被遍歷次數\(=\log n+1\) ,總時間複雜度則為 \(O(n(\log n+1))=O(n\log n)\) ,輸出答案花費 \(O(m)\)

\[\text{回到模板題} \]

\(\quad\)現在理解了基本思路後,再回到模板題看一看,大體思路是一樣的,只有統計答案的地方有些許不同,不知怎的我使用了線段樹來維護答案,每次單點修改,統計答案時只要記錄 \(Max[1].num\) 即可,用線段樹維護貌似加了時間複雜度 \(O(n\log ^2 n)\)

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
#define re register int
#define int long long
#define il inline
#define next nee
il int read()
{
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)&&ch!='-')ch=getchar();
  if(ch=='-')f=-1,ch=getchar();
  while(isdigit(ch))x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
  return x*f;
}
il void print(int x)
{
  if(x<0)putchar('-'),x=-x;
  if(x/10)print(x/10);
  putchar(x%10+'0');
}
const int N=1e5+5;
int n,m,next[N<<1],father[N],son[N],go[N<<1],head[N],tot,seg[N],rev[N];
int dep[N],size[N],col[N],ans[N];
il void Add(int x,int y)
{
  next[++tot]=head[x];
  head[x]=tot;go[tot]=y;
}
struct node{
  int num,tot;//num表示編號大小,tot表示數量
}Max[N<<2];
il void build(int k,int l,int r)//建樹
{
  if(l==r){Max[k].num=l;return;}//初始化為編號
  int mid=l+r>>1;
  build(k<<1,l,mid);build(k<<1|1,mid+1,r);
}
il void change(int k,int l,int r,int x,int y)
{
  if(l==r){Max[k].tot+=y;return;}
  int mid=l+r>>1;
  if(x<=mid)change(k<<1,l,mid,x,y);
  else change(k<<1|1,mid+1,r,x,y);
  if(Max[k<<1].tot>Max[k<<1|1].tot)Max[k]=Max[k<<1];//左兒子數量多
  else if(Max[k<<1].tot<Max[k<<1|1].tot)Max[k]=Max[k<<1|1];//右兒子數量多
  else Max[k].tot=Max[k<<1].tot,Max[k].num=Max[k<<1].num+Max[k<<1|1].num;//一樣多時,編號相加
}
il void add(int x){change(1,1,n,col[x],1);}
il void del(int x){change(1,1,n,col[x],-1);}
il void raise(int x){for(re i=seg[x];i<=seg[x]+size[x]-1;i++)add(rev[i]);}
il void clear(int x){for(re i=seg[x];i<=seg[x]+size[x]-1;i++)del(rev[i]);}
il void dfs1(int x,int fa)
{
  dep[x]=dep[fa]+1;father[x]=fa;seg[x]=++seg[0];rev[seg[x]]=x;size[x]=1;
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      if(y==fa)continue;dfs1(y,x);
      size[x]+=size[y];
      if(size[y]>size[son[x]])son[x]=y;
    }
}
il void dfs2(int x,int flag)
{
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      if(y==son[x]||y==father[x])continue;
      dfs2(y,0);
    }
  if(son[x])dfs2(son[x],1);
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      if(y==son[x]||y==father[x])continue;
      raise(y);
    }add(x);
  ans[x]=Max[1].num;
  if(!flag)clear(x);
}
signed main()
{
  n=read();
  for(re i=1;i<=n;i++)col[i]=read();//記錄每個點的顏色
  for(re i=1,x,y;i<n;i++)x=read(),y=read(),Add(x,y),Add(y,x);
  build(1,1,n);dfs1(1,0);dfs2(1,1);//預處理
  for(re i=1;i<=n;i++)print(ans[i]),putchar(' ');
  return 0;
}

\[\text{CF570D Tree Requests} \]

\(\quad\)題目連結:CF570D Tree Requests(洛谷的連結)

思路:

\(\quad\) Dsu on Tree模板題,每次用 \(cnt_{i,j}\) 陣列記錄深度為 \(i\) 中顏色 \(j\) 的出現情況,因為要構成迴文,所以最多隻能有一種字元出現次數為奇數。

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<queue>
using namespace std;
#define re register int
#define LL long long
#define il inline
#define pc putchar('\n')
#define next nee
#define inf 1e18
il int read()
{
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)&&ch!='-')ch=getchar();
  if(ch=='-')f=-1,ch=getchar();
  while(isdigit(ch))x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
  return x*f;
}
il void print(int x)
{
  if(x<0)putchar('-'),x=-x;
  if(x/10)print(x/10);
  putchar(x%10+'0');
}
const int N=5e5+5;
int n,m,next[N],head[N],go[N],tot,s[N],father[N],cnt[N][28];
int dep[N],size[N],son[N],seg[N],rev[N];
bool ans[N];
struct node{
  int x,y;};
vector<node>q[N];
il void Add(int x,int y)
{
  next[++tot]=head[x];
  head[x]=tot;go[tot]=y;
}
il void dfs1(int x,int fa)
{
  dep[x]=dep[fa]+1;size[x]=1;seg[x]=++seg[0];rev[seg[x]]=x;
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      dfs1(y,x);size[x]+=size[y];
      if(size[y]>size[son[x]])son[x]=y;//重兒子
    }
}
il bool check(int x)//統計答案
{
  int ret=0;
  for(re i=1;i<=26;i++)if(cnt[x][i]&1)ret++;
  return ret<=1;
}
il void add(int x){cnt[dep[x]][s[x]]++;}//單點增加
il void update(int x){for(re i=seg[x];i<=seg[x]+size[x]-1;i++)add(rev[i]);}//增加x子樹的貢獻
il void del(int x){cnt[dep[x]][s[x]]=0;}//單點減少
il void out(int x){for(re i=seg[x];i<=seg[x]+size[x]-1;i++)del(rev[i]);}//清空x子樹的貢獻
il void dfs2(int x,int flag)//flag表示是否為重兒子,1表示重兒子,0表示輕兒子
{
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      if(y==son[x])continue;
      dfs2(y,0);//先遍歷輕兒子
    }
  if(son[x])dfs2(son[x],1);//再遍歷重兒子
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      if(y==son[x])continue;
      update(y);//更新輕兒子的貢獻
    }add(x);//加上x結點本身的貢獻
  for(re i=0;i<q[x].size();i++)
    ans[q[x][i].y]=check(q[x][i].x);//更新答案
  if(!flag)out(x);//如果是輕兒子,就清空
}
signed main()
{
  n=read();m=read();
  for(re i=2;i<=n;i++){re x=read();father[i]=x,Add(x,i);}
  string ss;cin>>ss;
  for(re i=1;i<=n;i++)s[i]=ss[i-1]-'a'+1;
  for(re i=1,x,y;i<=m;i++)x=read(),y=read(),q[x].push_back((node){y,i});//vector儲存詢問,將詢問同一顆子樹的放一起
  dfs1(1,0);dfs2(1,0);
  for(re i=1;i<=m;i++)ans[i]?puts("Yes"):puts("No");
  return 0;
}

\[\text{CF208E Blood Cousins} \]

\(\quad\)題目連結:CF208E Blood Cousins(洛谷的連結)

\(\quad\)這題的思路幾乎和上一道題一樣,只需要稍微修改一下,因為是找一個點與多少個點擁有共同的 \(K\) 級祖先,那麼我們就可以先把它的K級祖先找出來(使用倍增),然後在找這個點有幾個 \(K\) 級後代,或者用深度表示成 \(dep_x+k\) ,注意這個圖不是連通的,每次要清空陣列。

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<queue>
using namespace std;
#define re register int
#define int long long
#define LL long long
#define il inline
#define next nee
#define inf 1e18
il int read()
{
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)&&ch!='-')ch=getchar();
  if(ch=='-')f=-1,ch=getchar();
  while(isdigit(ch))x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
  return x*f;
}
il void print(int x)
{
  if(x<0)putchar('-'),x=-x;
  if(x/10)print(x/10);
  putchar(x%10+'0');
}
const int N=1e5+5;
int n,m,next[N],go[N],head[N],tot,father[N][20],ans[N],dep[N],son[N],seg[N],rev[N],size[N],cnt[N];
struct node{int k,id;};
vector<node>q[N];
il int Max(int x,int y){return x>y?x:y;}//求較大值
il void Add(int x,int y)//鏈式前向新
{next[++tot]=head[x];head[x]=tot;go[tot]=y;}
il void add(int x){cnt[dep[x]]++;}//單點增加
il void raise(int x){for(re i=seg[x];i<=seg[x]+size[x]-1;i++)add(rev[i]);}//算上x子樹的貢獻
il void del(int x){cnt[dep[x]]=0;}//單點減少
il void clear(int x){for(re i=seg[x];i<=seg[x]+size[x]-1;i++)del(rev[i]);}//清空x子樹
il void dfs1(int x,int fa)
{
  dep[x]=dep[fa]+1;size[x]=1;seg[x]=++seg[0];rev[seg[x]]=x;
  for(re i=1;i<=18;i++)father[x][i]=father[father[x][i-1]][i-1];//倍增
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      dfs1(y,x);size[x]+=size[y];
      if(size[y]>size[son[x]])son[x]=y;
    }
}
il void dfs2(int x,int flag)
{
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      if(y==son[x])continue;
      dfs2(y,0);//先遍歷輕兒子
    }
  if(son[x])dfs2(son[x],1);//再遍歷重兒子
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {if(y==son[x])continue;raise(y);}//更新輕兒子的貢獻
  add(x);//加上x結點本身的貢獻
  for(re i=0;i<q[x].size();i++)
    ans[q[x][i].id]=cnt[dep[x]+q[x][i].k];//更新答案
  if(!flag)clear(x);//如果是輕兒子,就清空
}
il int find_father(int x,int y)//找x的第y級祖先
{
  for(re i=18;i>=0;i--){if(y>=(1<<i))y-=(1<<i),x=father[x][i];}
  return x;
}
signed main()
{
  n=read();
  for(re i=1,x;i<=n;i++)x=read(),father[i][0]=x,Add(x,i);
  for(re i=1;i<=n;i++)if(father[i][0]==0)dfs1(i,0);//預處理,倍增陣列、dfs序等樹上資訊
  m=read();
  for(re i=1,x,y,z;i<=m;i++){x=read(),y=read(),z=find_father(x,y);if(z)q[z].push_back((node){y,i});}
  for(re i=1;i<=n;i++)if(father[i][0]==0)dfs2(i,0);//找每棵樹的根節點,0表示輕兒子,這樣不用手動清空陣列
  for(re i=1;i<=m;i++)print(Max(ans[i]-1,0)),putchar(' ');//注意輸出要減一,要去除詢問節點
  return 0;
}

\[\text{CF246E Blood Cousins Return} \]

\(\quad\)題目連結:CF246E Blood Cousins Return(洛谷的連結)

\(\quad\)一道 \(Dcu\) 模板題,只需要用一個 \(map\) 陣列來維護這個名字(字串)是否出現過,用 \(set\) 也可以,貌似會慢一些,用 \(cnt\) 陣列來維護每一層的不同名字的數量即可,因為是一個森林,所以記得要清空陣列。

一個大坑點:

\(\quad\) \(WA\)\(50\) 個點的注意了,在儲存詢問時一定要判斷詢問這個第 \(k\) 級兒子的深度是否超過了 \(10^5\) ,因為最多隻有 \(10^5\) 個點,如果超過就不用儲存,答案一定是 \(0\) ,儲存的話會溢位陣列,遇到一些毒瘤資料就會WA。

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<map>
#include<vector>
using namespace std;
#define re register int
#define int long long
#define LL long long
#define il inline
#define next nee
#define inf 1e18
il int read()
{
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)&&ch!='-')ch=getchar();
  if(ch=='-')f=-1,ch=getchar();
  while(isdigit(ch))x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
  return x*f;
}
il void print(int x)
{
  if(x<0)putchar('-'),x=-x;
  if(x/10)print(x/10);
  putchar(x%10+'0');
}
const int N=1e5+5;
int n,m,next[N],go[N],head[N],tot,father[N],ans[N];
int dep[N],son[N],seg[N],rev[N],size[N],cnt[N];
struct node{int k,id;};
string s[N]; 
vector<node>q[N];
map<string,bool>c[N];

il void Add(int x,int y)
{
  next[++tot]=head[x];
  head[x]=tot;go[tot]=y;
}
il void add(int x)//單點增加
{
  if(!c[dep[x]][s[x]])c[dep[x]][s[x]]=1,cnt[dep[x]]++;
}
il void raise(int x)//算上x子樹的貢獻
{
  for(re i=seg[x];i<=seg[x]+size[x]-1;i++)add(rev[i]);
}
il void del(int x)//單點減少
{
  c[dep[x]].clear();
  cnt[dep[x]]=0;
}
il void clear(int x)//清空x子樹
{
  for(re i=seg[x];i<=seg[x]+size[x]-1;i++)
    del(rev[i]);
}
il void dfs1(int x,int fa)
{
  dep[x]=dep[fa]+1;size[x]=1;seg[x]=++seg[0];rev[seg[x]]=x;
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      dfs1(y,x);size[x]+=size[y];
      if(size[y]>size[son[x]])son[x]=y;
    }
}
il void dfs2(int x,int flag)
{
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      if(y==son[x])continue;
      dfs2(y,0);//先遍歷輕兒子
    }
  if(son[x])dfs2(son[x],1);//再遍歷重兒子
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {if(y==son[x])continue;raise(y);}//更新輕兒子的貢獻
  add(x);//加上x結點本身的貢獻
  for(re i=0;i<q[x].size();i++)
    ans[q[x][i].id]=cnt[dep[x]+q[x][i].k];//更新答案
  if(!flag)clear(x);//如果是輕兒子,就清空
}
signed main()
{
  n=read();
  for(re i=1,x;i<=n;i++)cin>>s[i],x=read(),father[i]=x,Add(x,i);
  for(re i=1;i<=n;i++)if(!father[i])dfs1(i,0);//預處理,倍增陣列、dfs序等樹上資訊,記得要用迴圈,從每棵樹的根節點出發
  m=read();
  for(re i=1,x,y;i<=m;i++)
    {
      x=read(),y=read();
      if(dep[x]+y>=N)continue;//注意,如果詢問的第K級兒子超過限制,不能儲存,原因上面有
      q[x].push_back((node){y,i});
    }
  for(re i=1;i<=n;i++)if(!father[i])dfs2(i,0);//找每棵樹的根節點,0表示輕兒子,這樣不用手動清空陣列
  for(re i=1;i<=m;i++)print(ans[i]),putchar('\n');
  return 0;
}

\[\text{CF1009F Dominant Indices} \]

\(\quad\)題目連結:CF1009F Dominant Indices(洛谷的連結)

\(\quad\) \(Dsu\) 板子題,用 \(cnt_i\) 陣列來表示深度為i的結點數量,另外要注意要在修改的時候記錄cnt最大的深度即可。

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<map>
#include<vector>
using namespace std;
#define re register int
#define int long long
#define LL long long
#define il inline
#define next nee
#define inf 1e18
il int read()
{
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)&&ch!='-')ch=getchar();
  if(ch=='-')f=-1,ch=getchar();
  while(isdigit(ch))x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
  return x*f;
}
il void print(int x)
{
  if(x<0)putchar('-'),x=-x;
  if(x/10)print(x/10);
  putchar(x%10+'0');
}
const int N=1e6+5;
int n,ans[N],next[N<<1],go[N<<1],head[N],tot,seg[N],son[N],father[N],size[N];
int cnt[N],dep[N],rev[N],Maxdep,Max,num;
il void Add(int x,int y)
{
  next[++tot]=head[x];
  head[x]=tot;go[tot]=y;
}
il void check(int x)//更新Max和num的值
{
  if(cnt[x]>Max)Max=cnt[x],num=x;
  else if(cnt[x]==Max&&x<num)num=x;//如果有相等的情況,取深度小的
}
il void dfs1(int x,int fa)//預處理
{
  father[x]=fa;size[x]=1;seg[x]=++seg[0];dep[x]=dep[fa]+1;rev[seg[x]]=x;//處理子樹大小,父親,深度,dfs序
  if(dep[x]>Maxdep)Maxdep=dep[x];
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      if(y==fa)continue;dfs1(y,x);
      size[x]+=size[y];
      if(size[y]>size[son[x]])son[x]=y;//記錄重兒子
    }
}
il void dfs2(int x,int flag)
{
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      if(y==father[x]||y==son[x])continue;
      dfs2(y,0);Max=num=0;//先遍歷輕兒子
      for(re j=seg[y];j<=seg[y]+size[y]-1;j++)
	{int z=rev[j];cnt[dep[z]]=0;}//順便清空cnt陣列,Max和num清零
    }if(son[x])dfs2(son[x],1);//再遍歷重兒子
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      if(y==father[x]||y==son[x])continue;
      for(re j=seg[y];j<=seg[y]+size[y]-1;j++)
	{
	  int z=rev[j];cnt[dep[z]]++;//更新輕兒子的貢獻
	  check(dep[z]);
	}
    }cnt[dep[x]]++;check(dep[x]);//加上x結點本身的貢獻
  ans[x]=num-dep[x];//更新答案
}
signed main()
{
  n=read();
  for(re i=1,x,y;i<n;i++)x=read(),y=read(),Add(x,y),Add(y,x);
  dfs1(1,0);dfs2(1,1);
  for(re i=1;i<=n;i++)print(ans[i]),putchar('\n');
  return 0;
}

\[\text{CF375D Tree and Queries} \]

\(\quad\)題目連結:CF375D Tree and Queries(洛谷的連結)

思路

\(\quad\)標準做法是動態規劃,但看到 \(4.5s\) 的時限,似乎可以樹上啟發式合併水過去,只要用 \(num_i\)\(cnt_k\) 的陣列來記錄出現顏色 \(i\) 的數量及超過 \(k\) 的顏色數量即可。

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
using namespace std;
#define re register int
#define int long long
#define LL long long
#define il inline
#define next nee
#define inf 1e18
il int read()
{
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)&&ch!='-')ch=getchar();
  if(ch=='-')f=-1,ch=getchar();
  while(isdigit(ch))x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
  return x*f;
}
il void print(int x)
{
  if(x<0)putchar('-'),x=-x;
  if(x/10)print(x/10);
  putchar(x%10+'0');
}
const int N=1e5+5;
int n,m,next[N<<1],go[N<<1],head[N],tot,seg[N],col[N];
int rev[N],size[N],son[N],father[N],cnt[N],ans[N],num[N];
struct node{int k,id;};
vector<node>q[N];
il void Add(int x,int y)
{
  next[++tot]=head[x];
  head[x]=tot;go[tot]=y;
}
il void add(int x){num[col[x]]++;cnt[num[col[x]]]++;}
il void raise(int x)
{
  for(re i=seg[x];i<=seg[x]+size[x]-1;i++)
    add(rev[i]);
}
il void clear(int x)
{
  for(re i=seg[x];i<=seg[x]+size[x]-1;i++)
    {
      int y=rev[i];
      cnt[num[col[y]]]--;num[col[y]]--;
    }
}
il void dfs1(int x,int fa)
{
  size[x]=1;seg[x]=++seg[0];rev[seg[x]]=x;father[x]=fa;
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      if(y==fa)continue;dfs1(y,x);
      size[x]+=size[y];
      if(size[y]>size[son[x]])son[x]=y;//重兒子
    }
}
il void dfs2(int x,int flag)//flag表示是否為重兒子,1表示重兒子,0表示輕兒子
{
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      if(y==father[x]||y==son[x])continue;
      dfs2(y,0);//先遍歷輕兒子
    }if(son[x])dfs2(son[x],1);//再遍歷重兒子
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      if(y==father[x]||y==son[x])continue;
      raise(y);//更新輕兒子的貢獻
    }add(x);//加上x結點本身的貢獻
  for(re i=0;i<q[x].size();i++)//更新答案
    ans[q[x][i].id]=cnt[q[x][i].k];
  if(!flag)clear(x);//如果是輕兒子,就清空
}
signed main()
{
  n=read();m=read();
  for(re i=1;i<=n;i++)col[i]=read();
  for(re i=1,x,y;i<n;i++)x=read(),y=read(),Add(x,y),Add(y,x);
  for(re i=1,x,y;i<=m;i++)x=read(),y=read(),q[x].push_back((node){y,i});
  dfs1(1,0);dfs2(1,1);
  for(re i=1;i<=m;i++)print(ans[i]),putchar('\n');
  return 0;
}

\[\text{CF741D Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths} \]

\(\quad\)題目連結:CF741D Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths(洛谷的連結)

\(\quad\)這其實算一道 Dsu 的壓軸題,據說是樹上啟發式合併演算法的創始者出的題。

\(\quad\)這題確實是有些難度的,總之我一開始連題解都沒有看懂。

\(\quad\)首先考慮迴文的問題,其他題解其實講的很清楚了,只要22個字母中最多有一個字母數量為奇數即可,也可都為偶數,所以一共23種情況,但考慮所有情況(只分奇偶)有 \(2^{22}\)中情況,可以用一個二進位制數表示,\(cnt_i\) 表示二進位制數為 \(i\) 的結點的最大深度,二進位制數指的是從這個結點到根節點的最短路徑的序列,\(num_x\) 表示結點 \(x\) 到根節點的最短路徑的序列,請仔細理解這句話,否則之後的程式碼可能會看不懂。

\(\quad\)然後我們對於兩個修改函式都講一遍。

\(\quad\)第一個修改函式,就是判斷是否有有符合條件的,如對於節點 \(x\) 來說,和TA到根節點的序列為 \(num_x\)\(cnt_{num_x}\) 表示之前出現的另一條大小為 \(num_x\) 序列,這樣這兩條路徑合併後字母數就都是偶數,之後的迴圈列舉的是有一個字母不同的情況,這兩種情況都是符合條件的。

il void add1(int x)
{
  ans[now]=max(ans[now],dep[x]+cnt[num[x]]);
  for(re i=0;i<=21;i++)ans[now]=max(ans[now],dep[x]+cnt[(1<<i)^num[x]]);
}

\(\quad\)對於第二個修改函式,就是把這個結點 \(x\) 的資訊載入 \(cnt\) 陣列,並且為了最後的序列最長,要儘可能選深度大的,顯然深度大的答案更優。

il void add2(int x)
{cnt[num[x]]=max(cnt[num[x]],dep[x]);}

\(\quad\)注意要先做修改操作 \(1\),再做修改操作 \(2\),也就是說先統計這個點的答案(或一棵子樹),再載入這個點的資料(或一棵子樹),否則答案會把自己也記進去,可以仔細思考一下這個點。

\(\quad\)接下來我們思考一個問題,因為我們是一棵子樹一棵子樹為單位修改的,如果這個最優答案在子樹中會怎麼樣?可以發現這樣的答案在子樹中一定被統計過了,當這條路徑的兩個端點的LCA被詢問時就以及被記錄了,所以還要跑一遍所有子樹,用子樹的答案來更新當前結點。

\(\quad\)另外我們還要注意節點 \(i\) 的答案的計算公式為

\[ans_i=\max (dep_x+dep_y-2\times dep_i) \]

\(\quad\)這其實就是 \(x\)\(y\) 兩點之間的距離公式( \(x\),\(y\) 為最短路徑的兩個端點),另外可以發現最優情況下結點 \(i\) 為結點 \(x\) 和結點 \(y\) 的LCA,因為結點 \(x\) 和結點 \(y\) 的在以 \(i\) 為根節點的子樹,若不是的話,那麼答案就會算多,但這顯然是錯誤的答案,所以我們是一棵子樹一棵子樹為單位修改的,這也算回答了上面的問題。

\(\quad\)最後來看看完整程式碼吧!

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
using namespace std;
#define re register int
#define il inline
#define next nee
#define inf 1e9+5
il int read()
{
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)&&ch!='-')ch=getchar();
  if(ch=='-')f=-1,ch=getchar();
  while(isdigit(ch))x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
  return x*f;
}
il void print(int x)
{
  if(x<0)putchar('-'),x=-x;
  if(x/10)print(x/10);
  putchar(x%10+'0');
}
const int N=5e5+5;
int n,m,next[N],go[N],head[N],tot,seg[N],son[N],father[N],now;
int size[N],rev[N],ans[N],s[N],dep[N],num[N],cnt[1<<23];
il int Max(int x,int y){return x>y?x:y;}
il void Add(int x,int y,int z)
{
  next[++tot]=head[x];
  head[x]=tot;go[tot]=y;s[tot]=z;
}
il void add1(int x)//修改操作1
{
  ans[now]=max(ans[now],dep[x]+cnt[num[x]]);
  for(re i=0;i<=21;i++)ans[now]=max(ans[now],dep[x]+cnt[(1<<i)^num[x]]);
}
il void add2(int x)//修改操作2
{cnt[num[x]]=max(cnt[num[x]],dep[x]);}
il void clear(int x)//清空操作
{
  for(re i=seg[x];i<=seg[x]+size[x]-1;i++)
    cnt[num[rev[i]]]=-inf;
}
il void dfs1(int x)
{
  dep[x]=dep[father[x]]+1;size[x]=1;seg[x]=++seg[0];rev[seg[x]]=x;
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      num[y]=num[x]^(1<<s[i]);dfs1(y);
      size[x]+=size[y];
      if(size[y]>size[son[x]])son[x]=y;
    }
}
il void dfs2(int x,int flag)
{
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      if(y==son[x])continue;
      dfs2(y,0);
    }if(son[x])dfs2(son[x],1);now=x;
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      if(y==son[x])continue;
      for(re i=seg[y];i<=seg[y]+size[y]-1;i++)add1(rev[i]);
      for(re i=seg[y];i<=seg[y]+size[y]-1;i++)add2(rev[i]);
    }add1(x),add2(x);//記得要修改x結點
  ans[x]-=(dep[x]<<1);//減去本身的深度
  for(re i=head[x],y;i,y=go[i];i=next[i])ans[x]=max(ans[x],ans[y]);
  if(!flag)clear(x);
}
signed main()
{
  n=read();char ch;
  for(re i=0;i<(1<<22);i++)cnt[i]=-inf;//一定要初始化為負值
  for(re i=2,x;i<=n;i++){x=read();father[i]=x;scanf("%c",&ch);Add(x,i,ch-'a');}
  dfs1(1);dfs2(1,1);
  for(re i=1;i<=n;i++)print(Max(ans[i],0)),putchar(' ');//可能會輸出負數
  return 0;
}

另外好像還有兩道題:P3224 永無鄉UVA1479 Graph and Queries,據說要用到平衡樹,蒟蒻現在還沒有學,以後有時間再寫吧!

還有這個黑題:CF715C Digit Tree

\(\quad\)講這麼多,有點累了,這 \(7\) 題寫了快兩天,都快調吐了。

\(\quad\)參考資料: