<style> .reveal .slides { font: 0.65em "Fira Mono", monospace; } .yellow { color: yellow; } </style> # 根號算法 I #### **Gino** --- ## 內容大綱 - ★☆☆☆☆ 序列分塊 — 暖身 - ★★★☆☆ 序列分塊 — 實戰 - ★★☆☆☆ 值域分塊 - ★★★★★ 根號分 case --- # [完整題單在此](https://hackmd.io/@penguin71630/r1Q1Ite_c) --- <!-- #################################################### --> ## 序列分塊 — 暖身 ---- ### 例題們 - [單點修改區間和](https://cses.fi/problemset/task/1648) - [區間修改區間和](https://codeforces.com/edu/course/2/lesson/5/2/practice/contest/279653/problem/D) ---- 我不打算直接先介紹分塊的原理 我覺得透過問題來講解會更好懂 所以先來點大家都會的 RMQ 當暖身 OwO<!-- .element: class="fragment" data-fragment-index="1" --> ---- ### [單點修改區間和](https://cses.fi/problemset/task/1648) 給一個長度為 $N$ 的陣列,你需要處理 $Q$ 筆詢問。詢問有兩種: 1. **修改**:把第 $k$ 個位置的值改成 $u$。 2. **詢問**:詢問區間 $[a, b]$ 的和。 $N, Q \leq 2 \times 10^5$ ---- 先假設你今天完全不會 BIT/線段樹 看到這個問題,你會怎麼做來加速 $O(NQ)$ 的作法?<!-- .element: class="fragment" data-fragment-index="1" --> ---- $O(NQ)$ 慢的地方在於,每次詢問都要一格一格慢慢掃過整個區間。 那要不要試試看把數字「分組」?<!-- .element: class="fragment" data-fragment-index="1" --> 維護好每一個組的總和,這樣詢問的時候就不用一格一格掃,而是一組一組掃。<!-- .element: class="fragment" data-fragment-index="2" --> P.S. 分組 = 分塊<!-- .element: class="fragment" data-fragment-index="3" --> ---- - 將序列每 $B$ 個數字分成一組($B$ 是自己決定的數字) - 每組額外維護整組內所有數字的和<!-- .element: class="fragment" data-fragment-index="1" --> ![](https://i.imgur.com/PXJ09so.png)<!-- .element: class="fragment" data-fragment-index="2" --> ###### 上圖以 $B = 3$ 為例<!-- .element: class="fragment" data-fragment-index="2" --> ---- - 對於第二種操作(詢問 $[l, r]$ 的區間和) - 我們可以將 $[l, r]$ 拆成開頭一些零碎的數字 + 中間一些塊 + 結尾一些零碎的數字 ![](https://i.imgur.com/eHckS3n.png) ---- - 以詢問 [2, 11] 為例 - 我們可以把它拆成 5 + 1 + **[3 2 4]** + **[8 7 6]** + 3 + 1<!-- .element: class="fragment" data-fragment-index="1" --> - 於是算答案的時候,我們直接掃過<!-- .element: class="fragment" data-fragment-index="2" --> - 開頭一些零碎的數字 (5 1)<!-- .element: class="fragment" data-fragment-index="3" --> - 中間的塊 (**[3 2 4]** **[8 7 6]**)<!-- .element: class="fragment" data-fragment-index="4" --> - 結尾一些零碎的數字 (3 1)<!-- .element: class="fragment" data-fragment-index="5" --> ![](https://i.imgur.com/0xZSlUw.png) ###### $5 + 1 + 9 + 21 + 3 + 1 = 40$<!-- .element: class="fragment" data-fragment-index="6" --> ---- - 修改的話,直接 $O(1)$ 更新原本的陣列還有其所在的塊的答案。 ---- - 假設把第 5 個數字修改成 10,見下圖: ![](https://i.imgur.com/Lu7tL7s.png) ---- ### 時間複雜度 - 預先處理每塊的資訊:$O(N)$。<!-- .element: class="fragment" data-fragment-index="1" --> - 一次修改:$O(O(1))$。<!-- .element: class="fragment" data-fragment-index="2" --> - 一次詢問:掃過開頭跟結尾不完整的塊 $O(B)$ + 中間的塊 $O(\frac{N}{B})$。<!-- .element: class="fragment" data-fragment-index="3" --> 總時間複雜度:$O(N + Q (B + \frac{N}{B}))$。<!-- .element: class="fragment" data-fragment-index="4" --> ---- ### $B$ 怎麼決定 $B$ 如果取太小的話(例如 2 之類的)會發現效率上還是很差 因為你還是要掃過很多東西才能算答案 ![](https://i.imgur.com/9eFdznM.png) ---- ### $B$ 怎麼決定 可是 $B$ 如果取太大的話(例如 $N$ 的一半之類的)會發現幾乎用不到塊的答案 有維護跟沒有一樣 ![](https://i.imgur.com/SStXaih.png) ---- 感覺上好像有個中間點...? ---- 「總時間複雜度:$O(N + Q (B + \frac{N}{B}))$」 中間這條 $B + \frac{N}{B}$ 是一個 U 型函數(有最小值) ---- 而算幾不等式告訴我們 $B + \frac{N}{B}$ 的最小值發生在 $B = \sqrt{N}$ 的時候<!-- .element: class="fragment" data-fragment-index="1" --> 故令 $B = \sqrt{N}$ 可得最佳複雜度 $O(N + Q \sqrt{N})$。<!-- .element: class="fragment" data-fragment-index="2" --> ---- $N, Q \leq 2 \times 10^5$ $Q \sqrt{N}$ 大概是<!-- .element: class="fragment" data-fragment-index="1" --> $9 \times 10^7$<!-- .element: class="fragment" data-fragment-index="1" --> 但分塊常數很小,因此這樣還是可以過的<!-- .element: class="fragment" data-fragment-index="2" --> ![](https://i.imgur.com/9iHzaRK.png)<!-- .element: class="fragment" data-fragment-index="2" --> ###### Code:<!-- .element: class="fragment" data-fragment-index="2" -->https://cses.fi/paste/d6e8d376cbc86fca3c5719/<!-- .element: class="fragment" data-fragment-index="2" --> ---- ### 實作 - 陣列還有塊都從 0 開始編號<!-- .element: class="fragment" data-fragment-index="1" --> - 第 i 個數字所屬的塊編號是 i/B<!-- .element: class="fragment" data-fragment-index="2" --> - 第 k 個塊的開頭是 k × B<!-- .element: class="fragment" data-fragment-index="3" --> ---- ### 實作 - 詢問區間 [l, r] 的時候要分兩種情況 - l 跟 r 在同一塊 (l/B == r/B):暴力從 l 掃到 r<!-- .element: class="fragment" data-fragment-index="1" --> ![](https://i.imgur.com/nTZTicw.png)<!-- .element: class="fragment" data-fragment-index="1" --> - l 跟 r 不在同一塊:開頭 + 中間 + 結尾<!-- .element: class="fragment" data-fragment-index="2" --> ![](https://i.imgur.com/eC4ESOe.png)<!-- .element: class="fragment" data-fragment-index="2" --> ---- ### 實作 ```cpp= int N, Q; vector<long long> v; int B; // B 是一個塊的大小 vector<ll> blks; // 維護每一塊的答案 void init() { cin >> N >> Q; B = sqrt(N); v.clear(); v.resize(N); for (auto& i : v) cin >> i; blks.clear(); blks.resize(N/B+1, 0); for (int i = 0; i < N; i++) { // 塊從 0 開始編號 // 第 i 個位置屬於第 i/B 個塊 blks[i/B] += v[i]; } } void solve() { while (Q--) { int op; cin >> op; if (op == 1) { /* 修改 */ int k; long long u; cin >> k >> u; k--; blks[k/B] += (u - v[k]); // 更新塊的答案 v[k] = u; // 更新原本陣列的答案 } else { /* 詢問 */ int l, r; cin >> l >> r; l--, r--; long long ans = 0; if (l/B == r/B) { // 特判:如果詢問左界跟右界在同一塊,那就直接暴力做 for (int i = l; i <= r; i++) ans += v[i]; } else { // 先掃過開頭零碎的一些數字 for (int i = l; i < B*(l/B+1); i++) ans += v[i]; // 再掃過中間的塊 for (int bi = l/B+1; bi < r/B; bi++) ans += blks[bi]; // 最後掃過結尾零碎的一些數字 for (int i = B*(r/B); i <= r; i++) ans += v[i]; } cout << ans << endl; } } } ``` ---- ### [區間修改區間和](https://codeforces.com/edu/course/2/lesson/5/2/practice/contest/279653/problem/D) 給一個長度為 $N$ 的陣列,一開始全部為 0,你需要處理 $M$ 筆詢問。詢問有兩種: 1. **修改**:把區間 $[l, r)$ 的所有值加上 $v$。 2. **詢問**:詢問區間 $[l, r)$ 的和。 $N, Q \leq 10^5$ ---- 一樣把序列每 $O(\sqrt{N})$ 個切成一塊 ---- 跟上一題類似,只不過單點修改變成區間修改。 還記得懶標線段樹怎麼寫嗎?<!-- .element: class="fragment" data-fragment-index="1" --> ---- - 我們一樣用懶標的想法 - 當修改的區間包含某個完整的塊時<!-- .element: class="fragment" data-fragment-index="1" --> - 就在那個塊打上懶標<!-- .element: class="fragment" data-fragment-index="1" --> - 懶標的意義是「那一塊裡面的所有數字都要更新」<!-- .element: class="fragment" data-fragment-index="2" --> ---- - 修改某個區間 $[l, r]$ 時 - 一樣可以把 $[l, r]$ 拆成一些零碎的數字 + 中間一些完整的塊 + 結尾一些零碎的數字 - **零碎的數字** $\Rightarrow$ 暴力修改<!-- .element: class="fragment" data-fragment-index="1" --> - **完整的塊**<!-- .element: class="fragment" data-fragment-index="2" --> $\Rightarrow$<!-- .element: class="fragment" data-fragment-index="2" --> <span class="yellow">修改塊的答案,並打上懶標</span><!-- .element: class="fragment" data-fragment-index="2" --> ---- - 詢問某個區間 $[l, r]$ 時 - 一樣可以把 $[l, r]$ 拆成一些零碎的數字 + 中間一些完整的塊 + 結尾一些零碎的數字 - **零碎的數字** $\Rightarrow$ $O(\sqrt{N})$ 暴力下推該塊的懶標、暴力詢問<!-- .element: class="fragment" data-fragment-index="1" --> - **完整的塊**<!-- .element: class="fragment" data-fragment-index="2" --> $\Rightarrow$<!-- .element: class="fragment" data-fragment-index="2" --> <span class="yellow">不用推懶標</span>,直接拿塊的答案<!-- .element: class="fragment" data-fragment-index="2" --> ---- ### 時間複雜度 - <span class="yellow">**預先處理每塊的資訊**:$O(N)$</span><!-- .element: class="fragment" data-fragment-index="1" --> - <span class="yellow">**一次修改**:$O(\sqrt{N})$</span><!-- .element: class="fragment" data-fragment-index="2" --> - 暴力更新開頭跟結尾不完整的塊:$O(\sqrt{N})$。<!-- .element: class="fragment" data-fragment-index="3" --> - 掃過中間 $O(\sqrt{N})$ 個塊,$O(1)$ 打上懶標:$O(\sqrt{N})$。<!-- .element: class="fragment" data-fragment-index="4" --> - <span class="yellow">**一次詢問**:$O(\sqrt{N})$</span><!-- .element: class="fragment" data-fragment-index="5" --> - 掃過開頭跟結尾不完整的塊 + 下推懶標:$O(\sqrt{N})$。<!-- .element: class="fragment" data-fragment-index="6" --> - 掃過中間的塊:$O(\sqrt{N})$。<!-- .element: class="fragment" data-fragment-index="7" --> <span class="yellow">總時間複雜度:$O(N + Q \sqrt{N})$</span><!-- .element: class="fragment" data-fragment-index="8" --> ---- 搞不太懂?舉個栗子 - $N = 11, \langle a_N \rangle = [2, 5, 1, 3, 2, 4, 8, 7, 6, 3, 1, 9]$ ![](https://i.imgur.com/f9fDEP1.png) ---- - 將區間 $[2, 11]$ 加上 $5$ ![](https://i.imgur.com/vfMidct.png) ---- - 將區間 $[2, 11]$ 加上 $5$ ![](https://i.imgur.com/nyEQo6Y.png) ###### 不完整的塊:暴力更新/完整的塊:打懶標 ---- - 將區間 $[2, 11]$ 加上 $5$ ![](https://i.imgur.com/Zr6rUL2.png) ---- - 將區間 $[1, 10]$ 加上 $3$ ![](https://i.imgur.com/rEDySgc.png) ---- - 將區間 $[1, 10]$ 加上 $3$ ![](https://i.imgur.com/085J7Z3.png) ###### 不完整的塊:暴力更新/完整的塊:打懶標 ---- - 將區間 $[1, 10]$ 加上 $3$ ![](https://i.imgur.com/BwfPn42.png) ---- - 詢問區間 $[2, 11]$ 的和 ![](https://i.imgur.com/X4eIM1E.png) ---- - 詢問區間 $[2, 11]$ 的和 ![](https://i.imgur.com/BYBVm95.png) ###### 對於不完整的塊,先暴力下推那些塊的懶標 ---- - 詢問區間 $[2, 11]$ 的和 ![](https://i.imgur.com/FJpBkyn.png) ---- - 詢問區間 $[2, 11]$ 的和 ![](https://i.imgur.com/onMhCc9.png) ###### 接下來處理詢問,對於不完整的塊暴力算,完整的直接拿該塊的答案,不用理會懶標 ---- - 詢問區間 $[2, 8]$ 的和 ![](https://i.imgur.com/V7rufEF.png) ---- - 詢問區間 $[2, 8]$ 的和 ![](https://i.imgur.com/UKYMJnj.png) ###### 對於不完整的塊,先暴力下推那些塊的懶標 ---- - 詢問區間 $[2, 8]$ 的和 ![](https://i.imgur.com/Z6gIKix.png) ---- - 詢問區間 $[2, 8]$ 的和 ![](https://i.imgur.com/E2B3LC3.png) ###### 接下來處理詢問,對於不完整的塊暴力算,完整的直接拿該塊的答案,不用理會懶標 ---- ```cpp= int n, q, B; vector<ll> v; vector<ll> blks, tag; void init() { cin >> n >> q; B = (int)sqrt(n); v.clear(); v.resize(n, 0); blks.clear(); blks.resize(n/B + (n%B != 0), 0); tag.clear(); tag.resize(n/B + (n%B != 0), 0); } inline int len(int l, int r) { return r-l+1; } inline void push(int id) { for (int i = B*id; i < min(B*(id+1), n); i++) v[i] += tag[id]; tag[id] = 0; } void solve() { while (q--) { int op; cin >> op; if (op == 1) { int l, r; ll x; cin >> l >> r >> x; r--; if (l/B == r/B) { for (int i = l; i <= r; i++) { v[i] += x; blks[l/B] += x; } } else { for (int i = l; i < B*(l/B+1); i++) { v[i] += x; blks[l/B] += x; } for (int bi = l/B+1; bi < r/B; bi++) { tag[bi] += x; blks[bi] += x * len(B*bi, min(n-1, B*(bi+1)-1)); } for (int i = B*(r/B); i <= r; i++) { v[i] += x; blks[i/B] += x; } } } else { int l, r; cin >> l >> r; r--; ll ans = 0; if (l/B == r/B) { if (tag[l/B]) push(l/B); for (int i = l; i <= r; i++) ans += v[i]; } else { if (tag[l/B]) push(l/B); for (int i = l; i < B*(l/B+1); i++) { ans += v[i]; } for (int bi = l/B+1; bi < r/B; bi++) { ans += blks[bi]; } if (tag[r/B]) push(r/B); for (int i = B*(r/B); i <= r; i++) { ans += v[i]; } } cout << ans << endl; } } } ``` --- ## 重點思路整理 ---- 跟分治型資料結構(BIT/線段樹)不同 分塊算是暴力的改進<!-- .element: class="fragment" data-fragment-index="1" --> 透過把序列分組的方法巧妙地讓複雜度降到可以在時限內 AC<!-- .element: class="fragment" data-fragment-index="2" --> 這也是今天的主題「根號算法」的由來<!-- .element: class="fragment" data-fragment-index="2" --> 而且分塊的實作比較容易,常數也小<!-- .element: class="fragment" data-fragment-index="3" --> 在 $N$ 不算太大的時候($10^5$ 以下)<!-- .element: class="fragment" data-fragment-index="4" --> 執行時間往往能和線段樹並駕齊驅甚至更少<!-- .element: class="fragment" data-fragment-index="5" --> 所以不失為一個平常解題可以考慮的方法 OwO<!-- .element: class="fragment" data-fragment-index="6" --> --- ## 休息 10 分鐘/提問 --- ## 序列分塊 — 實戰 ---- ### 例題們 - [Holes](https://codeforces.com/problemset/problem/13/E) - [中國人插隊問題](https://neoj.sprout.tw/problem/213/) - [Anton and Permutation](https://codeforces.com/problemset/problem/785/E) ---- ### [Holes](https://codeforces.com/problemset/problem/13/E) 地面上有 $N$ 個彈簧床排成一列,由左到右編號 $1 \sim N$。 每個彈簧床的彈力為 $a_i$,能讓球從第 $i$ 個位置彈到第 $i + a_i$ 個位置。 接下來有 $M$ 個事件發生。 - 0 a b:第 a 個彈簧床的彈力改為 b。 - 1 a:在第 a 個彈簧床丟一顆球,輸出它最後碰到的彈簧床編號 + 它彈了幾次。 $N, M \leq 10^5$ ---- ### [中國人插隊問題](https://neoj.sprout.tw/problem/213/) 一開始有 $N$ 個人排成一個隊伍,接下來有 $M$ 個事件發生。 - ADD i x:編號為 x 的人插隊到第 i 個位置 - LEAVE i:第 i 個位置的人離開隊伍 - QUERY i:輸出第 i 個位置的人的編號 $N, M \leq 10^5$ ---- ### [Anton and Permutation](https://codeforces.com/problemset/problem/785/E) 給你一個長度為 $N$ 的序列 $\langle a_N \rangle$,一開始 $\langle a_N \rangle = [1, 2, ..., N]$。 接下來會有 $Q$ 個操作。 每個操作會給兩個數字 $l, r \ (l < r)$,代表要交換 $a_l$ 和 $a_r$。 請在每次操作完後輸出整個序列有多少個逆敘數對。 $N \leq 2 \times 10^5, \ Q \leq 5 \times 10^4$ --- ## 值域分塊 ---- ### 例題們 - [第 Z 小](https://neoj.sprout.tw/problem/742/) - (No Judge) 數列第 $k$ 小 ---- ### [第 Z 小](https://neoj.sprout.tw/problem/742/) 你有一個容器,一開始沒有任何東西,接下來會有 $Q$ 次操作。 - 1 x y:**[加值]** 把 y 個 x 加入容器 - 2 x y:**[刪除]** 把 y 個 x 從容器刪除 - 3 z:查詢第 z 小的數字 $Q, x \leq 10^5$ $y \leq 10^9$ ---- 這題數字個數可能很大,但值域很小 直接用陣列紀錄每個數字出現幾次!<!-- .element: class="fragment" data-fragment-index="1" --> ---- - 開一個陣列 cnt[x] 紀錄目前有多少個數字 x - 加值跟刪除直接 $O(1)$ 改 cnt[x]<!-- .element: class="fragment" data-fragment-index="1" --> - 查詢?<!-- .element: class="fragment" data-fragment-index="2" --> ---- - 把值域每 $O(\sqrt{N})$ 個切成一塊(切 cnt 陣列)。 - 查詢的時候先查大塊,看第 z 個數字落在哪一塊,接著暴力查詢那一塊,找到第 z 個的具體位置。<!-- .element: class="fragment" data-fragment-index="1" --> - 大塊最多 $O(\sqrt{N})$ 個,小塊也只有 $O(\sqrt{N})$ 個數字。<!-- .element: class="fragment" data-fragment-index="2" --> - 因此單次查詢複雜度 $O(\sqrt{N})$。<!-- .element: class="fragment" data-fragment-index="3" --> - 總時間複雜度 $O(N + M\sqrt{N})$。<!-- .element: class="fragment" data-fragment-index="4" --> ---- ```cpp= int q, k; ll arr[maxn], b[maxn]; inline void modify(int x, ll val) { arr[x] += val; b[x/k] += val; } inline int query(ll z) { int bi, i; ll cnt = 0; for (bi = 0; cnt + b[bi] < z; bi++) cnt += b[bi]; for (i = bi*k; cnt + arr[i] < z; i++) cnt += arr[i]; return i; } int main() { fastio cin >> q; k = (int)sqrt(100000) + 1; while (q--) { int md; cin >> md; if (md == 1) { int x; ll val; cin >> x >> val; modify(x, val); } else if (md == 2) { int x; ll val; cin >> x >> val; modify(x, -val); } else { ll z; cin >> z; cout << query(z) << endl; } } return 0; } ``` ---- ### (NoJudge) 數列第 $k$ 小 給你 $N, x, a, b$,現有一個長度為 $N$ 的數列由以下方式生成: - $s_1 = x$ - $s_{i+1} = (a \cdot s_{i} + b) \ \mathrm{mod} \ (10^9+7)$ 有 $Q$ 筆詢問,每筆詢問給一個 $k \ (1 \leq k \leq N)$,請輸出此數列第 $k$ 小的數字。 - $1 \leq N \leq 2 \times 10^7, 1 \leq Q \leq 20$ - 時間限制:10 秒,**記憶體限制:1MB** --- ## 休息 10 分鐘/提問 --- ## 根號分 case ---- 有些問題可能存在兩種或以上的作法 而每種作法各有優缺點<!-- .element: class="fragment" data-fragment-index="1" --> 缺點是指在某些情況下該作法會 TLE 或 MLE<!-- .element: class="fragment" data-fragment-index="2" --> ---- 接下來要教大家的解題技巧 概括來說是當這樣的問題(存在兩解或多解)出現時<!-- .element: class="fragment" data-fragment-index="1" --> 我們可以取彼所長,合併兩種解<!-- .element: class="fragment" data-fragment-index="2" --> 最終會很神奇地得到通往複雜度夠好的解法<!-- .element: class="fragment" data-fragment-index="3" --> ---- 我這樣講你們聽得懂才怪 直接看例題就知道我在說什麼了 OwO --- ## 根號分 case — 例題 ---- ### 例題們 - [Array Queries](https://codeforces.com/contest/797/problem/E) - [背包問題](https://neoj.sprout.tw/problem/743/) - [Counting Triangles](https://neoj.sprout.tw/problem/252/) ---- ### [Array Queries](https://codeforces.com/contest/797/problem/E) 給長度為 $N$ 的正整數序列 $a$,接下來你要回答 $Q$ 筆詢問。 每筆詢問給 $p, k$。 有個變數 $x$ 一開始設為 $p$,每隔一秒 $x$ 就會變成 $x + a_x + k$。 輸出經過幾秒後 $x$ 會大於 $N$? $1 \leq N, Q \leq 10^5$ - 子題一:所有詢問的<!-- .element: class="fragment" data-fragment-index="1" --> $k = 0$ <!-- .element: class="fragment" data-fragment-index="1" --> - 子題二:所有詢問的<!-- .element: class="fragment" data-fragment-index="1" --> $k \leq 400$ <!-- .element: class="fragment" data-fragment-index="1" --> - 子題三:無其他限制 <!-- .element: class="fragment" data-fragment-index="1" --> ---- ### 子題一:所有詢問的 $k = 0$ - 「$x$ 一開始設為 $p$,每隔一秒 $x$ 就會變成 $x + a_x$」<!-- .element: class="fragment" data-fragment-index="1" --> - dp[i] 代表 $x$ 等於 i 的時候再過幾秒就會大於 <!-- .element: class="fragment" data-fragment-index="2" --> $N$ <!-- .element: class="fragment" data-fragment-index="2" --> - 倒著做 dp(從 dp[N] 做到 dp[1])<!-- .element: class="fragment" data-fragment-index="3" --> - if (i + a[i] > N) dp[i] = 1;<!-- .element: class="fragment" data-fragment-index="3" --> - else dp[i] = dp[i + a[i]] + 1;<!-- .element: class="fragment" data-fragment-index="3" --> - $O(N + Q)$<!-- .element: class="fragment" data-fragment-index="4" --> ---- ### 子題二:所有詢問的 $k \leq 400$ - 注意到 $400 \times 10^5 = 4 \times 10^7$,在時間和空間限制內。<!-- .element: class="fragment" data-fragment-index="1" --> - 對每個可能的 $k$(1 到 400)預先蓋好 dp 陣列。<!-- .element: class="fragment" data-fragment-index="2" --> - 詢問的時候就可以 $O(1)$ 輸出答案。<!-- .element: class="fragment" data-fragment-index="3" --> - $O(Nk + Q)$<!-- .element: class="fragment" data-fragment-index="4" --> ---- ### 子題三:$k$ 有可能很大 - 沒辦法對每個可能的 $k$ 預處理 dp 陣列,會噴到 $O(N^2)$。<!-- .element: class="fragment" data-fragment-index="1" --> - 但我們可以延續上個子題的想法。<!-- .element: class="fragment" data-fragment-index="2" --> - 對於夠小的 $k$ 我們一樣預處理 dp。<!-- .element: class="fragment" data-fragment-index="3" --> - 大的 $k$ 呢?<!-- .element: class="fragment" data-fragment-index="4" --> ---- ### 子題三:$k$ 有可能很大 - 大的 $k$ 呢? - 有發現嗎,$k$ 越來越大代表 $x$ 很快就會大於 $N$。 ### 暴力算答案!<!-- .element: class="fragment" data-fragment-index="1" --> ---- - 我們以 $\sqrt{N}$ 為分界。 - <span class="yellow">對於 $\leq \sqrt{N}$ 的 $k$ 預處理 dp 陣列。</span><!-- .element: class="fragment" data-fragment-index="1" --> - 複雜度 $O(N \sqrt{N})$。<!-- .element: class="fragment" data-fragment-index="2" --> - <span class="yellow">對於 $> \sqrt{N}$ 的 $k$ 暴力算答案。</span><!-- .element: class="fragment" data-fragment-index="3" --> - 答案不會超過 $\sqrt{N}$,因此暴力複雜度是 $O(Q \sqrt{N})$。<!-- .element: class="fragment" data-fragment-index="4" --> - $O((N + Q) \sqrt{N})$<!-- .element: class="fragment" data-fragment-index="5" --> ###### Code:<!-- .element: class="fragment" data-fragment-index="6" -->https://codeforces.com/contest/797/submission/157664240<!-- .element: class="fragment" data-fragment-index="6" --> ---- ### [背包問題](https://neoj.sprout.tw/problem/743/) 給你 $N$ 個正整數 $a_1, a_2, ..., a_N$,這 $N$ 個正整數總和為 $K$。 請分別判斷對於 $1 \sim K$ 的每個正整數,是否可以從這 $N$ 個數選一些數湊出來。 $N, K \leq 2 \times 10^5$ ---- 直接做 0/1 背包? $O(NK) \Longrightarrow$ TLE<!-- .element: class="fragment" data-fragment-index="1" --> ---- ### 奇怪的條件 $a_1 + a_2 + ... + a_N = K$ 而且 $K \leq 2 \times 10^5$ ---- ### 重要性質 如果我們以 $\sqrt{K}$ 為分界點,把 $a_i$ 分成 $\leq \sqrt{K}$ 和 $> \sqrt{K}$ 那麼會得到一個重要的性質:<span class="yellow">$> \sqrt{K}$ 的數字最多只會有 $\sqrt{K}$ 個!</span><!-- .element: class="fragment" data-fragment-index="1" --> ---- - 小數字 ($\leq \sqrt{K}$) 種類不多,最多只會有 $\sqrt{K}$ 種數字 - <span class="yellow">對小數字做有限背包</span><!-- .element: class="fragment" data-fragment-index="2" --><span class="yellow">,複雜度 $O(K \sqrt{K})$</span><!-- .element: class="fragment" data-fragment-index="3" --> - 大數字 ($> \sqrt{K}$) 個數不多,最多只會有 $\sqrt{K}$ 個數字<!-- .element: class="fragment" data-fragment-index="1" --> - <span class="yellow">對大數字做 0/1 背包</span><!-- .element: class="fragment" data-fragment-index="2" --><span class="yellow">,複雜度 $O(K \sqrt{K})$</span><!-- .element: class="fragment" data-fragment-index="3" --> ---- 總時間複雜度 $O(K \sqrt{K})$ ---- 根據 $a_1 + a_2 + ... + a_N = K$ 還可以得到一個重要性質: ### $a_i$ 的種類少於 $\sqrt{K}$ 種<!-- .element: class="fragment" data-fragment-index="1" --> 如果種類超過 $\sqrt{K}$ 種,那 $a_1 + ... + a_N$ 無論如何都會 $> K$,和題目矛盾<!-- .element: class="fragment" data-fragment-index="2" --> ---- ### 另解 因此原本的題目我們可以直接用有限背包做,不用對數字分大小<!-- .element: class="fragment" data-fragment-index="1" --> 複雜度依然是 $O(K \sqrt{K})$。<!-- .element: class="fragment" data-fragment-index="2" --> ---- ### [Counting Triangles](https://neoj.sprout.tw/problem/252/) 給一張 $N$ 點 $M$ 邊的簡單無向圖,請算出這張圖有幾個「三角形」? 三角形是指一組數對 $(x, y, z)$ 滿足 $(x, y)$、$(y, z)$、$(z, x)$ 都有邊連接。 ![](https://i.imgur.com/JB3Ede6.png) $N, M \leq 10^5$ ---- ### 核心想法 - 枚舉其中一個點,固定這個點之後枚舉剩下兩個點<!-- .element: class="fragment" data-fragment-index="1" --> - 同個三角形會被重複算三次(因為三角形有三個頂點)<!-- .element: class="fragment" data-fragment-index="2" --> - 枚舉完所有點以後答案除以 3 才是正確答案<!-- .element: class="fragment" data-fragment-index="3" --> ---- ### 重要假設 - 方便講解,假設我們能快速回答兩個點 $(u, v)$ 是否有邊連接($O(1)$ 或 $O(\log N)$ 之類的) - 具體怎麼做等等會說明<!-- .element: class="fragment" data-fragment-index="1" --> ---- ### 作法一 - 枚舉每一個點 $u$ 當頂點<!-- .element: class="fragment" data-fragment-index="1" --> - 然後枚舉其中兩個 $u$ 的鄰居,檢查他們有沒有邊連接<!-- .element: class="fragment" data-fragment-index="2" --> - 有的話就是一個三角形<!-- .element: class="fragment" data-fragment-index="2" --> - $O(d_u^2)$,其中 $d_u$ 是 $u$ 的點度<!-- .element: class="fragment" data-fragment-index="4" --> ![](https://i.imgur.com/khjOnCi.png)<!-- .element: class="fragment" data-fragment-index="3" --> ---- ### 作法二 - 一樣枚舉每一個點 $u$ 當頂點<!-- .element: class="fragment" data-fragment-index="1" --> - 然後枚舉圖上的每條邊,檢查它的兩個端點有沒有都跟 $u$ 連接<!-- .element: class="fragment" data-fragment-index="2" --> - 有的話就是一個三角形<!-- .element: class="fragment" data-fragment-index="2" --> - $O(M)$,$M$ 是整張圖的邊數<!-- .element: class="fragment" data-fragment-index="4" --> ![](https://i.imgur.com/48hYKWl.png)<!-- .element: class="fragment" data-fragment-index="3" --> ---- - 單純只用作法一或作法二都會 TLE - 作法一(枚舉兩個鄰點)碰到點度很大的點會 TLE<!-- .element: class="fragment" data-fragment-index="2" --> - 作法二(枚舉整張圖的邊)要 $O(NM)$,一樣會 TLE<!-- .element: class="fragment" data-fragment-index="3" --> ---- 那...如果把點按照度數大小分類呢? ---- - 以 $\sqrt{M}$ 為分界點 - 點度不到 $\sqrt{M}$ 稱為<!-- .element: class="fragment" data-fragment-index="1" --><span class="yellow">輕點</span><!-- .element: class="fragment" data-fragment-index="1" --> - 點度超過 $\sqrt{M}$ 稱為<!-- .element: class="fragment" data-fragment-index="2" --><span class="yellow">重點</span><!-- .element: class="fragment" data-fragment-index="2" --> ---- - 對於輕點,我們使用作法一($O(d_u^2)$ 枚舉兩個鄰點) - 輕點點度不大,比較適合作法一<!-- .element: class="fragment" data-fragment-index="1" --> - 對於重點,我們使用作法二($O(M)$ 枚舉整張圖的邊)<!-- .element: class="fragment" data-fragment-index="2" --> - 重點數量不會太多,比較適合作法二<!-- .element: class="fragment" data-fragment-index="3" --> ---- ### 很神奇的,這樣子分 case 會是好的! ---- ### 複雜度證明 這裡先提點一些重要性質<!-- .element: class="fragment" data-fragment-index="1" --> - <span class="yellow">輕點度數不會太大,是 $O(\sqrt{M})$ 的量級</span><!-- .element: class="fragment" data-fragment-index="2" --> - <span class="yellow">重點不會有太多個,不會超過 $O(\sqrt{M})$ 個</span><!-- .element: class="fragment" data-fragment-index="3" --> - 邊只有 $M$ 條,所有點的度數和為 $2M$。<!-- .element: class="fragment" data-fragment-index="4" --> - 一個重點就至少佔了 $\sqrt{M}$ 條邊<!-- .element: class="fragment" data-fragment-index="5" --> - 所以重點個數 $\leq \frac{2M}{\sqrt{M}} = 2\sqrt{M}$。<!-- .element: class="fragment" data-fragment-index="6" --> ---- ### 複雜度證明 — 輕點 - 剛剛假設我們可以快速查詢兩個點是否有邊連接,在此先設為 $O(1)$。<!-- .element: class="fragment" data-fragment-index="1" --> - 則處理所有輕點的複雜度為 $O(\sum d_u^2)$。<!-- .element: class="fragment" data-fragment-index="2" --> - 也就是所有輕點度數的平方和<!-- .element: class="fragment" data-fragment-index="2" --> - $O(\sum d_u^2) = O(\sum \sqrt{M} \cdot d_u) = O(\sqrt{M} \sum d_u) = O(M \sqrt{M})$<!-- .element: class="fragment" data-fragment-index="3" --> ### 處理輕點只要 $O(M \sqrt{M})$!<!-- .element: class="fragment" data-fragment-index="4" --> ---- ### 複雜度證明 — 重點 - 算一個重點的答案複雜度是 $O(M)$。<!-- .element: class="fragment" data-fragment-index="1" --> - 重點只有 $O(\sqrt{M})$ 個<!-- .element: class="fragment" data-fragment-index="2" --> ### 處理重點一樣只要 $O(M \sqrt{M})$!<!-- .element: class="fragment" data-fragment-index="3"--> ---- 總時間複雜度:$O(M \sqrt{M})$ 皆大歡喜 OwO ---- 總時間複雜度:$O(M \sqrt{M})$ 皆大歡喜 OwO? ---- 「假設我們可以快速查詢兩個點是否有邊連接」 對,現在我們來解決這個問題<!-- .element: class="fragment" data-fragment-index="1" --> ---- - 把存圖的 `vector<vector<int>>` 改成 `vector<set<int>>` - 這樣就可以 $O(\log N)$ 查詢<!-- .element: class="fragment" data-fragment-index="1" --> - 總複雜度會變成 $O(M \sqrt{M} \log N)$。<!-- .element: class="fragment" data-fragment-index="2" --> ---- 但其實我們可以利用一些技巧,把查詢從 $O(\log N)$ 降成 $O(1)$。 ---- ### 更好的作法 我們現在知道,給定一個點 $u$,我們可以: 1. $O(M)$ 算出圖中有幾個包含點 $u$ 的三角形<!-- .element: class="fragment" data-fragment-index="1" --> - 開一條 bool 陣列 adj,adj[i] 表示 $u, i$ 之間有沒有邊<!-- .element: class="fragment" data-fragment-index="3" --> 2. $O(d_u^2)$ 算出圖中有幾個包含點 $u$ 的三角形<!-- .element: class="fragment" data-fragment-index="2" --> - 我們只在乎所有枚舉到的點對 $(x, y)$ 有多少是 true(有邊連接)<!-- .element: class="fragment" data-fragment-index="4" --> - 因此可以離線處理,把全部詢問按照端點分類存,最後再一起做<!-- .element: class="fragment" data-fragment-index="5" --> ---- ```cpp= const int maxn = 1e5 + 2; int n, m; int deg[maxn]; // 每個點的度數 vector<int> g[maxn]; // 圖 vector<int> qry[maxn]; // 要離線處理的詢問 vector<pii> E; // 所有邊的集合 bitset<maxn> adj; ``` ---- ```cpp= int B = sqrt(m); long long ans = 0; for (int u = 0; u < n; ++u) { if (deg[u] < B) { // 輕點枚舉兩個鄰居,把詢問存起來離線處理 for (int i = 0; i < deg[u]-1; ++i) { for (int j = i+1; j < deg[u]; ++j) { qry[ g[u][i] ].emplace_back( g[u][j] ); } } } else { // 重點用 O(M) 做 for (auto& i : g[u]) adj[i] = true; for (auto& e : E) if (adj[e.first] && adj[e.second]) ans++; for (auto& i : g[u]) adj[i] = false; } } // 離線處理詢問 for (int u = 0; u < n; u++) { for (auto& i : g[u]) adj[i] = true; for (auto& v : qry[u]) if (adj[v]) ans++; for (auto& i : g[u]) adj[i] = false; } cout << ans/3 << endl; ``` --- ## 重點思路整理 ---- - 有些問題如果用不同的角度來看會有很多種作法 - 以 Counting Triangle 為例,可以枚舉鄰居/枚舉整張圖的邊<!-- .element: class="fragment" data-fragment-index="1" --> - 而單純只用一種解往往會在某些情況炸掉<!-- .element: class="fragment" data-fragment-index="2" --> - 枚舉鄰居:碰到點度很大的節點會 TLE<!-- .element: class="fragment" data-fragment-index="3" --> - 枚舉整張圖的邊:做一次 $O(M)$ 代價太高,不能做太多次<!-- .element: class="fragment" data-fragment-index="3" --> ---- - 根號算法的手法便是設定一個臨界值,小於跟大於臨界值的情況套用不同解 - 點度小的點適合「枚舉鄰居」,因為點度夠小<!-- .element: class="fragment" data-fragment-index="1" --> - 點度大的點適合「枚舉整張圖的邊」,因為這樣的點並不多<!-- .element: class="fragment" data-fragment-index="1" --> - 而會稱為「根號算法」,純粹是因為最佳複雜度帶有根號(和算幾不等式有關)<!-- .element: class="fragment" data-fragment-index="2" --> <!-- ##################################################################### --> --- ## 休息/提問/[寫題單的題目](https://hackmd.io/@penguin71630/r1Q1Ite_c)
{"metaMigratedAt":"2023-06-17T00:35:45.863Z","metaMigratedFrom":"YAML","title":"根號算法 I","breaks":true,"slideOptions":"{\"transition\":\"fade\"}","contributors":"[{\"id\":\"ac1507e0-f05c-4708-bdd2-c56d13fb0dbb\",\"add\":36586,\"del\":9449}]"}
    936 views