--- tags: CSES題解 --- --- tags: CSES題解 --- # CSES Problem Set — Path Queries II 題解 ## 題目 給定一個 $n$ 個節點的樹,每個節點依序被編號為 $1, 2, \ldots, n$。每個節點都有一個數值,目標是要執行下面兩種查詢。 1. 將節點 $s$ 的數值改成 $x$ 2. 尋找節點 $a$ 到 $b$ 之間的路徑之間所有節點的最大值 ### 輸入 - 第一行有兩個正整數 $n, q$ ($1 \le n, q \le 2 \cdot 10^5$) - 第二行有 $n$ 個正整數 $v_1, v_2, \ldots, v_n$ ($1 \le v_i \le 10^9$),代表第 $i$ 個節點的數值 - 接下來 $n-1$ 行,每行有兩個正整數 $a, b$ ($1 \le a, b \le n$),代表節點 $a$ 和 $b$ 之間有一條邊 - 最後 $q$ 行輸入代表查詢,格式為 $1\ s\ x$ 或 $2\ a\ b$ ($1 \le s \le 10^9, 1 \le x \le n$) ### 輸出 對於每個查詢 $2$,輸出一個數字代表答案(分別以空白隔開)。 ## 範例測資 ``` Input: 5 3 2 4 1 3 3 1 2 1 3 2 4 2 5 2 3 5 1 2 2 2 3 5 Output: 4 3 ``` ![image](https://hackmd.io/_uploads/Syam8syvA.png) ## 先備知識 1. 用線段樹做動態 RMQ 問題 ## 想法 這個問題的難點在於「需要動態更新,並且維護樹上任意兩點的最大值」,但我們並沒有好用的資料結構去維護樹的動態更新 RMQ 問題。 因此有個想法:「如果把樹拆成一個一個鏈,就可以用線段樹做動態 RMQ 了。 因為是分解成鏈,因此是要把所有需要的部分透過線段樹取 $\max$,最後再把所有結果取 $\max$。 但要怎麼拆才會比較好呢?如果拆的太爛,就會一直用線段樹查詢,進而使時間複雜度變得糟糕。 --- 我們得用輕重鏈剖分(Heavy-Light Decomposition, 以下簡稱 HLD)的方式分解,因為 HLD 有個特別的性質是分解過後**任意兩點只會經過至多 $\log{N}$ 個鏈**,綜合上面的想法可以發現我們將得到以下的時間複雜度。 - 查詢 $1$:只需要更新其中一條鏈的值:$O(\log{N})$ - 查詢 $2$:至多跑過 $\log{N}$ 條鏈,每條鏈會用 $O(\log{N})$ 的時間做 RMQ,因此時間複雜度會是 $O(\log{\log{N}})$ 這篇題解就不詳細介紹 HLD,可以參考 [本篇教學文章](https://usaco.guide/plat/hld?lang=cpp),但更多本題實作細節可以參考下方程式碼與註解。 --- 最後,這題的時間複雜度卡的比較緊,記得將程式碼做以下操作: - 加上 `ios::sync_with_stdio(0), cin.tie(0);` - 不使用 `long long`,僅用 `int` - 加上 `#pragma GCC optimize("O3")` 若仍過不了,可以參考以下操作: - 改用非遞迴式線段樹 - 使用更快的輸入優化 :::spoiler 輸入優化程式碼 ```cpp= // fast IO inline char readchar(){ static char buffer[BUFSIZ], * now = buffer + BUFSIZ, * end = buffer + BUFSIZ; if (now == end) { if (end < buffer + BUFSIZ) return EOF; end = (buffer + fread(buffer, 1, BUFSIZ, stdin)); now = buffer; } return *now++; } inline int nextint(){ int x = 0, c = readchar(), neg = false; while(('0' > c || c > '9') && c!='-' && c!=EOF) c = readchar(); if(c == '-') neg = true, c = readchar(); while('0' <= c && c <= '9') x = (x<<3) + (x<<1) + (c^'0'), c = readchar(); if(neg) x = -x; return x; // returns 0 if EOF } // 將原本程式碼中 cin >> a >> b 改成 a = nextint(); b = nextint(); ``` ::: ## 程式 ```cpp= #include <bits/stdc++.h> using namespace std; #pragma GCC optimize("O3") // 務必打開 const int MAX_N = 2e5+10; const int INF = 2e9; int n, q; vector<int> v(1); // 數值陣列,(based-1) vector<vector<int>> G(MAX_N); // 樹 vector<int> sz(MAX_N); // sz[i] = i 的子樹大小為何 vector<int> parent(MAX_N); // parent[i] = i 的父節點為何 vector<int> dep(MAX_N); // dep[i] = i 的深度為何,以下稱「高」為靠近根,「低」則反之 int now_id = 0; // 計數器 vector<int> id(MAX_N); // id[i] = 將樹分解後,每個節點的新編號(based-0) vector<int> lead(MAX_N); // lead[i] = i 目前在的重鏈中最上高是誰 struct Segment_Tree{ struct Node{ int ma = -INF; }; vector<Node> arr; Segment_Tree(int _n){ arr.resize(_n*4+10); } // 將兩個節點的數值合併 Node pull(Node a, Node b){ Node c; c.ma = max(a.ma, b.ma); return c; } // 將 [ll, rr) 的數值建在線段樹中 void build(vector<int> &v, int idx = 0, int ll = 0, int rr = n){ if (rr-ll==1){ arr[idx].ma = v[ll]; return; } int mid = (ll+rr)/2; build(v, idx*2+1, ll, mid); build(v, idx*2+2, mid, rr); arr[idx] = pull(arr[idx*2+1], arr[idx*2+2]); } // 將在 pos 的節點數值改成 val void update(int pos, int val, int idx = 0, int ll = 0, int rr = n){ if (rr-ll==1){ arr[idx].ma = val; return; } int mid = (ll+rr)/2; if (pos<mid) update(pos, val, idx*2+1, ll, mid); else update(pos, val, idx*2+2, mid, rr); arr[idx] = pull(arr[idx*2+1], arr[idx*2+2]); return; } // 查詢 [ql, qr) 的最大值 Node query(int ql, int qr, int idx = 0, int ll = 0, int rr = n){ if (rr<=ql || qr<=ll) return Node(); if (ql<=ll && rr<=qr) return arr[idx]; int mid = (ll+rr)/2; return pull(query(ql, qr, idx*2+1, ll, mid), query(ql, qr, idx*2+2, mid, rr)); } }; void dfs_sz(int now, int pre){ // now = 目前的節點, pre = now 的父節點 sz[now]++; parent[now] = pre; dep[now] = dep[pre]+1; for (auto x : G[now]){ if (x==pre) continue; dfs_sz(x, now); sz[now] += sz[x]; } return; } void dfs_HLD(int now, int pre, int now_lead){ // now = 目前的節點, pre = now 的父節點, now_lead = 目前的鏈,誰是最高的節點 id[now] = now_id++; lead[now] = now_lead; // 找到重鏈應該往哪裡走 int ma = -INF, ma_id = -1; // ma_id = 哪個子節點的子樹大小最大, ma = ma_id 的子樹大小 for (auto x : G[now]){ if (x==pre) continue; if (sz[x]>ma){ ma = sz[x]; ma_id = x; } } // 如果已經沒有子節點了,就停止遞迴 if (ma_id==-1) return; // 先走重鏈,now_lead 跟現在的同一個 dfs_HLD(ma_id, now, now_lead); // 再走輕鏈,此時所有輕鏈自成一個重鏈,因此他們的 now_lead 都是自己 for (auto x : G[now]){ if (x==pre || x==ma_id) continue; dfs_HLD(x, now, x); } return; } int query(Segment_Tree &st, int x, int y){ // 全部答案要取的最大值 int ma = -INF; // 若 x 跟 y 在不同鏈上,則用類似 LCA 的感覺往上跳 while (lead[x]!=lead[y]){ // x 是比較低的點,也就是要往上跳的點 if (dep[lead[x]]<dep[lead[y]]) swap(x, y); // 更新 ma,擷取 x 到 lead[x] 之間的最大值(因為右邊是開區間,所以 +1) ma = max(ma, st.query(id[lead[x]], id[x]+1).ma); // 跳到 lead[x] 的父節點,也就是另一條鏈上 x = parent[lead[x]]; } // 若 x 跟 y 在同一條鏈上,則擷取他們之間的最大值 if (dep[x]<dep[y]) swap(x, y); ma = max(ma, st.query(id[y], id[x]+1).ma); return ma; } int main(){ // setup ios::sync_with_stdio(0), cin.tie(0); // 務必打開 // input cin >> n >> q; for (int i=1 ; i<=n ; i++){ // 記得是 based-1 int tmp; cin >> tmp; v.push_back(tmp); } for (int i=0 ; i<n-1 ; i++){ int a, b; cin >> a >> b; G[a].push_back(b); G[b].push_back(a); } // 找出每個人的子樹大小 dfs_sz(1, 0); // 蓋出 HLD dfs_HLD(1, 0, 1); // 蓋線段樹 Segment_Tree st(n); vector<int> p(MAX_N); // p[i] = 重新編號為 i 的節點的數值 for (int i=1 ; i<=n ; i++){ p[id[i]] = v[i]; } st.build(p); // queries for (int i=0 ; i<q ; i++){ int ty; cin >> ty; if (ty==1){ int s, x; cin >> s >> x; st.update(id[s], x); }else if (ty==2){ int a, b; cin >> a >> b; cout << query(st, a, b) << " "; } } return 0; } ``` ![image](https://hackmd.io/_uploads/ryoUxnyvC.png) > 執行時間比較危險,記得壓點常數