樹鏈剖分的一種妙用與一類樹鏈修改單點查詢問題的時間復雜度優化——2018ACM陝西邀請賽J題


題目描述

有一棵樹,每個結點有一個燈(初始均是關着的)。每個燈能對該位置和相鄰結點貢獻1的亮度。現有兩種操作:

(1)將一條鏈上的燈狀態翻轉,開變關、關變開;

(2)查詢一個結點的亮度。

數據規模:\(1 \le n,q \le 10^5\)

簡要題解

對於這種題,很容易想到任意指定一個根轉化為有根樹,每個結點維護值\(a_i\)表示它的所有兒子的貢獻之和,這樣再加上自己以及父親的貢獻就能回答一個詢問了。

然而經過一波思考發現問題在於鏈修改時根本沒法維護\(a_i\)。因此需要用鏈修改時的常規操作——樹鏈剖分理論來解決本題了。

對於樹鏈剖分,每一條鏈被剖分成了\(O(\log n)\)條重鏈,下面考慮修改每條重鏈\((s,t)\),不妨設\(depth[s]<depth[t]\)。可以發現修改后只會改變\((father[s],father[t])\)這條鏈上的\(a_i\),但是注意到鏈\((s,father[t])\)上每個結點都是它父親的重兒子,如果我們\(a_i\)不維護重兒子,讓\(a_i\)表示它的所有輕兒子的貢獻之和,那么每條重鏈只需暴力修改一個\(a_{father[s]}\)。這樣對於每個修改,更新\(a\)數組的時間復雜度為\(O(\log n)\);同時當然還需要更新鏈上燈的狀態,即進行鏈異或1的操作,如果采用樹鏈剖分加樹狀數組可達到\(O(\log ^2 n)\)的時間復雜度。對於每個詢問\(x\),只需要查詢\(a_x\)(對應輕兒子),以及\(father[x],x,son[x]\)(分別對應父親、重兒子、自身)的狀態即可(這里\(son[x]\)表示\(x\)的重兒子),查詢時間復雜度\(O(\log n)\)。

總時間復雜度\(O(n+q \log ^2 n)\)。(PS:感謝隊友ddd教我這么巧的方法!)

進一步的改進

這一做法雖然很巧妙,但是其唯一的瓶頸\(O(\log ^2 n)\)為樹狀數組修改,其它所有操作(無論\(a_i\)修改還是樹狀數組查詢)均為\(O(\log n)\),顯得很不協調(強迫症患者的我當然不舒服了)。針對本題鏈上異或單點查詢問題,這里再給出一個更為高效的做法。

構造一個DFS序列,對每棵子樹\(i\),序列定義為\(i\)開頭,然后是\(i\)的每棵子樹的DFS序列依次拼起來,最后\(i\)結尾,這樣序列總長度為\(2n\)。設\(h_1[i]\)和\(h_2[i]\)分別為\(i\)第一次和第二次在序列中出現的位置。維護一個長為\(2n\)的數組\(b\),對於每次鏈\((x,y)\)異或1,執行如下算法:

(1)將\(b[h_2[x]]\)到\(b[2n]\)全部異或1;

(2)將\(b[h_2[y]]\)到\(b[2n]\)全部異或1;

(3)將\(b[h_2[lca(x,y)]]\)異或1。

定理:對於上述算法,每次單點查詢\(x\)時,只需求\(b[h_1[x]] \oplus b[h_2[x]]\)即為答案。

為證明該定理,首先證明以下引理:

引理1:若結點\(t\)是結點\(x\)的祖先(可以是自身),當且僅當\(h_1[t] \le h_1[x]\)且\(h_2[t] \ge h_2[x]\)。

根據DFS序列的定義,證明顯然。

引理2:對於結點\(x\),將\(b[h_2[x]]\)到\(b[2n]\)全部異或1后,只有\(x\)祖先\(t\)(包括自身)的\(b[h_1[t]] \oplus b[h_2[t]]\)值改變;

證明:首先證明充分性。若結點\(t\)是結點\(x\)的祖先,根據引理1,\(b[h_1[t]]\)不變,\(b[h_2[t]]\)異或了1,正確;

接下來證明必要性。若結點\(t\)不是結點\(x\)的祖先,根據引理1,分三種情況:

情況1:\(h_1[t] \le h_1[x]\)且\(h_2[t] < h_2[x]\),此時\(b[h_1[t]]\)和\(b[h_2[t]]\)均不變;

情況2:\(h_1[t] > h_1[x]\)且\(h_2[t] \ge h_2[x]\),此時必有\(h_1[t] > h_2[x]\)。若不然,根據\(h_2[x] \gt h_1[t]\)以及\(h_2[x] \le h_2[t]\)可知\(t\)是\(x\)的祖先,根據引理1,這與 \(h_1[t] > h_1[x]\)矛盾。因此\(b[h_1[t]]\)和\(b[h_2[t]]\)均異或了1;

情況3:\(h_1[t] > h_1[x]\)且\(h_2[t] < h_2[x]\),由於\(h_1[t] < h_2[t]\),故\(h_1[t] < h_2[x]\),此時\(b[h_1[t]]\)和\(b[h_2[t]]\)均不變。

以上三種情況\(b[h_1[t]] \oplus b[h_2[t]]\)均不變,必要性證畢。事實上,以上三種情況畫圖恰好對應了\(x\)左側結點、\(x\)右側結點和\(x\)的子樹。

定理證明:根據引理2易得。

實現時,只需維護樹狀數組即可,每次鏈修改的時間復雜度為\(O(\log n)\)。這樣整個題目的時間復雜度便被優化到了\(O(n+q \log n)\),已經達到了時間復雜度的極限,不可能再優了。

事實上,上述方法做出適當的修改可用於樹上鏈加,單點求和的情形。廣義來說,只要運算構成一個群,都可以用此方法優化時間復雜度。

AC代碼

以下代碼是改進之前的,更好寫一些。

  1 #include<cstdio>
  2 #include<cstring>
  3 #include<vector>
  4 using namespace std;
  5 vector<int> v[100001];
  6 int father[100001], depth[100001], top[100001], id[100001], son[100001];
  7 int cnt;
  8 int dfs1(int i, int fa)
  9 {
 10     father[i] = fa;
 11     depth[i] = depth[fa] + 1;
 12     son[i] = 0;
 13     int ret = 0, maxSize = 0;
 14     for (unsigned int j = 0; j < v[i].size(); j++){
 15         int t = v[i][j];
 16         if (t == fa)continue;
 17         int size = dfs1(t, i);
 18         ret += size;
 19         if (size > maxSize){
 20             maxSize = size;
 21             son[i] = t;
 22         }
 23     }
 24     return ret + 1;
 25 }
 26 void dfs2(int i, int tp)
 27 {
 28     top[i] = tp;
 29     id[i] = ++cnt;
 30     if (son[i])dfs2(son[i], tp);
 31     for (unsigned int j = 0; j < v[i].size(); j++){
 32         int t = v[i][j];
 33         if (t != father[i] && t != son[i])dfs2(t, t);
 34     }
 35 }
 36 void init()
 37 {
 38     cnt = 0; depth[0] = 0;
 39     dfs1(1, 0); dfs2(1, 1);
 40 }
 41 
 42 bool tree[100001];
 43 int light[100001], treeLen;
 44 int sum(int i)
 45 {
 46     int ret = 0;
 47     for (; i > 0; i -= i&-i)ret ^= tree[i];
 48     return ret;
 49 }
 50 void flip(int i)
 51 {
 52     for (; i <= treeLen; i += i&-i)
 53         tree[i] ^= 1;
 54 }
 55 void modify(int s, int t)
 56 {
 57     int top1 = top[s], top2 = top[t];
 58     while (top1 != top2){
 59         if (depth[top1] < depth[top2]){
 60             flip(id[top2]); flip(id[t] + 1);
 61             t = father[top2];
 62             if (son[t] != top2)light[t] += sum(id[top2]) ? 1 : -1;
 63             top2 = top[t];
 64         }
 65         else{
 66             flip(id[top1]); flip(id[s] + 1);
 67             s = father[top1];
 68             if (son[s] != top1)light[s] += sum(id[top1]) ? 1 : -1;
 69             top1 = top[s];
 70         }
 71     }
 72     if (depth[s] > depth[t])swap(s, t);
 73     flip(id[s]); flip(id[t] + 1);
 74     if (son[father[s]] != s)light[father[s]] += sum(id[s]) ? 1 : -1;
 75 }
 76 int main()
 77 {
 78     int n, m, x, y, z;
 79     scanf("%d%d", &n, &m);
 80     for (int i = 1; i < n; i++){
 81         scanf("%d%d", &x, &y);
 82         v[x].push_back(y);
 83         v[y].push_back(x);
 84     }
 85     init(); treeLen = n;
 86     while (m--){
 87         scanf("%d", &z);
 88         if (z == 1){
 89             scanf("%d%d", &x, &y);
 90             modify(x, y);
 91         }
 92         else{
 93             scanf("%d", &x);
 94             int ans = father[x] ? sum(id[father[x]]) : 0;
 95             if (son[x])ans += sum(id[son[x]]);
 96             ans += sum(id[x]) + light[x];
 97             printf("%d\n", ans);
 98         }
 99     }
100 }

 


免責聲明!

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



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