根號算法
credit by nkhg (t1016d) modified by yp155136
2021/06/05
課程大綱
• 暖身:折半枚舉
• RMQ(Range Minimum Query)
• 區間加值、區間求和
• 根號算法應用(一)
• 根號算法應用(二):Counting Triangles
暖身:芽芽的撲克牌
• 芽芽有 張撲克牌,每張撲克牌都有它的美好度
• 請你從這 張撲克牌挑一些,使得美好度的總和恰好為 M
•
• M、每個 、 的總和都在
芽芽的撲克牌
• 我會做
• 大約是
• 好像有點大
• QQ (?)
• 折半枚舉
芽芽的撲克牌
• 如果我們可以把問題分割成「規模相近」的兩部分
• 而且可以用「好方法」把問題的兩個部分合併
• 那就有機會藉此減少時間複雜度!(類似分治)
• 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}
芽芽的撲克牌
• 如果我們想要湊出總和為 M
• 左半邊總和分別為 {0,3,7,12,10,15,19,33}
• 右半邊總和分別為 {0,5,1,4,6,5,9,10}
• 如果左邊所取的物品,價值總和為 i
• 右邊就必須拿到 M-i
芽芽的撲克牌
• 分別考慮左邊的所有物品能湊出的價值
• 分別考慮右邊的所有物品能湊出的價值
• 需要多少時間? 分別需要
• 我們總共想要價值M
• 枚舉左邊集合的所有價值 i
• 如果在右邊找的到 M-i,那就成功了
• 如果右邊找不到M-i呢? 那就表示不存在「從左邊集合取出總和 i, 右邊集合取出總和 M-i 的方法」
芽芽的撲克牌
• 如何從右邊的集合中尋找 M-i 呢?
• 先排序! 再搜尋
• 每一個
• ->
• 雖然不太爽,但至少比原先的 快多了
芽芽的撲克牌
• 要如何找出一組可行的方案?
• Struct Poker {
• long long sum; //儲存總和
• bool used[N]; //紀錄每個元素是否用到
• }
芽芽的撲克牌
• Struct Poker { // 照sum排序
• long long sum; // 儲存總和
• int used; // 紀錄每個元素是否用到,用一個int就夠了
• } //上面其實也可以用std::pair寫
• 因為搜索的問題本身規模不會太大
• 位元運算是方便好寫的好選擇
• 很快,很節省空間
芽芽的撲克牌
• 實做範例
• 常用一個整數 mask 代表每個數字選擇 的狀態
• 例如:假設 nn = 3 如果 mask = 6 , 寫成二進制是 110 , 代表選擇 a[1] 跟 a[2]
暖身完畢
• 暖身完畢,進入正題吧~
RMQ(Range minimum query)
• 給你一個長度 N 的序列 A[0], A[1], ..., 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)
• 時間複雜度
• 特性:min(a, b, c) = min(a, min(b, c))
• 可以對一部分數字先求最小值,再從每個部份的最小值中取最小 的!
RMQ(Range minimum query)
• 一部分?
• 每連續 k 個分一塊,先把每塊的最小值算好並存下來
• 對於詢問
• 綠色每塊都先算好了,時間花費是塊數:最多
• 紅色部份每個數字跑一遍,時間花費是一塊大小:最多 2k
• 下面的說明中,綠色會是「大塊」,紅色會是「小塊」
RMQ(Range minimum query)
• 每次詢問
取個好的 k?
• 算幾不等式:
• 在 也就是 時最小
• 所以取 k 為 就有了每次詢問 的解法了
• 預處理 O(N)
RMQ(Range minimum query)
• 於是大家獲得了人生第一個根號算法 (?)
• RMQ 是很經典的問題
• 線段樹:預處理 O(N) - 詢問 O(log N)
• sparse table:預處理 O(N log N) - 詢問 O(1)
• sparse table 套 sparse table:預處理 O(N log log N) - 詢問 O(1)
• RMQ 轉成 LCA 問題
• 線性 RMQ:預處理 O(N) - 詢問 O(1)
• 資芽只會教到線段樹
實作上
實做技巧
•
實做技巧
•
區間加值、區間求和
• 給你一個長度為 N 的序列 a[0], a[1], ..., a[N - 1]
• 現在有兩種操作:
• 區間加值:給定 L, R, x ,
請把 a[L], a[L + 1], ..., a[R] 加上 x
• 區間求和:給定 L, R ,
請求出 a[L] + a[L + 1] + ... + a[R]
•
• A = {3, -5, 4, 2}
區間加值 (1, 2, 10) 後,A = {3, 5, 14, 2}
區間詢問 (0, 2) ,得到總和 3 + 5 + 14 = 22
單點加值、區間求和
• 區間加值太難了,先試試看單點加值吧
• 給你一個長度為 N 的序列 a[0], a[1], ..., a[N - 1]
• 現在有兩種操作:
• 單點加值:給定 pos, x , 請把 a[pos] 加上 x
• 區間求和:給定 L, R ,
請求出 a[L] + a[L + 1] + ... + a[R]
•
單點加值、區間求和
• 最簡單的想法:
• 單點加值 O(1)
• 區間求和 O(N)
• TLE
單點加值、區間求和
• 區間求和太慢了
• 試試看跟 RMQ 一樣,把連續 k 個東西分成一塊
• 每塊維護裡面元素的總和
• 詢問 a[x] 到 a[y] 的總和,也可以跟 RMQ 一樣,分成大塊
(綠色部份)以及小塊(紅色部份)。把這些部份的總和加起來,就 是答案。
單點加值、區間求和
• 把連續 k 個東西分成一塊,每塊維護裡面元素的總和
• 對於單點加值,除了把那個點的值更新之外,還需要把那個點所 位於的塊的總和也更新。
• 區間求和 ,單點改值 O(1)
• 取 ,可以得到 查詢,O(1) 修改的複雜度
單點加值、區間求和
• 實做範例
• 跟前面 RMQ 一樣,
常用 i / K 代表 a[i] 所在的塊
單點加值、區間求和
• 實做範例
區間加值、區間求和
• 會單點加值後,來試試看區間加值吧~
• 可不可以把區間加值看成很多單點加值呢?
• 不行
• 區間加值複雜度最慘是 O(n) * O(1) = O(n)
• 還是太慢了 QQ
• 有沒有辦法改進呢?
區間加值、區間求和
• 把連續 k 個東西分成一塊,每塊維護裡面元素的總和
• 如果把區間加值的左界、右界,也看成上面那樣(小塊 + 大塊 + 小塊),會發生什麼事情呢?
• 可以發現,除了左右兩個小塊之外,「對於大塊來說,就是全部 的元素通通加上 x」。
• 既然是全部的元素通通加上一個值,我們可不可以從中得到一 些啟發呢?
區間加值、區間求和
• 把連續 k 個東西分成一塊,每塊維護裡面元素的總和
• 除了左右兩個小塊之外,「對於大塊來說,就是全部的元素通通 加上 x」。
• 因此,對於每個大塊,除了開一個變數 sum[i] 維護這個塊的 總和之外,還可以額外開一個變數 tag[i] ,維護這個塊「全部 的維護一起被加上多少」。
• 有了 tag 後,區間加值加到大塊的部份,直接寫 tag[i] +=
x; 就行了。
區間加值、區間求和
• 把連續 k 個東西分成一塊,每塊維護裡面元素的總和
• 對於每個大塊,除了開一個變數 sum[i] 維護這個塊的總和之 外,還可以額外開一個變數 tag[i] ,維護這個塊「全部的維護 一起被加上多少」。
• 有了 tag 後,區間加值加到大塊的部份,直接寫 tag[i] +=
x; 就行了。
• 複雜度分析:區間加值 ,區間求和 ,整體 的複雜度取 ,可以得到 ,是一個可以接受的 複雜度!
區間加值、區間求和
• 實做細節頗多,這邊留給大家回去練習
有趣的應用題
• 給你 n 個正整數 a[1], a[2], ..., a[n] ,並且保證 a[1] + a[2] + ... + a[n] = K 。接著,有 Q 筆詢問,
每筆詢問會給你一個數字 x ,請你回答,x 是不是可以寫成那 n 個數字中的某些數字的總和。
•
• 假設 a = {3, 6, 2, 2}
• 7 可以寫成 3 + 2 + 2 ,11 可以寫成 3 + 6 + 2 ,但是 12 就沒有辦法寫成 a 的某些數字的總和。
• 先備知識:有限背包問題
有趣的應用題
• 好像可以 dp
• dp[x] = {true / false},代表 x 可不可以被組出來(寫成 那 n 個數字的總和)
• 或者是可以想像成有 n 個物品,第 i 個物品的重量、價值都 是 a[i] 。最後如果 dp[x] == x ,就代表 x 可以被組出 來。
• 複雜度?
• worst case O(n) * O(K) ,似乎是個 ,吃個大大的 TLE 。
• 似乎有些性質沒有用到?
有趣的應用題
• 一個怪怪的條件:a[1] + a[2] + ... + a[n] = K
• 而且
• 或許我們可以從這方面著手(?)
• 如果我們選擇一個 x ,把 a 的數字分成兩類: 和 這兩種
• 會發生什麼有趣的事情呢?
• 的數字中,最多只會有 個。
有趣的應用題
• 一個怪怪的條件:a[1] + a[2] + ... + a[n] = K
• 而且
• 的數字中,最多只會有 個。
• 因此,如果對 的數字做「有限背包問題」(統計每個數字出 現過得個數), 的數字做正常的背包問題
• 我們可以得到
• 取 ,就可以得到 的複雜度
• 是一個可以接受的複雜度!
有趣的應用題
• 從上面的題目中,我們可以進一步發現一個性質:
• 如果有 a[1] + a[2] + ... + a[n] = K 這個條件,那 麼「a 陣列最多只會有 種不同的數字」。
• 有的時候,這個性質往往可以帶來很好的事情
• 根號算法不一定是單純的把序列每 個切成一塊,也可以 是設定某個臨界值,小於臨界值做某一些事情,大於臨界值做另 外一種事情。
• 臨界值的選擇常常是根號。
• 根號算法可以用的地方比想像中多很多,也常常配合一些不同 的演算法使用,敬請期待(?)
在線與離線
對於剛剛很多題這種有多個詢問、操作的題目
• 在線 (online):程式/演算法 必須對前一個詢問或操作做出 回答,之後才能知道下一個詢問或操作。
(換句話說就是寫程式讀了一行就要馬上處理它)
(不用懷疑,一定有辦法作到這件事情XD)
• 離線 (offline):程式/演算法 不須先對前一個詢問或操作 做出回答,就能知道下一個詢問或操作
(換句話說你的程式可以先把全部輸入讀進來再來想辦法)
在線與離線
• 為什麼要區分在線與離線呢?
• 哪一個比較簡單?
在線與離線
• 將來有很多離線比較簡單的例子在等著大家 xdd
Counting Triangles
• 給你一個 N 個點、M 條邊的簡單無向圖,請算出這張圖裡有幾 個「三角形」?
• 假設點被編號為 0 到 N - 1,三角形定義為一組數對 (x, y, z) 使得 且
(x, y)、(y, z)、(z, x) 均有邊
•
•
Counting Triangles
• 假設我們能夠 O(1) 回答某個給定的 pair (x, y) 是否為圖上的一條邊
• 那麼指定任一個點 v,我們能夠
a. 在 O(M) 時間內算出圖中有幾個包含 v 的三角形 b. 在 時間內算出圖中有幾個包含 v 的三角形
(其中 d 為 v 的點度)
• 每個三角形包含恰 3 個點,
所以對所有點算出「包含它的三角形個數」加總除以 3,
就是答案了!
Counting Triangles
• 假設我們能夠 O(1) 回答某個給定的 pair (x, y) 是否為圖上的一條邊
• 按照點度是否大於 K 分兩條
• 大於 K 的: 作法 a. (O(M))
• 這種點不超過 2M / K 個,總共
• 不超過 K 的:作法 b. (O(d^2))
• 這種點最多 N 個,每個 d 最大到 K,總共
Counting Triangles
• 假設我們能夠 O(1) 回答某個給定的 pair (x, y) 是否為圖上的一條邊
• 按照點度是否大於 K 分兩條
• 大於 K 的: 作法 a. (O(M))
• 這種點不超過 2M / K 個,總共
• 不超過 K 的:作法 b. (O(d^2))
• 這種點最多 N 個,每個 d 最大到 K,總共 O(N * K^2) ???
Counting Triangles
• 假設我們能夠 O(1) 回答某個給定的 pair (x, y) 是否為圖上的一條邊
• 按照點度是否大於 K 分兩條
•
• 又是算幾砸下去,取
• 得到
• 於是這題被完美的解決了(?)
Counting Triangles
•
假設
我們能夠 O(1) 回答某個給定的 pair (x, y) 是否為圖上的一條邊• 那麼指定任一個點 v,我們能夠
a. 在 O(M) 時間內算出圖中有幾個包含 v 的三角形
• 開一條 bool 陣列 adj,adj[i] 表示 v 與 i 之間是否有邊
b. 在 時間內算出圖中有幾個包含 v 的三角形
(其中 d 為 v 的點度)
• 我們只在乎這邊給出的所有 (x, y) 詢問總共有幾個為 true 而且照 a. 的作法,只要一堆連續的詢問有共同端點我們就會做了
離線!
把全部詢問照端點分類存起來最後再做Counting Triangles
• 實做的部份會等到最後面的時候再來討論
Counting Triangles
• 另一個結論可能比較簡潔但思路似乎沒那麼自然的作法
(不過它或許會給大家一些啟示)
• 對於一條邊 (u, v),我們可以用 O(d[u] + d[v]) = O(max(d_u, d_v)) 算出包含這條邊的三角形個數(d_u, d_v 是 u 和 v 的點度)
• 只要一開始把每個點的相鄰點排序過,就可以好好的在 O(d_u + d_v) 內算出 了
• 如果 (u, v) 中點度較大的那個點,我們有它的相鄰矩陣,那麼同一件 事可以做到 O(min(d_u, d_v))
Counting Triangles
• 把點度 的點叫做「重點」,其他叫「輕點」
• 重點只有
• 所以把「重點」對所有點的相鄰矩陣建出來
• 空間也只要
• 對所有邊計算它被幾個三角形包含
• 輕點-輕點:用 O(max(d_u, d_v)) 的作法,也才
• 輕點-重點:用 O(min(d_u, d_v)) 的作法,也才
• 重點-重點:用 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 最多只有 個
• 所以以 d_u 為 cost 的邊的花費加總起來也才
•
• 重點只有 個,它們的總花費就是
• 三種情況都在 內,解決!
Counting Triangles
• 把點度 的點叫做「重點」,其他叫「輕點」
• 重點只有 個
• 所以把「重點」對所有點的相鄰矩陣建出來
• 空間也只要
• 對所有邊計算它被幾個三角形包含
• 輕點-輕點:用 O(max(d_u, d_v)) 的作法,也才
• 輕點-重點:用 O(max(d_u, d_v)) 的作法,也才 ...?
• 重點-重點:用 O(min(d_u, d_v)) 的作法,也才
Counting Triangles
Counting Triangles
• 把點度 的點叫做「重點」,其他叫「輕點」
• 重點只有 個
• 所以把「重點」對所有點的相鄰矩陣建出來
• 空間也只要
• 對所有邊計算它被幾個三角形包含
• 輕點-輕點:用 O(max(d_u, d_v)) 的作法,也才
• 輕點-重點:用 O(min(d_u, d_v)) 的作法,也才
• 重點-重點:用 O(max(d_u, d_v)) 的作法,也才 ...?
Counting Triangles
Counting Triangles
• 重點整理與啟發
• 第一種作法枚舉點、第二種作法枚舉邊
• 找出枚舉後的複雜度關鍵是什麼,對那個東西分大小
• 第一種作法用到的技巧
• 離線
• 可重複利用並快速歸零的 bool array
• 第二種作法用到的技巧
• 「重點」很少,可以維護一個「重點」X「所有點」的二維陣列
• 小心估複雜度
Counting Triangles
• 有一天,你在路上撿到一份報紙
• 報紙上寫著:「假設可以 O(1) 回答 (x, y) 是否為圖中的一個 邊。對於每條邊 (u, v) ,你花 O(min(d_u, d_v)) 去算出包 含這條邊的三角形個數。只要這樣做,你就可以 AC counting triangles 了。假設題目條件的限制跟前面一樣」
• 那份報紙的敘述真的是正確的嗎?
• 如果是的話,請試著證明看看
• 如果不是的話,請試著構造一個反例
• 鼓勵大家這時候按個暫停,想想看這樣是不是正確的
Counting Triangles
• 這個作法是好的,最終的複雜度為
• 想法說明如下:
• 不失一般性假設一條邊 (u, v) 中,d_u <= d_v
• 如果 u 是輕點,那麼最慘為 ,整體複雜度為
• 如果 u 是重點,考慮到符合 d_u <= d_v 的點最多只會有 個,因此產生出來的詢問邊數最多只有
而因為重點最多只有 個,因此最終的複雜度為
• 優點是非常的好寫(只需要處理均攤 O(1) 查詢一條邊有沒有 出現在圖中)
• 複雜度的證明相較起來,沒有那麼的直觀
Counting Triangles
• 實做細節如下:
Counting Triangles
• 實做細節如下: