電資圈接力 Challenge
本來想寫連通分量跟 tarjan 的介紹,但我發現可能會寫得又臭又長,加上我不會畫畫,於是就決定來介紹更酷的東西。
第一篇教學文/
廢話不多說,直接進入正文
懶人包:已經會的話可以直接跳過這一部分。
要有一個可以產生亂數的工具,首先必須備齊三大工具:
1. 亂數種子(Random Seed)
亂數的生成幾乎都是用一些複雜的數學公式(偽隨機),而種子的功能就是做為這些公式的某些初始參數。
常用的種子是時間資訊,取用方法有 time(NULL)
或是 chrono::steady_clock::now().time_since_epoch().count()
。
另外也可以用 std::random_device
,這東西會抓系統的一些隨機資訊來產生亂數,詳細介紹可以看上面那篇參考資料。
.
2. 引擎(Generator)
隨機算法的核心,把種子餵給他後,就可以幫你產生一連串的亂數。
個人最常用的是 std::mt19937
,背後的演算法是梅森旋轉演算法。
宣告一個引擎的語法:引擎 名稱(種子);
例如:std::mt19937 gen( time(NULL) );
其他引擎可以參考上面那篇文。
.
3. 數值分布(Distribution)
分布的功能就是將引擎產生的數值,依照我們的需求轉換為各種機率分布,例如均勻分布、常態分布、二項分布等等。
基本上如果沒有特別的需求,使用 uniform distribution
(均勻分布)就夠了。
如果是使用整數,語法是 std::uniform_int_distribution<int> 分布名稱(最小值, 最大值)
。
如果是使用小數,語法是 std::uniform_real_distribution<double> 分布名稱(最小值, 最大值)
。
舉例:std::uniform_int_distribution<int> dis(0, 100);
而產生一個亂數的語法:分布(引擎)
其他數值分布可以參考上面的文章。
.
題目來源:改編自 2020 資訊之芽團體賽
給一個正整數 ,令 ,請計算 的結果。
,測試資料共 筆。
完蛋,看起來好難喔,怎麼辦 QQ?
別忘了,解一道題目最重要的步驟便是好好觀察題目的性質。
注意到 ,並且答案只有 跟 兩種。
因此我們可以不管 ,直接隨機決定要輸出 還是 就好。
這個做法的 AC 機率 。期望上只要丟 8 次就會過了,夠有緣的甚至一發爽爽 AC。
類題練習
延續例題,只是測試資料從 筆變成 筆。
提示一
🥧 = pie
提示二
pie =
作法
,和例題的作法一樣,有 的機率可以 AC,期望只要丟 256 次就行了。
先備知識:前綴和
輸入一個長度為 的陣列 ,接下來有 筆詢問。
第 筆詢問有兩個數字 ,問每種數字在區間 出現次數是否恰好為 的倍數?
對於每筆詢問,輸出一行 YES 或 NO。
測資只有一筆。
首先先盯著這個問題,然後會發現一個關鍵的性質 — ,且答案只有兩種。類似剛才所學到的作法,枚舉這三筆詢問要輸出的是 YES 或 NO,最多只要丟 8 次就會過了。不要跟我說什麼 O(NQ) 暴力統計出現次數就可以解決我不聽我不聽我不聽
.
接著是加強版:
輸入一個長度為 的陣列 ,接下來有 筆詢問。
第 筆詢問有兩個數字 ,問每種數字在區間 出現次數是否恰好為 的倍數?
對於每筆詢問,輸出一行 YES 或 NO。
有個很聰明的作法,用文字不太好形容,直接舉例子。
假設陣列是 。
讓 映射到任意數字 , 映射到任意數字 。
定義陣列 如下:
陣列 | 1 | 2 | 1 | 1 | 2 | 1 | 2 | 2 |
---|---|---|---|---|---|---|---|---|
陣列 |
概念就是,若數字為 ,其對應到的數字為 ,若該數字由左往右出現第 「 的倍數」次,則它在 陣列中的值就要被設為 ,反之則設為 。
有了 陣列,該如何判斷答案呢?
詢問的時候,若每種數字在區間 出現次數皆為 3 的倍數,
這時就輸出 YES,否則輸出 NO。
而只要套用前綴和的技巧, 回答 ,就可在 的時間內解決這題。
.
然而這作法還是有問題,因為 的時候答案不一定是 YES。
舉例
陣列 1 1 1 2 陣列 讓 ,詢問 便會判斷錯誤。
問題在於 中每個數字的映射範圍不能太小,否則很容易就會發生判斷錯誤的情況。
這時只要保證 的每個數字隨機映射到的數字夠大、夠分散,例如隨機映射到 之間的數字,就有很高的機率能夠避開判斷失誤,穩穩 AC。
類題練習
先備知識:二分搜
經典例題:資芽 OJ 794 — 區間絕對眾數
若某個數字 在一個序列中出現次數 嚴格超過 序列長度的一半,稱 為該序列的絕對眾數。
輸入一個長度為 的正整數序列 ,接下來有 筆詢問。
每筆詢問輸入 ,輸出區間 的絕對眾數,若不存在請輸出 。
如果我在一個區間隨機戳一個數字的話…
.
建議直接點開以下提示,因為我本人想了一年多才在偶然獲得的提示下想到這題的關鍵。
對,整整一年。
.
如果我在一個區間隨機戳一個數字的話,那我戳中絕對眾數的機率會是 。
.
那我們不妨在這個區間內隨機選 個數字,然後一一檢查它們是不是絕對眾數,而連續 次都戳不中絕對眾數的機率略小於 ,基本上可以忽略這微小的機率,假裝一定會戳中。
因此單筆詢問的正確率為 ,連續 筆詢問都正確的機率為 ,若 ,則 AC 的機率約為 ,幾乎可以一發 AC,一次不過第二次也會過。如果有人可以連續兩次都 WA 的,請截圖 + 傳 code 私訊我,請你吃三碗拉麵。
現在還有一個問題要解決:要怎麼快速知道一個數字在某個區間內是不是絕對眾數?或者,要怎麼快速知道一個數字在某個區間內出現幾次?
有個比較直覺的作法,先開一個大小 的 vector 陣列 , 存 這個數字在原序列中依序出現的位置。
.
舉例如下:
1 2 3 4 5 6 1 2 1 1 2 2 則 pos 陣列如下:
1 1, 3, 4 2 2, 5, 6
要詢問一個數字 的在區間 的出現次數,可以換個角度,改成詢問 中有幾個數字介於 之間。
由於 嚴格遞增,因此對 做二分搜,搜尋 「第一個大於等於 的數字」的位置,以及 「第一個大於 的數字」的位置。兩個搜到的答案相減,就是 在該區間出現的次數。
.
舉例如下:
沿用上例的 pos 陣列,假設詢問 1 在區間 出現幾次,
則可以反過來問 中,有幾個數字介於 之間。而符合條件的數字以紅色標示:
1 1, 3, 4 2 2, 5, 6 這件事情可以用二分搜做到。
懶的寫二分搜的話可以用 lower_bound 函式,我是怪人我每次二分搜都還是會自己刻。
.
這個做法的總複雜度為 , 是每次詢問挑的數字個數(上面的作法 )。
如果會 TLE 的話可以把 改小一點,會 WA 的話就改大一點。
.
這題還有另外一個非隨機做法,不僅速度更快(經實測時間是隨機的一半),而且正確率 ,有興趣的可以上網搜尋「戰鬥線段樹」,也是一個超酷的作法。
或是參考台中一中 x 資奧二階大佬 abc864197532 的文章:
https://abc864197532.github.io/2021/02/07/tioj-2140/
隨機有趣的題目遠不止這些,甚至經典分治題 —最近點對— 也有隨機演算法。
使用電資圈接力的 tag 以解鎖更多