CSP-S2024 因為不會智障貪心而考崩潰錯失一等的小夥不想再被別人看不起,故作此部落格以總結解題技巧。
此外,為了增強騙分能力,我還總結了一下隨機化演算法的一些東西,以及隨機化貪心的使用方法。
- 貪心篇
- 基礎模型
- 鄰位關係的處理方法
- 反悔堆
- 隨機化篇
- 普通隨機化
- color-coding
- 模擬退火
- 隨機化貪心
- 隨機化貪心原理
- 經典隨機化貪心習題
貪心篇
基礎模型
初學者學貪心時必然會接觸到四個經典的區間貪心問題:
區間選點問題:數軸上有 \(n\) 個區間 \([l_i,r_i]\),要求標記儘可能少的點,使得每個區間內都至少有 \(k\) 個點被標記。
解決策略:把所有區間 \([l_i,r_i]\) 按照 \(r_i\) 從小到大進行排序。對於排序後的第一個區間 \([l'_1,r'_1]\),我們將這個區間最靠右的 \(k\) 個點進行標記,即對 \([r'_1-k+1,r'_1]\) 的所有點進行標記,接著對於第 \(2\) 個區間 \([l'_2,r'_2]\),我們計算當前區間中已經被標記過的點的數量 \(m\),若 \(k\leq m\),則跳到下一個區間,否則就在 \([l'_2,r'_2]\) 中最靠右的 \(k-m\) 個未被標記過的點進行標記,以此類推,我們就能使得 \(n\) 個區間中都至少包含 \(k\) 個標記點,且標記點數量最少。
實現方法:資料大時,可以使用數線段樹維護區間標記點的個數,每次對區間右側的若干點進行標記時,我們可以二分一個位置 \(x\) 使得 \([r_i-x+1,r_i]\) 在此次修改之後全為標記點。此時我們線上段樹上的修改就變成了區間賦 \(1\)。
區間覆蓋問題:數軸上有 \(n\) 個區間 \([l_i,r_i]\),要求選出儘可能少的區間,使得它們可以完全覆蓋 \([L,R]\) 這個區間。
解決策略:把所有區間 \([l_i,r_i]\) 按照 \(l_i\) 從小到大排序,我們維護一個指標 \(t\) 表示 \([t,R]\) 的區間還未被覆蓋,初始有 \(t=L\)。接著對於當前的 \(t\),我們在排序後的區間中列舉每一個 \(l\) 小於等於 \(t\) 的區間,記錄這些區間中最大的右端點 \(r_m\),此時我們就貪心的選擇這個右端點最大的區間,讓 \(t\leftarrow r_m\),然後重複上述過程。
實現方法:沒有複雜的細節,只需要移動 \(t\) 指標以及當前列舉到的下標的指標 \(j\)。
區間劃分問題:數軸上有 \(n\) 個區間 \((l_i,r_i)\),要求把它們劃分成儘可能少的集合,使得每個集合內的區間兩兩不相交。
解決策略:把所有區間 \((l_i,r_i)\) 按照 \(l_i\) 從小到大排序,然後先開一個集合 \(S_1\),初始時 \(S_1\) 裡存放著 \((l'_1,r'_1)\)。接著對於每一個區間 \((l'_i,r'_i)\),如果它能放到已經存在的某一個集合 \(S_j\) 裡面,那麼我們就放進去,如果有多個滿足條件的集合 \(S_j\),我們就隨便選一個;否則就新開一個集合,並把 \((l'_i,r'_i)\) 放進去。
實現方法:直接執行上述過程的時間複雜度最劣是 \(\mathcal{O}(n^2)\) 的。我們可以維護一個優先佇列,裡面存放每一個已經存在的集合 \(S_i\),令 \(R_i\) 表示 \(\max_{(l,r)\in S_i}\{r\}\),我們就按照 \(R_i\) 將它們排序,\(R_i\) 越小就越靠近隊首,每次就判斷隊首的 \(R_i\) 是否大於當前區間的 \(l\),如果是,則在佇列裡新加一個集合,否則就把隊首的 \(R_i\) 替換成 \(r\)。
不相交區間問題:數軸上有 \(n\) 個區間 \((l_i,r_i)\),要求選出儘可能多的區間,使得它們兩兩之間不相交。
解決策略:把所有區間 \((l_i,r_i)\) 按照 \(r_i\) 從小到大排序,在列舉的過程中,我們面對當前的區間能選就選,即維護當前選擇的若干個區間的最大右端點 \(R\),如果 \(l'_i\geq R\),則 \(i\) 可選,否則不選。
實現方法:模擬上述過程。
同時還要另一道非常經典的烙餅問題:
烙餅問題形式化:給定 \(n\) 個正整數 \(a_i\),每次可以至多選擇其中的 \(m\) 個數並讓它們的值減少 \(1\),問最少需要多少次才能讓所有的 \(a_i\) 都等於 \(0\)。
解決策略:每次選擇 \(a_i\) 最大的 \(m\) 個數,然後將它們減 \(1\)。
實現方法:直接模擬還是有點難度的,但是我們有一個關於此題的結論:此問題的答案應為 \(\max\{\max_{i=1}^n\{a_i\},\dfrac{\sum_{i=1}^n a_i}{m}\}\)。
在大賽中還是有不少題套了這些貪心模板的。需要注意的一點是,雖然說它們是模板,但是它們的思維難度並不亞於另一批陌生的貪心題,因此無論如何還是要把它們牢牢掌握的,不然場上推不出來的話就會吃大虧。
鄰位交換/Exchange Argument
在眾多貪心問題中,有大部分都涉及到了鄰位關係上的處理。而這種處理的目的基本上都是為了比較兩者的優先順序,把對答案產生的貢獻更大的排在前面。定義 \(val_i\) 為一個權值比較引數 Exchange Argument,對於相鄰兩位 \(i,i+1\),如果 \(val(i+1)>val(i)\),則 \(i+1\) 的優先順序大於 \(i\),因此我們會進行鄰位交換。說的直白一點,其實就是按照某種偏序關係對所有元素進行排序。一般來說,我們處理鄰位關係的難點就在於 Exchange Argument 上,即我們應該按照什麼資訊去排序。
那麼我們如何找到這個排序的資訊呢?我們可以用假設法,假設交換 \(i,i+1\) 的次序,然後比較交換之前兩者的整體貢獻 \(f\) 和交換之後兩者的整體貢獻 \(f'\),如果 \(f\) 劣於 \(f'\),那麼顯然會考慮交換。由此我們可以找到 \(f\) 和 \(f'\) 分別對應的代數式,然後透過不等關係判斷 \(f\) 和 \(f'\) 之間的優劣關係,從而進行鄰位交換。上文我們有提到“按照某種偏序關係對所有元素進行排序”,因此根據偏序關係的傳遞性,對於任意兩個位置 \(x,y\) 的元素,若 \(x<y\),則 \(x\) 必然是優於 \(y\) 的,如果此時交換 \(x,y\) 位置上的元素必然會使得答案不優,這是鄰位交換最核心的性質,我們可以用它去理解很多貪心排序的正確性。
排序完成之後,我們還有可能會面臨其它的問題,比如貪心排序後進行 DP,或者在排序後的序列上套資料結構維護各種資訊。這些就都要靠讀者自己的能力了。
[ABC366F] Maximum Composition
先思考這樣一個問題:如果我們已經確定了這 \(k\) 個函式的編號集合 \(P\),那麼我們該怎樣規定函式巢狀的順序 \((P_1,p_2,\dots,P_k)\) 使得 \(f_{P_k}(f_{P_k-1}(\dots f_{P_1}(1)\dots))\) 的值最大(注意這裡為了方便思考,是跟原題順序的下標相反的)。
這裡我們就可以考慮鄰位交換法,透過一種排序方式使得最終形成的序列 \((P_1,P_2,\dots,P_k)\) 按照順序進行函式巢狀可以得到最優解。對於相鄰的兩個函式 \(f_i(x)\) 和 \(f_{i+1}(x)\),我們計算交換兩者巢狀順序前後的具體變化。變化之前是 \(f_{i+1}(f_i(x))\),變化之後是 \(f_i(f_{i+1}(x))\),我們直接帶入函式式:
若要交換 \(i\) 和 \(i+1\) 的次序,則需滿足變化後的函式值更小,即:
因此我們可以把每個函式的 \(a,b\) 放到同一個結構體 node
中,定義兩個 node
變數 \(x,y\) 之間的 \(x<y\) 關係表示 \(a_xb_{y}+b_x<a_{y}b_x+b_{y}\),然後我們就能直接 sort
排序了。那麼在最開始的問題中,我們就可以直接用排序後的函式來一次巢狀,根據鄰位交換的性質,我們就能保證得到最優解。
由此,如果知道了我們要選擇的函式的集合 \(P\),那麼我們就有固定的方法求的它們的最優解 \(f(P)\)。現在我們重新考慮原問題,其實就是找到一個大小為 \(k\) 的函式集合使得它的 \(f\) 值最大。我們可以用上述方法對所有的 \(n\) 個函式進行排序,然後 DP 一下。設 \(dp_{i,j}\) 表示在排序後的前 \(i\) 個函式中選擇 \(j\) 個依次巢狀可以得到的最優解,且保證第 \(i\) 個函式必選,用 \(f'(i)\) 表示排序後的第 \(i\) 個函式。有如下轉移:
由於 \(f'\) 是單調的,我們只需要維護一下 \(dp_{i,j}\) 的一個字首最大值即可進行一輪最佳化,時間複雜度為 \(\mathcal{O}(n\log n+nk)\)。
[TJOI2013] 拯救小矮人
不難發現一個人梯內部的排列方式也是非常重要的。如果我們確定了人梯要選擇的人的集合為 \(S\),我們就應該按照某種順序將 \(S\) 重排,然後依次組成人梯,這樣在使得其餘人爬走之後,\(S\) 內部的人依然可以伸出手臂從而夠到頂。
如何知道排序的方式?我們可以透過假設法計算一下。對於人體從上往下第 \(i\) 和第 \(i+1\) 個相鄰的人,假定他們都可以出去,那麼我們要做的就是儘可能的減少 \(i,i+1\) 作為整體時,他們腳底下踩的人梯的高度 \(h\),因為這樣就能用更少的人組成高度為 \(h\) 的人梯,同時就有更多人可以逃出去。在交換 \(i,i+1\) 之前,\(i\) 會踩著 \(i+1\) 上去,則有 \(h+a_{i+1}+a_i+b_i\geq H\),等 \(i\) 走後又有 \(h+a_{i+1}+b_{i+1}\geq H\)。交換了 \(i,i+1\) 之後,同理得到 \(h'+a_i+a_{i+1}+b_{i+1}\geq H\) 以及 \(h'+a_i+b_i\geq H\)。整理一下得到:
如果有 \(h'\leq h\),則表示交換之後 \(i,i+1\) 腳下的人梯的高度下界更小,這就代表交換之後是一定不劣的。轉換一下:
\(h'\leq h\Rightarrow \min\{a_{i+1}+b_{i+1}+a_i,a_i+b_i\}\geq \min\{a_i+b_i+a_{i+1},a_{i+1}+b_{i+1}\}\)
因此我們就按照上面的關係式判斷鄰位是否交換,用 sort
排序得到最終的序列。
但是我們並不知道人梯 \(S\) 到底由哪些人組成,以及 \(S\) 在整個過程中的變化。因此考慮 DP,設 \(dp_{i,j}\) 表示排序後前 \(i\) 個人中有 \(j\) 個人已經出去時,剩餘的人梯的最大高度,注意這裡的人梯應該是所有 \(n\) 個人中沒有出去的,即應該包括 \((i,n]\) 的人。那麼顯然有:
求答案的時候就是找到最大的 \(i\) 使得 \(dp_{n,i}\) 有值,從 \(n\) 到 \(1\) 依次列舉判斷即可。
Complete the Projects (easy version)
首先很顯然的一點就是我們肯定會先把 \(b_i\) 非負的專案全部完成,然後再去完成 \(b_i\) 為負的專案。那麼對於 \(b_i\) 非負的專案,我們可以將它們按照 \(a_i\) 從小到大排序,然後依次完成它們,如果中途有個專案的 \(a_i\) 無法達到,那麼就是無解。
接著我們再去考慮 \(b_i\) 為負的,但是注意這裡我們就不能按照 \(a_i\) 從大到小排序了,因為有這樣一類資料:假設有兩種專案 \((a_i,b_i)\) 分別為 \((10,-8),(9,-1)\),初始的 rating 為 \(11\),那麼我們就只能先完成 \((9,-1)\) 的專案,再完成 \((10,-8)\) 的專案,如果先完成 \((10,-8)\),那麼另一個專案就無法完成了。
這裡我們就要考慮鄰位交換了。設兩個相鄰的位置為 \(i,i+1\),表示第 \(i\) 個計劃完成的專案和第 \(i+1\) 個計劃完成的專案,考慮什麼時候需要交換兩者的位置。不難想到這樣一種判定方式,由於我們的要求是完成每一個專案,那我們肯定是希望到了第 \(i\) 個專案時,當前剩餘 rating 要滿足條件的限制更寬鬆,這就和上一道題很像了。設 \(r\) 為原始位置的 rating 下限,\(r'\) 為交換位置後的 rating 下限。有 \(r=\max\{a_i,a_{i+1}-b_i\}\) 和 \(r'=\max\{a_{i+1},a_i-b_{i+1}\}\),若要交換,則需滿足:
按照上述不等關係排序,然後在過程中記錄當前 rating,如果遇到無法完成的專案那就是無解。再判一下完成所有專案後 rating 是否變成了負數,剩餘的情況就是有解了。
Complete the Projects (hard version)
貪心的大致思路和 easy version 是一樣的,但是這道題中對於 \(b_i<0\) 的部分要用一下 \(\mathcal{O}(nV)\) 的 DP,設 \(dp_{i,j}\) 表示選到排序後的第 \(i\) 個 \(b_i<0\) 的專案,當前剩餘 rating 為 \(j\) 的完成專案數的最大值。首先有 \(dp_{i+1,j}\leftarrow dp_{i,j}\),接著,若 \(j\geq a_{i+1}\) 且 \(j+b_{i+1}\geq 0\),則有 \(dp_{i+1,j+b_{i+1}}\leftarrow \max\{dp_{i+1,j+b_{i+1}},dp_{i,j}+1\}\)。DP 完之後取一個最大值,然後加上 \(b_i\geq 0\) 的答案即可。
皇后遊戲
非常經典的一道貪心題。
由於題目是求一種排列順序使得某個值最小,那我們就可以嘗試貪心。考慮鄰位交換,對於一對鄰位 \(i,i+1\),思考該透過什麼資訊對它們進行分析。我們認真讀一下題,發現需要最小化的值其實就是 \(c\) 序列的末尾元素。又觀察到 \(c_i\) 的轉移式中存在 \(\max\{c_{i-1},\sum_{j=1}^i a_j\}\) 這一項,如果我們可以使得 \(c_{i-1}\) 儘可能小,那麼 \(c_i\) 的值必然不可能更大。因此我們的大致思路是對於一對鄰位 \(i,i+1\),計算交換 \((a_i,b_i),(a_{i+1},b_{i+1})\) 前後的 \(c_{i+1}\) 哪個更小。記 \(pre\) 表示當前排列中的 \(c_{i-1}\),\(S\) 表示 \(\sum_{j=1}^{i-1} a_j\),則交換前後的 \(c_{i+1}\) 分別為:
容易發現轉換後的兩個式子中都涉及到了對 \(pre+b_i+b_{i+1}\) 取 \(\max\),如果我們刪掉這一項,最後得到的結果顯然不可能更劣。因此直接刪掉它。
現在,如果我們要交換 \(i,i+1\) 的資訊,那麼就需要滿足交換後的 \(c_{i+1}\) 更小,令 \(t=S+a_i+b_i+a_{i+1}+b_{i+1}\),則:
因此我們只需要比較 \(\min\{a_{i+1},b_i\}\) 和 \(\min\{a_i,b_{i+1}\}\) 即可。但是還需要注意到另一件事,如果 \(\min\{a_{i+1},b_i\}=\min\{a_i,b_{i+1}\}\),那麼我們就要考慮最小化 \(\sum_{j=1}^i a_j\),此時我們就要按照 \(a_i\) 從小到大對它們進行排序。
時間複雜度 \(\mathcal{O}(n\log n)\)。
反悔堆
反悔堆通常會在這樣的問題中被使用:要在 \(n\) 個元素中選擇儘可能多的元素,且選出來的元素的集合 \(S\) 滿足某個條件,求選出來的元素個數的最大值。
試想這樣一種情況,假設對於一個集合 \(A\) 我們已經求出了可以選出的元素個數最大值,且這些被選擇的元素集合為 \(B\),現在我們考慮在集合 \(A\) 中加入另外一個元素 \(x\) 且需要求出 \(A\) 此時最多可以選出的元素,那麼只會有三種情況出現:將 \(x\) 直接加入 \(B\);用 \(x\) 替換 \(B\) 中的某一個元素;不選擇 \(x\) 這個元素。我們分別思考一下:
- 將 \(x\) 直接加入 \(B\):我們只需要判斷一下 \(B\cup \{x\}\) 這個集合是否滿足條件即可。
- 用 \(x\) 替換 \(B\) 中的某一個元素:我們可以找出原來的 \(B\) 中負面影響最大的元素 \(u\)(這裡的負面影響因題而異,後面可以藉助例題理解),如果我們發現 \(x\) 的負面影響是嚴格少於 \(u\) 的,即 \(x\) 的決策優於 \(u\),則我們可以直接用 \(x\) 把 \(u\) 替換掉,這個操作稱為反悔操作。
- 不選擇 \(x\) 這個元素:如果上面兩個情況都不能滿足,則我們就不再考慮將 \(x\) 放到 \(B\) 裡面。
對於第一種情況,我們通常都是可以快速判斷的,而對於第二種情況,我們可以維護一個優先佇列去儲存 \(B\) 集合,然後對 \(B\) 中的元素按照優劣性進行排序,使得堆頂元素為 \(B\) 中最劣的,我們每次就判斷 \(x\) 和這個棧頂元素哪一個更優即可。這個堆就是反悔堆,即維護那些可能被執行反悔操作的元素的堆。
運用到題目之中,我們需要在 \(n\) 個元素中選出儘可能多的元素,那我們可以按照某種特定順序依次將每個下標加入 \(A\),然後同時用反悔堆去維護 \(B\) 集合。這樣就能從每一層的區域性最優解一直更新到全域性最優解。
此外,反悔堆也有可能會使用到“最大化權值之和”這一類的題目之中,不過大致思路基本上都是類似的。下面就用幾道例題來詳細的展示一下反悔堆的作用。
[JSOI2007] 建築搶修
這是關於反悔堆的最經典的題目之一。我們的目標是在 \(n\) 個建築中選擇 \(m\) 個建築,這 \(m\) 個建築需滿足以下條件:在將這 \(m\) 個建築按照 \(T_2\) 從小到大排序之後,\(\forall i\in [1,m]\),有 \(\sum_{j=1}^i T_1(p_j)\leq T_2(p_i)\)。我們需要最大化這個 \(m\)。因此我們可以把所有建築先按照 \(T_2\) 從小到大排序,那麼問題就相當於選擇一個儘可能長的子序列,使得依次加入子序列中的元素之後滿足條件。
於是考慮反悔貪心,設排序後在前 \(i\) 個元素中可以選出的最長的合法子序列為 \(S\),我們思考如何得到前 \(i+1\) 個元素的最長合法子序列 \(S'\)。根據反悔操作的基本思路,我們先看 \(S\cup \{p_{i+1}\}\) 是否是合法的,這裡要判斷的就是 \(\sum_{x\in S}T_1(x)+T_1(p_{i+1})\leq T_2(p_{i+1})\),我們只需要維護一下 \(S\) 內元素的 \(T_1\) 之和即可。如果上面這個條件不滿足,我們就嘗試反悔。
如何進行反悔操作?設要被替換的元素為 \(a\),此時我們只考慮 \(p_{i+1}\) 的限制 \(\sum_{x\in S}T_1(x)-T_1(a)+T_1(p_{i+1})\leq T_2(p_{i+1})\),由此我們可以找到 \(S\) 中 \(T_1\) 最大的元素 \(u\),如果它滿足 \(\sum_{x\in S}T_1(x)-T_1(u)+T_1(p_{i+1})\leq T_2(p_{i+1})\),則我們直接刪掉 \(u\) 這一元素,然後在 \(S\) 末尾加入 \(p_{i+1}\),這樣便完成了一次反悔操作。那如果 \(S\) 中的次大元素也滿足條件,我們是否可以用 \(p_{i+1}\) 替換掉它的?這樣做在當前是可行的,但是我們需要考慮到對後面的影響,因此我們在 \(|S|\) 最大的情況下,必須保證 \(S\) 內 \(T_1\) 的和是儘可能小的。
那麼問題就可以解決了。用堆維護 \(T_1\) 最大的被選擇元素,以及一個變數 \(t\) 表示被選擇元素的 \(T_1\) 之和,按照上面的過程模擬一下即可。
[AGC018C] Coins
先思考一下兩種權值該怎麼做(即 \(z=0\) 的情況)。現在我們需要從 \(x+y\) 個元素中選擇 \(x\) 個元素取 \(a_i\) 權值,剩餘的都是取 \(b_i\) 權值,那麼我們就嘗試反悔貪心,令 \(S\) 為取 \(a_i\) 權值的元素集合,保證 \(|S|=x\),初始就把編號在 \([1,x]\) 中的元素全部存入 \(S\),然後依次將 \([x+1,x+y]\) 的元素納入我們考慮的範圍。設當前被納入考慮範圍的元素是 \(i\),如果我們要用 \(i\) 去替換掉 \(S\) 中的某個元素 \(j\),為了保證答案最優,我們需要滿足 \(\sum_{x\in S}a_x-a_j+b_j+a_i> \sum_{x\in S}+b_i\),整理可得 \(a_i-b_i>a_j-b_j\)。因此我們用一個反悔堆維護 \(S\) 且將 \(S\) 按照 \(a_x-b_x\) 的值排序,使得堆頂的 \(a_x-b_x\) 最小。每次要考慮一個新的元素 \(i\) 的時候,如果 \(a_i-b_i\) 比堆頂元素的 \(a_x-b_x\) 更大,那就把堆頂元素刪掉並加入 \(i\) 元素。
考慮完所有 \(x+y\) 個元素過後得到的 \(S\) 即為最優情況下取 \(a_i\) 權值的元素集合。不難發現,上述反悔貪心的過程所求的其實就是所有 \(x+y\) 個元素中 \(a_i-b_i\) 最小的 \(x\) 個元素。因此我們可以直接把它們按照 \(a_i-b_i\) 從小到大排序。這個思路其實還可以透過鄰位交換法得到。
現在回到原問題,我們可以考慮一個轉換:先假定所有元素都取 \(a_i\) 權值,現在我們要讓其中的任意 \(y\) 個元素取 \(b_i\) 權值,再讓剩餘部分中的任意 \(z\) 個元素取 \(c_i\) 權值。這顯然可以和我們最開始考慮的兩種權值的情況掛鉤,即令 \(p_i=b_i-a_i,q_i=c_i-a_i\),我們要在所有元素中選 \(y\) 個取 \(p_i\) 權值,選 \(z\) 個取 \(q_i\) 權值,剩餘的取 \(0\),要求最大化權值之和。最後加上 \(\sum_{i=1}^{x+y+z}a_i\) 即為最終答案。
根據兩種權值的問題的結論,我們按照 \(p_i-q_i\) 的方法對所有元素進行排序,那麼必然存在一個分割線 \(mid\) 使得 \(y\) 個取 \(p_i\) 的元素在 \([1,mid]\) 中,\(z\) 個取 \(q_i\) 權值的元素在 \((mid,x+y+z]\) 中。因此我們用反悔堆預處理 \(pre_i\) 和 \(suf_i\) 分別表示 \([1,i]\) 中 \(p\) 最大的 \(y\) 個元素的 \(p\) 權值之和,\([i,x+y+z]\) 中 \(q\) 最大的 \(z\) 個元素的 \(q\) 權值之和。最終的答案即為 \((\sum_{i=1}^{x+y+z}a_i)+(\sum_{i=y}^{x+y}pre_i+suf_{i+1})\)。
Cardboard Box
我們發現星星的數量只有 \(6\times 10^5\),這代表著我們可以對它們進行列舉。假設我們已經知道了收集 \(m\) 顆星星的最優解 \(w\) 以及這個解中各個關卡所收集的星星數量 \(S_i\),那麼我們就可以思考收集 \(m+1\) 顆星星的最優解 \(w'\) 以及每個關卡手機的的星星數量 \(S'_i\)。
根據反悔貪心的基本思路,我們會考慮“加入”和“替換”兩種情況,這裡我們也嘗試分開討論它們:
-
選擇一個未獲得兩顆星的關卡並多收集一顆星星。即令 \(S'=S\),然後選擇 \(S'\) 中一個不等於 \(2\) 的值 \(S'_i\) 執行 \(S'_i\leftarrow S'_i+1\) 操作。如果這個值原本為 \(0\),貪心地想,我們答案的增量 \(\Delta\) 必然為 \(\begin{aligned} \min_{S_x=0}\{a_x\} \end{aligned}\);同理,如果這個值原本為 \(1\),增量 \(\Delta\) 的值就會是 \(\begin{aligned}\min_{S_x=1}\{b_x-a_x\} \end{aligned}\)。這兩種最小值都是可以用反悔堆去維護的。
-
放棄一個已經被收集的星星,並另外選擇兩個未被收集的星星。其中,已經被收集的星星必然出現在 \(S_i\geq 1\) 的關卡中,設這個關卡為 \(u\),而兩個未被收集的星星應當都出現在同一個 \(S_i=0\) 的關卡中,設這個關卡為 \(v\)。先思考應該放棄哪個被收集的星星,此時必然是選擇代價最劣的一顆去作為替換,可以分為 \(S_v=1/2\) 兩種情況,如果 \(S_v=1\),則最劣的代價為 \(\begin{aligned}\max_{S_x=1}\{a_x\} \end{aligned}\),如果 \(S_v=2\),則最劣的代價為 \(\begin{aligned}\max_{S_x=2}\{b_x-a_x\} \end{aligned}\)。再思考應該用哪個關卡的星星去替換,此時必然是選擇代價最優的,考慮到兩顆星星必須在同一個關卡中,則我們的最優代價為 \(\begin{aligned}\min_{S_x=0}\{b_x\} \end{aligned}\)。分 \(S_u=1,S_v=0\) 和 \(S_u=2,S_v=0\) 兩種情況討論,然後取最優值即可。整理一下,這裡需要用 \(3\) 個不同的反悔堆去儲存三種代價。
綜上所述,我們需要維護 \(5\) 個反悔堆去維護 \(S_x=0/1/2\) 時分別需要用到的極值,然後在上面的所有情況中比較出最優解。但是由於 \(S_x=0\) 時各需要用兩個堆,如果一個堆中的元素 \(a_i\) 被刪除,那麼對應的另一個堆中關於 \(i\) 的元素 \(b_i-a_i\) 也會被刪除,這樣就變成了可刪除型別的堆。我們可以把堆換成 multiset
,這樣既能維護每種極值,也能簡單的用 erase
刪除掉對應的元素。時間複雜度 \(\mathcal{O}(n\log n)\)。
[NOI2019] 序列
我們發現題目就是讓我們欽定恰好 \(L\) 個下標的 \(a_i,b_i\) 同時被選,然後在剩下的 \(a_i,b_i\) 中選出 \(k-L\) 個最大的,即保證 \(a,b\) 中選出的元素都有恰好 \(k\) 個。普通的反悔貪心題一般都只有一個“恰好”的限制,但是這道題有兩個,我們思考怎麼兼顧兩種限制。
一個經典的思想就是對於兩個限制 \(A,B\),我們可以先設定出一種滿足 \(A\) 但不一定滿足 \(B\) 的狀態,然後透過不斷調整使得 \(A,B\) 都能被滿足,俗稱調整法。這個方法也體現在了[省選聯考 2021 A 卷] 矩陣遊戲,我們可以構造一組 \(a\) 使得 \(b_{i,j}=a_{i,j}+a_{i+1,j}+a_{i,j+1}+a_{i+1,j+1}\) 成立,然後再透過差分約束使得 \(0\leq a_{i,j}\leq 10^6\)。
在這裡我們先把所有 \(a_i,b_i\) 分別從大到小排序,並分別選出排序後 \(a,b\) 中的前 \(k\) 個元素,這樣就能滿足“恰好 \(k\) 個”的限制,設此時有 \(m\) 個下標 \(i\) 滿足其對應的 \(a_i,b_i\) 都被選擇,若 \(m\geq L\),則現在的狀態也滿足限制 \(B\),否則我們就可以反悔貪心,把 \(a_i,b_i\) 同時被選的下標 \(i\) 的個數從 \(m\) 轉移到 \(m+1\),一直轉移到 \(L\) 為止。
現在我們就來思考有哪些情況可以使得 \(m\leftarrow m+1\),一共有 \(4\) 種情況:
- 取消一個被選擇的 \(a_i\) 且 \(i\) 需滿足 \(b_i\) 沒有被選,接著重新選擇一個 \(a_j\) 且 \(j\) 需滿足 \(b_j\) 已經被選。代價為 \(a_j-a_i\)。
- 取消一個被選擇的 \(b_i\) 且 \(i\) 需滿足 \(a_i\) 沒有被選,接著重新選擇一個 \(b_j\) 且 \(j\) 需滿足 \(a_j\) 已經被選。代價為 \(b_j-b_i\)。
- 取消一對同時被選擇的 \(a_i,b_i\),接著重新選擇一個 \(a_j\) 和 \(b_k\),其中 \(j\) 需滿足 \(b_j\) 已經被選,\(k\) 需滿足 \(a_k\) 已經被選。代價為 \(a_j+b_k-a_i-b_i\)。
- 取消一個被選擇的 \(a_i\) 和一個被選擇的 \(b_j\),其中 \(i\) 需滿足 \(b_i\) 沒有被選,\(j\) 需滿足 \(a_j\) 沒有被選,接著重新選擇一對 \(a_k,b_k\)。代價為 \(a_k+b_k-a_i-b_j\)。
注意“重新選擇”的一個或兩個元素需要滿足當前狀態下還沒有被選擇。
整理一下,我們需要維護 \(6\) 個堆:
- 堆 \(1\):儲存 \(a_i\) 沒有被選但是 \(b_i\) 已經被選的下標 \(i\)。排序方式應使得堆頂為其中 \(a_i\) 最大的 \(i\)。
- 堆 \(2\):儲存 \(b_i\) 沒有被選但是 \(a_i\) 已經被選的下標 \(i\)。排序方式應使得堆頂為其中 \(b_i\) 最大的 \(i\)。
- 堆 \(3\):儲存 \(a_i,b_i\) 都沒有被選擇的下標 \(i\)。排序方式應使得堆頂為其中 \(a_i+b_i\) 最大的 \(i\)。
- 堆 \(4\):儲存 \(a_i\) 已經被選但是 \(b_i\) 沒有被選的下標 \(i\)。排序方式應使得堆頂為其中 \(a_i\) 最小的 \(i\)。
- 堆 \(5\):儲存 \(b_i\) 已經被選但是 \(a_i\) 沒有被選的下標 \(i\)。排序方式應使得堆頂為其中 \(b_i\) 最小的 \(i\)。
- 堆 \(6\),儲存 \(a_i,b_i\) 都被選擇過的下標 \(i\)。排序方式應使得堆頂為其中 \(a_i+b_i\) 最小的 \(i\)。
維護好每一個堆,在每一輪增加 \(m\) 的時候就選擇四種情況中代價最大的一個,一直到 \(m=L\) 的時候輸出被選元素的權值之和即可。
[PA2013] Raper
觀察到問題的本質是求一個最小值,而這個最小值有一個限制:在恰好生產 \(k\) 張光碟下的最小花費。結合另一個關於反悔貪心的一個性質:反悔貪心的本質是模擬費用流;費用流在 EK 演算法中增廣路長度不斷增長,故費用是關於流量的凸函式。 因此針對這一類“恰好”的問題,我們的答案是一個凸函式且斜率是具有單調性的。
那麼我們就可以考慮 WQS 二分,在平面直角座標系中標出 \(n\) 個點 \((i,f_i)\),其中 \(f_i\) 表示原問題中生產出 \(i\) 張光碟的最小花費。我們二分一個斜率 \(a\) 表示凸函式上的某一條切線的斜率,設切點為 \((u,f_u)\),則這條切線的截距 \(l=f_u-a\times u\)。根據 WQS 二分的原理,這個截距 \(l\) 就表示讓所有 \(b_i\leftarrow b_i-a\) 之後,生產任意數量光碟可以得到的最小花費(不再有光碟數量的限制)。
在求得 \(l\) 的值之後,我們找到此時可能的生產光碟數最大值為 \(m\),如果 \(m\geq k\),則我們的斜率 \(a\) 應該調小,否則就應該調大。這種我們就能得到過 \((k,f_k)\) 的切線以及此時的截距 \(l_k\),直接用 \(f_k=l_k+a\times k\) 表示答案就可以了。
因此現在的問題變為了求生產任意數量光碟可以得到的最小花費。考慮反悔貪心,我們從 \(1\sim n\) 依次考慮每一個 \(b_i\),每次有 \(3\) 種可能:
- 選取一個 \(j\in [1,i]\) 的未被匹配的 \(a_j\) 並讓 \(a_j,b_i\) 進行配對。
- 選取一個 \(j\in [1,i]\) 的已經被匹配過的 \(b_j\) 並用 \(b_i\) 去替換掉 \(b_j\),從而讓 \(b_j\) 失配。
- 什麼都不做。
我們顯然應該取這其中代價最小的。對於第一種情況,我們會選擇滿足條件的最小 \(a_j\)。對於第二種情況,我們會選擇滿足條件的最大 \(b_j\)。那麼我們就可以維護兩個反悔堆,一個用來維護未被匹配的 \(a_j\),一個用來維護已經被匹配的 \(b_j\)。如果當前兩個堆頂的 \(a_j=-b_j\),那麼我們優先選擇 \(a_j\) 去和 \(b_i\) 配對。這樣會使得當前斜率下生產的光碟數儘可能大。
於是 WQS 二分的 check
函式就搞定了。我們最後就可以直接透過 \(l_k\) 求得我們期望的答案 \(f_k\)。時間複雜度 \(\mathcal{O}(n\log n\log V)\)。
隨機化篇
普通隨機化
普通隨機化所解決的問題往往都是沒有那麼複雜的。設所有可能的答案的可重集為 \(S\),最優解的可重集為 \(T\),\(T\subseteq S\),那麼在完全隨機條件下,我們從 \(S\) 中取出最優解的機率為 \(\frac{|T|}{|S|}\),反推回來,我們在 \(S\) 中任取元素時可以取到最優解的期望次數為 \(\frac{|S|}{|T|}\)。
在實現中,我們可以取一個引數 \(c≈ \frac{|S|}{|T|}\) 以表示我們隨機選取的次數,通常情況下應確保選中最優解的機率不低於 \(95\%\)。因此若資料組數為 \(m\),那麼 \(c\) 的最優取值為 \(\frac{|S|}{|T|}\times m\times k\),其中 \(k\in [1.1,+∞)\)。
當然,這裡只是給一個示例,更多時候我們應該根據題目的極限資料去進行調參。
Graph Reconstruction
本題就相當於讓我們在 \(G=(V,E)\) 的反圖 \(G'\)上連 \(m\) 條邊使得每個點的度數不超過 \(2\)。
不難發現當 \(n\) 很大的時候,\(G\) 的反圖 \(G'\) 是相當稠密的,因為 \(G'\) 的邊數為 \(\frac{n(n-1)}{2}-m\),且題目保證 \(m\leq n\)。我們考慮在包含 \(n\) 個點的完全圖中任意找環,計算出這個環上不屬於 \(E\) 的邊數 \(u\),若 \(u\geq m\),則我們只需要輸出這些不屬於 \(E\) 的邊中的任意 \(m\) 條。
如果我們執行多次上述的過程,那就可以在一定的時間內找到一組解。若進行了長時間的隨機化後依然找不到,那就判定為無解。
為了證明這個隨機化的正確性,我們可以計算一下機率:首先在完全圖中找到的本質不同的環個數為 \(\frac{(n-1)!}{2}\),然後對於每一條給出的邊 \((u,v)\),包括它的環的個數為 \((n-3)!\),最劣情況下假設每條邊都會各自影響到 \((n-3)!\) 個不同的環,總計 \(m(n-3)!\) 個環,取 \(m\) 最大值 \(n\),那麼我們取到不合法的環的機率為 \(\frac{2n(n-3)!}{(n-1)!}=\frac{2n}{(n-1)(n-2)}\)。由於一次隨機的時間複雜度為 \(\mathcal{O}(n\log n)\),我們可以在 \(3.00\) 秒內執行 \(150\) 次隨機,那麼正確率就會大幅度提升,對於小資料我們可以執行更多次的隨機,幾乎可以排除錯失正解的可能性。
[PA2013] Filary
取 \(m=2\),我們可以發現此時的答案至少為 \(\lceil \frac{n}{2}\rceil\),這也就說明我們隨機在 \(n\) 個數中選出一個數 \(a_i\),它在最優解中的機率至少為 \(\frac{1}{2}\)。
考慮利用這一個性質去進行隨機化,我們任取一個數 \(a_x\),設 \(S\) 為包含 \(a_x\) 的最優解元素集合,則顯然 \(\forall y\in S\wedge y≠x\),\(m\) 必然整除 \(|a_x-a_y|\),因此最大的 \(m\) 為 \(\gcd_{y\in S,y≠x}|a_x-a_y|\)。
由此,我們可以列舉每一個質因子 \(p\),看有多少個 \(y\) 滿足 \(p\) 整除 \(|a_x-a_y|\),設這些 \(y\) 的集合為 \(f(p)\)。我們找到所有質數中 \(|f(p)|\) 最大的一個 \(p\),那麼顯然 \(f(p)\) 就是上文我們提到的 \(S\),因此我們處理出它們的最大公約數即可。
為了最佳化時間,我們可以提前預處理出 \(l_i\) 表示 \(i\) 的所有質因子中最小的那一個,然後對於每個 \(v=|a_x-a_y|\),我們就先更新 \(l_{v}\) 這個質因子的貢獻,然後除去 \(v\) 中的所有 \(l_v\) 因子,繼續更新其它的質數。每次隨機完之後的清空操作也應該這樣操作,於是時間複雜度就會降低許多,原題資料可過。
Love-Hate
注意到“不少於 \(\lceil \frac{n}{2}\rceil\) 個人”的限制,這說明對於一個人 \(i\),他在最優解中的機率大致為 \(\frac{1}{2}\)。因此我們考慮隨機選一個人 \(i\),欽定他必選,然後在剩餘部分中選出儘可能多的元素。
由於 \(1\leq m\leq 60\),我們可以用 long long
對每個人儲存一個二進位制狀態 \(sta_i\),若 \(sta_i\) 的第 \(j\) 個二進位制位上是 \(1\),則表明第 \(i\) 個人喜歡第 \(j\) 中貨幣。設隨機欽定 \(i\) 之後的最優解的貨幣狀態為 \(S\),則顯然有 \(S\subseteq sta_i\),因此我們可以列舉 \(sta_i\) 的所有子集 \(t\),然後在所有 \(n\) 個狀態中查詢有多少個 \(sta_j\) 滿足 \(t\subseteq sta_j\)。
但是單次隨機的 \(\mathcal{O}(3^pn)\) 時間複雜度是不能接受的。於是我們可以使用高維字首和的手段去寫一個 DP,這個方法是套路的。最佳化之後的時間複雜度就變為了 \(\mathcal{O}(2^pp)\)。
每次隨機的正確率都是 \(\frac{1}{2}\),時限比較寬鬆,我們可以隨機個 \(50\) 次,這樣下來整個演算法的錯誤率就趨近於 \(0\) 了,可以忽略不計。
Find a Gift
考慮已知某個石頭的位置 \(x\) 之後我們該怎麼找答案。由於題目是讓我們求最靠前的不是石頭的位置,那麼我們就先比較 \(1,x\),如果互動得到的結果為 EQUAL
,則說明兩者都是石頭,否則答案就是 \(1\) 位置。此時為了更好地利用當前資訊,我們可以讓 \(A=\{1,x\},B=\{2,3\}\) 然後比較 \(A,B\),如果互動得到的結果為 EQUAL
,結合“石頭質量嚴格大於禮物質量”的條件,我們可以推斷出 \(2,3\) 也都是石頭,否則答案就在 \(2,3\) 之中。接著,我們比較 \(A=\{1,2,3,x\}\) 和 \(B=\{4,5,6,7\}\),思路和上面的一樣,以此類推。
不難發現整個過程我們詢問的次數是 \(\mathcal{O}(\log n)\) 級別的,因為每次我們的兩個集合的大小都在成倍增長。設當前集合的大小為 \(len\),則我們的 \(A\) 集合應當是 \(\{x\}\) 並上 \(\complement_{U}{\{x\}}\) 中最小的 \(len-1\) 個元素所組成的集合,\(B\) 則是 \(\complement_{U}A\) 中最小的 \(len\) 個元素。特殊地,如果 \(len>\frac{n}{2}\),則我們讓 \(B\leftarrow \complement_{U}A\)。
當我們在一次比較中發現 \(A,B\) 的比較結果不再是 EQUAL
,那就說明 \(B\) 中必然存在一個禮物。此時我們只需要在 \(B\) 中進行二分,每次比較 \(A,B\) 各自的長度為 \(mid\) 的字首,最後二分到的位置就是第一個禮物的位置。這裡二分需要花費的詢問次數同樣為 \(\mathcal{O}(\log n)\)。
由於 \(k\geq 1\),所以上述過程一定可以找到解。
那麼剩下的唯一問題就是如何確定一個石頭的位置 \(x\)。觀察到題目中 \(k\leq \frac{n}{2}\) 這個性質,說明在完全隨機條件下,我們隨機取到一個石頭的機率是不低於 \(50\%\) 的。由於題目要求詢問次數不超過 \(50\),且上述倍增與二分的過程所花費的最多詢問次數為 \(2\log n\leq 20\),因此留給我們去隨機尋找的次數上限為 \(50-20=30\)。假定我們隨機 \(25\) 次,那麼我們找不到石頭的機率最大為 \((\frac{1}{2})^{25}=\frac{1}{33554432}\),考慮到最多會有 \(500\) 組資料,所以對於每個測試點我們的錯誤機率為 \(\frac{500}{33554432}≈0.0000149012\),CF 原題測試點數為 \(100\),那麼無法得到滿分的機率為 \(0.00149012\),足以透過。
color-coding
color-coding 中文翻譯過後的意思是“隨機染色”,即隨機構造出一組對映 \(f\),將一個集合 \(S\) 內的所有元素 \(x\) 都對映到一種顏色上。
這樣的隨機染色思路最開始用在了 k-path 問題上:判斷無向圖 \(G=(V,E)\) 中是否存在一條長度為 \(k\) 的簡單路徑(不經過重複的點或邊)。
這個問題其實是 NP-Hard 的,但是在 \(k\) 足夠小的時候我們可以用一種隨機化的手段去解決這個問題。我們給 \(G\) 中的每一個節點 \(i\) 都染上一個 \(1\sim k\) 中的顏色 \(c_i\),然後我們嘗試找到這樣一條路徑 \(P\) 使得所有 \(k\) 種顏色都在 \(P\) 中出現恰好 \(1\) 次。這就是一個典型的狀態壓縮 DP 了,我們設 \(dp_{i,S}\) 表示當前點在 \(i\) 且經過的顏色集合為 \(S\) 的路徑是否存在,\(0/1\) 分別表示不存在和存在,初始有 \(dp_{i,\{c_i\}}=1\),轉移時對於每一條邊 \((u,v)\) 若 \(c_u\notin S\) 則執行 \(dp_{u,S\cup\{c_u\}}\leftarrow dp_{u,S\cup\{c_u\}} \vee dp_{v,S}\),用 \(u\) 更新 \(v\) 的轉移同理。用 \(U\) 表示 \(1\sim k\) 所有顏色的全集,我們只需要判斷是否存在一個 \(dp_{i,U}\) 的值為 \(1\) 即可。時間複雜度 \(\mathcal{O}(2^km)\)。
若 \(G\) 中存在一條長度為 \(k\) 的簡單路徑,那麼上述演算法的正確率應為 \(\frac{k!}{k^k}\),也即我們找到答案的期望次數為 \(\frac{k^k}{k!}\),故我們可以反覆執行 \(\frac{k^k}{k!}+T\) 次上述演算法(\(T\) 為一較小常數),這樣我們的正確性就會提升不少。時間複雜度就變為了 \(\mathcal{O}(\frac{k^k}{k!}2^km)\),這也就是為什麼在 \(k\) 足夠小的時候我們可以幾乎解決這個問題。
這個隨機化演算法的重點思路就是進行隨機染色 color-coding,相較於上一節中的普通隨機化,它不再是拘泥於對一種元素的隨機,而是進行了全域性隨機化,為所有元素都隨機出一種對映方案,然後利用其餘的演算法巧妙地解決一些特殊問題。
一般來講,color-coding 可以解決的題目都會有個顯然的特點,就是存在一個值域在 \([1,5]\) 左右的變數 \(k\)。所以在遇到這種怪異的題目時,我們不僅要想狀壓和斯坦納樹,還要思考一下它是否可以利用隨機化去解決。
[CSP-S 2022] 假期計劃
這題正解是 meet in the middle,但其實也可以用 color-coding 巧妙地解決。
首先不難想到用 BFS 預處理出每兩個點之間的距離 \(dist(i,j)\),因此對於當前景點 \(u\),若存在一個地方 \(v\) 使得 \(dist(u,v)\leq k+1\),則 \(v\) 可以成為 \(u\) 的下一個景點。我們用這個關係建邊,即構造一個新圖 \(G'=(V',E')\),其中每一條邊 \((u,v)\) 表示原圖 \(G\) 中 \(dist(u,v)\leq k+1\)。
那麼現在的問題就是在 \(G'\) 找到一個經過 \(1\) 號節點的有向五元環使得環上每個點的點權之和最大。由於我們要找的環的節點個數很小,因此考慮 color-coding,對除了 \(1\) 以外的所有節點隨機染上 \(1\sim 4\) 中的顏色,那麼現在我們嘗試找到一個點權之和最大的有向五元環,滿足其包含 \(1\) 節點且 \(1\) 節點接下來走到的 \(4\) 個節點的顏色依次為 \(1,2,3,4\)。
這個問題可以記憶化搜尋解決,設 \(c_i\) 為 \(i\) 節點的顏色,特殊地讓 \(1\) 的顏色 \(c_1\) 為 \(0\),定義 DP 陣列 \(dp_i\) 表示從 \(i\) 開始的大小為 \(4-c_i+1\) 的一條點權和最大的有向鏈,滿足鏈上的點的顏色依次為 \(c_i,c_i+1,\dots,4\) 且鏈尾與 \(1\) 存在 \(E'\) 中的連邊。轉移:
最終的答案顯然為 \(dp_1\)。由於 \(|E'|\) 是 \(\mathcal{O}(n^2)\) 級別的,因此我們 DP 的時間複雜度最劣為 \(\mathcal{O}(n^2)\)。
上述演算法可以得到最優解的機率為 \(\frac{2}{4^4}=\frac{1}{128}\),我們理應執行 \(128+T\) 次上述演算法。當然我們也可以用 clock()
函式卡時。理論來講這樣的 color-coding 演算法的迴圈次數會達到 \(8\times 10^8\) 甚至更多,但是即使是構造出了 \(|E'|=2500^2-2500\) 的資料,我們發現它在開了 O2 最佳化的 \(2s\) 時限內竟然能執行 \(500\) 多次隨機化,因此正確率可以得到極大保證。
Turtle and Three Sequences
先思考樸素的暴力演算法,考慮狀壓 DP,設 \(dp_{i,S}\) 表示在前 \(i\) 個數中,選出的若干個數的 \(b_i\) 並集為 \(S\),且強制 \(i\) 必選的最優答案。轉移顯然為:
其中 \(S\) 需滿足 \(b_i\in S\wedge |S|\leq m\)。最終答案即為所有 \(dp_{i,S}\) 的最大值。
但是 \(b_i\) 太大了,我們無法直接進行狀壓。注意到 \(m\) 的值很小,我們考慮 color-coding,對於 \(1\sim n\) 中的每一個 \(i\) 隨機染色為 \(col_i\),保證 \(col_i\in [1,m]\),然後我們令 \(b_i\leftarrow col_{b_i}\),將原問題轉換為選出一個子序列 \(\{p_1,p_2,\dots,p_m\}\) 使得 \(a_{p_1}\leq a_{p_2}\leq \dots \leq a_{p_m}\) 且 \(b_{p_i}\) 互不相同,由於 \(b_i\) 最多隻有 \(5\) 種,因此這個問題可以根據上文的狀壓 DP 思路解決。
我們要取得正確答案 \(\{p'_1,p'_2,\dots,p'_m\}\) 就必須使得 \(b_{p'_i}\) 對應的顏色互不相同,機率顯然為 \(\frac{m!}{m^m}\leq \frac{24}{625}\),則我們應該執行至少 $\lceil \frac{m^m}{m!}\rceil $ 次隨機。但是上述演算法的暴力時間複雜度為 \(\mathcal{O}(n^22^m)\),我們考慮用樹狀陣列最佳化一下,對每個 \((i,S)\) 維護所有 \(a_j\in [1,i]\) 的 \(dp_{j,S}\),時間複雜度降為 \(\mathcal{O}(2^mn\log n)\)。我們只需要執行 \(500\) 次迴圈就能以極大的機率找到最優解。
[THUSCH2017] 巧克力
題目中給出了兩個問題,一個是找到最小的合法連通塊,一個是在保證合法連通塊最小的情況求出最小的中位數。先思考前者怎麼解決。不妨將整個網格圖想象成一個無向圖 \(G=(V,E)\),考慮一個和“無向圖求斯坦納樹”類似的狀態壓縮 DP,設 \(dp_{i,S}\) 表示最小的連通塊大小,使得這個連通塊包含 \(i\) 號節點,且它內部節點的所有 \(c_i\) 的並集是 \(S\) 的超集,那麼轉移有兩種可能:
第一種可以直接轉移,第二種可以透過用最短路更新。設 \(c_i\) 的種類數為 \(p\),則時間複雜度為 \(\mathcal{O}(3^pnm+2^pnm\log nm)\)。由於 \(k\) 很小,我們可以用 color-coding 將 \(p\) 降為 \(k\) 的級別,即對於每個 \(i\) 隨機一個顏色 \(col_i\in [1,k]\),然後令 \(c_{i,j}\leftarrow col_{c_{i,j}}\)。這樣我們得到最優解的機率為 \(\frac{k!}{k^k}\),多隨機幾次就可以以極高機率獲得最優解。
現在思考第二個問題——怎麼最小化中位數?套路地,我們二分一個值 \(x\),令所有 \(a_{i,j}\leq x\) 的點的權值為 \(-1\),所有 \(a_{i,j}>x\) 的點的權職為 \(1\),我們希望在所有最小的連通塊中找到一個連通塊 \(A\) 使得 \(A\) 中元素的權值非正。這個問題很簡單,我們只需要將第二種 DP 轉移改為:
為了避免 \(\text{SPFA}\) 演算法被卡,我們可以把 \(a_{i,j}\leq x\) 的權值設定為 \(99999\),將其餘點的權值設定為 \(100001\),然後找到一個最小連通塊使得其節點的權值之和不超過最小連通塊大小的 \(100000\) 倍。
設我們每次隨機的次數為 \(T\),這樣的時間複雜度為 \(\mathcal{O}(T3^knm\log V+T2^knm\log nm\log V)\)。這個 \(V\) 比較大,我們可以把所有 \(a_{i,j}\) 離散化,這樣時間複雜度就最佳化成了 \(\mathcal{O}(T3^knm\log nm+T2^knm\log^2 nm)\),將 \(T\) 設定為 \(200\),原題資料最慢的點跑了 \(3.26s\)。
模擬退火
模擬退火 Simulated Annealing(簡稱 SA)是隨機化演算法中十分巧妙的一個結合了物理知識的演算法。這裡引入一下百度百科中的解釋:
模擬退火演算法來源於固體退火原理,是一種基於機率的演算法,將固體加溫至充分高,再讓其徐徐冷卻,加溫時,固體內部粒子隨溫升變為無序狀,內能增大,而徐徐冷卻時粒子漸趨有序,在每個溫度都達到平衡態,最後在常溫時達到基態,內能減為最小。
對於一個非單調且非單峰函式 \(f(x)\),我們嘗試在定義域 \(R\) 中找到一個點 \(u\) 使得 \(f(u)\) 最大,由於 \(f(x)\) 是雜亂無序的,因此常規的二分法和三分法都無法解決。考慮模擬退火演算法。我們在當前最優解 \(u\) 的附近隨機找點 \(a\),如果 \(f(a)\) 優於我們的當前最優解 \(f(u)\),那麼必然有 \(u\leftarrow a\) 的更新,否則,我們以一定的機率 \(p\) 去接受這個新解。
結合退火的原理,我們希望在最開始的時候 \(p\) 更大,這樣就能呈現出一個無序狀態,而到了後期我們就讓 \(p\) 變小,這樣就相當於一個粒子趨近於有序狀態。設 \(T\) 為在退火過程中我們當前的溫度,\(\Delta E\) 表示 \(f(a)-f(u)\),針對於 \(\Delta E\) 我們接受 \(a\) 這個解的機率 \(P(\Delta E)\) 為:
每次考慮完一個 \(a\) 時,我們就讓 \(T\) 逐漸減小。這樣便能契合退火的過程。
具體實現的話,我們可以調出三個引數 \(T_s,T_e,\delta\) 分別表示起始溫度,終止溫度,每次隨機完一個數之後 \(T\) 的變化量(\(T\leftarrow T\times \delta\)),其中 \(\delta\) 也叫降溫係數。我們初始時讓 \(T\leftarrow T_s\),每次隨機之後更新 \(T\) 為 \(T\times \delta\),如果 \(T\leq T_e\) 則停止退火過程。通常來講,\(\delta\) 是一個非常趨近於 \(1\) 的小數,\(T_e\) 是一個非常趨近於 \(0\) 的小數。
另外,當我們以 \(e^{\frac{-\Delta E}{T}}\) 的機率保留當前解時,我們可以用 exp((-DeltaE)/T)>=(double)rand()/RAND_MAX
的程式碼來判斷。
特別注意的是,我們的 \(T_s,T_e\) 應該是和值域 \(V\) 對應的,因為 \(\frac{-\Delta E}{T}\) 中 \(\Delta E\) 和 \(V\) 是同一級別的,那麼我們的溫度必然要與這個值域相關,這就確定了我們調參的方向。
為了儘可能避免調參具體數值對答案的影響,一個通用的方法是先將 \(T_s,\delta\) 儘可能取大一點,\(T_e\) 儘可能取小一點,這樣不能保證時間限制,但是在一定程度上可以保證答案更優,然後我們將 \(T_s,T_e\) 逐漸靠攏,\(\delta\) 針對於一對確定的 \((T_s,T_e)\) 在 \([0.911,0.999]\) 選取。這樣下來我們調參的時間也能少一點。
單次退火可能無法取得最優解,我們可以在 \(\text{TimeLimit}\) 內多執行幾次 SA。在更難的題目當中,我們的模擬退火可能還需要伴隨另一些隨機技巧。下面透過例題介紹一下。
[TJOI2010] 分金幣
先考慮普通隨機化,我們每次從序列中任意選出 \(\lfloor \frac{n}{2}\rfloor\) 個數分為一組,剩下的數分為另外一組,然後兩者做差,更新全域性最小值。
但是這樣做太低效了,考慮模擬退火,設當前最優答案為 \(m\),最優分組方法為 \((A,B)\),即將原序列分成 \(A,B\) 兩組,我們隨機交換 \(A,B\) 中的一個元素得到 \((A',B')\),然後計算新的解 \(v=|\sum_{x\in A'}x-\sum_{x\in B'} x|\),即 \(\Delta E=v-m\),此時若 \(\Delta E<0\) 則必然會用 \((A',B')\) 代替原來的解,否則我們就以 \(e^{\frac{-\Delta E}{T}}\) 的機率接受這一組解。
由於每次隨機只會有兩個數發生變化,因此我們是可以 \(\mathcal{O}(1)\) 維護兩組數的差值的。
取每次退火的引數為 \(T_s=10^4,T_e=10^{-5},\delta=0.9651\),我們發現單次退火的正確率不夠高,那麼我們就增加退火的次數為 \(1200\),每次退火之前,我們的初始 \((A,B)\) 應當重新隨機選取,而不是直接繼承上一次 SA 結束後留下來的 \((A',B')\),這樣就能讓我們隨機的物件更加多樣化,也儘可能保證了答案的正確率。
原題資料可過,但是也存在一些高強度的資料可以 Hack 掉模擬退火做法,這裡不再討論解決這些 Hack 資料的方法。
[SCOI2008] 城堡
還是先從普通隨機化開始考慮,每次任選基環樹上的 \(k\) 個未被選擇的點,然後求出 \(\max\{dist(c)\}\) 的值,這個 \(\max\{dist(c)\}\) 我們可以在預處理任意兩點之間的最短路之後 \(\mathcal{O}(n^2)\) 求,可以直接用 \(\text{Dijkstra}\) 多源最短路 \(\mathcal{O}(n\log n)\) 解決,甚至可以用換根 DP 做到 \(\mathcal{O}(n)\) 求解,但是本題資料範圍較小,我們可以使用 \(\mathcal{O}(n^2)\) 的樸素演算法。
然後就和上一道題很類似,我們嘗試從當前狀態擴充出另一種新的解,即在當前選出的 \(k\) 個點中選擇一個點移除掉,然後用另一個未被選擇的點替補上去,按照這個方式我們直接模擬退火。調參為 \(T_s=10^4,T_e=10^{-11},\delta=0.9952\),每次 SA 的正確率可能還不夠,我們就執行 \(150\) 次 SA。
注意到時限比較緊,我們的每次計算 \(\Delta E\) 時的 \(\mathcal{O}(n^2)\) 做法可以稍作改進,假設我們列舉的過程中已經取到的 \(\max\{dist(c)\}\) 為 \(M\),對於當前點 \(x\),如果已經列舉到的若干個點中存在一個與 \(x\) 的距離小於等於 \(M\) 的,那麼我們就可以直接跳過對於 \(x\) 的統計。這樣的話 \(\mathcal{O}(n^2)\) 在很大機率上都跑不滿。
這種做法可以透過原資料,但是加強版的 Hack 過不了,這個可以透過調整隨機種子解決。
[ABC157F] Yakiniku Optimization Problem
模擬退火也可以解決一些二維平面上的問題。
在這道題中,我們可以觀察到資料範圍比較小,且浮點數精度的限制要求也不算緊,那麼我們就可以思考模擬退火。我們可以從隨機一個點 \((s_x,s_y)\) 開始,每次用 \(\mathcal{O}(n\log n)\) 的時間複雜度計算出 \(c_i\times \sqrt{(X-x_i)^2+(Y-y_i)^2}\) 的 \(k\) 小值,然後用模擬退火的模板去對更新當前解。每次確定了當前解之後,我們就要在 \((X,Y)\) 的附近尋找一個新的點,考慮到最優解的座標可能不是整數,我們就可以使用 uniform_real_distribution
去在特定範圍內隨機出一個浮點數,對橫縱座標都找一遍隨機數,然後湊成一個新的座標 \((X',Y')\),接著就按照模擬退火的流程走就行了。
為了防止出題人刻意卡掉模擬退火的解法,我們可以將整個平面直角座標系旋轉 \(\alpha\) 度,其中 \(\alpha\) 為一個 \([0,180]\) 的隨機浮點數,此時每個點原來的座標 \((x_i,y_i)\) 都會轉換為 \((x_i\cos \alpha-y_i\sin \alpha,x_i\sin \alpha + y_i\cos \alpha)\)。我們還是按照上面的思路執行 SA,引數分別為 \(T_s=10^4,T_e=10^{-13},\delta =0.997\),任意調整隨機種子執行大約 \(40\) 次 SA 就可以透過原題資料。
[JOISC2020] 伝説の団子職人
不難發現問題的中心在於白色糰子,每個白色糰子都最多有三種狀態:從左上到右下可以連成一個合法串,從上到下可以連成一個合法串,從右上到左下可以連成一個合法串。分別設這三種狀態為 \(0/1/2\)。
設二元組 \((i,j)\) 表示第 \(i\) 個白色糰子的狀態為 \(j\) 時它所在的合法串覆蓋的範圍,原題就轉換為選出儘可能多的二元組,使得任意兩個被選的二元組 \((i,s_1),(j,s_2)\) 滿足 \((i,s_1)\cap (j,s_2)\not= \varnothing\)。如果我們對兩個相交的二元組 \((i,s_1),(j,s_2)\) 連一條邊,那麼問題其實就轉換為了求圖中的最大獨立集。
然而求最大獨立集是 NP-Hard 問題,我們只能透過隨機化的手段去取得儘可能優的解。由於本題是提交答案題,因此我們在本地執行時無需考慮任何時間限制,我們要做的就是讓隨機化儘可能快速地取得更好的解。
先考慮一個樸素的隨機化貪心(這個在後面的章節會講到),把所有點按照任意順序排序,然後依次遍歷每一個點,如果當前點 \(x\) 不存在已經被選擇的相鄰點,那麼就強制選擇 \(x\),否則跳過。這樣不斷隨機下去,得到符合要求的解的耗時其實是比較長的。我們可以用模擬退火去最佳化這個過程,在初始時我們 random_shuffle
得出一個排列,每次退火操作前都任意交換這個排列中的兩個點的順序,得到 \(\Delta E\),根據 \(P(\Delta E)\) 對當前排列進行更新。
這道題的時間非常開放,我們可以根據自己的想法進行模擬退火,比如每次退火前都任意交換更多個點的順序,或者退火過程中在一定機率下重新 random_shuffle
原排列。引數也不是特定的,不用刻意地去糾結最好的引數,主要在足夠的時間裡跑出來就行了。
對於每個輸入資料,我們可以無限迴圈執行若干次 SA,直到最優解滿足這個資料的要求。實現較好的話,最慢的測試點得到合法答案的時間不超過 \(3\) 分鐘。
隨機化貪心
隨機化貪心原理
隨機化貪心是大多數的人類智慧做法所用到的東西。當我們在做一道關於最優解的題目時,如果想到了一種偽貪心(通常是沒有考慮到當前狀態對後續狀態的影響的一類貪心),我們其實用不著棄掉這個做法,相反,我們可以結合隨機化,對問題的序列反覆執行 random_shuffle
並進行貪心,從而獲得近似最優解。所謂近似最優解,就是與嚴格最優解接近,但是無法完全保證與嚴格最優解相同的解。此外,有的題還可以用微擾法,每次任意交換序列中的兩個元素,但這樣的例子還是比較少的。
為了使我們的答案契合最優解的機率增大,我們可以採用 (double)clock()/CLOCKS_PER_SEC<=T
卡時,不過其中一定要注意 \(T\) 的調參,如果調整不當,是很有可能導致整道題不得分的。一般來講應該 \(T\) 應該取到 \(\text{TimeLimit}-0.1\),如果單次偽貪心的時間複雜度較大或者常數較大,可以再適當減去一點。千萬不要為了這 \(0.1\) 秒以內的時間而把 \(T\) 增大,因為這一點時間在 \(\text{TimeLimit}\) 中的佔比算不了什麼。
實際上,大多數隨機化貪心的題目都有一個共同點,就是這個問題不對順序作嚴格要求。如果一道題目強制作了順序要求,那麼我們每次的貪心都只會按照它固有的順序去執行,因此我們的隨機化無法影響到貪心的答案。當然這些還是要因題而論,如果一種貪心思路不受順序限制,那麼我們依舊可以去考慮隨機化貪心。
最後,如果實測下來對於極限資料的透過機率比較小時,我們還可以利用模擬退火進行最佳化,一般來講可能會多 \(10\) 分左右。因此如果一道題是作為拉分題的,那麼隨機化貪心加模擬退火的組合往往會帶來巨大收益。
經典隨機化貪心習題
Leaving the Bar
假設我們已經求出了 \(1\sim i-1\) 的向量按照某種組合加起來的座標 \((u,v)\),考慮第 \(i\) 個向量 \((x,y)\),我們有 \((u-x,v-y),(u+x,v+y)\) 兩種結果,那麼我們就選擇其中模最小的那一個向量,這樣可以儘可能保證當前模在 \(1.5\times 10^6\) 範圍內,但是可能會對後續選擇造成負面影響。比如給出三個向量 \(a,b,c\),有 \(a=-b,|a|>0,|c|=1.5\times 10^6\) 且 \(a,b,c\) 共線,如果只考慮 \(a,b\) 向量,那麼我們的座標顯然會轉移到 \((0,0)\),但是此時再算上 \(c\) 的話我們的模就會達到 \(1.5\times 10^6\) 而不滿足條件,實際上我們可以取 \(a+b±c\) 中模最小的那一個,這個最小的模顯然不會超過範圍。
雖然這是一個偽貪心,但是我們可以採取隨機化貪心。在上面的那個例子中,如果我們按照 \(c,a,b\) 的順序去考慮,那麼貪心得到的答案其實是合法的。因此我們可以把所有向量執行 random_shuffle
操作,然後每次按照上述貪心思路求解,如果當前座標的模小於 \(1.5\times 10^6\),那麼直接輸出當前解。由於題目保證有解,我們可以不用調時間引數,直接隨機無限次,當找到正解時停止即可。CF 上可過。
[HAOI2006] 均分資料
可以發現其中 \(\bar{x}\) 是固定的,我們只需要在意分組的方式。
考慮一個貪心,我們從 \(1\sim n\) 遍歷每一個 \(i\),然後把 \(a_i\) 放入當前組 \(S\) 中,如果 \(\sum_{v\in S}v > \bar{x}\),我們就比較 \(|\sum_{v\in S}v-\bar{x}|\) 和 \(|\sum_{v\in S}v-a_i-\bar{x}|\) 的值,如果前者更小,那麼就保留 \(S\) 中的 \(a_i\),從下一個數開始就是新的一組;否則就把 \(a_i\) 從 \(S\) 中刪除,讓他單獨成一組,在後續考慮 \(a_j\) 時就往 \(a_i\) 所在的組裡放入 \(a_j\)。這個貪心存在一定正確率,但是並不能保證取得最優解。
由於 \(n\) 的規模較小,我們可以套路地進行隨機化貪心,每次都打亂一遍原序列,把時間卡到 \(0.97s\),然後輸出隨機化過程中得到的最小 \(\sigma\) 即可。
[春季測試 2023] 密碼鎖
考慮一個偽貪心,對於第 \(i\) 個撥圈,假設我們已經確定了前 \(i-1\) 個撥圈的狀態,我們找到它可以撥到的最優的狀態,使得前 \(i\) 個撥圈組成的密碼鎖的鬆散度最小,這個過程可以 \(\mathcal{O}(k^2)\) 列舉。
但是這個偽貪心必然是錯誤的。由於本題的答案和最值有關,而一個序列的最值不受順序影響,因此考慮隨機化貪心。每次都隨機打亂所有撥圈的順序,然後依次執行偽貪心。
手造大樣例可以發現,如果我們執行 \(185\) 次偽貪心,則能夠以極大機率取到最優解。當然也可以使用 clock()
函式。
從思維難度和實現難度上都遠遠簡單于正解的分類討論和掃描線。為了逼近最優解,我們可以透過手造大樣例的方式進行引數除錯,一般來說,如果小規模的資料與暴力對拍無誤,且大規模資料輸出的答案近乎穩定,那麼這個隨機化貪心的正確性就是非常高的。
[HNOI2011] 任務排程
觀察到 \(t_i=3\) 的 \(i\) 不超過 \(10\),因此我們可以直接 \(\mathcal{O}(2^{10})\) 列舉每個 \(t_i=3\) 的 \(i\) 的狀態 \(1/2\),表示 \(t_i\leftarrow 1/2\)。那麼當前我們就是對於一個固定的局面進行求解。
我們肯定是希望兩個機器處理每個任務的時間儘可能連續,那初始時,我們先讓機器 \(A\) 處理完所有 \(t_i=1\) 的任務,機器 \(B\) 處理完 \(t_i=2\) 的任務,接著交叉處理,讓 \(A\) 處理 \(t_i=2\) 的任務,\(B\) 處理 \(t_i=1\) 的任務。
以機器 \(A\) 為例,我們記錄 \(s_1\) 表示機器 \(A\) 將要處理第 \(j\) 個 \(t_i=2\) 的任務 \(p_j\) 時機器 \(A\) 的執行時間(包括最開始時處理 \(t_i=1\) 的任務的時間),\(s_2\) 表示機器 \(B\) 處理完前 \(j-1\) 個 \(t_i=2\) 的任務的執行時間.
現在考慮加入任務 \(p_j\),如果 \(s_1\geq s_2+b_{t_j}\),那麼顯然我們可以無縫銜接地把 \(t_j\) 安排到機器 \(A\),此時 \(s_1\leftarrow s_1+a_{t_j}\),否則,我們的機器 \(A\) 就要等 \(t_j\) 被機器 \(B\) 處理完,此時 \(s_1\leftarrow s_2+a_{t_j}\)。對於 \(s_2\),顯然有 \(s_2\leftarrow s_2+b_{t_j}\)。我們只需要在不斷加入任務的同時按照上述方法維護 \(s_1,s_2\) 即可。
對於機器 \(B\) 的總執行時間也是按照同樣的方法進行處理,不再贅述。
設兩臺機器的執行時間分別為 \(T_1,T_2\),則我們當前狀態的最少總時間為 \(\max\{T_1,T_2\}\)。
這個貪心看著很複雜,但其實還是不能保證正確性,比如這個資料:有 \(2\) 個 \(t_i=1\) 的任務,對應的 \((a_i,b_i)\) 分別是 \((1,100),(100,1)\),如果我們在機器 \(A\) 上先解決 \((100,1)\) 再解決 \((1,100)\),那麼總時間就是 \(201\),如果先解決 \((1,100)\) 再解決 \((100,1)\),那麼總時間就是 \(102\)。
我們發現錯誤原因是貪心時的順序不優秀,因此我們對所有 \(t_i\) 相等的任務執行 \(2000\) 次 random_shuffle
,這樣下來正確的機率就會提升,原題資料中最慢的點跑了 \(0.679s\)。此外,這題還可以用微擾法進行隨機化貪心,即按照執行時間的多少進行排序,然後每次都隨機交換兩個元素。
[WC2018] 通道
這道題在我的「圖論」樹的進階 中的“0x01 虛樹” 一節中有講到,其做法非常複雜且難以實現,這裡就用另一種簡潔的思路去解決它。
回憶一下樹的直徑的求法,我們先從任意一個點 \(x\) 開始進行 DFS,找到離 \(x\) 最遠的一個點 \(y\),然後再從 \(y\) 開始 DFS,找到的最長距離即為這棵樹的直徑。
在這道題中,我們也可以嘗試套用這樣的思路進行貪心。從任意一個點 \(x\) 開始,我們分別在 \(T_1,T_2,T_3\) 三棵樹中進行 DFS,然後找到一個點 \(y\) 使得 \(dis_1(x,y)+dis_2(x,y)+dis_3(x,y)\) 的值最大。接著我們就從這個新的 \(y\) 開始 DFS,找到使 \(dis_1(y,z)+dis_2(y,z)+dis_3(y,z)\) 最大的 \(z\),然後轉移到 \(z\) 上面來,不過此時我們並不能證明此時 \(y,z\) 在三棵樹上的距離之和就是最優解,因此我們還可以繼續 DFS 下去。
注意到可能會出現這樣一種情況:我們從 \(x\) 開始 DFS 找到了 \(y\),再從 \(y\) 開始 DFS 又重新找到了 \(x\),如果我們一直這樣找下去,那麼就會在 \(x,y\) 之間來回迴圈,有可能永遠都找不到最優解。那麼我們就考慮隨機化,每次如果走到已經以前走過的點,那就隨機一個新的點去 DFS。
但是這樣還存在另一個問題,在上述演算法中,我們一開始選擇的起點 \(x\) 其實基本決定了後續搜尋的走向,那麼我們可以規定一個層數 \(d\),當這個起點已經向後找到了 \(d\) 個不同的點進行 DFS,那麼就重新更新一個起點,按照同樣的方式去迴圈。類似於迭代加深。
減小一下程式碼的常數可以卡時到 \(3.85s\),每個起點向後擴充套件的次數 \(d\) 可以定為 \(8\),這樣就能以較高的正確率透過,實驗下來最低的得分都還有 \(97\) 分,思維難度和得分的價效比幾乎已經碾壓正解了。