二分搜

概念

固定一個區間 \([l,r]\)

利用 \(mid = (l+r)/2\) 的結果

決定要如何移動 \(l,r\)

最經典的例子

猜數字

我想一個介於 \([1,100]\) 的數字 \(x\)

每次猜測後,你會知道這個數字太大或太小

想辦法猜到這個數字

最常見的應用

  1. 在一個排序好的陣列中尋找 \(x\)
  2. 在一個排序好的陣列中尋找 \(\ge x\) 的第一個數字
  3. 在一個排序好的陣列中尋找 \(> x\) 的第一個數字

 

 

這些東西都在 STL 當中有內建

  1. std::set.find(x)
  2. std::set.lower_bound(x)
  3. std::set.upper_bound(x)

寫法

  1. 在一個排序好的陣列中尋找 \(x\)

 

 

int x;
cin >> x;

int l = 0, r = n-1;
while(l < r){
    int mid = (l+r)/2;
    if(arr[mid] > x) r = mid-1;
    else if(arr[mid] < x) l = mid+1;
    else{
        cout << mid << "\n";
    }
}

寫法

2. 在一個排序好的陣列中尋找 \(\ge x\) 的第一個數字

 

 

int x;
cin >> x;

int l = 0, r = n;
while(l < r){
    int mid = (l+r)/2;
    if(arr[mid] >= x) r = mid;
    else l = mid+1;
}


if(r==n){
    cout << -1 << "\n";
}else{
    cout << r << "\n";
}

寫法

3. 在一個排序好的陣列中尋找 \(> x\) 的第一個數字

 

 

int x;
cin >> x;

int l = 0, r = n;
while(l < r){
    int mid = (l+r)/2;
    if(arr[mid] > x) r = mid;
    else l = mid+1;
}


if(r==n){
    cout << -1 << "\n";
}else{
    cout << r << "\n";
}

小細節

寫法如下

int l = 0, r = n-1;

while(l < r){
    int mid = (l+r+1)/2;
    if(check(mid)) l = mid;
    else r = mid-1;
}

cout << l << "\n";

Alternative: 倍增法二分搜

從 \(l\) 開始,利用位元運算尋找 \(r-l\) 的距離

int l = 0, r = n-1;
for(int i = LOGN; i >= 0; i--){
    //1<<i 表示將 1 左移 i 位,等價於 2^i
    if(l+(1<<i) < n && arr[l+(1<<i)] <= x){
        l += (1<<i);
    }
}

if(arr[l]==x) cout << l << "\n";
else cout << -1 << "\n";

優點: 不用在意如 \(r-l=1\) 時的細節

應用

前面那些都可以直接使用 STL 的函數

那我們為什麼要學二分搜呢?

在一個型如 \(\{0,0,\cdots,0,1,\cdots,1,1\}\) 的陣列中尋找 \(0,1\) 交界

思考一下

你可能會覺得這就是剛剛講過的 lower_bound(0)

就可以解決的問題 那有什麼特別的點

如果把 \(1\) 想成 true

然後把 \(0\) 想成 false

二分搜答案!

當題目問的是以下幾種問題時,可以試著轉成二分搜思考

  1. 尋找最大滿足某條件的 \(x\)
  2. 尋找最小滿足某條件的 \(x\)

我們來看幾個例題

浮點數二分搜

與整數的二分搜有何不同?

我們無法精確找到確切的那個值

題目會要求誤差在 \(10^{-6}, 10^{-7}, 10^{-8}\) 以內

處理方式

定一個 \(\epsilon\) (EPS) 為一個很小的數字 \((10^{-7}, 10^{-8})\)

然後當 \(r-l \le \epsilon\) 的時候跳出迴圈

const double EPS = 1e-7;

double l = 0, r = n;
while(r-l <= EPS){
    double mid = (l+r)/2;
    if(check(mid)) r = mid;
    else l = mid;
}

更好的方式

直接執行夠多的次數 \(100\) 次等等

答案就夠精確了

double l = 0, r = n;
for(int i = 0;i < 100;i++){
    double mid = (l+r)/2;
    if(check(mid)) r = mid;
    else l = mid;
}

最優比率

當題目出現以下式子

\max (\frac{a_1+a_2+\cdots+a_n}{b_1+b_2+\cdots+b_n})

我們也可以二分搜!

考慮對答案二分搜

如果我們有一個數字 \(x\)

要怎麼檢查 

\frac{a_1+a_2+\cdots+a_n}{b_1+b_2+\cdots+b_n} \le x

考慮對答案二分搜

如果我們有一個數字 \(x\)

要怎麼檢查 

\frac{a_1+a_2+\cdots+a_n}{b_1+b_2+\cdots+b_n} \le x

轉換式子

考慮對答案二分搜

如果我們有一個數字 \(x\)

要怎麼檢查 

(a_1-b_1 x)+(a_2- b_2 x)+\cdots+(a_n- b_n x) \le 0

轉換式子

例題

二分搜

By sam571128

二分搜

  • 84