根號算法
by nkhg (t1016d)
RMQ(Range minimum query)
• 給你一個長度 N 的序列 A[0], A[2], ..., A[N - 1]
• 緊接著 Q 筆詢問,每筆詢問是一個數對 x, y
• 請你回答 min{ A[x], A[x + 1], ..., A[y] }
• A = {5, 2, 8, 1, 7}
• Query = {(0, 2), (1, 4), (2, 2)}
• 答案:2 1 8
RMQ(Range minimum query)
• 時間複雜度 O(QN)
• 特性:min(a, b, c) = min(a, min(b, c))
• 可以對一部分數字先求最小值,再從每個部份的最小值中取最小 的!
RMQ(Range minimum query)
• 一部分?
• 每連續 k 個分一組,先把每組的最小值算好並存下來
• 對於詢問
• 綠色每塊都先算好了,時間花費是塊數:最多 N/k
• 紅色部份每個數字跑一遍,時間花費是一塊大小:最多 2k
RMQ(Range minimum query)
• 每次詢問 O(N/k + k) 取個好的 k?
• 算幾不等式:N/k + k <= 2*sqrt(N/k * k) = 2*sqrt(N)
• 在 N/k = k 也就是 k = sqrt(N) 時最小
• 所以取 k 為 sqrt(N) 就有了每次詢問 O(sqrt(N)) 的解法 了
• 預處理 O(N)
RMQ(Range minimum query)
• 於是大家獲得了人生第一個根號算法 (?)
• RMQ 是很經典的問題
• 線段樹:預處理 O(N) - 詢問 O(log N)
• sparse table:預處理 O(N log N) - 詢問 O(1)
• 線性 RMQ:預處理 O(N) - 詢問 O(1)
• 資芽只會教到線段樹
實作上
中國人插隊問題
• 一開始給一個長度 N 的序列(index 1 到 N)
• 接著 Q 次操作,每次可能是
a. 請在第 i 個位子插入數字 x(index >= i 的數字往後一格)
b. 拔掉第 i 個位子的數字(index > i 的數字往前一格)
c. 請你回答第 i 個位子的數字是多少
• 例如
• 初始:1, 2, 3, 4, 5
• 操作:(a, 1, 6), (a, 7, 7), (c, 4), (b, 3), (b, 3), (c, 5)
• 過程:
• 6, 1, 2, 3, 4, 5
• 6, 1, 2, 3, 4, 5, 7 (輸出 3)
• 6, 1, 3, 4, 5, 7
• 6, 1, 4, 5, 7 (輸出 7)
中國人插隊問題
• 天上傳來一道聲音,叫你再試試看剛剛那樣「分塊」
中國人插隊問題
• 嘗試讓每塊儲存了 K 個數字 (若 K = 4)
A1 A2 A3 A4
A5 A6 A7 A8
A9 Aa Ab Ac
1
2
3
中國人插隊問題
• 一開始給一個長度 N 的序列(index 1 到 N)
• 接著 Q 次操作,每次可能是
a. 請在第 i 個位子插入數字 x(index >= i 的數字往後一格)
b. 拔掉第 i 個位子的數字(index > i 的數字往前一格)
c. 請你回答第 i 個位子的數字是多少
• 例如
• 初始:1, 2, 3, 4, 5
• 操作:(a, 1, 6), (a, 7, 7), (c, 4), (b, 3), (b, 3), (c, 5)
• 過程:
• 6, 1, 2, 3, 4, 5
• 6, 1, 2, 3, 4, 5, 7 (輸出 3)
• 6, 1, 3, 4, 5, 7
• 6, 1, 4, 5, 7 (輸出 7)
中國人插隊問題
• 增加一個數字(第 3 個位置插入 B)
A1 A2 A3 A4
A5 A6 A7 A8
A9 Aa Ab Ac
1
2
3
中國人插隊問題
• 增加一個數字(第 3 個位置插入 B)
A1 A2 A3 A4
A5 A6 A7 A8
A9 Aa Ab Ac
1
2
3
中國人插隊問題
• 增加一個數字(第 3 個位置插入 B)
A1 A2 A3 A4
A5 A6 A7 A8
A9 Aa Ab Ac
1
2
3
B
中國人插隊問題
• 增加一個數字(第 3 個位置插入 B)
A1 A2 A3 A4
A5 A6 A7 A8
A9 Aa Ab Ac
1
2
3
B
中國人插隊問題
• 增加一個數字(第 3 個位置插入 B)
A1 A2 A3
A4 A5 A6 A7 A8
A9 Aa Ab Ac
1
2
3
B
中國人插隊問題
• 增加一個數字(第 3 個位置插入 B)
A1 A2 A3
A4 A5 A6 A7 A8
A9 Aa Ab Ac
1
2
3
B
中國人插隊問題
• 增加一個數字(第 3 個位置插入 B)
A1 A2 A3
A4 A5 A6 A7
1
2
3
B
A8 A9 Aa Ab Ac
中國人插隊問題
• 增加一個數字(第 3 個位置插入 B)
A1 A2 A3
A4 A5 A6 A7
1
2
3
B
A8 A9 Aa Ab
4 Ac
中國人插隊問題
• 一開始給一個長度 N 的序列(index 1 到 N)
• 接著 Q 次操作,每次可能是
a. 請在第 i 個位子插入數字 x(index >= i 的數字往後一格)
b. 拔掉第 i 個位子的數字(index > i 的數字往前一格)
c. 請你回答第 i 個位子的數字是多少
• 例如
• 初始:1, 2, 3, 4, 5
• 操作:(a, 1, 6), (a, 7, 7), (c, 4), (b, 3), (b, 3), (c, 5)
• 過程:
• 6, 1, 2, 3, 4, 5
• 6, 1, 2, 3, 4, 5, 7 (輸出 3)
• 6, 1, 3, 4, 5, 7
• 6, 1, 4, 5, 7 (輸出 7)
中國人插隊問題
• 拔掉一個數字(拔掉第 3 個位置)
A1 A2 A3 A4
A5 A6 A7 A8
A9 Aa Ab Ac
1
2
3
中國人插隊問題
• 拔掉一個數字(拔掉第 3 個位置)
A1 A2 A4
A5 A6 A7 A8
A9 Aa Ab Ac
1
2
3
中國人插隊問題
• 拔掉一個數字(拔掉第 3 個位置)
A1 A2 A4
A5 A6 A7 A8
A9 Aa Ab Ac
1
2
3
中國人插隊問題
• 拔掉一個數字(拔掉第 3 個位置)
A1 A2 A4 A5
A6 A7 A8
A9 Aa Ab Ac
1
2
3
中國人插隊問題
• 拔掉一個數字(拔掉第 3 個位置)
A1 A2 A4 A5
A6 A7 A8
A9 Aa Ab Ac
1
2
3
中國人插隊問題
• 拔掉一個數字(拔掉第 3 個位置)
A1 A2 A4 A5
A6 A7 A8 A9
Aa Ab Ac
1
2
3
中國人插隊問題
• 拔掉一個數字(拔掉第 3 個位置)
A1 A2 A4 A5
A6 A7 A8 A9
Aa Ab Ac
1
2
3
中國人插隊問題
• 一開始給一個長度 N 的序列(index 1 到 N)
• 接著 Q 次操作,每次可能是
a. 請在第 i 個位子插入數字 x(index >= i 的數字往後一格)
b. 拔掉第 i 個位子的數字(index > i 的數字往前一格)
c. 請你回答第 i 個位子的數字是多少
• 例如
• 初始:1, 2, 3, 4, 5
• 操作:(a, 1, 6), (a, 7, 7), (c, 4), (b, 3), (b, 3), (c, 5)
• 過程:
• 6, 1, 2, 3, 4, 5
• 6, 1, 2, 3, 4, 5, 7 (輸出 3)
• 6, 1, 3, 4, 5, 7
• 6, 1, 4, 5, 7 (輸出 7)
中國人插隊問題
• 第 i 個數字:第 _______________ 塊的第 ___________ 個
A1 A2 A3 A4
A5 A6 A7 A8
A9 Aa Ab Ac
1
2
3
中國人插隊問題
• 第 i 個數字:第 (i - 1) / K + 1 塊的第 (i - 1) % K 個
A1 A2 A3 A4
A5 A6 A7 A8
A9 Aa Ab Ac
1
2
3
中國人插隊問題
• 如果有一個可以 O(1) 從頭尾兩端刪除或增加的資料結構來維 護每一塊的話
(內部的查找、刪除都是與儲存的大小成線性)
• 插入:該塊內部 O(K)、之後每塊都要修一下 O(N / K)
• 刪除:該塊內部 O(K)、之後每塊都要修一下 O(N / K)
• 詢問:該塊內部 O(K)
• 找到第 i 塊?
• 把一堆這種資料結構開成陣列:O(1)
• 把一堆這種資料結構串起來(linked list):O(N / K)
• 總之全部合起來就是 O(K + N / K) => 又是根號了!
中國人插隊問題
•
如果
有一個可以 O(1) 從頭尾兩端刪除或增加的資料結構來 維護每一塊的話(內部的查找、刪除都是與儲存的大小成線性)
• 方法很多,例如 雙向鏈結串列(Doubly linked list)
在線與離線
對於剛剛兩題這種有多個詢問、操作的題目
• 在線 (online):程式/演算法 必須對前一個詢問或操作做出 回答,之後才能知道下一個詢問或操作。
(換句話說就是寫程式讀了一行就要馬上處理它)
• 離線 (offline):程式/演算法 不須先對前一個詢問或操作 做出回答,就能知道下一個詢問或操作
(換句話說你的程式可以先把全部輸入讀進來再來想辦法)
在線與離線
• 為什麼要區分在線與離線呢?
• 哪一個比較簡單?
在線與離線
在線與離線
• 將來有很多離線比較簡單的例子在等著大家 A_A
Counting Triangles
• 給你一個 N 個點、M 條邊的簡單無向圖,請算出這張圖裡有幾 個「三角形」?
• 假設點被編號為 0 到 N - 1,三角形定義為一組數對 (x, y, z) 使得 0 <= x < y < z < N 且
(x, y)、(y, z)、(z, x) 均有邊
• 3 <= N <= 10^5
• 0 <= M <= 10^5
Counting Triangles
• 假設我們能夠 O(1) 回答某個給定的 pair (x, y) 是否為圖上的一條邊
• 那麼指定任一個點 v,我們能夠
a. 在 O(M) 時間內算出圖中有幾個包含 v 的三角形 b. 在 O(d^2) 時間內算出圖中有幾個包含 v 的三角形
(其中 d 為 v 的點度)
• 每個三角形包含恰 3 個點,
所以對所有點算出「包含它的三角形個數」加總除以 3,
就是答案了!
Counting Triangles
• 假設我們能夠 O(1) 回答某個給定的 pair (x, y) 是否為圖上的一條邊
• 按照點度是否大於 K 分兩條
• 大於 K 的: 作法 a. (O(M))
•
• 不超過 K 的:作法 b. (O(d^2))
•
Counting Triangles
• 假設我們能夠 O(1) 回答某個給定的 pair (x, y) 是否為圖上的一條邊
• 按照點度是否大於 K 分兩條
• 大於 K 的: 作法 a. (O(M))
• 這種點不超過 2M / K 個,總共 O(M^2 / K)
• 不超過 K 的:作法 b. (O(d^2))
• 這種點最多 N 個,每個 d 最大到 K,總共 O(N * K^2)
Counting Triangles
• 假設我們能夠 O(1) 回答某個給定的 pair (x, y) 是否為圖上的一條邊
• 按照點度是否大於 K 分兩條
• 大於 K 的: 作法 a. (O(M))
• 這種點不超過 2M / K 個,總共 O(M^2 / K)
• 不超過 K 的:作法 b. (O(d^2))
• 這種點最多 N 個,每個 d 最大到 K,總共 O(N * K^2)
Counting Triangles
• 假設我們能夠 O(1) 回答某個給定的 pair (x, y) 是否為圖上的一條邊
• 按照點度是否大於 K 分兩條
• O(M^2 / K + MK)
• 又是算幾砸下去,取 K = sqrt(M)
• 得到 O(M sqrt(M))
• 於是這題被完美的解決了(?)
Counting Triangles
•
假設
我們能夠 O(1) 回答某個給定的 pair (x, y) 是否為圖上的一條邊• 那麼指定任一個點 v,我們能夠
a. 在 O(M) 時間內算出圖中有幾個包含 v 的三角形
•
b. 在 O(d^2) 時間內算出圖中有幾個包含 v 的三角形
(其中 d 為 v 的點度)
•
Counting Triangles
•
假設
我們能夠 O(1) 回答某個給定的 pair (x, y) 是否為圖上的一條邊• 那麼指定任一個點 v,我們能夠
a. 在 O(M) 時間內算出圖中有幾個包含 v 的三角形
• 開一條 bool 陣列 adj,adj[i] 表示 v 與 i 之間是否有邊
b. 在 O(d^2) 時間內算出圖中有幾個包含 v 的三角形
(其中 d 為 v 的點度)
•
Counting Triangles
•
假設
我們能夠 O(1) 回答某個給定的 pair (x, y) 是否為圖上的一條邊• 那麼指定任一個點 v,我們能夠
a. 在 O(M) 時間內算出圖中有幾個包含 v 的三角形
• 開一條 bool 陣列 adj,adj[i] 表示 v 與 i 之間是否有邊
b. 在 O(d^2) 時間內算出圖中有幾個包含 v 的三角形
(其中 d 為 v 的點度)
• 我們只在乎這邊給出的所有 (x, y) 詢問總共有幾個為 true
而且照 a. 的作法,只要一堆連續的詢問有共同端點我們就會做了
Counting Triangles
•
假設
我們能夠 O(1) 回答某個給定的 pair (x, y) 是否為圖上的一條邊• 那麼指定任一個點 v,我們能夠
a. 在 O(M) 時間內算出圖中有幾個包含 v 的三角形
• 開一條 bool 陣列 adj,adj[i] 表示 v 與 i 之間是否有邊
b. 在 O(d^2) 時間內算出圖中有幾個包含 v 的三角形
(其中 d 為 v 的點度)
• 我們只在乎這邊給出的所有 (x, y) 詢問總共有幾個為 true
而且照 a. 的作法,只要一堆連續的詢問有共同端點我們就會做了
離線!
把全部詢問照端點分類存起來最後再做Counting Triangles
• 開一條 bool 陣列 adj,adj[i] 表示 v 與 i 之間是否有邊 這條陣列怎麼寫?
Counting Triangles
• 開一條 bool 陣列 adj,adj[i] 表示 v 與 i 之間是否有邊 這條陣列怎麼寫?
Counting Triangles
• 開一條 bool 陣列 adj,adj[i] 表示 v 與 i 之間是否有邊 這條陣列怎麼寫?
Counting Triangles
• 另一個結論可能比較簡潔但思路似乎沒那麼自然的作法
(不過它或許會給大家一些啟示)
• 對於一條邊 (u, v),我們可以用 O(max(d_u, d_v)) 算出包含這條 邊的三角形個數(d_u, d_v 是 u 和 v 的點度)
• 只要一開始把每個點的相鄰點排序過
• 如果 (u, v) 中點度較大的那個點,我們有它的相鄰矩陣,那麼同一件 事可以做到 O(min(d_u, d_v))
Counting Triangles
• 把點度 >= sqrt(M) 的點叫做「重點」,其他叫「輕點」
• 重點只有 2*sqrt(M) 個
• 所以把「重點」對所有點的相鄰矩陣建出來
• 空間也只要 O(N sqrt(M))
• 對所有邊計算它被幾個三角形包含
• 輕點-輕點:用 O(max(d_u, d_v)) 的作法,也才 O(sqrt(M))
• 輕點-重點:用 O(min(d_u, d_v)) 的作法,也才 O(sqrt(M))
• 重點-重點:用 O(min(d_u, d_v)) 的作法,也才 ...?
Counting Triangles
• 對一個重點 u,如果計算某條邊 (u, v) 的 cost O(min(d_u, d_v)) 是 d_u
• 表示這條邊連接的點 v 具有 d_v > d_u
• 這種點 v 最多只有 2M / d_u 個
• 所以以 d_u 為 cost 的邊的花費加總起來也才
• (2M / d_u) * d_u = 2M = O(M)
• 重點只有 O(sqrt(M)) 個,它們的總花費就是 O(M sqrt(M))
• 三種情況都在 O(M sqrt(M)) 內,解決!
Counting Triangles
• 把點度 >= sqrt(M) 的點叫做「重點」,其他叫「輕點」
• 重點只有 2*sqrt(M) 個
• 所以把「重點」對所有點的相鄰矩陣建出來
• 空間也只要 O(N sqrt(M))
• 對所有邊計算它被幾個三角形包含
• 輕點-輕點:用 O(max(d_u, d_v)) 的作法,也才 O(sqrt(M))
• 輕點-重點:用 O(max(d_u, d_v)) 的作法,也才 O(sqrt(M))
• 重點-重點:用 O(min(d_u, d_v)) 的作法,也才 ...?
Counting Triangles
Counting Triangles
• 把點度 >= sqrt(M) 的點叫做「重點」,其他叫「輕點」
• 重點只有 2*sqrt(M) 個
• 所以把「重點」對所有點的相鄰矩陣建出來
• 空間也只要 O(N sqrt(M))
• 對所有邊計算它被幾個三角形包含
• 輕點-輕點:用 O(max(d_u, d_v)) 的作法,也才 O(sqrt(M))
• 輕點-重點:用 O(min(d_u, d_v)) 的作法,也才 O(sqrt(M))
• 重點-重點:用 O(max(d_u, d_v)) 的作法,也才 ...?
Counting Triangles
Counting Triangles
• 重點整理與啟發
• 第一種作法枚舉點、第二種作法枚舉邊
• 找出枚舉後的複雜度關鍵是什麼,對那個東西分大小
• 第一種作法用到的技巧
• 離線
• 可重複利用並快速歸零的 bool array
• 第二種作法用到的技巧
• 「重點」很少,可以維護一個「重點」X「所有點」的二維陣列
• 小心估複雜度
邪惡收集大作戰
• 你有 N 個物品,每個物品都有它的邪惡值 A_i
• 請你從這 N 個物品中挑一些,使得邪惡值總和為 K
• N <= 40
• K、每個 A_i、A_i 的總和都在 [1, 10^18]
邪惡收集大作戰
• 我會做 2^N
• 2^40 大約是 10^12
邪惡收集大作戰
• 如果我們可以把問題分割成「規模相近」的兩部分
• 而且可以用「好方法」把問題的兩個部分合併
• 那就有機會藉此減少時間複雜度!
• 40個物品…任意分割成兩個大小類似的集合
邪惡收集大作戰
3 7 12 5 1 4
• 左半邊可以組合出什麼組合呢
• {}、{3}、{7}、{12}、{3,7}、{3,12}、{7,12}、{3,7,12}
• 總和分別為 {0,3,7,12,10,15,19,33}
• 那麼, 右半邊呢
• {}、{5}、{1}、{4}、{5,1}、{1,4}、{5,4}、{5,1,4}
• 總和分別為 {0,5,1,4,6,5,9,10}
邪惡收集大作戰
• 如果我們想要湊出總和為 K
• 左半邊總和分別為 {0,3,7,12,10,15,19,33}
• 右半邊總和分別為 {0,5,1,4,6,5,9,10}
• 如果左邊所取的物品,價值總和為 i
• 右邊就必須拿到 M-i
邪惡收集大作戰
• 分別考慮左邊的所有物品能湊出的價值
• 分別考慮右邊的所有物品能湊出的價值
• 需要多少時間? 分別需要 O(2^(N/2))
• 我們總共想要價值M
• 枚舉左邊集合的所有價值 i
• 如果在右邊找的到 M-i,那就成功了
• 如果右邊找不到M-i呢? 那就表示不存在「從左邊集合取出總和 i, 右邊集合取出總和 M-i 的方法」
邪惡收集大作戰
• 如何從右邊的集合中尋找 M-i 呢?
• 先排序! 再搜尋
• 每一個 O(log(2^(N/2)))
• O(2^20 * log2 (2^20)) -> 2 * 10^7
• 雖然不太爽,但至少比原先的 O(2^40) 快多了
邪惡收集大作戰
• 要如何找出一組可行的方案?
• Struct XDDD {
• long long sum; //儲存總和
• bool used[N]; //紀錄每個元素是否用到
• }
邪惡收集大作戰
• Struct XDDD { //照sum排序
• long long sum; //儲存總和
• int used; //紀錄每個元素是否用到,用一個int就過了
• }
• 因為搜索的問題本身規模不會太大
• 位元運算是方便好寫的好選擇
• 很快,很節省空間
題目
https://oj.icpc.tw/c/28/D (緊接著有飲料盃)
• 給你一個 N 個點、M 條邊的簡單無向圖
• Q 筆詢問,每筆詢問輸入一個環,請計算這個環有幾個「弦」
• N, M, 每個詢問的環大小總和 <= 3 * 10^5
題目
SRM 675 Div.1 Middle
• 給你 n, x0, a, b,表示有一個長度 n 的數列:
• x[0] = x0
• for (int i = 1; i < n; i++)
• x[i] = (x[i - 1] * a + b) % (10^9 + 7)
• 你要回答 Q 個詢問,每個詢問給你一個 k (0 <= k < n),
請你回答數列 x 中第 k 小的數字(k=0 為最小)
• n <= 5 * 10^6
• Q <= 100
• 時限 10sec、額外儲存空間限制 1MB