Aliens 優化
前言
Aliens 優化是從 IOI 2016 Aliens 來的一個技巧,而大陸其實在 2012 年就已經發明這個演算法了,那邊叫做 wqs二分,有興趣的可以看一下原始論文:
接下來的內容會使用到裡面的例題,而我會用我自己的理解來說明 Aliens 優化。
概述
Aliens 優化是用來解決 K-約束問題的一種演算法。什麼叫 K-約束問題呢?
我們會遇到像這樣的問題:給定個物品,規定以某種方式選擇若干個物品,而選擇物品的不同會有不同代價,規定必須選個物品的前提下最大/小化花費。舉個簡單的例子:給你一個長度為的序列,叫你恰好劃分成段,最大/小化價值;諸如此類的問題,就叫做約束問題。
上面這類的 DP 問題的複雜度通常會至少是 的,因為需要的空間複雜度。(令代表前個物品選個之類的)。而 Aliens 優化可以將這個複雜度優化到 ,其中 即二分值域,視題目而定。
而 Aliens 優化的使用上有一個先備條件:
令代表選恰好個物品的最小價值,是一個凸性函數。也就是滿足差分遞減/增 (),的函數。
如果滿足上面條件的話就可以使用 Aliens 優化。而這個 trick 雖然放在 DP 優化,但事實上並不是只有 DP 才可以使用。
上面的論文裡面附了兩個很好的例子,一題是黑白生成樹、另一題是APIO Commando那題的改編版。接下來我們用這兩個例子來介紹 Aliens 優化要怎麼使用。讓我們先來看黑白生成樹這題:
黑白生成樹
給你一張點邊的無向帶邊權連通圖,每條邊會是黑色或白色,請求出一個恰有條白色邊的最小生成樹的權值和。
一看到這題可能會想要先 Greedy 選完白邊之後再選黑邊。但很可惜這是錯的。如果先選白邊的話可能會影響到樹的連通性,導致一些比較優的黑邊不能被選到。

而透過觀察可以發現,這題是一個約束問題,那麼我們可以試著用看看 Aliens。
令代表恰選條白邊的最小生成樹權值和。顯然是不能直接求出來的,但是我們可以觀察感覺可以發現是一個下凸函數。

在上面這張圖中,的值是最小的,也就是說原圖的最小生成樹會有恰好條白邊。這時候如我我們想要求其他的點 呢?我們知道是沒有辦法被快速求出來的,但最低點可以用 Kruskal 快速算出來。那如果,我們透過某些變換讓變成最低點呢?
由於是下凸的,所以如果我們能找到一條在的切線的話(這裡):

也就是說,如果我們求出在的一條切線,那麼把帶進去就會是我們的。所以我們就把問題轉換成了找切線這個動作。
如果我們隨機選一條斜率為的切線去切,這條直線不一定會切在上。但是因為是一個下凸函數,所以斜率越大會切到的點越大,也就是斜率單調。所以如果我們知道斜率為的直線會切在哪裡,我們就可以調整直線斜率讓他最終切到。而這個過程也就是二分搜。
所以我們現在變成要知道的是一條斜率為的直線會切在哪裡。
假設我們已經知道了,令一條斜率為的直線。這條直線與的交點就會是,整理一下就可以變成。

而透過觀察發現,切線一定會落在最小的地方,也就是最小的地方。令,其中就代表著每多選一條白邊需要多付的代價下所求得的最小生成樹的最小權值(因為選條要多付)。
那麼我們就把原圖的所有白邊邊權加上再跑一次 Kruskal 就可以得到了!至此再結合上面所述的二分搜就可以以的複雜度解掉這題了。
雖然我們看起來已經做完了,這邊還有一些細節要注意:可能會有某些點是沒辦法被整數斜率的直線切到的。舉個例子:令是要切的點,斜率的線切到的是而切到的是,此時就不能被整數切到。這時候就可以使用 小數二分。但是小數二分的效率並不高,在某些題目可能會 TLE。事實上很多時候可以透過題目性質來避免掉小數二分。例如這題就是一個例子。如果出現了上述的這種情形,那就代表一定存在某條黑邊的邊權=某條白邊的邊權。這時候在做最小生成樹的時候就只要優先選擇白邊就可以了。證明留給讀者自行思考。下面參考資料也有。
#define pii pair<int, int>
struct E {
int a, b, v, c;
} e[maxn], ae[maxn];
int pa[maxn], sz[maxn];
void init_dsu(int n) { for(int i = 1; i <= n; i ++) pa[i] = i, sz[i] = 1; }
int get(int x) { return pa[x] == x? x : pa[x] = get(pa[x]); }
void uni(int a, int b) {
a = get(a), b = get(b);
if(a == b) return;
if(sz[a] > sz[b]) swap(a, b);
sz[b] += sz[a];
pa[a] = b;
}
pii kruskal(int n, int m) {
init_dsu(n);
sort(e + 1, e + m + 1, [](E a, E b) {
if(a.v == b.v) return a.c < b.c;
return a.v < b.v;
});
int ret = 0, cnt = 0, val = 0;
for(int i = 1; i <= m; i ++) {
int a = get(e[i].a), b = get(e[i].b);
if(a == b) continue;
uni(a, b);
cnt ++;
if(e[i].c == 0) ret ++;
val += e[i].v;
}
return make_pair(ret, val);
}
int n, m, tar;
void solve() {
scanf("%d%d%d", &n, &m, &tar);
for(int i = 1; i <= m; i ++) {
int a, b, c, v;
scanf("%d%d%d%d", &a, &b, &v, &c);
a ++, b ++;
ae[i] = {a, b, v, c};
}
int l = -100, r = 100, tt = 10, ans = 1e18;
while(l <= r) {
int mid = (l + r) >> 1;
for(int i = 1; i <= m; i ++) {
e[i] = ae[i];
if(e[i].c == 0) e[i].v -= mid;
}
pii tp = kruskal(n, m);
int t = tp.f, v = tp.s;
if(t < tar) l = mid + 1;
else {
r = mid - 1;
ans = v + mid * tar;
}
}
printf("%d\n", ans);
}
PS. 這題還可以做得更快,先把白邊跟黑邊分開 sort 。之後 Kruskal 要排序的時候用雙指針排序即可。複雜度少一個log。
上面介紹了 Aliens 優化的用法,下面這邊來一題套用在 DP 上的用法。
APIO 2010 Commando 魔改題
給你一個長度為 的正整數列 以及一個二次方程 。你可以將這個數列分成連續的很多塊,而該分法的價值就是每塊的總和帶入 之後的函數值的總和。求最多分成塊可以獲得的最大價值為何。
題目連結 幾乎同一題
現在多了一個最多塊的限制,但顯然選少於塊是不優的。所以我們就把他當成約束問題來做就好了。
容易推出一個暴力的DP式:代表前個分成塊的答案。
一樣,透過觀察打表可以發現是一個凸的函數,所以我們可以考慮使用Aliens。使用 Aliens 的話也就是每多選一塊需要多花的價值,所以 DP 式變成:
代表前個的答案,
於是這題就變成 Commando 那題 的Case了,斜率優化解決!總複雜度:。
總結
使用 Aliens Trick 的時機:K約束問題+凸性函數
如何判斷是否為凸性函數?
寫一個暴力作法打表看有沒有差分遞減/增
小心假設、大膽猜測、不用證明!
通常證明都很不可愛,打表比較實際
檢查一下題目需不需要小數二分,或是可以靠題目性質轉成整數二分
題目
參考文獻
Creeper_LKF,博客园,『关于WQS二分算法以及其一个细节证明』
Last updated
Was this helpful?