# Leetcode刷題學習筆記--Binary Search ## Introduction binary search的精隨就是在一個排序過的數列裡面,找出所要的值。因為每次都切半,所以可以時間複雜度達到O(logN)。相較於暴力破解法,例如答案在1~N之內,==如果測試函數可以讓你知道數字是往更大或更小的方向,就可以用binary search。== > 1. remove special case(答案不在可能的範圍內) > 因為繼續使用binary search,如果答案比範圍內的還大,會得到left = sz。(還可以判斷) > 如果答案比範圍內的還小,會得到left = 0。(無法判斷) > 1. 定義出答案可能出現的範圍left和right。 > 因為計算mid的方法,right必須是範圍最大值+1。 > 2. 決定中間點,mid = left + (righ - left) / 2; > 避免有overflow的問題。 > 3. ==判斷中間點是否超過要搜尋的值==,例如**278**是用isBadVersion(), 668是看有多少數值超過目前的值。 > 4. 更新left, right。==通常right為錯誤的那一邊==。所以判斷完中間點是錯的,需要把區間往下則right = mid,如果是對的,則區間往上,left = mid +1; > 5. 直到left < right不成立為止。 > 最後left會等於right。 > 6. 最後結果會在上一次的right上。所以要根據題目決定要取目前這一個(left)還是left - 1的結果。 ## Code Snippet ```cpp= int left = 0; int right = arr.size() - 1; // 或是 right = arr.size(); 端看最後一個值是不是已經確定了 while(left < right) { int mid = left + (right - left) / 2; // 避免overflow // 如果要拿mid來計算, // 值域會超過int(4)則改用long(8) int val = ...; // 使用mid計算出新的值。 // 如果計算結果會overflow則可改用long(8) if(val == target) // 依題目需求決定要步要用 else if(val > target) // 或是使用 val >= target right = mid; else left = mid + 1; } ``` ## Corner Case ### 當長度為1 left = mid = right,都會指向同一個element。 ![](https://i.imgur.com/VxdYOCv.png) ### 當長度為2 left = mid, right = mid + 1, 只有left和mid一樣。所以竟可能拿mid和right來判斷。 ![](https://i.imgur.com/UX18d8g.png) ## Special Case 當使用 while(left<right)的時候,最左邊和最右邊的答案不會檢查。所以當答案有可能在這兩個之外,必須在while之外做額外的檢查。 case 1 : 如果答案比index 0還小,則最後left/right會停在node 0而離開while loop,但是沒檢查node 0。 ![](https://i.imgur.com/COSzae9.png) case 2 : 如果答案比index 5還大,則最後left/right會停在node 5而離開while loop,但是沒檢查node 5。 ![](https://i.imgur.com/d8Wwwnj.png) 這種情況必須視題目而定。如果答案一定是在node0~node5之間,那就直接return沒關係。如果除了node0~node5之外還有額外的答案,那就必須檢查left/right使否正確。 ## 從答案的範圍,用binary search 猜測是否正確。 另一種binary search的應用。只知道答案的範圍,使用binary search來逼近正確的答案。如下的code: ```cpp int left = ANS_MAX, right = ANS_MIN; while(left < right) { int mid = left + (right - left) / 2; // 根據題目計算由mid得到的結果 // 和題目給的條件去比較,並更新left, right } ``` ### case 1 : 當搜尋範圍就是比較的範圍 例如 : 從已排列好的array中,找出4的數字。 ![](https://i.imgur.com/SFswoi1.png) 題目: + [34. Find First and Last Position of Element in Sorted Array](/u7dufNJ-SAq7z0WmS-Semw?both#34-Find-First-and-Last-Position-of-Element-in-Sorted-Array) ### case 2 : 當搜尋範圍和比較的範圍不一樣 ### case 2-1 : 比較範圍是遞增數列 ### case 2-2 : 比較範圍是遞減數列 搜尋的範圍和比較的範圍不一樣,且比較的範圍是遞減數列。 ![](https://i.imgur.com/SyCu8hQ.png) 題目: + [1760. Minimum Limit of Balls in a Bag](/u7dufNJ-SAq7z0WmS-Semw?both#1760-Minimum-Limit-of-Balls-in-a-Bag) + [875. Koko Eating Bananas](/u7dufNJ-SAq7z0WmS-Semw?both#875-Koko-Eating-BananasMedium) + [275. H-Index II]() ## Search Range 針對binary search,首先一定要有一個搜尋範圍。通常使用left和right來表示欲搜尋的上下界。 + 因為我們都是用mid來測試條件是否成立。所以mid要可以達到答案範圍內的每個值。 + 如果,left = 1, right = 30,且mid = left + (right - left) / 2,則mid的範圍是mid = 1(left = 1, right = 2)..., mid = 29(left = 29, right = 30),mid就無法達到30。 + 所以要讓mid達到上界,則right = 31。mid = 30(left = 30, right = 31) + 因為答案不可能達到(上界+1),所以最後可以判斷這個(left == 上界+1)排除掉。 ```cpp= // setup search range int left = 1, right = 31; while(left < right) { int mid = left + (right - left) / 2; int result{0}; // test mid // update the search range if(result >= k) right = mid; else left = mid + 1; } // 因為結果不可能出現31 return left == 31 ? -1 : left; ``` 例如:[1870. Minimum Speed to Arrive on Time](https://leetcode.com/problems/minimum-speed-to-arrive-on-time/) ```cpp= class Solution { // speed : 1, 2, 3, [4], 5, 6, 7, 8, 9, ... // hour : 10, 9, 9, 8, 8, 8, 7, 7, 7, ... // > <= public: int minSpeedOnTime(vector<int>& dist, double hour) { int sz = dist.size(); if(hour <= sz - 1) return -1; int left = 1, right = 1e9 + 1; while(left < right) { int mid = left + (right - left) / 2; int need{0}; for(int i = 0; i < sz - 1; ++i) need += (dist[i] + (mid - 1)) / mid; double fneed = need + dist.back() * 1.0 / mid; if(fneed <= hour) right = mid; else left = mid + 1; } return left == 1e9 + 1 ? -1 : left; } }; ``` ## Update the Search Range 有了search range,就可以得出mid,這時就必須測試mid是否在範圍之內,然後更新left或是right來縮小搜尋範圍。==通常mid得出的結果必須是遞增或是遞減數列==,這樣才有機會使用binary search。 例如, mid的範圍如下,可以得到測試結果rtn為遞減數列。 ```cpp // mid = 1, 2, 3, 4, 5, 6, 7, 8, 9, // rtn = 9, 9, 8, 8, 7, 7, 6, 6, 5 ``` 再根據題目,通常會限制rtn為某個數,假設是7,因為mid通常在前一個, ```cpp // mid = 1, 2, 3, 4, 5, 6, 7, 8, 9, // rtn = 9, 9, 8, 8, [7], 7, 6, 6, 5 // > <= // mid ``` 所以根據上面的圖示可以寫出 ```cpp= int k = 7; if(rtn <= k) right = mid; else left = mid + 1 return left; ``` 最後答案就是left。 另一種情況是 ```cpp // mid = 1, 2, 3, 4, 5, 6, 7, 8, 9, // rtn = 9, 9, 8, 8, 7, [7], 6, 6, 5 // >= < // mid ``` 可以寫成 ```cpp int k = 7; if(rtn < k) right = mid; else left = mid + 1; return left - 1; ``` 必須返回left - 1,因為最後left == right,會是下一個。 ## Maximum-Minimum or Minimum-Maximum problem 一個題目要求尋找一個答案的最小值,但是這個值是某個運算的最大值,通常都是binary search的題目。 例如:[2560. House Robber IV](https://leetcode.com/problems/house-robber-iv/description/) 其中,robber的capability的定義是偷過的金額中==最大值==。但是必須返回某個條件下(最少必需偷k個房子)的==最小值==。Minimum capability(which is maximum of all stealed money) ```cpp= class Solution { // capability(maximum) 偷的金額中最大的 // [2, 3, 5, 9], k = 2(minimum) 最少要偷兩間 // 回傳 最小(minimum)的capability。 min-max problem --> binary search? // capability 2 ~ 9 // mid = capability : 2, 3, 4, 5, 6, 7, 8, 9 // k 1 1 [2] 2 (為了符合capability, 最多能偷的house數) // < >= public: int minCapability(vector<int>& nums, int k) { auto [minv, maxv] = minmax_element(nums.begin(), nums.end()); int left = *minv, right = *maxv; while(left < right) { int mid = left + (right - left) / 2; int count = 0; for(int i = 0; i < nums.size(); ++i) { if(nums[i] <= mid) { count++; i++; } } if(count >= k) right = mid; else left = mid + 1; } return left; } }; ``` ## Example ### [278. First Bad Version(Easy)](https://leetcode.com/problems/first-bad-version/) 找出第一個錯誤的版本。一定會有一個錯誤的版本。 n = 1, [1]:只有一個版本,則一定是錯誤版本。 n = 2, [1,2]:有兩種情況[X,X]都是錯誤版本和[O,X]一對一錯。 n = 3, [1,2,3]:有三種情況[XXX][OXX][OOX] 使用binary search,right一定會錯誤版本。 ```cpp= int firstBadVersion(int n) { int left = 1, right = n, mid; while(right > left) { mid = left + (right - left) / 2; if(isBadVersion(mid)) right = mid; // right為錯誤版本 else left = mid + 1; } return right; // 因為是找錯誤版本 } ``` 算中間點有兩種算法 ```cpp= mid = (left + right) / 2; ``` 或是 ```cpp= mid = left + (right - left) / 2; ``` 答案都會是一樣,差別在於如果left和right很大,則left + right有可能overflow,但是第二種方法不會。 ==right一定是指向錯誤的版本==。所以code才會寫成以下的樣子。而且最後一定是==return right==。 ```cpp= if(isBadVersion(mid)) right = mid; // right為錯誤版本 else left = mid + 1; // 下一個版本。 ``` ---- c++的STL有提供binary search的function。 lower_bound和upper_bound,都是用binary search來找目標值,差別在於 > lower_bound : 返回==等於或大於==目標值的最小位置。 > upper_bound : 返回==大於==目標值的最小位置。 ![](http://bajamircea.github.io/assets/2018-08-09-lower-bound/01-lower_bound.png) **返回值為iterator**。 所以取值必須用 ```cpp= value = *it; ``` 在array中的index必須用 ```cpp= idx = it - nums.begin(); ``` ### [668. Kth Smallest Number in Multiplication Table(Hard)](https://leetcode.com/problems/kth-smallest-number-in-multiplication-table/) 在一個乘法表裡面(width(n) x height(m),都是1開頭遞增)找出第k個最小的數字。 例如:n = 3, m = 3, k = 5,其中第5個最小的數字為3。 ![leetcode668_example](https://assets.leetcode.com/uploads/2021/05/02/multtable1-grid.jpg) 解題思路: > 1. 在 1 ~ m*n的數字中,找出一個數字,剛好小於這個數字的數量等於k。=> binary search。 > 2. 因為數字都是倍數關係,所以可以用 ==mid / i==來算出小於mid的數量。 ![](https://i.imgur.com/BUZlChl.png) ![](https://i.imgur.com/EPKeJUX.png) ```cpp= int findKthNumber(int m, int n, int k) { // 第一個和最後一個不用算 if(k == 1 || k == m * n) return k; int left = 1, right = m * n; while (left < right) { int mid = left + (right - left) / 2, cnt = 0; // 計算比mid小的數值有多少個 for (int i = 1; i <= m; ++i) { cnt += (mid > n * i) ? n : (mid / i); } // 比較小,區間往上,所以left一定是下一個數 if (cnt < k) left = mid + 1; // 大於等於,區間往下,所以right = mid else right = mid; } return right; } ``` ### [540. Single Element in a Sorted Array(Medium)](https://leetcode.com/problems/single-element-in-a-sorted-array/) 給你一個排列過的整數數列nums,其中每個數都是兩兩一對,除了一個數只有一個,問你怎麼找出那一個數。題目又說希望執行效率事O(logN)。 例如: nums = [1,1,2,3,3,4,4,8,8],output = 2。 解題思路 > 1. 一開始看到就想到XOR,這樣就可以找到那個數,但是時間是O(N)。 > 2. 看到O(logN),直覺就想到binary search。但是怎麼判斷範圍要往前還是往後是個問題。 > 3. 先想一下special case。 > 3-1. 題目說長度從1開始,因為兩兩一對只有一個是單獨,所以長度一定是奇數。 > 3-2. 長度 == 1,那不用判斷一定是那一個。 > 3-3. 長度 == 3,那不是第一個,就是最後一個。 > 3-4. 長度 == 7,可以發現要找的數字,和mid和前後關係不太依樣。 ![](https://i.imgur.com/zZ2moYk.png) > 4. 判斷完mid之後,可以把數列變短,問題又是一樣。 照以上的思緒,程式碼如下: ```cpp= int singleNonDuplicate(vector<int>& nums) { auto n = nums.size(); if(n == 1) return nums[0]; int left = 0, right = n - 1, mid; while(left < right) { if(right - left <= 3) { if(nums[left] != nums[left + 1]) return nums[left]; if(nums[right] != nums[right - 1]) return nums[right]; } mid = left + ((right - left) >> 1); if(mid % 2) { if(nums[mid] == nums[mid + 1]) right = mid - 1; else left = mid + 1; } else { if(nums[mid] != nums[mid + 1]) right = mid; else left = mid; } } return 0; } ``` ### [300. Longest Increasing Subsequence(Medium)](https://leetcode.com/problems/longest-increasing-subsequence/) 給你一個vector<int> nums找出,這個當中找出最長的strictly increasing subsequence. + subsequence的定義是,在nums中的前後順序依樣,但是可以不用連續。 + strictly increasing,每個數字都比前一個大。 > 1. 一開始的想法是,每看到一個數字就去map找比他還小的數字 > 2. 然後,更新dp[i]和比他還小的數字連起來最長。 > 3. 這個方法效率不好O(NlogN)。 ```cpp= int lengthOfLIS(vector<int>& nums) { auto n = nums.size(); if(n == 1) return 1; map<int, vector<int>> m; vector<int> dp(n, 1); int maxlen{1}; m[nums[0]].push_back(0); for(int i = 0; i < n; ++i) { for(auto& item : m) { if(item.first >= nums[i]) break; for(auto& idx : item.second) { dp[i] = max(dp[i], dp[idx] + 1); } } maxlen = max(maxlen, dp[i]); m[nums[i]].push_back(i); } return maxlen; } ``` > 網路上的解法 > 1. 使用一個vector來記錄走過的subsequence > 2. 找出lower_bound(大於等於)換掉 > 3. 不然就是推到最後面。 ```cpp= int lengthOfLIS(vector<int>& nums) { vector<int> v; for(auto& n : nums) { auto it = lower_bound(v.begin(), v.end(), n); if(it == v.end()) v.push_back(n); else *it = n; } return v.size(); } ``` ### [875. Koko Eating Bananas(Medium)](https://leetcode.com/problems/koko-eating-bananas/) 請參考原題目。 > 1. 這題思考很久,最後看到題目提示是用binary search才恍然大悟。 > 2. 因為速度k 和最後多少個小時h吃完成線性關係。也就是速度越快,所用的h越少,所以可以用binary search來完成。 > 3. 定義left和right。因為piles最大值為1e9,h最小值為1,所以left = 1, right = 1e9 > 4. 計算k的時候需要無條件進位,等於是(b + mid - 1)/ mid > 5. 最後是希望最小的k,所以設定right >= h ![](https://i.imgur.com/MO22Jjr.png) > 2022/04/25 > 1. 因為吃的速度越快,需要的時間就越短 > 2. 但是koko偏好吃慢一點, 如下圖所示就是2的位置。 > 3. 計算時間使用[無條件進位](https://hackmd.io/3I17t2spT6GDLFRftfEOZA?view#%E7%84%A1%E6%A2%9D%E4%BB%B6%E9%80%B2%E4%BD%8D)。 ![](https://i.imgur.com/LcrRi06.png) ```cpp= int minEatingSpeed(vector<int>& piles, int h) { int left = 1, right = 1e9; while(left < right) { int mid = left + (right - left) / 2; int t = 0; for(auto& n : piles) t += (n + mid - 1) / mid; if(t <= h) right = mid; else left = mid + 1; } return left; } ``` ### [1539. Kth Missing Positive Number](https://leetcode.com/problems/kth-missing-positive-number/) 給你一個vector<int>為嚴格遞增序列(strictly increasing order),找出第k個missing number。 ```cpp= int findKthPositive(vector<int>& arr, int k) { // 把right設定成arr.size(); 是因為當 k 比最大還大時 // right還可以等於arr.size(); int left = 0, right = arr.size(); while(left < right) { int mid = left + (right - left) / 2; int val = arr[mid] - (mid + 1); // 等於也要,因為也在arr[mid - 1] ~ arr[mid] 中間的範圍內。 if(val >= k) right = mid; else left = mid + 1; } return right + k; } ``` ### [1608. Special Array With X Elements Greater Than or Equal X](https://leetcode.com/problems/special-array-with-x-elements-greater-than-or-equal-x/) 找出一個數x,使得在vector<int> nums中,有x個數大於等於x。 ex: Input: nums = [0,4,3,0,4] Output: 3 Explanation: There are 3 values that are greater than or equal to 3. > 1. 因為下面的程式使用了只有當 mid == cnt,才return正確值,反則return -1。所以mid一定要走訪到所有可能的答案[1~sz]。 > 2. 當length = 2的情況,left = mid,所以right必須要等於sz + 1,才可以讓mid走訪所有可能的答案。 ```cpp int specialArray(vector<int>& nums) { int sz = nums.size(); int left = 1, right = sz + 1; while(left < right) { int mid = left + (right - left) / 2; int cnt = 0; int cnt = count_if(nums.begin(), nums.end(), [&](int& a) { return a >= mid; }); // 因為用了mid == cnt 才return, // 所以mid的位置就很重要,因為當length == 2時,mid == left // 所以right的位置要+1 if(mid == cnt) return mid; else if(mid > cnt) right = mid; else left = mid + 1; } return -1; } ``` ### [34. Find First and Last Position of Element in Sorted Array](https://leetcode.com/problems/find-first-and-last-position-of-element-in-sorted-array/) 在一個排序的數列中,找出target的左右位置。 > 1. 根據以下的圖,可以得知,在做binary search的比較關係。 ![](https://i.imgur.com/SFswoi1.png) > 2. 有兩個special case > 2-1. 當數字出現在第一個,這個不會影響判斷 ![](https://i.imgur.com/ymBFlst.png) > 2-2. 當數字出現在最後一個位置。 ![](https://i.imgur.com/q5JSwPu.png) > 因為我們都是拿mid來比較,會造成, mid只會停在倒數第二個的位置。 ![](https://i.imgur.com/uXCqukG.png) > 所以必須把right的位置往後移一個,讓mid有機會判斷到最後一個位置的數字。 ![](https://i.imgur.com/wyRf58s.png) > 並且答案為 left - 1,因為最後會停在 > target的位置。 ```cpp vector<int> searchRange(vector<int>& nums, int target) { int sz = nums.size(); if(sz == 0 || target < nums[0] || target > nums[sz - 1]) return {-1, -1}; vector<int> rtn(2); int left = 0, right = sz - 1; while(left < right) { int mid = left + (right - left) / 2; if(nums[mid] >= target) right = mid; else left = mid + 1; } rtn[0] = nums[left] == target ? left : -1; left = 0, right = sz; while(left < right) { int mid = left + (right - left) / 2; if(nums[mid] > target) right = mid; else left = mid + 1; } rtn[1] = nums[left - 1] == target ? left - 1 : -1; return rtn; } ``` ### [1760. Minimum Limit of Balls in a Bag](https://leetcode.com/problems/minimum-limit-of-balls-in-a-bag/) operatin的定義是,可以對數字進行切割成兩個。切割的兩個數字可以自己決定。 所有的operation次數不可以超過maxOperations。想辦法讓最大的數字最小化。 > 1. 使用binary serach假設答案是某個數字。因為切割為多次,penalty就會越小。如下圖所示: ![](https://i.imgur.com/VB6oHDr.png) ```cpp int minimumSize(vector<int>& nums, int maxOperations) { int left = 1, right = *max_element(nums.begin(), nums.end()); while(left < right) { int mid = left + (right - left) / 2; int cnt = 0; for(auto& n : nums) cnt += (n - 1) / mid; if(cnt <= maxOperations) right = mid; else left = mid + 1; } return left; } ``` 2022/11/4 ```cpp // 使用binary search。則mid要搜尋什麼? // 1. 找penalty? // 2. 找ops (好像不太容易) // 把 left , mid, right所表達的意義遞增排序,就可以得到ops為遞減排序 // penalty : 1, 2, 3, 4, 5, 6, 7 // ops : 8, 7, 6, 5, 5, [5], 4 // 假設maxOperations為5,並選到了[5]這一個, // 因為往左還會有更小的penalty,所以使用小於等於,繼續往左移動 // 相當於是lower_bound的概念。 public: int minimumSize(vector<int>& nums, int maxOperations) { int left = 1, right = *max_element(nums.begin(), nums.end()); while(left < right) { // mid 為欲達成的penalty,則所需的ops為? // 因為ops越多,penalty越少 // ops 1 2 3 4 [5] 6 7 8 <-- 因為這個mid得到的ops次數 = 5 // p 9 8 7 6 [6] 5 4 3 <-- mid int mid = left + (right - left) / 2; int ops = 0; for(auto& n : nums) ops += (n - 1) / mid; if(ops <= maxOperations) right = mid; else left = mid + 1; } return left; } ``` ### [275. H-Index II](https://leetcode.com/problems/h-index-ii/) ```cpp int hIndex(vector<int>& citations) { int sz = citations.size(); int left = 0, right = sz + 1; // h index : 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... // cnt : 8, 7, 7, 6, 6, 5, 5, 4, 3, 2, ... // >= < // h-index : h篇論文,每個都被引用至少h次 // 所以h[越小],[越多]論文被引用至少h次 while(left < right) { int mid = left + (right - left) / 2; int cnt = 0; for(auto& n : citations) if(n!= 0 && n >= mid) cnt++; if(cnt < mid) right = mid; else left = mid + 1; } return left - 1; } ``` 2022/11/4 ```cpp // 使用binary search。則mid要搜尋什麼? // 1. 找penalty? // 2. 找ops (好像不太容易) // 把 left , mid, right所表達的意義遞增排序,就可以得到ops為遞減排序 // 因為要找maxOperations,且越大越好,所以等於是找小於等於那一邊 public: int minimumSize(vector<int>& nums, int maxOperations) { int left = 1, right = *max_element(nums.begin(), nums.end()); while(left < right) { // mid 為欲達成的penalty,則所需的ops為? // 因為ops越多,penalty越少 // ops 1 2 3 4 [5] 6 7 8 <-- 因為這個mid得到的ops次數 = 5 // p 9 8 7 6 [6] 5 4 3 <-- mid int mid = left + (right - left) / 2; int ops = 0; for(auto& n : nums) ops += (n - 1) / mid; if(ops <= maxOperations) right = mid; else left = mid + 1; } return left; } ``` ### [658. Find K Closest Elements](https://leetcode.com/problems/find-k-closest-elements/) 給以一個排列過的遞增數列,找出K個最接近X的數,結果也要遞增排序。 > 這題一看直覺就是用sort來排序,但是效率比較差O(NlogN + KlogK + K) > 比較好的解法是使用binary search。只要找出left,則整個數列就會出來。所以使用binary search找出答案array的left在哪裡。 > 所以我們可以列出四種狀況 ![](https://i.imgur.com/Dm6iAwr.png) > case1 : x在left的左邊 > case2 : x在left和right的中間,但是比較靠近left > case3 : x在left和right的中間,但是比較靠近right > case4 : x在right的右邊 > 其中,case1和case2必須把array往左邊移動。因為必須讓x在中間,case3和case4必須把array往右邊移動。 > 上面的情況是left和right不一樣的時候,使用兩種判斷式都可以正確的移動array。 > 另外一種情況是數字有重複,則使用abs來判斷就會失效。如下圖。 ![](https://i.imgur.com/IFzpVBc.png) > 使用abs,三種情況都會是一樣。但是如果是使用 ```cpp if(x - arr[mid] < arr[mid + k] - x) { } ``` 或是使用 ```cpp if(arr[mid] - x > x - arr[mid + k]) { } ``` > 因為 x - arr[mid] 和 arr[mid + k] - x,會有==正負的差別==,所以可以判斷出需要往左還是往右移動。 > 相等的時候為什麼是向左移動? 因為我們是使用arr[mid + k]。 > arr[mid + k] 是在k之外的一個數值,如果相等表示需要向左移動。 ```cpp vector<int> findClosestElements(vector<int>& arr, int k, int x) { int left = 0, right = arr.size() - k; while (left < right) { int mid = (left + right) / 2; if (x - arr[mid] > arr[mid + k] - x) left = mid + 1; // 向右移動 else right = mid; // 向左移動 } // arr.begin() + left + k 不會取 return vector<int>(arr.begin() + left, arr.begin() + left + k); } ``` 因為binary search只找了N-K的範圍長度。 return的答案占了K長度的vector。 | Time | Space | | -------- | -------- | | $O(log(N-K))$ | $O(K)$ | ### [1898. Maximum Number of Removable Characters](https://leetcode.com/problems/maximum-number-of-removable-characters/description/) 給你兩個字串s和p,其中p為s的subsequence。在給你一個vector<int> removeable,表示可以要依序刪除的index。試問最多依序可以刪幾個char還可以保持p為s的subsequence。 > 2022/11/07 > 一開始做這一題完全沒想法,看了答案之後覺得是一題好題目。 ```cpp class Solution { // 圖1, mid為刪除char的個數,j為s中有p的個數 // mid = 0, 1, [2], 3, 4, 5, 6, 7, 8, ... // j = 4, 4, 4, 3, 3, 3, 2, 2, 1, ... // >= < // 圖2, 說明為什了是map[i] >= mid // string index 0, 1, 2, [3], 4, 5, 6, 7, 8 // mid = 3, 刪除3個(0, 1, 2), 保留6個(3, 4, 5, 6, 7, 8) // <--刪除-| |------保留------> // < >= // 0, 1, 2 // removable : 3, 1, 0 // 0, 1, 2, [3], 4, 5 // m : 2 1 M 0 M M , M : 表示不會被刪除 public: int maximumRemovals(string s, string p, vector<int>& removable) { int sz = s.size(); vector<int> map(sz, INT_MAX); for(int i = 0; i < removable.size(); ++i) map[removable[i]] = i; int left = 0, right = removable.size() + 1; while(left < right) { int mid = left + (right - left) / 2, j = 0; for(int i = 0; i < s.size() && j < p.size(); ++i) { // 圖2 if(map[i] >= mid && s[i] == p[j]) ++j; } //圖1 if(j >= p.size()) left = mid + 1; else right = mid; } return left - 1; // 圖1, 從上圖會選到p.size() - 1那一個,所以要選前一個 } }; ``` ### [792. Number of Matching Subsequences](https://leetcode.com/problems/number-of-matching-subsequences/description/) 找出vector<string> words中,會是s的subsequence的個數。 > 1. 一開始我是用trie結果TLE > 2. 先建立每個char會出現的index,方便之後尋找。 > 3. 使用 int x = -1, 則使用upper_bound第一次會找到第一個。 > 4. 依序找出每個char會出現的位置。這邊也有Greedy的意味在。因為每次只會找到大於x的最小值。 ```cpp int numMatchingSubseq(string s, vector<string>& words) { // 建立char和index的對應vector vector<vector<int>> alpha(26); for(int i = 0; i < s.size(); ++i) alpha[s[i] - 'a'].push_back(i); int rtn = 0; for(auto& word : words) { int found = true; int x = -1; //** 很重要,因為第一次會找到第一個 for(auto& c : word) { auto it = upper_bound(alpha[c - 'a'].begin(), alpha[c - 'a'].end(), x); if(it == alpha[c - 'a'].end()) {found = false; break;} else x = *it; } if(found) rtn++; } return rtn; } ``` ### [2517. Maximum Tastiness of Candy Basket](https://leetcode.com/problems/maximum-tastiness-of-candy-basket/description/) ```cpp= class Solution { public: int maximumTastiness(vector<int>& price, int k) { sort(price.begin(), price.end()); int left = 0, right = price.back() - price.front() + 1; while(left < right) { // 基於這個tastiness之下可以選到最多的candies int mid = left + (right - left) / 2; int cnt{1}; for(int i = 1, j = 0; i < price.size(); ++i) { // 因為排序過,所以最小差值只會發生在 // 目前的(i)和選到的candy最大值(j)之間 if(price[i] - price[j] >= mid) { cnt++; j = i; } } if(cnt >= k) left = mid + 1; else right = mid; } return left - 1; } }; // [13, 5, 1, 8, 21, 2], k = 3 // [1, 2, 5, 8, 13, 21] after sorting // search range : 0, 21 - 1 // // [1, 2, 5, 8, 13, 21] // t : 10 因為排序過,最小的差值只會出現在鄰近兩個之間。 // tastiness : 0, 1, 2, 3, [4], 5, 6, // cnt : 5, 4, 4, 3, 3, 2, 1 tastiness越大,可以選的candy越少。 // >= < ``` ### [2448. Minimum Cost to Make Array Equal](https://leetcode.com/problems/minimum-cost-to-make-array-equal/description/) 給你一個vector<int> nums和cost,每對一個數字nums[i]加一或是減一的行為需要cost[i],試問讓nums數字一樣的最小cost。 > 0. 有想到用binary search但是對於二次曲線不知道怎麼用。參考[lee215](https://leetcode.com/problems/minimum-cost-to-make-array-equal/solutions/2734162/java-c-python-binary-search/)的解答。 > 1. 首先想到讓vector的數字一樣,那個數字一定是在最小值和最大值之間。 ```cpp int left = 0, right = *max_element(nums.begin(), nums.end()); ``` > 2. 如果x軸是平均數,y軸是cost則會得到一個二次向上凹的曲線。一般的binary search就不適用。 > 3. 必須針對 mid, mid+1來判斷目前在曲線的那個部分。 ```cpp= long long y1 = f(mid), y2 = f(mid + 1); if(y1 < y2) right = mid; else left = mid + 1; ``` + 完整解答。 ```cpp= class Solution { public: long long minCost(vector<int>& nums, vector<int>& cost) { long long l = 1, r = *max_element(nums.begin(), nums.end()), res = LONG_MAX, x; auto f = [&](int x) { long long res{}; for(int i = 0; i < nums.size(); ++i) res += 1L * abs(nums[i] - x) * cost[i]; return res; }; while(l < r) { x = l + (r - l) / 2; long long y1 = f(x), y2 = f(x + 1); res = min({res, y1, y2}); if(y1 < y2) r = x; else l = x + 1; } return res; } // time : O(N) + O(logk * (2N)) = O(N + 2NlogK) = O(NlogK) // N : size of nums // k : the difference between minimal and maximum value // space : O(1) }; ``` ### [1851. Minimum Interval to Include Each Query](https://leetcode.com/problems/minimum-interval-to-include-each-query/description/) > 1. 一開始我也打算使用binary search來解,但是卡住了。因為我用queries來尋找interval,但是這樣會有問題。 > 2. 反過來使用intervals來尋找覆蓋到的queries就簡單多了。 > 3. 先對intervals的長度來排序。 > 4. 從最小的intervals取出來,看看是否有覆蓋的queries,有的話就跟新ans。 ```cpp= class Solution { public: vector<int> minInterval(vector<vector<int>>& intervals, vector<int>& queries) { // O(NlogN) sort(intervals.begin(), intervals.end(), [](vector<int>& a, vector<int>& b){ // O(NlogN) return a[1] - a[0] < b[1] - b[0]; // 從短到長排序 }); // O(MlogM) set<pair<int, int>> st; for(int i = 0; i < queries.size(); ++i) // O(QlogQ) st.insert(make_pair(queries[i], i)); // 把queries從小帶大排序 //O(N(logM + MlogM)) vector<int> ans(queries.size(), -1); for(int i = 0; i < intervals.size() && !st.empty(); ++i) { int s = intervals[i][0], e = intervals[i][1]; int len = e - s + 1; auto it = st.lower_bound({s, 0}); while(it != st.end() && it->first <= e) { ans[it->second] = len; advance(it, 1); st.erase(prev(it)); } } return ans; } }; // time :O(NlogN + MlogM + NMlogM) // sapce : O(M) ``` ### [852. Peak Index in a Mountain Array](https://leetcode.com/problems/peak-index-in-a-mountain-array/description/) > 0. 根據montain array的定義,arr.front()和arr.back()不會是peak。 > 1. 在peak的右邊一定滿足,arr[i] > arr[i + 1] > 2. 在peak的左邊一定滿足,arr[i] < arr[i + 1] > 3. 所以可以使用上面任一個條件來判斷。 + 使用peak左側來判斷。 ```cpp= while(left < right) { int mid = left + (right - left) / 2; if(arr[mid] < arr[mid + 1]) left = mid + 1; else right = mid; } return left; ``` + 使用peak右側來判斷 ```cpp= class Solution { public: int peakIndexInMountainArray(vector<int>& arr) { int sz = arr.size(); if(sz == 3) return 1; int left = 0, right = sz - 1; while(left < right) { int mid = left + (right - left) / 2; if(arr[mid] > arr[mid + 1]) right = mid; else left = mid + 1; } return left; } }; // * // 1, 2, 3, 4, 5, 4, 3, 2, 1 // ------------- // -------------- // arr[i] > arr[i + 1] // arr[i] < arr[i + 1] // case1 : 4, 5 , left = mid + 1 = answer // l/m r // case2 : 5, 4, right = left = 5 = answer // l/m r ``` + 這邊用左邊判斷取left,合理。但是用右邊判斷為什麼不用left - 1? + 這邊沒有target,所以之前的分析都不成立? + 只和自己的左邊和右邊比較,來更新left和right。 + 使用arr[mid] > arr[mid + 1] 來判斷會得到以下關係。 ``` // 0,10,5,2 // F, T,T,T ``` + 使用arr[mid] < arr[mid + 1] 來判斷會得到以下關係 ``` // 0,10,5,2 // T, F,F,F ``` + 可以發現不管是使用哪個判斷式,最後會收斂在0,10也就是狀態轉換的邊界,所以答案不用left - 1。 + 後來想想這種pattern其實和[278-First-Bad-VersionEasy](https://hackmd.io/u7dufNJ-SAq7z0WmS-Semw#278-First-Bad-VersionEasy)一樣,都是TTFFFF,找出TF的交界,答案會是left。 ### [2141. Maximum Running Time of N Computers](https://leetcode.com/problems/maximum-running-time-of-n-computers/) > 1. 參考官方解答。 > 2. 假設現在可以跑target的時間,則n個computer必須要有 n * target的電量。 > 3. 多少電量可以分配? 大於等於target的電池沒辦法額外使用? 假設n個電腦可以跑到target的時間,那這個電池剩下的電量也就無法使用。所以 ```cpp extra += min(target, batteries[i]); ``` > 4. 判斷式為 extra >= n * target,則可以得到以下的關係 ``` target : 1, 2, 3, [4], 5 result : T T T T F ``` > 因為要找出True的最大值,從上面的例子就是4,所以就可以得到以下的判斷式。 ```cpp= if(extra >= n * target) left = target + 1; else right = target; ``` > 5. 從上圖得知,最後結果為 left - 1。 ```cpp= class Solution { public: long long maxRunTime(int n, vector<int>& batteries) { long sum = accumulate(batteries.begin(), batteries.end(), 0L); // O(M) long left = 1, right = sum / n + 1; // K : range from left to right while(left < right) { // O(logK * M) long target = left + (right - left) / 2; long extra{}; for(auto& b : batteries) extra += min((long)b, target); if(extra >= (long)(n * target)) left = target + 1; else right = target; } return left - 1; } // time : O(M + MlogK) = O(MlogK) // space : O(1) }; // target : 1, 2, 3, 4, 5 // result : T T T T F ``` ###### tags: `leetcode` `刷題`