[CSP2019] 樹的重心 解題報告


[CSP2019] 樹的重心

題意

\(T\) 組數據 \((1 \le T \le 5)\), 每次給定一棵 \(n\) 個點的樹 \((1 \le n \le 299995)\).
\(E\) 為樹的邊集, \(V'_x,\ V'_y\) 分別為刪去邊 \((x,y)\) 后 點 \(x\) 所在的點集和點 \(y\) 所在的點集.

\[\sum_{(x,y) \in E} \left( \sum_{x \in V'_x} [x 是 V'_x 的重心] * x + \sum_{y \in V'_y} [y 是 V'_y 的重心] * y \right) \]

思路

40 pts

暴力枚舉每一條邊, 求重心即可.

100 pts

做法 1

總的思路是從枚舉邊變為枚舉點,
具體操作 :
枚舉每一個點 \(u\), 對該點的每一棵子樹都處理出 \(w_i\) 表示, 該子樹內權值為 \(i\) 的邊的個數,
這里邊的權值定義為 : 以 \(u\) 為根節點時, 刪去這條邊后, 刪掉的節點個數.

再分類討論當前枚舉到的子樹是不是權值最大的子樹, 找到一個區間 \([l,r]\), 使刪去的邊的權值 \(e_i \in [l,r]\) 時, 點 \(u\) 為新樹的重心, 用樹狀數組區間求和即可.

問題在於, 我們每次要獨立地獲得每個子樹的邊的數量, 所以就要用到線段樹合並或者主席樹, 不然的話每次重新獲取子樹的邊的復雜度會達到 \(O(n^2)\), 然后本人線段樹合並與主席樹都不會, 所以....

做法 2

總思路: 利用倍增優化找重心的過程.
突破口: 樹的重心一定在根節點的重路徑上.

證明:
定義 \(son[u]\) 為節點 \(u\) 的重兒子.

設該樹的根節點為 \(t\), \(v,\ u\) 為該樹上的兩個節點, 且 \(fa[v]=u\), \(v \not = son[u]\).
反證法: 假設 \(v\) 為以 \(t\) 為根節點的樹重心,
則 $ sz[t]-sz[v] \le \frac{sz[t]}{2} $,
所以 $ sz[v] \ge \frac{sz[t]}{2} $.
\(p\), 滿足 \(fa[p]=u\), 且 \(p \not = v\),
\(sz[p] \le sz[u]-1-sz[v] \le sz[u]-1-\frac{sz[t]}{2} \le sz[t]-1-\frac{sz[t]}{2} \le sz[v]\)
所以 \(v\)\(u\) 的重兒子, 產生矛盾, 假設不成立,
得證.

具體做法:
\(s[u][i]\) 為從節點 \(u\) 沿着重路徑往下跳 \(2^i\) 步后到達的點.
\(t\) 為當前樹的根節點.
\(sz[s[u][i]] > \frac{sz[t]}{2}\) 時, \(u=s[u][i]\),
不斷往下跳, 直到找到第一個有可能成為重心的點, 然后判斷這個點以及它的重兒子是不是重心 (因為一棵樹最多只能由兩個重心, 所以只需判斷這兩個點), 若是, 則加入答案中.

首先, 我們隨便找一個點作為根節點, 那么這里我們默認選擇 \(1\) 號點.
然后, 對於每一個節點, 我們枚舉它連向子節點的每一條邊, 然后分類討論, 重新選擇 \(u\) 的重兒子 (詳見代碼).

總時間復雜度 \(O(n\log n)\)

代碼

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=299995+7;
const int L=20;
int T,n,sz[N],son[N][2],s[N][L+7];
ll ans;
int lst[N],nxt[2*N],to[2*N],tot;
void add(int x,int y){ nxt[++tot]=lst[x]; to[tot]=y; lst[x]=tot; }
void rfs(int u){
  s[u][0]=son[u][0];
  for(int i=1;i<=L;i++)
    s[u][i]=s[s[u][i-1]][i-1];
}
void pre(int u,int fa){
  for(int i=lst[u];i;i=nxt[i]){
    int v=to[i];
    if(v==fa) continue;
    pre(v,u);
    sz[u]+=sz[v];
    if(sz[v]>sz[son[u][0]]){ son[u][1]=son[u][0]; son[u][0]=v; }
    else if(sz[v]>sz[son[u][1]]) son[u][1]=v;
  }
  sz[u]++;
  rfs(u);
}
void find(int u){
  int t=u;
  for(int i=L;i>=0;i--)
    if(sz[s[u][i]]>sz[t]/2)
      u=s[u][i];
  if(sz[t]-sz[u]<=sz[t]/2) ans+=(ll)u;
  u=s[u][0];
  if(sz[t]-sz[u]<=sz[t]/2) ans+=(ll)u;
}
void run(int u,int fa){
  int t1=son[u][0],t2=sz[u];        // 先備份
  for(int i=lst[u];i;i=nxt[i]){
    int v=to[i];
    if(v==fa) continue;
    if(v==t1) son[u][0]= sz[son[u][1]]>n-t2 ?son[u][1] :fa;        // 重新選取 u 的重兒子.
    else son[u][0]= sz[t1]>n-t2 ?t1 :fa;
    sz[u]=n-sz[v];
    rfs(u);    // 記得把 son[u][i] 也更新一遍
    find(u); find(v);
    run(v,u);
  }
  son[u][0]=t1;    // 還原
  sz[u]=t2;
  rfs(u);    //更新
}
int main(){
  //  freopen("gc.in","r",stdin);
  cin>>T;
  while(T--){
    scanf("%d",&n);
    memset(lst,0,sizeof(lst));
    memset(son,0,sizeof(son));
    memset(sz,0,sizeof(sz));
    tot=ans=0;
    int x,y;
    for(int i=1;i<n;i++){
      scanf("%d%d",&x,&y);
      add(x,y); add(y,x);
    }
    pre(1,0);
    run(1,0);
    printf("%lld\n",ans);
  }
  return 0;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM