# 排序與搜尋 ## 3/1 社課 --- ## 排序 ---- 簡單來講 就是某些題目需要讓序列按照某種順序排列 就需要排序 ---- **std::sort !!** ~~他太好用了所以我們就只講他吧~~ ---- ``#include <algorithm>`` 如何 sort 一個序列 ```cpp= sort(begin_iter, end_iter, comp_func); ``` `begin_iter` 和 `end_iter` 就是儲存資料的位址 如果用靜態陣列,第 $i$ 個就是 `arr+i` 如果用 vector 那就是 `v.begin()+i` 左閉右開區間 ---- ### compare_function 排序的方法 不輸入的話就是預設排序(由小到大排序) 否則這裡需要輸入一個比較函式 ---- ### 比較函式的輸入方法 ```cpp= bool cmp(data_type a, data_type b){ // ... } ``` - 回傳值是布林值 - `data_type` 是你要排序的資料型態 - 要回傳的就是 **a 是否要排在 b 前面** - 不能發生 `cmp(a, b)=cmp(b, a)=true` 的情況 ---- 排序成遞減序列 ```cpp= bool cmp(int a, int b){ return a>b; // 不能有等號 } ``` ---- 複雜度 $O(n\log{n})$ ---- [TOI 2023 pA. 房屋推薦 (house)](https://zerojudge.tw/ShowProblem?problemid=k184) ![](https://hackmd.io/_uploads/BJjWNdj2p.png) ---- 用剛剛的 sort 怎麼排? ---- 直接照他想要的順序排! ```cpp= struct house{ int id, r; long long x, y; long long mnd=9e18; } arr[100005]; bool cmp(house a, house b){ if(a.mnd!=b.mnd) return a.mnd<b.mnd; if(a.r!=b.r) return a.r<b.r; return a.id<b.id; } ``` 對每個捷運站找最小距離平方 然後就可以排序,複雜度 $O(nm+n\log{n})$ 很緊,要加 IO 優化才會過 --- ## 搜尋 --- ## 暴搜(枚舉) ---- 在某些範圍很小的題目 可以枚舉每一種情況 ---- ### [ABC 335 B - Tetrahedral Number](https://atcoder.jp/contests/abc335/tasks/abc335_b) 找出所有非負整數 $(x,y,z)$ 滿足 $x+y+z\le N$ 按字典序輸出 $0\le N\le 21$ ---- 看到 $N$ 很小 就知道可以枚舉 ---- 因為要照字典序排 所以就按 $x,y,z$ 的順序枚舉 ```cpp= #include<bits/stdc++.h> #define fastio ios::sync_with_stdio(0); cin.tie(0); using namespace std; int n; int main(){ fastio cin >> n; for(int i=0; i<=n; i++){ for(int j=0; i+j<=n; j++){ for(int k=0; i+j+k<=n; k++){ cout << i << ' ' << j << ' ' << k << '\n'; } } } } ``` ---- ### 怎麼找質因數 ---- 從 $1$ 找到 $N$ 看是不是整除? $O(n)$ 太慢了 ---- 縮小範圍一下 一個數字 $N$ 不會有超過一個 $\ge\sqrt{N}$ 的質因數 所以我們只需要枚舉到 $\sqrt{N}$ 就好了! ---- $O(\sqrt{N})$ ```cpp= #include<bits/stdc++.h> #define fastio ios::sync_with_stdio(0); cin.tie(0); using namespace std; int n; int main(){ fastio cin >> n; for(int i=2; i<=sqrt(n); i++){ int a=0; while(n%i==0){ // i 整除 n if(a==0) cout << i; n/=i; a++; } if(a>1) cout << "^" << a; // 指數大於 1 if(n==1) break; // 質因數分解完了 if(a!=0) cout << " * "; } if(n>1) cout << n << '\n'; // 大於 sqrt(n) 的質因數 } ``` --- ## 二分搜 ---- **猜數字遊戲** 在範圍 $[1,1000]$ 裡猜一個數字 $n$ 如果猜的數字 $x<n$ 會告訴你太小了 如果猜的數字 $x>n$ 會告訴你太大了 怎麼猜比較快? ---- 如果現在猜了一個數字 $x<n$ 那就表示 $\le x$ 的數都是不可能的 所以猜的範圍就變小了 ---- 那就每次都猜範圍內的最中間! 每次範圍都會少一半 複雜度 $O(\log{n})$! (在計算機當中 $\log{}$ 常代表 $\log_2{}$) ---- ```cpp= int l=0, r=1000; while(r-l>1){ int mid=(l+r)/2; if(mid<n) l=mid; else r=mid; } ``` ---- 最後答案是什麼? ---- $r$ 都是大於等於 $n$ 的 $l$ 都是小於 $n$ 的 也就是 $l$ 不可能等於 $n$ 最後答案就是 $r$ ---- **實作細節** 要注意 $l$ 和 $r$ 分別代表兩種狀態 像上面這個 $l$ 就是小於 $n$ 的 $r$ 就是大於等於 $n$ 的 所以 `mid=(r+l)/2` 如果 $mid$ 小於 $n$ 就要讓 $l=mid$,反之亦然 ---- 如果 $(l+r)/2$ 會溢位 可以用 $l+(r-l)/2$ 達到同樣的效果 ---- 抱著上面的想法 我們可以歸納出 把整段數字分成 **符合條件的** 和 **不符合的** 分別代表兩邊 然後再用二分搜縮小範圍 ---- ### [APCS 2017-0304-4基地台](https://zerojudge.tw/ShowProblem?problemid=c575) 要設置基地台 假設在點 $x$ 設置了一個基地台 範圍 $[x-\frac{D}{2}, x+\frac{D}{2}]$ 都會被覆蓋到 現在給定 $N$ 個點座標,最多能蓋 $K$ 個基地台 求 $D$ 最少要多少才能覆蓋到所有點 (基地座標可以是浮點數) ---- 既然他要求長度 那就對長度做二分搜! ---- 每次看看某個長度 能不能把需要建造的基地台數壓到 $K$ 個以下 如果長度 $D$ 可以,那長度 $D+1$ 一定可以! 所以問題就回到了 **可以/不可以** 兩種情況了 二分搜! ---- 至於判斷可不可以 就從左掃到右 一旦有一個沒被前面放的那個掃到 就把這裡當作下個基地台的起點 ---- $O(N(\log{N}+\log{(\max{P_i})}))$ ```cpp= #include<bits/stdc++.h> #define fastio ios::sync_with_stdio(0); cin.tie(0); using namespace std; int n, k, p[50005]; bool check(int len){ int cnt=1, r=p[0]+len; // cnt: 基地台數,r: 現在基地台右界 for(int i=0; i<n; i++){ if(p[i]<=r) continue; // 這個座標包含在上個基地台範圍內 else{ r=p[i]+len; // 把這個點當作新的左界,更新右界 cnt++; } } return cnt<=k; // 回傳是否可以設置 k 個以內 } int main(){ fastio cin >> n >> k; for(int i=0; i<n; i++) cin >> p[i]; sort(p, p+n); int l=0, r=1e9+5; // 座標最大 1e9 -> 長度最大 1e9 while(r-l>1){ // r 表示可以的長度 int m=(l+r)/2; if(check(m)) r=m; else l=m; } cout << r << '\n'; } ``` ---- 一些好用的函式 ```cpp lower_bound(begin_iter, end_iter, num); upper_bound(begin_iter, end_iter, num); ``` `lower_bound()` 可以找到序列中第一個大於等於 num 的指標 `upper_bound()` 可以找到序列中第一個大於 num 的指標 都是 $O(\log{n})$ 尋找 ---- ### [ABC 334 D - Reindeer and Sleigh](https://atcoder.jp/contests/abc334/tasks/abc334_d) 有 $N$ 個雪橇,第 $i$ 個雪橇需要 $R_i$ 隻馴鹿才能拉動 有 $Q$ 筆詢問,每筆詢問給定一個 $X$ 問你當有 $X$ 隻馴鹿時可以拉動幾個雪橇 ---- 雪橇的順序不影響我們選哪個 所以可以先按馴鹿需求量排序 每個雪橇需求量越少可以拉越多雪橇 也就是從需求量少的開始選 一定可以選到最多雪橇 ---- 把排序好的序列做前綴和再二分搜就好了! `upper_bound()` 可以找到大於 $X$ 的第一個位置 這個位置減去最前面的位置 就是小於等於 $X$ 的序列長度 也就是我們要的答案! ---- $O((N+Q)\log{N})$ ```cpp= #include<bits/stdc++.h> #define fastio ios::sync_with_stdio(0); cin.tie(0); using namespace std; int n, q, arr[200005]; long long pre[200005]; int main(){ fastio cin >> n >> q; for(int i=0; i<n; i++) cin >> arr[i]; sort(arr, arr+n); pre[0]=arr[0]; for(int i=1; i<n; i++) pre[i]=pre[i-1]+arr[i]; // 前綴和 for(int i=0; i<q; i++){ long long x; cin >> x; cout << upper_bound(pre, pre+n, x)-pre << '\n'; } } ``` --- ## 三分搜 ---- 現在給你一個上凹函數 請你求出他的最小值 ![image](https://hackmd.io/_uploads/Sk57ogT2T.png) ---- 一樣開始有一個區間讓我們搜尋 每次可以平分成三塊 有兩個切點 $lmid, rmid$ 如果 $f(lmid)<f(rmid)$ 那麼答案就會在 $[l, rmid]$,反之亦然 ![image](https://hackmd.io/_uploads/B1fHTep2T.png) ---- 怎麼判斷要什麼時候結束搜尋? 可以設定一個 $eps$ 表示很小的數 當 $r-l<eps$ 的時候就結束 另一種方法就是跑固定次數就停止 ---- 浮點數三分搜 ```cpp= double l, r; for(int i=0; i<100; i++){ double lm=(2*l+r)/3; double rm=(l+2*r)/3; if(f(lm)<f(rm)) r=rm; else l=lm; } ``` ---- 三分搜在 APCS 或競賽裡比較少見 主要是處理一些凹函數找極值的問題
{"description":"type: slide","contributors":"[{\"id\":\"1a0296c8-ce58-4742-acda-22c02ae81a74\",\"add\":6077,\"del\":288}]","title":"03/01 C++社課"}
    146 views