---
# System prepended metadata

title: 搜尋基礎

---

## 定義
從一堆資料中找到想要的資料，這就是搜尋，比如在一堆考卷中找到自己的考卷
有一個最簡單的方法，就是從頭開始一張張翻看名字，直到找到自己的考卷
## 線性搜尋
所謂的線性搜尋，就是剛剛說的方法，也是最直覺的方式
```cpp=
int l_search(int n, int cur) {
    for (int i=0;i<n;i++)
        if (a[i] == cur) // 找到了
            return i ;  // 回傳位置
    return n ; // 沒找到 回傳範圍外的值
}
```
這樣做的時間複雜度是 $O(n)$，如果要找的次數過多，數據的量過大，找起來就會非常耗時
所以這時候就會需要一個速度快一點的搜尋方式
## 二分搜尋
二分搜尋最簡單的例子就是在猜數字，猜 $1\sim 100$，每次都猜範圍的中間
如果用概念去想一下就知道，因為每次範圍都可以減半，所以找起來特別快
不過這麼快的搜尋速度是有條件的，就是陣列必須滿足單調性
所謂的單調性可以簡單理解成遞增或遞減，需要這個條件的原因比較重要
查找的時候需要判斷數值的大小關係，右(左)邊一定要皆 $\ge(\le)$ 中點
這樣才能透過判斷來縮減查找的範圍，而實作上需要用 sort() 或題目自帶滿足單調性
由於這種搜尋一次減少一半範圍的做法，二分搜的時間複雜度為 $O(log(n))$
```cpp=
int b_search(int n, int cur) {
    int L = 0, R = n ; // 範圍的左右端點

    while (L < R) {
        int M = (L+R) / 2 ; // 中間點
        if (a[M] < cur) // 點在右半邊
            L = M+1 ;    // 拋棄左半邊
        else if (a[M] > cur) // 點在左半邊
            R = M ;         // 拋棄右半邊
        else                  // 找到了
            return M ;
    }

    return n ; // 沒找到 回傳範圍外的值
}
```
不過這樣其實很容易寫錯，因為實作方式其實有很多種，比如上述就是用左閉右開的概念
換句話說就是判斷的範圍包含左邊，但不包含右邊，"純搜尋"建議用左閉右閉的方法
並且找 $\ge cur$ 的第一個位置，比較不會出錯，使用範圍也比較廣泛
不過在更進階的題目上，二分搜的使用需要更靈活，建議還是用推的會比較好
通常只要知道左右的開(閉合)情況，就可以快速地寫出對應的二分搜
```cpp=
int b_search_p(int n, int cur) {
    int L = 0, R = n-1 ;

    while (L < R) {
        int M = (L+R) / 2 ; // 中間點
        if (a[M] < cur) // 點在右半邊
            L = M+1 ;    // 拋棄左半邊
        else if (a[M] > cur) // 點在左半邊
            R = M-1 ;         // 拋棄右半邊
        else                  // 找到了
            return M ;
    }

    return L ; // return R 也可以
}
```
但在"只需要"搜尋的場合，還是會用 STL 內建的函式來解決
```cpp=
auto l_pos = lower_bound(v.begin(), v.end(), 6) ; // iterator
auto u_pos = lower_bound(v.begin(), v.end(), 6) ; // iterator

cout << "index: " << l_pos-v.begin() ; // position
cout << ' ' << u_pos-v.begin() << '\n' ; // position
cout << "value: " << *l_pos << ' ' << *u_pos << '\n' ;
```

實際上用到搜尋的例子會不僅限於"在資料內找到某數"的概念
在比較困難的題目就有可能會遇到其他方式的搜尋，比如"在範圍內找到最小可通過解"
這時候通常就會用搜尋+函式判斷是否能通過，一般都用二分搜
這種比較困難的題目，最後面會放一題相對簡單的，有興趣的可以試試看

## 例題-ZJ f071. 2. 刮刮樂 (Lottery)
[題目連結](https://zerojudge.tw/ShowProblem?problemid=f071)
解題思路 :
這題不難，就是判斷資料當中是否有對應的數字，並根據規則做加減
不過有幾個需要注意的地方，也算是題目敘述不足的部分
第一就是幸運數字可以重複使用，舉個例子，假設你的幸運數字是 $1$，號碼有五個 $1$
那這五個 $1$ 的獎金都可以被你拿走，第二個是幸運數字有可能相同
這個情況比較複雜，假設第一個跟第二個幸運數字相同，那一個號碼的獎金還是一份
假設第一(二)個跟第三個幸運數字相同，那因為獎金增加又減少，所以等於不變
至於搜尋的部分，因為這裡數字沒排序，而且只需要找 $3\times 5$ 次，所以用線性搜尋就好
```cpp=
#include<bits/stdc++.h>
using namespace std ;

int main() {
    ios::sync_with_stdio(0), cin.tie(0) ;

    int a, b, c ;
    cin >> a >> b >> c ;

    vector<int> v(5), m(5) ; // 輸入
    for (int i=0;i<5;i++)
        cin >> v[i] ;
    for (int i=0;i<5;i++)
        cin >> m[i] ;

    bool havec = false ; // 是否出現過 c
    int ans = 0 ;        // 總和
    for (int i=0;i<5;i++) {
        if (v[i] == a || v[i] == b) // 遇到 a 或 b (a == b 算一份獎金)
            ans += m[i] ;
        if (v[i] == c) {            // 遇到 c (注意 a== c or a == b)
            ans -= m[i] ;
            havec = true ;
        }
    }

    if (havec) // 有出現過 c
        cout << max(ans, 0) <<  '\n' ;
    else        // 沒出現過 c
        cout << ans*2 << '\n' ;
    return 0 ;
}
```



## 例題-ZJ d732. 二分搜尋法
[題目連結](https://zerojudge.tw/ShowProblem?problemid=d732)
解題思路 :
題目問是否存在一個整數存在數列當中，通常用 lower_bound() 就可以了
因為 lower_bound() 是找大於等於的第一個數，所以找到的數可能不是題目要求的
如果找到的數跟題目要求的不同，那就代表沒找到，不過還有一種情況要考慮
如果題目要求的數字 $>$ 數列的最大數，那找到的位置就會是陣列的範圍外
所以這時候不能直接轉換成數字，而是要用 if 去把它過濾掉
```cpp=
#include<bits/stdc++.h>
using namespace std ;

int main() {
    ios::sync_with_stdio(0), cin.tie(0) ;

    int n, k ;
    cin >> n >> k ;

    vector<int> v(n) ; // 輸入
    for (int i=0;i<n;i++)
        cin >> v[i] ;

    int cur ;
    for (int i=0;i<k;i++) { // 找 k 個數
        cin >> cur ;
        
        // 二分搜找位置
        auto pos = lower_bound(v.begin(), v.end(), cur) ;
        if (pos == v.end() || *pos != cur) // 未找到
            cout << "0\n" ;
        else                               // 找到了
            cout << (pos-v.begin())+1 << '\n' ;
    }

    return 0 ;
}
```
注意我在第 $19$ 行有用到 if 的特性，所以 `pos == v.end()` 成立就不會判斷 `*pos != cur`
也就是前面說的小心使用到範圍外的位置，會導致錯誤

## 例題-ZJ f581. 3. 圓環出口
[題目連結](https://zerojudge.tw/ShowProblem?problemid=f581)
解題思路 :
(建議先學過基礎演算法-前綴和與差分再解這題)
這題的題目敘述就有點不太好理解了，直接用題目的例子去理解會比較快
假設房間給與的點數分別為 $2,1,5,4,3,5,3$，要求的點數分別為 $8,9,12$
那第一個要 $8$ 點，所以 $2+1+5 = 8$，停下來的地方是 "點數為 $4$ 的房間"
所以是滿足條件後的一個房間，第二個要 $9$ 點，所以 $4+3+5=12$
從 $4$ 開始，所以每次都是從停下的地方開始加，加到滿足後再往後一個房間
接下來就是如何快速找到滿足條件的房間，這裡就需要用到前綴和了
因為前綴和可以快速計算兩個房間路徑上的點數總和，不過有些問題
假設要從第 $5$ 間房間到第 $3$ 間房間，一般的前綴和就無法輕鬆解決
所以我們可以把前綴和擴充，把最後一間到第一間到...到最後二間的前綴和也加進去
```cpp=
vector<int> pre(2*n-1) ; // 前綴和
for (int i=0;i<n;i++) {
    cin >> pre[i] ;
    pre[i] += pre[i-1] ; // 直接計算前綴和
}

for (int i=n;i<2*n-1;i++) // 把前綴和拆成 pre[n-1] 與其他
    pre[i] = pre[n-1] + pre[i%n] ;
```
這樣就如果需要 need 個點數，用二分搜找 `pre[(pos+n-1)%n]+need` 就可以找到滿足條件的位置
最後在記得把位置往後一位，然後把位置取 $n$ 的餘數，因為前綴和的大小超過 $n$
```cpp=
#include<bits/stdc++.h>
using namespace std ;

int main() {
    ios::sync_with_stdio(0), cin.tie(0) ;

    int n, m ;
    cin >> n >> m ;

    vector<int> pre(2*n-1) ; // 前綴和
    cin >> pre[0] ;

    // 前綴和
    for (int i=1;i<n;i++) {
        cin >> pre[i] ;
        pre[i] += pre[i-1] ;
    }
    for (int i=n;i<2*n-1;i++)
        pre[i] = pre[n-1] + pre[i%n] ;

    int need, pos = 0 ;
    for (int i=0;i<m;i++) {
        cin >> need ;
        
        // 找到滿足條件的位置
        auto np = lower_bound(pre.begin(), pre.end(), pre[(pos+n-1)%n]+need) ;
        pos = (np-pre.begin()+1) % n ; // 再往後一間
    }

    cout << pos << '\n' ;
    return 0 ;
}
```

## 例題-ZJ f815. TOI_y21m4_a01遊戲升等
[題目連結](https://zerojudge.tw/ShowProblem?problemid=f815)
(這題是比較困難的題目，如果能看得懂就看，看不懂可以先跳過)
解題思路 :
首先要說明，如果升兩等不可以升兩次一等，題目好像沒說，在這裡補充一下
再來就是計算每個角色升到 $x$ 等的花費有沒有超過持有的金幣，這個應該不難，用 for-loop
```cpp=
bool cost(vector<int> &v, int lvl) {
    // 如果都升到 lvl，需要花費的金幣有沒有在 c 之內
    LL now = 0 ;
    for (int i=0;i<n;i++) { // 計算需要花費多少金幣
        if (lvl > v[i]) {    // 需要升級
            now += (LL)(lvl-v[i])*(lvl-v[i]) ;
            if (now > c)      // 花費超過 c
                return false ;
        }
    }
    return true ; // 算完沒超過 c
}
```
接下來才是比較困難的地方，就是要找到滿足條件的最大可能，最簡單的方式就是線性搜尋
也就是從 $1$ 開始，慢慢數字往上加，就可以找到滿足條件的最大可能了
```cpp=
for (int i=1;i<m;i++) {
    if (!cost(i)) {
        cout << i-1 << '\n' ;
        return 0 ;
    }
}
```
不過這樣的做法時間複雜度有點高，假設等級範圍是 $0 \sim m$，時間複雜度就是 $O(nm)$
可能有人不知道等級範圍怎麼算，其實只要考慮最極端的情況就可以了
首先金幣只花在一個人，升的等級肯定最多，再來那個人本身的等級越高越好
所以金幣最多 $10^{14}$，可以升 $10^7$ 等，角色原先最多 $10^7$ 等，加起來最多 $2\times 10^7$
那已經知道線性搜尋效率太差，那要不要試試看二分搜，畢竟概念上可以把可能性看陣列
陣列的內的元素就會像這樣: $\{cost(1), cost(2), ..., cost(m)\}$
那就可以用二分搜去找滿足條件的最大值，不過跟一般的二分搜不太一樣
```cpp=
#include<bits/stdc++.h>
using namespace std ;
typedef long long LL ;

int n ;
LL c ;

bool cost(vector<int> &v, int lvl) {
    // 如果都升到 lvl，需要花費的金幣有沒有在 c 之內
    LL now = 0 ;
    for (int i=0;i<n;i++) { // 計算需要花費多少金幣
        if (lvl > v[i]) {    // 需要升級
            now += (LL)(lvl-v[i])*(lvl-v[i]) ;
            if (now > c)      // 花費超過 c
                return false ;
        }
    }
    return true ; // 算完沒超過 c
}

int main() {
    ios::sync_with_stdio(0), cin.tie(0) ;

    cin >> n >> c ;

    vector<int> v(n) ;
    for (int i=0;i<n;i++)
        cin >> v[i] ;

    // 用二分搜去找滿足條件的最大可能
    int L = 0, R = INT_MAX ;
    while (L <= R) { 
        int M = (L+R) / 2 ;
        if (cost(v, M)) // 滿足條件
            L = M+1 ;
        else             // 不滿足條件
            R = M-1 ;
    }
    cout << R << '\n' ;

    return 0 ;
}
```
這個二分搜跟我們上面介紹的有點不一樣，只要用推的去理解就可以發現差異帶來的影響
二分搜只要考慮三種問題，第一是能不能找到對的值，第二是能不能停下來，第三是答案是 L 還是 R
通常會卡在第二與第三，考試或檢定通常會有紙，記得用比較小的情況推一下才不會出錯
