# 演算法課程題解 - 二分搜尋 # Kattis - Sort of Sorting ## 題目 https://open.kattis.com/problems/sortofsorting 今天有很多字串要做排序,排序的方式是這樣的 要比較兩個字串的大小,我們只需要看這兩個字串的前兩個字元即可 而這兩組的兩個字元所組成的兩組字串比較方式是字典序 如果說這兩組字串的字典序是相同的,那麼就依照原本的順序排即可 :::info 補充: 字典序 字典序指的是依照 `A-Z` 以及 `a-z` 的方式排列,更廣義的來說,你可以參考 ascii table 每個字所對應的數值,我們就是依照這個大小來比較的 在 C++ 當中,我們可以用 `int(c)` 來找到字元 `c` 所對應到的數值 ![](https://i.imgur.com/h5PB6X0.png) ::: 舉例來說,要排序兩個字串 `Poincare` 以及 `Pochhammmer` 因為前兩個字的字典序相同,所以依照原本的順序即可 `Poincare Pochhammmer` 再舉一個例子,要排序兩個字串 `Beethoven` 以及 `Bach` 的一個字元的字典序相同,而第二個字的字典序 `a` 比 `e` 小,所以放在前面 `Bach Beethoven` ## 想法 By Koios1143 其實這個排序方式就是所謂的 stable sort, 在 C++ 當中已經有這個函數可以使用 不同的是排序的方式,所以我們要另外寫 compare 函數 比較的方式在前面也有提到就不贅述,詳細可以直接看 code 當中的 `cmp()` ### 程式碼 ```cpp= //By Koios1143 #include<iostream> #include<algorithm> using namespace std; string arr[205]; bool cmp(string p, string q){ // 先比較第一個字元 if(int(p[0]) != int(q[0])){ return int(p[0]) < int(q[0]); } // 再比較第二個字元 else{ if(int(p[1]) != int(q[1])){ return int(p[1]) < int(q[1]); } } // false 表示字典序 p>=q return false; } int main(){ int n; bool out=false; while(cin>>n && n){ // 在輸出之間需要有換行 if(out) cout<<"\n"; else out = true; for(int i=0 ; i<n ; i++){ cin>>arr[i]; } stable_sort(arr, arr+n, cmp); for(int i=0 ; i<n ; i++){ cout<<arr[i]<<"\n"; } } return 0; } ``` ### 時間複雜度分析 每筆測資輸入時間複雜度 $O(n)$ 每筆測資排序時間複雜度 $O(nlogn)$ 每筆測資輸出時間複雜度 $O(n)$ 每筆測茲的總時間複雜度約為 $O(n + nlogn)$ # Kattis - Sideways Sorting ## 題目 https://open.kattis.com/problems/sidewayssorting 一般來說我們排序字串都是由左到右,但是本題要我們由上到下來看 輸入第一行包含兩個正整數 $r, c$,表示 row 與 column 接下來有 $r$ 行,每行 $c$ 個字元 最後輸出垂直方向由上到下的**穩定排序** 並且排序過程當中忽略大小寫 舉例來說 ``` oTs nwi eox ``` 從垂直的方向來看,我們會得到三個字串 ``` one Two six ``` 接下來排序這幾個字串 ``` one six Two ``` 最後排回原本的橫向 ``` osT niw exo ``` ## 想法 By Koios 因為排序的方向是垂直的,我們可以直接以垂直的方式儲存這些字串即可 接下來穩定排序過後在改變輸出的方向即可 至於排序的部分,我們可以先把所有要比較的字串先都轉換成小寫之後再來比較大小 ### 程式碼 ```cpp= //By Koios1143 #include<iostream> #include<algorithm> using namespace std; string arr[20]; bool cmp(string p, string q){ // 先轉換成小寫 for(int i=0 ; i<p.size() ; i++){ p[i] = tolower(p[i]); } for(int i=0 ; i<q.size() ; i++){ q[i] = tolower(q[i]); } // 接下來再比較大小 if(p != q) return p<q; return false; } int main(){ int r,c; char tmp; bool out=false; while(cin>>r>>c && (r!=0 && c!=0)){ // 輸出之間需要有換行 if(out) cout<<"\n"; else out=true; // 初始化所有字串 for(int i=0 ; i<c ; i++){ arr[i] = ""; } for(int i=0 ; i<r ; i++){ for(int j=0 ; j<c ; j++){ // 把字串串起來 cin>>tmp; arr[j]+=tmp; } } stable_sort(arr, arr+c, cmp); for(int i=0 ; i<r ; i++){ for(int j=0 ; j<c ; j++){ // 記得輸出方向是相反 cout<<arr[j][i]; } cout<<"\n"; } } return 0; } ``` ### 時間複雜度分析 每筆測資輸入時間複雜度為 $O(rc)$ 每筆測資初始化時間複雜度為 $O(rc)$ 每筆測資排序時間複雜度為 $O((len(p) + len(q)) \times nlogn)$ 每筆測資輸出時間複雜度為 $O(rc)$ 每筆測資總時間複雜度約為 $O(rc + nlogn)$ ($len(p) + len(q)$ 很小,可以忽視) # TOJ 47 ## 題目 https://toj.tfcis.org/oj/pro/47/ 給一串長度為 $n$ 的序列,有 $m$ 筆詢問 對於每一筆詢問 - 若 $q$ 存在於序列當中則直接輸出 $q$ - 若 $q$ 介於序列中的某兩連續元素之間,輸出那兩個元素 - 若 $q$ 小於序列的第零項(也就是 $q$ 小於序列中的所有元素),則輸出`no` 以及第零個元素 - 若 $q$ 大於序列的最後項(也就是 $q$ 大於序列中的所有元素),則輸出最後項以及 `no` ## 想法 By Koios 首先因為我們的答案可能藉在某兩個連續的元素之間,所以我們應該要嘗試讓序列是有**嚴格遞增**或是**嚴格遞減**的性質 所以我們第一步是要先將序列排序 排序過後,我們可以先處理兩個包含 `no` 的特例 直接判斷 $q$ 和 `arr[0]` 以及 `arr[n-1]` 的關係即可 如果 $q < arr[0]$ ,就輸出 `no arr[0]` 如果 $q > arr[n-1]$ ,就輸出 `arr[n-1] no` 接下來就是要來找我們的詢問 $q$ 是在序列中的哪個位置 我們可以用二分搜來序列當中第一個 $\geq q$ 的元素位置 (也就是 `lower_bound`) 接下來,如果找到的 $res$ 在 $arr$ 上就是 $q$ 那就直接輸出 否則直接輸出 `arr[res-1] arr[res]` ### 程式碼 ```cpp= //By Koios1143 #include<iostream> #include<algorithm> using namespace std; int n, m, q, res, arr[1000010]; int Lower_bound(int l, int r, int p){ while(l!=r){ // >= p 的要留下,其他的篩掉 int mid = (l+r)/2; if(arr[mid] < p) l = mid+1; else r = mid; } return l; } int main(){ cin>>n; for(int i=0 ; i<n ; i++){ cin>>arr[i]; } sort(arr, arr+n); cin>>m; for(int i=0 ; i<m ; i++){ cin>>q; // 先判斷包含 no 的情況 if(arr[0] > q) cout<<"no "<<arr[0]<<"\n"; else if(arr[n-1] < q) cout<<arr[n-1]<<" no\n"; // 再來判斷 q 必定包含在序列當中的狀況 else{ res = Lower_bound(0, n, q); if(arr[res] == q) cout<<q<<"\n"; else cout<<arr[res-1]<<" "<<arr[res]<<"\n"; } } return 0; } ``` :::info 實際上我們可以不用自行實作 lower_bound 在 C++ 可以直接 `#include<algorithm>`,裡面就有 `lower_bound` 以及 `upper_bound` 可以直接使用,使用方式如下 ```cpp= #include<iostream> #include<algorithm> using namespace std; int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int main(){ cout<<*lower_bound(arr, arr+10, 5)<<'\n'; // 輸出 5 return 0; } ``` `lower_bound` 以及 `upper_bound` 回傳的都是指標,所以要用 `*` 取出值 如果想要得到元素是位在 $arr$ 中的哪個位置,可以用 `lower_bound(arr, arr+10, 5)-arr` 取得 ::: ### 時間複雜度分析 輸入時間複雜度為 $O(n)$ 排序時間複雜度為 $O(nlogn)$ 每筆詢問進行二分搜的時間複雜度為 $O(logn)$ 總時間複雜度為 $O(n + (m+n)log)$ # TOJ 468 ## 題目 https://toj.tfcis.org/oj/pro/468/ 輸入包含 $n$ 個元素的序列,接下來 $m$ 筆詢問 每筆詢問包含兩個正整數 $p, q$,求在序列當中有多少百分比的元素介於 $p$ 至 $q$ 小數精確至小數點後第 $k$ 位 ## 想法 By Koios 先對序列做排序 接下來題目要問的是有多少元素介於 $p, q$ 之間 如果我們能找到第一個 $\geq p$ 的元素位置以及第一個 $> q$ 的元素位置,那麼這兩個元素之間的個數就是我們的答案 要找第一個 $\geq p$ 的元素位置就等同於找 lower_bound 要找第一個 $> q$ 的元素位置就等同於找 upper_bound ### 程式碼 ```cpp= //By Koios1143 #include<iostream> #include<algorithm> #include<iomanip> using namespace std; int n,k,m,l,r,arr[100010]; int Lower_bound(int l, int r, int q){ while(l!=r){ // >= p 的要留下,其他的篩掉 int mid = (l+r)/2; if(arr[mid] < q) l = mid+1; else r = mid; } return l; } int Upper_bound(int l, int r, int q){ while(l!=r){ // > p 的要留下,其他的篩掉 int mid = (l+r)/2; if(arr[mid] <= q) l = mid+1; else r = mid; } return l; } int main(){ cin>>n>>k; for(int i=0 ; i<n ; i++){ cin>>arr[i]; } sort(arr, arr+n); cin>>m; for(int i=0 ; i<m ; i++){ cin>>l>>r; if(l>r) swap(l, r); cout<<fixed<<setprecision(k)<<(double)((Upper_bound(0, n, r)) - (Lower_bound(0, n, l)))*100.0/n<<"%\n"; } return 0; } ``` :::info 跟上一題一樣的,實際上沒有必要自己實作 `lower_bound` 以及 `upper_bound` 可以直接引用 C++ `algorithm` 裡的函數即可 ::: ### 時間複雜度分析 輸入時間複雜度為 $O(n)$ 排序時間複雜度為 $O(nlogn)$ 找 lower_bound 以及 upper_bound 總時間複雜度為 $O(2logn)$ 總時間複雜度為 $O(n + nlogn)$ # UVa 907 ## 題目 http://domen111.github.io/UVa-Easy-Viewer/?907 有一群人要去旅行,旅行的路線必須要經過 $n$ 個暫停點休息 已經知道包含起點以及終點這 $n+1$ 條邊之間的距離是多少 請問這一趟旅程當中一天最多走多長的距離,才可以在 $k$ 個晚上 (也就是 $k+1$ 天) 內到從起點達終點? ## 想法 如果說一天走 $m$ 個距離可以在 $k+1$ 天內到達,那麼 $m+1$ 也一定可以 所以我們發現到了行走距離是具有單調性的 有了單調性,要我們搜尋最佳答案就可以使用二分搜 我們可以二分搜一天最少所需的行走距離,如果說距離 $m$ 不能在 $k+1$ 天內走到,那就更新左界至 $m+1$,否則更新右界至 $m$ ### 程式碼 ```cpp= //By Koios1143 #include<iostream> using namespace std; int n, k, Min, Max, ans, arr[610]; int search(int l, int r, int p){ while(l!=r){ int mid = (l+r)/2; int days = 0, dis = 0; for(int i=0 ; i<n+1 ; i++){ if(arr[i] + dis > mid){ // 如果過去的距離加上當前的距離會超過,就表示需要多一天來走 // 並且要從當前距離開始繼續計算 days++; dis = arr[i]; } else{ dis += arr[i]; } } // 最後檢查有沒有剩下的路段 if(dis > 0) days++; // 記得 p 指的是多少個晚上,等同於天數-1 if(days-1 > p) l = mid+1; else r = mid; } return l; } int main(){ while(cin>>n>>k){ Min = Max = 0; for(int i=0 ; i<n+1 ; i++){ cin>>arr[i]; // 最短的單次距離至少是長度的最大值 Min = max(Min, arr[i]); // 最長的單次距離是一次走到終點 Max += arr[i]; } cout<<search(Min, Max, k)<<"\n"; } return 0; } ``` ### 時間複雜度分析 輸入時間複雜度為 $O(n+1)$ 二分搜的範圍最差會是總距離,也就是 $\sum_{i=0}^{n+1}{arr[i]}$ 搜尋時間複雜度為 $O(log{\sum_{i=0}^{n+1}{arr[i]}})$ 總時間複雜度約為 $O(n + log{\sum_{i=0}^{n+1}{arr[i]}})$ # ZJ c575 ## 題目 https://zerojudge.tw/ShowProblem?problemid=c575 有一條路上有 $n$ 個電信的服務點 $p_i$,現在我們要裝設最多 $k$ 個基地台,讓這 $n$ 個電信服務點都能接收到訊號 基地台可以放在座標點上的任意位置,並且有一個直徑為 $R$ 的訊號涵蓋範圍 給定這 $n$ 個點的座標,是否能找到一個最小 $R$,使得所有服務點都能接收到訊號 ## 想法 By Koios 如果說直徑 $R$ 可以在 $k$ 個基地台內涵蓋整個區域,那麼 $R+1$ 也一定可以 所以我們觀察到 $R$ 具有單調性,並且基地台是可以任意擺放的,哪麼要找 $R$ 的最小值就可以用二分搜來求解 我們可以二分搜 $R$ 的大小 從 $p_0$ 開始每次增加 $R$,把涵蓋在 $p_0$ ~ $p_0 + R$ 的服務點移除 接著從沒被移除掉的點中找到最左端的服務點繼續檢查 最後如果需要用的基地台數量大於 $k$ 就更新左界成 $R+1$,否則更新右界成 $R$ ### 程式碼 ```cpp= //By Koios1143 #include<iostream> #include<algorithm> using namespace std; long long n, k, Min, Max, arr[50010]; int search(long long l, long long r, long long p){ while(l!=r){ long long mid = (l+r)/2; long long dis = 0, cnt = 0; for(long long i=0, now=0 ; i<n ; ){ // 把涵蓋在 arr[i] ~ arr[i]+mid 的服務點移除(忽略) while(arr[now] <= arr[i]+mid && now<n) now++; cnt++; i = now; } // 最後檢查是否還有沒加上的基地台 if(dis > 0) cnt++; if(cnt > p) l = mid+1; else r = mid; } return l; } int main(){ while(cin>>n>>k){ Min = Max = 0; for(long long i=0 ; i<n ; i++){ cin>>arr[i]; } // 題目並無保證座標已經過排序 sort(arr, arr+n); // 不需要特別去找最小直徑,那需要額外 O(n) 的時間 Min = 1, Max = (arr[n-1]-arr[0])/k+1; cout<<search(Min, Max, k)<<"\n"; } return 0; } ``` ### 時間複雜度分析 輸入時間複雜度為 $O(n)$ 排序時間複雜度為 $O(nlogn)$ 二分搜的值為 $D = max(arr) - min(arr) + 1$ 搜尋時間複雜度為 $O(D)$ 本題的 $D$ 不超過 $10^6$ 總時間複雜度約為 $O(n + nlogn + log(D)$ # TOJ 556 ## 題目 https://toj.tfcis.org/oj/pro/556/ 有 $N$ 項工作,為了好好管理進度,我們把每一份工作都分成了 $K$ 份 對於第 $i$ 項工作每天可以完成 $a_i$ 份,並且每天都會做每一項工作 因為怕工作會做不完,我們有 $M$ 筆經費可以將工作外包 每一經費對於第 $i$ 項工作可以將其中的 $b_i$ 份工作外包,並且不需考慮外包的作業時間 請問在最佳分配經費的情況下,最少可以在幾天之內完成 $N$ 份工作 $$ 1 \leq N \leq 10^5\\ 1 \leq K \leq 10^9\\ 0 \leq M \leq 10^9\\ 1 \leq a_i \leq 10^9\\ 1 \leq b_i \leq 10^9 $$ ## 想法 By Koios 如果說 $X$ 天可以把工作處理完成,那麼 $X+1$ 天也一定可以 發現到天數是具有單調性的,那麼要求最小的天數就可以用二分搜來完成 二分搜最小所需要的天數 $X$,確認 $X$ 是否可行來調整左界與右界 那麼要怎麼確認 $X$ 天能不能完成呢? 我們可以把每一項工作都先減去 $X$ 天自己做可以完成的工作量 那麼剩下的所有工作就必須要外包才能完成了 最後計算需要外包的數量,就可以判斷是否可以在 $X$ 天完成了! ### 程式碼 ```cpp= //By Koios1143 #include<iostream> using namespace std; long long n, k, m, L=0, R=1e9+10, A[100005], B[100005], tmp[100005]; long long search(long long l, long long r, long long p){ while(l<r){ long long mid = (l+r)/2, cnt=0; for(long long i=0 ; i<n ; i++){ // 先扣除 mid 天可以自己完成的工作量 tmp[i] = k - A[i]*mid; if(tmp[i] > 0){ // 剩下的全部外包 cnt += tmp[i]/B[i]; if(tmp[i] % B[i] > 0) cnt++; } } if(cnt > p) l = mid + 1; else r = mid; } return l; } int main(){ cin>>n>>k>>m; for(long long i=0 ; i<n ; i++){ cin>>A[i]; R = min(R, A[i]); } for(long long i=0 ; i<n ; i++){ cin>>B[i]; } // 最大工作天數為 (工作份數K / 一天處理最小的工作量) R = k/R + 1; cout<<search(L, R, m)<<"\n"; return 0; } ``` ### 時間複雜度分析 輸入時間複雜度為 $O(n)$ 二分搜的右界為 $R' = k / min(R) + 1$ 搜尋時間複雜度為 $O(nlog{R'})$ 總時間複雜度為 $O(n + nlog{R'})$ # UVa 10282 ## 題目 http://domen111.github.io/UVa-Easy-Viewer/?10282 題目會先給你每個英文字對應到的外國文字,這群對應的字保證是一對一,形成一個字典 接下來會有多筆詢問,輸入一個外國文字,輸出相對應的英文字,如果不存在則輸出 `eh` ## 想法 By Koios 這題可以直接用 map 對應,但是這裡我們用二分搜來實作看看 首先先把字典建立好,可以用一個 struct 來建立每個英文字對應到的外國字,接下來排序,方便我們查詢 最後用二分搜就可以了 ### 程式碼 ```cpp= //By Koios1143 #include<iostream> #include<sstream> #include<algorithm> using namespace std; int i=0, res, res2; string s, key, value, q; struct word{ string key; string value; }; word dict[100010]; bool cmp(word p, word q){ if(p.value != q.value) return p.value < q.value; return false; } int search(int l, int r, string p){ while(l!=r){ int mid = (l+r)/2; if(dict[mid].value == p) return mid; if(dict[mid].value < p) l = mid+1; else r=mid; } return -1; } int main(){ while(getline(cin, s) && s!=""){ // 因為輸入是用換行間格,一次讀取一整行比較好判斷 // 接下來用 stringstream 分割字串 stringstream ss; ss<<s; ss>>key>>value; dict[i].key = key; dict[i].value = value; i++; } sort(&dict[0], &dict[i], cmp); while(cin>>q){ res = search(0, i, q); if(res == -1) cout<<"eh\n"; else cout<<dict[res].key<<"\n"; } return 0; } ``` ### 時間複雜度分析 假設有 $N$ 筆輸入以及 $Q$ 筆詢問 輸入時間複雜度為 $O(N)$ 排序時間複雜度為 $O(NlogN)$ 搜尋時間複雜度為 $O(QlogN)$ 總時間複雜度為 $O(N + (N+Q)logN)$ # UVa 10341 ## 題目 http://domen111.github.io/UVa-Easy-Viewer/?10341 今天有一個算式 $p * e^{-x} + q * sin(x) + r * cos(x) + s * tan(x) + t * x^{2} + u = 0$ 已知 $0 \leq x \leq 1$,給定 $p, q, r, s, t, u$,求 $x$ $0 \leq p, r \leq 20 \\ -20 \leq q, s, t \leq 0$ ## 想法 By Koios 首先先來觀察一下算式,將算式中的每個部份拆分出來 我們只討論 $x$ 所在的的範圍 $0 \leq x \leq 1$ - $e^{-x}$ 為遞減函數 - $sin(x)$ 為遞增函數 - $cos(x)$ 為遞減函數 - $tan(x)$ 為遞增函數 - $x^{2}$ 為遞增函數 接下來把係數也考慮進去 - $p * e^{-x}$ 為遞減函數 - $q * sin(x)$ 為遞減函數 - $r * cos(x)$ 為遞減函數 - $s * tan(x)$ 為遞減函數 - $t * x^{2}$ 為遞減函數 由此可知, $p * e^{-x} + q * sin(x) + r * cos(x) + s * tan(x) + t * x^{2} + u$ 是**遞減函數** 令這個函數為 $f(x)$ 知道這個函數是呈現遞減的狀態,我們要求其解就想到利用二分搜 透過二分搜,我們可以枚舉 $x$,並且有三種情況 - $f(x) = 0$ x 就是答案 - $f(x) > 0$ 因為函數是遞減,當答案大於 0 則應該要調整邊界**向右移動** - $f(x) < 0$ 因為函數是遞減,當答案大於 0 則應該要調整邊界**向左移動** 但是程式會有浮點數的誤差存在,所以很難判斷到底應不應該繼續往下尋找答案 但是因為 $f(x)$ 是連續函數,我們可以用勘根定理找到是否存在解,只要除去不存在解的情況,我們就可以放心地往下繼續找答案 除去無解的情況,接下來找答案就很快了 但是浮點數誤差一定要小心處理 :::info $f(x)$ 當中有很多數學函數,在 C++ 當中可以直接 `#include<math.h>` 其中 $sin(x), cos(x), tan(x)$ 都可以直接使用 $e^{x}$ 則可以用 `exp(x)` 表示 ::: ### 程式碼 ```cpp= //By Koios1143 #include<iostream> #include<iomanip> #include<math.h> using namespace std; double p, q, r, s, t, u, esp=1e-9; double abs2(double x){ // 因為沒有針對 double 的 abs 存在,自己寫一個 return (x<0 ? -x : x); } double ans(double x){ // 直接回傳 x 帶入算式的結果 return p*exp(-x) + q*sin(x) + r*cos(x) + s*tan(x) + t*x*x + u; } double search(double l, double r){ double mid, ret=1; // 只要答案還在誤差範圍內就放心搜尋 while(abs2(ret) > esp){ mid = (l+r)/2.0; ret = ans(mid); if(ret < 0) r = mid; else l = mid; } return mid; } int main(){ while(cin>>p>>q>>r>>s>>t>>u){ // 當左右邊界的值帶入後答案相乘大於 0 則不存在解 (勘根定理) if(ans(1) * ans(0) > 0) cout<<"No solution\n"; else{ double x = search(0, 1+esp); cout<<fixed<<setprecision(4)<<x<<"\n"; } } return 0; } ``` ### 時間複雜度分析 每筆測資輸入時間複雜度為 $O(1)$ 每筆測資找答案的時間複雜度為 $O(logN)$ $N = 10^{9}$ 每筆測資總時間複雜度約為 $O(logN)$ # TOJ 55 ## 題目 https://toj.tfcis.org/oj/pro/55/ 輸入 $n$ 個數字,接下來有 $m$ 筆詢問,想知道詢問的數字 $q$ 在序列當中出現的次數 ## 想法 By Koios 直接計算 upper_bound ($>q$ 的第一個位置) - lower_bound ($\leq q$ 的第一個位置) 因為這一題的輸入數量有點大,需要加上輸入優化 :::info 加上了輸入優化後,就不可以將 c 和 c++ 的 IO 語法混用,要注意這一點 可以參考 https://hackmd.io/@wiwiho/CPN-io-optimization ::: ### 程式碼 ```cpp= //By Koios1143 #include<iostream> #include<algorithm> using namespace std; int n, m, arr[1000010]; int main(){ // 輸入優化 ios::sync_with_stdio(false); cin.tie(0); cin>>n; for(int i=0 ; i<n ; i++){ cin>>arr[i]; } sort(arr, arr+n); cin>>m; for(int i=0, q ; i<m ; i++){ cin>>q; cout<<upper_bound(arr, arr+n, q) - lower_bound(arr, arr+n, q)<<"\n"; } return 0; } ``` ### 時間複雜度分析 輸入時間複雜度為 $O(n)$ 排序時間複雜度為 $O(nlogn)$ 二分搜時間複雜度為 $O(logn)$ 總時間複雜度約為 $O(n + nlogn)$ # UVa 10110 ## 題目 http://domen111.github.io/UVa-Easy-Viewer/?10110 在一條走廊上有編號 $1$~$n$ 的電燈泡 有個人在這條走廊上來回走 $n$ 趟,第 $i$ 次出發會將編號為 $i$ 的倍數的電燈泡開關轉換一次(也就是 `開 -> 關` `關 -> 開`),而在走回來不會做任何事 請問來回走完 $n$ 趟之後,第 $n$ 顆電燈泡是亮的還是暗的? (一開始所有電燈泡都是關的) ## 想法 By Koios 如果第 $n$ 個電燈泡是關的,那麼他一定被操作了 $2k$ 次 ($k \in \mathscr{R}$) 反過來說,如果第 $n$ 個電燈泡是開的,那麼他一定被操作了 $2k-1$ 次 ($k \in \mathscr{R}$) 那何時電燈泡會被操作呢? 只有在他的因數出現的時候會被操作到 那麼如果某數字有奇數個因數,則最後會是開的,反之是關的 任何數字 $p$ 的因數 $q$ 都會有相對應的數字 $r$ 使得 $p = qr$,並且 $r$ 也是 $p$ 的因數 如果說因數是奇數,那就表示最中間的因數 $q$ 找到相對應的數字也是 $q$,使得 $p = q*q$ 發現到,當因數個數為奇數,也就是 $p$ 是**完全平方數**的時候第 $n$ 個燈泡會是亮的,反之是暗的 那麼,我們只需要做開根號的動作,去看看開根號後的值平方以及原本的數字是否相同即可 不過這裡要練習二分搜,所以就二分搜開根號吧 ### 程式碼 ```cpp= //By Koios1143 #include<iostream> using namespace std; long long n, tmp; void search(long long l, long long r, long long p){ long long mid, res; while(l!=r){ mid = (l+r)/2; res = mid*mid; if(res == p){ cout<<"yes\n"; return; } if(res < p) l = mid+1; else r = mid; } cout<<"no\n"; return; } int main(){ while(cin>>n && n){ search(0, n+1, n); } return 0; } ``` ### 時間複雜度分析 每筆測資輸入時間複雜度為 $O(1)$ 每筆測資找到答案的時間複雜度為 $O(logn)$ 每筆測資總時間複雜度為 $O(logn)$ # UVa 10474 ## 題目 http://domen111.github.io/UVa-Easy-Viewer/?10474 每一筆測資有 $n$ 筆資料以及 $q$ 筆詢問 要求資料排序過後,每筆詢問的數字在資料當中第一次出現的位置,從一開始數 ## 想法 By Koios 要找排序後的位置,當然先排序好 在排序完成後,要找第一次出現的位置,那就直接帶入 lower_bound 的概念即可 ### 程式碼 ```cpp= //By Koios1143 #include<iostream> #include<algorithm> using namespace std; int n, q, res, arr[10010], Case=1; int main(){ while(cin>>n>>q && (n!=0 && q!=0)){ for(int i=0 ; i<n ; i++){ cin>>arr[i]; } sort(arr, arr+n); cout<<"CASE# "<<Case++<<":\n"; for(int i=0, m ; i<q ; i++){ cin>>m; res = lower_bound(arr, arr+n, m)-arr; if(arr[res] == m){ cout<<m<<" found at "<<res+1<<"\n"; } else{ cout<<m<<" not found\n"; } } } return 0; } ``` ### 時間複雜度分析 每筆測資輸入時間複雜度為 $O(n)$ 每筆測資找搜尋時間複雜度為 $O(logn)$ 每筆測資總時間複雜度為 $O(n + qlogn)$ # UVa 10530 ## 題目 http://domen111.github.io/UVa-Easy-Viewer/?10530 現在正在玩猜數字的遊戲,對方會告訴你你猜的數字太高或是太低,但是我們懷疑對方會說謊 假設當我們猜到正確數字時對方並不會說謊,給定每回合猜數字的結果,請幫忙判斷對方是否有說謊 猜數字的範圍在 $1$~$10$ 之間 ## 想法 By Koios 給定一個數字,對方回覆太大(`too high`),那麼如果沒說謊,我們的數字應該比他還小,那麼我們只需要紀錄 `too high` 當中最小的數字即可 反過來說,對方回覆太大(`too low`),那麼如果沒說謊,我們的數字應該比他還大,那麼我們只需要紀錄 `too low` 當中最大的數字即可 如果我們猜到的數字介在 `too high` 以及 `too low` 之間,那就表示對方沒有說謊 ### 程式碼 ```cpp= //By Koios1143 #include<iostream> using namespace std; int n, Upper_bound = 10, Lower_bound = 1; string s; int main(){ while(cin>>n && n){ getchar(); getline(cin, s); if(s == "too high"){ // 數字最大是 n-1 Upper_bound = min(Upper_bound, n-1); } else if(s == "too low"){ // 數字最小是 n+1 Lower_bound = max(Lower_bound, n+1); } else if(s == "right on"){ if(n >= Lower_bound && n <= Upper_bound){ cout<<"Stan may be honest\n"; } else{ cout<<"Stan is dishonest\n"; } Upper_bound = 10, Lower_bound = 1; } } return 0; } ``` ###### tags: `SCIST 演算法 題解`