# 2sum 3sum 4sum discussion
###### tags: `Steven` `Leetcode`
# [1. Two Sum](https://leetcode.com/problems/two-sum/)
The most classic version.
## Binary search
Time complexity: $O(nlog(n))$, where n is the number of elements in the array
Since we are using binary search, the precondition is that the array must be sorted.
### Search for value x
```c++
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
vector<int> orig(nums); // bug: the original index must be preserved
sort(nums.begin(), nums.end());
// nums[i], target - nums[i]
for(int i = 0; i < nums.size(); i++) {
int l = 0, r = nums.size(); // [l, r)
int cand = target - nums[i];
while(r - l > 1) {
int mid = l + (r - l) / 2;
if(nums[mid] == cand) {
l = mid;
break;
} else if(nums[mid] < cand) {
l = mid;
} else {
r = mid;
}
}
if(nums[l] == cand && i != l) {
int x = -1, y = -1;
for(int j = 0; j < orig.size(); j++) {
if(x == -1 && nums[i] == orig[j])
x = j;
else if(nums[l] == orig[j])
y = j;
}
return vector<int>{x, y};
}
}
assert(1 == -1); // bug
}
};
```
### Using lowerbound
```c++
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
vector<int> orig(nums); // bug: the original index must be preserved
sort(nums.begin(), nums.end());
// nums[i], target - nums[i]
for(int i = 0; i < nums.size(); i++) {
int l = 0, r = nums.size(); // [l, r)
int cand = target - nums[i];
// vvvxxxxx
// lr
while(r - l > 1) {
int mid = l + (r - l) / 2;
if(nums[mid] <= cand) {
l = mid;
} else {
r = mid;
}
}
if(nums[l] == cand && i != l) {
int x = -1, y = -1;
for(int j = 0; j < orig.size(); j++) {
if(x == -1 && nums[i] == orig[j])
x = j;
else if(nums[l] == orig[j])
y = j;
}
return vector<int>{x, y};
}
}
assert(1 == -1); // bug
}
};
```
## Hashmap
Time complexity: $O(n)$, where n is the number of elements in the array, and there is no significant collision during the hash map operations.
Sorting is not required.
```c++
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> seen;
for(int i = 0; i < nums.size(); i++) {
int cand = target - nums[i];
if(seen.count(cand) == 1) {
return vector<int>{seen[cand], i};
}
seen[nums[i]] = i;
}
assert(1 == -1);
}
};
```
## 2 pointers
It's actually the next problem.
# [167. Two Sum II - Input array is sorted](https://leetcode.com/problems/two-sum-ii-input-array-is-sorted/)
The code above should both work. Notice that the index has been changed to 1-based.
## Binary search
Time complexity: $O(nlog(n))$, where n is the number of elements in the array
```c++
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
// nums[i], target - nums[i]
for(int i = 0; i < nums.size(); i++) {
int l = 0, r = nums.size(); // [l, r)
int cand = target - nums[i];
while(r - l > 1) {
int mid = l + (r - l) / 2;
if(nums[mid] == cand) {
l = mid;
break;
} else if(nums[mid] < cand) {
l = mid;
} else {
r = mid;
}
}
if(nums[l] == cand && i != l) {
return vector<int>{min(i + 1, l + 1), max(i + 1, l + 1)};
}
}
assert(1 == -1);
}
};
```
## Hashmap
Time complexity: $O(n)$, where n is the number of elements in the array, and there is no significant collision during the hash map operations.
Sorting is not required.
```c++
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> seen;
for(int i = 0; i < nums.size(); i++) {
int cand = target - nums[i];
if(seen.count(cand) == 1) {
return vector<int>{seen[cand], i + 1};
}
seen[nums[i]] = i + 1;
}
assert(1 == -1);
}
};
```
## 2 pointers
The main idea is that if you move the left pointer to the right, the value can only go up; if you move the right pointer to the left, the value can only go down!
```c++
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int l = 0, r = nums.size() - 1;
while(r - l > 0) {
if(nums[l] + nums[r] == target) {
return vector<int>{l + 1, r + 1};
} else if(nums[l] + nums[r] < target) {
l++;
} else {
r--;
}
}
assert(1 == -1);
}
};
```
# [170. Two Sum III - Data structure design](https://leetcode.com/problems/two-sum-iii-data-structure-design/)
# [653. Two Sum IV - Input is a BST](https://leetcode.com/problems/two-sum-iv-input-is-a-bst/)
# [1099. Two Sum Less Than K](https://leetcode.com/problems/two-sum-less-than-k/)
# [15. 3Sum](https://leetcode.com/problems/3sum/)
# [16. 3Sum Closest](https://leetcode.com/problems/3sum-closest/)
# [259. 3Sum Smaller](https://leetcode.com/problems/3sum-smaller/)
# [18. 4Sum](https://leetcode.com/problems/4sum/)
# [454. 4Sum II](https://leetcode.com/problems/4sum-ii/)
# Binary search - great article
Source(重新排版): [我已經不會寫二分搜](https://m.facebook.com/nt/screen/?params=%7B%22note_id%22%3A376367007072759%7D&path=%2Fnotes%2Fnote%2F)
在開始之前,必須要強調一下我個人的專長是胡說八道。如果有寫錯的東西,請大家用力吐槽。另外廣告一下這是認捐給競程日記 http://blog.icpc.tw 的文章。
二分搜 (Binary search) 幾乎是一個大家都會的演算法,最常見的用途是在一個有單調性的序列中,找某個東西是不是存在,譬如在 $1,2,5,7,9,10$ 這個單調遞增的數列中,搜尋一個特定的值 $x$ 是否存在。而 Binary search 的作法通常是先判斷 $x$ 是否介於第一項$(1)$與最後一項$(10)$之間,如果不是,那肯定沒有。反之,則維護可能的位置下限 $L$ 及上限 $U$,每次選取 $L$ 與 $U$ 之間的一個數字 $M$,通常是 $L$ 與 $U$ 的平均值,檢查一下第 $M$ 項與 $x$ 之間的大小關係。如果相等,那就找到了。如果 $x$ 較小,那麼答案應該在第 $L$ 項與第 $M-1$ 項之間。如果 $x$ 比較大,那麼答案應該在第 $M+1$ 項與第 $U$ 項之間。就反覆做到找到 $x$,或是藉由不相等的結果維護 $L$ 與 $U$,直到 $L \gt U$ 為止:此情況時可以宣布找不到。
自從年紀大了以後,我經常沒有辦法寫出一個不會進入無窮迴圈的 Binary search,理由是每次要更新 $L$ 或 $U$ 時,更新錯邊界值了。感覺方法如此簡單,可是我常常會寫錯,我現在甚至不確定上面那樣寫對不對,有錯請大家吐槽一下。我現在使用的程式碼,大部分是 Binary search 的變形,也就是本文要敘述的方法,優點是在絕大多數的場合,只要 $a < b$ 不會判錯,那麼用在二分逼近法時,可以充分的壓榨出 double 的精確度,還有我幾乎沒有把這個方法寫到無窮迴圈過。
我們先考慮一下另外一個問題:在一個單調遞增的數列中,找出最後一個 $<x$ 的數字所在的位置。如果數列的第一項就 $\geq x$ 那麼就沒答案。如果數列的最後一項還是 $<x$,那他就是答案。其他的情況,如果能查詢每一個位置,答案的形態將會是: $<x <x <x <x <x <x \geq x \geq x \geq x \geq x \geq x$。在多數情況下,我們可以使用 Binary search 找比 $x$ 略小一些些的 $x-\epsilon$,然後最後的 $U$ 便會是最後一個 $<x$ 的位置,而最後的 $L$ 便會是 $\geq x$ 的位置。當然,我們也能用這個問題的答案來解決平常 Binary search 所要處理的問題。
而本文要提的方法框架則是,當數列有 $n$ 項、形態是 $<x <x ... <x <x \geq x \geq x\ ... \geq x \geq x \geq x$ 且 index 是 0-base 時,先選定一個 step size $s$, $\frac{n}{2} \leq s < n$,並設定候補答案 $cand=0$。每一個回合,先判斷 $cand+s$ 是否超過邊界,即 $cand+s < n$ 是否在邊界內。如果在邊界內,則判斷第 $cand+s$ 項與 $x$之間的關係。如果是 $<x$ 則將 $cand$ 加上 $s$。如果是 $\geq x$ 或是超界,那麼將 $s$ 減半 (整數除法),而當 $s$ 被減到 $0$ 時,第 $cand$ 項就是最後一個 $<x$ 的。寫成 code 大概像是這樣:
```c++
cand = 0;
s = n/2;
while (s != 0)
{
if(cand+s < n && a[cand+s] < x)
cand = cand+s;
else
s = s/2;
}
```
這個方法保證 $a[cand]$ 總是 $<x$,且終究會把 $s$ 降到 $0$。而 $s$ 降到 $0$ 之前必須要經過 $1$,而由 $1$ 降到 $0$ 的時間點,必然是 $cand+1 = n$ 或第 $cand+1$ 項 $\geq x$。由這個觀察,我們能確定結束時第 $cand$ 項是最後一個 $<x$ 的,而這個方法所需要的時間,其實也是 $O(log(n))$:頂多三次 if 判定就可以把 $$ 減半,頂多減半 $O(log(n))$ 次就能把 $s$ 歸零。
行文到此,其實還沒有把真正要用的方法說出來。大家仔細觀察一下,便可以發現只要 $s$ 初始值介於 $\frac{n}{2}$ 到 $n-1$ 之間就可以得到上述的複雜度與正確性保證,而我們總是可以在 $\frac{n}{2}$ 到 $n-1$ 之中,找到一個 $2$ 的整數次方。假定 $f(n)$ 是 $\frac{n}{2}$ 到 $n-1$ 之中,最大的 $2$ 的整數次方,例如 $f(9)=8$。那我們選定 $f(n)$ 為 $s$ 的初始值,會有一些有趣的事情發生。當我們第一次判定,其實是判定 $a[f(n)]$ 是否 $<x$。如果 $a[f(n)]<x$,那麼第二回合,我們保證 $cand+s$ 會超界,會把 $s$ 設定成 $\frac{f(n)}{2}$。如果 $a[f(n)] \geq x$,我們第一回合馬上就會把 $s$ 設定成 $\frac{f(n)}{2}$,殊途同歸。接下來考慮是 $s$ 減半之後的那個回合(假定是第 $r$ 回合),如果 `if` 判定成立,再接下來第 $r+1$ 回合,所查詢的位置,必然是第 $r-1$ 回合所查詢的位置,因為過程中所有的 $s$ 都是 $2$ 的整數次方,減半後走兩步就會踩到先前判過的!既然我們早就知道該位置是超界或是 $\geq x$,那第 $r+1$ 回合連比都不用比,直接把 $s$ 減半就好。因此我們能將程式碼改寫為:
```c++
for (cand = 0, s = f(n); s != 0; s = s/2)
{
if(cand+s < n && a[cand+s] < x)
cand = cand+s;
}
```
此時大家再看仔細一點: $cand$ 變化的過程。這個方法在過程中,$cand$ 從 $0$ 變成最終型態的過程,就是每一回合確定一個 bit 是 $0$ 或是 $1$,從高位開始。呃,大家可能會想說這跟浮點精確度有什麼關係。讓我們回顧一下浮點數的結構,大多數場合,浮點數是二進位的科學記號表示法,比如說 $10.0$ 是二進位的 $1.01$ 乘上 $2$ 的 $3$ 次方。當我們在透過二分逼近法找答案時,假定 $2$ 的 $k$ 次方 $<$ 答案,$2$ 的 $k+1$ 次方 $\geq$ 答案,那麼如果將 $cand$ 初始化為 $2$ 的 $k$ 次方,$s$ 初始化為 $2$ 的 $k-1$ 次方,再套用上述方法的框架,每次能夠精準的決定一個 bit,而 double 只有小數點下 $52$ 位,只要做 $52$ 回合便能全面壓榨出 double 的精確度,只要每次跟答案比大小不會錯的話。
(按:自己吐槽自己一下,其實用二分搜的框架,找到正確的 $L$ 與 $U$ 之後,也是做幾次就多幾個 bit 的精確度。不過這個手法還可以用在維護 $\{1,2,...,n\}$ 的子集合並提供 $O(log(n))$ 的下列三種操作:插入、刪除、查詢第 $k$ 大的元素,以及透過 jump pointer 花 $O(log(n))$ 去查樹狀圖的 path aggregation ,這就是後話了,以後有空再寫。)