這週要介紹的搜尋方法是以數列呈現
顧名思義,是將集合的所有子集挑出來,所有可能子集的集合又稱冪集合[1]
例如
void powerset(int dep) {
if(dep == N) {
for (int i = 0; i < N; i++) printf("%d ", bit[i]);
putchar('\n');
return;
}
bit[dep] = 1;
powerset(dep+1);
bit[dep] = 0;
powerset(dep+1);
}
1 1 1
1 1 0
1 0 1
1 0 0
0 1 1
0 1 0
0 0 1
0 0 0
for (int i = 0; i < (1<<N); i++) {
for (int p = 0; p < N; p++) printf("%d ", bool(i&(1<<p)));
putchar('\n');
}
0 0 0
1 0 0
0 1 0
1 1 0
0 0 1
1 0 1
0 1 1
1 1 1
在許多場合,我們不一定只有 0 和 1 兩種佔位符 (placeholder),可能有三種甚至更多
所以遞迴法或許比二進制法還要更為泛用。
許多文章介紹二分搜沒交代好前提,是採用大量案例佐證二分搜的實作很正確。
我認為這樣不優,這裡會花較多的篇幅把一些約定俗成的事情交代清楚再給出實作
給定一個
當目標有 1 個以上,這時有兩種位置: upper bound 與 lower bound
考慮數列
0
, 1
3
, 4
, 5
, 6
, 7
, ..當然,通常會希望 upper bound 與 lower bound 越緊越好
所以上面拿的 upper bound 要是 3
、lower bound 要是 1
才好?
在繼續進到實作之前,先討論信仰
upper bound 與 lower bound 真的越緊越好嗎?
以上面例子,有沒有可能 lower bound 取 0
、upper bound 取 4
在應用中會比較易用?
給定長度
大部份習慣數數從
但根據皮亞諾公設對自然數的定義,數應該要以 0
開始[3]。
總之,對於這個數列,可以用整數子集
這會比符號
而且若是問
但問
對所有自然數除以
表達這件事只要寫
有時會需要用到空區間這個狀態,這時可以
先前提及的,C++的 STL 中,迭代器是以左閉右開區間來實作的。
講這麼多左閉右開區間的好(?),回到信仰話題,
普遍實作中會認為 lower bound 為 1
、upper bound 為 4
會比較好。
(以後 lower bound & upper bound 若沒特別設定,都依此慣例)
若要搜尋的數不在數列中,那應該輸出哪個 index?
普遍實作會輸出當此數插入到這個數列中它最適合的位置
何謂最適合?就是要保持著數列仍然單調[5]。
對於數列的 index 區間
欲搜尋的數如果大於整個數列,它最適合的位置就是
而對於所有可能輸出的 bound,可以用空區間表示:
也就是
雖然意義上都是一個空區間,但看符號還能看出 表達的 index 是啥
先簡單思考,用最直覺的枚舉方式
來實作找 lower bound 的演算法吧:
假設
簡單的作法就是,一直遞增左界:
while (l != r) {
int m = l;
if (A[m] >= target) r = l;
else l = m + 1;
}
return l;
另一個從右界遞減的演算法:
while (l != r) {
int m = r - 1; // m 需在區間內
if (A[m] >= target) r = m;
else l = r;
}
return l;
兩個演算法都使 l
, r
相等且得到
回到小節標題,二分搜?這名字就是演算法的動機,將數列切成兩份以做到搜尋:
將上述兩種 linear search 演算法合併起來會得到
/* random binary search for lower bound */
while (l != r) {
int m = l + rand()%(r-l); // 切成兩份
if (A[m] >= target) r = m;
else l = m + 1;
}
return l;
因為不知道 m
該遵從哪個演算法,就改成在區間中隨機挑了
這一步非常重要,先思考這樣寫真的正確嗎?
而 lower bound 普遍的二分搜實作,就僅把 m
改成均等的兩份:
int m = (l+r)/2;
m
其實就是 middle 的縮寫哦
而 upper bound 的二分搜實作也是類似的:
/* upper bound */
while (l != r) {
int m = (l+r)/2;
if (A[m] <= target) l = m + 1;
else r = m;
}
return l;
二分搜複雜度為
讀者們就跟著 lower bound 的發明過程,嘗試實作 upper bound 二分搜!
光只會使用 STL 中的std::lower_bound
, std::upper_bound
函數還不夠對付所有問題,因應不同場合常得親自設計 (e.g. 三分搜、隱式數列)
在第三週教材的練習中,這題的 small dataset 很輕易的就能用枚舉做出來,但對於 large dataset,
仔細想想,雖然對數列排序後不會使
但排序後,當不考慮乘積為
這樣想,二分搜就有武用之地了!
乘積
sort(A, A+N);
long long cnt = 0; // cnt := counter
for (int i = 0; i < N-1; i++) {
for (int j = i+1; j < N; j++) {
long long t = A[i]*A[j]; // t := target
if (t || !A[j]) cnt += upper_bound(A+j+1, A+N, t) - lower_bound(A+j+1, A+N, t);
else cnt += upper_bound(A+i+1, A+j, 0) - lower_bound(A+i+1, A+j, 0);
}
}
此題其實還能讓複雜度從
int const maxn = 1001 + 10;
簡單的,採用枚舉的方式去找出哪個"最大傷害強度"是可行的
把可行的"最大傷害強度"找出來,接著在其之中把最小值輸出就行了
首先做出一個可判定此"最大傷害強度"是否可行的函數:
bool check(int strength) {
int cost = 0, cnt = 0;
for (int i = 0; i < n; i++) {
cost += s[i];
if (cost > strength) cost = s[i], cnt++;
}
return cnt <= w;
}
接著枚舉一下:
int l = *max_element(s, s+n), r = maxn*maxn;
for (int i = l; i <= r; i++)
if (check(i)) return i;
可以發現到,第一個遇到可行的"最大傷害強度" (strength
) 就是題目要求的最終答案。
但可惜的是,這樣
其中
來自於 r = maxn*maxn
研究一下 check
函數可知,因為每次把 strength
條大,會造成 cnt
越來越小,所以返回的 bool
值形成一個單調數列,
於是,可以使用二分搜去改進原本枚舉的做法:
while (l != r) {
int m = (l+r)/2;
if (check(m)) r = m;
else l = m+1;
}
return l;
複雜度改進成
在給定
則 LIS 為 或
建議複習第三週教材的 LIS 做法
貪心的看,當前 LIS 末項數字越小,那麼越有可能使得 LIS 繼續變長
例如
有 LIS ,但之後欲接 ,則只有 能接得
則定義狀態
例如
的 前綴為 ,則
求解
而考慮讓 LIS 增長的話,
綜合上述,狀態轉移方程:
有可能無解, 或許找不到長度為 的 LIS
邊界:
S[1][1] = a[1];
int L = 1; // LIS 當前長度,由於現在只解出 i = 1 時的狀態
for(int i = 2; i <= N; i++) {
for(int l = 1; l <= L; l++) {
if(a[i] > S[i-1][l-1]) S[i][l] = min(S[i-1][l], a[i]);
else S[i][l] = S[i-1][l];
}
if(a[i] > S[i-1][L]) L++, S[i][L] = a[i]; // 考慮讓 LIS 增長
}
有了
for(int l = 1; l <= L; l++) printf("%d ", S[n-L+l][l]);
由於狀態的解是
S[1] = a[1];
int L = 1;
for(int i = 1, j; i <= N; i++) { // j 紀錄哪個 S[l] 被解
for(int l = 1; l <= L; l++)
if(a[i] > S[l-1] && a[i] <= S[l]) j = l;
if(a[i] > S[L]) L++, j = L; // 考慮讓 LIS 增長
// 紀錄 LIS 的樣子
p[j] = i;
if(j > 1) f[i] = p[j-1];
else f[i] = i;
S[j] = a[i];
}
但因為把前綴 f
紀錄 LIS
其中 p[j]
為 S[j]
在原本
如第三週所教的操作,可將 LIS 利用 f
印出
void print_LIS(int i) {
if(i == f[i]) {
printf("%d ", a[i]);
return;
}
print_LIS(f[i]);
printf("%d ", a[i]);
}
等一等…
仔細觀察
於是每次要用 a[i]
解 s[j]
時,就使用二分搜!
for(int i = 1; i <= N; i++) {
int j = lower_bound(S+1, S+1+L, a[i]) - S;
if(j > L) L++;
p[j] = i;
if(j > 1) f[i] = p[j-1];
else f[i] = i;
S[j] = a[i];
}
複雜度為
UVa OJ 10131 Is Bigger Smarter?
UVa OJ 10534 Wavio Sequence
UVa OJ 437 The Tower of Babylon
CODEFORCES 1257E The Contest
Sprout OJ 48 二元搜尋樹
AIZU Online Judge 0524 星座探し
LeetCode 15 3Sum
CODEFORCES 1118D Coffee and Coursework
CODEFORCES 1111C Creative Snap
GCJ Kickstart Round G 2018 B Combining Classes: Small dataset
CODEFORCES 1263C Everyone is a Winner!
CODEFORCES 1077D Cutting Out