ALGO[1]

雙指針&二分搜&分治

INDEX

Two Pointer

Binary Search

Divide & Conquer

Two Pointers

雙指針

1. 正反指針

2. 快慢指針

Two Pointers

雙指針

就是兩根指針

(啊說是指針但他不是指標)

有正反指針、快慢指針、指向不同陣列的指針......

正反指針

array

快慢指針

array

Two Pointers

例子

Sum of Two Values

給你    個正整數,找到兩個不同位置的數字相加為    的索引

n
x
for (int i = 1; i <= n; i++) {
	for (int j = 1; j <= n; j++) {
    	if (arr[i] + arr[j] == x) {
        	cout << i << ' ' << j << '\n';
            return 0;
        }
    }
}

如果這樣寫

複雜度

O(N^2)

會 TLE

1 \leq N \leq 2 \times 10^5

怎麼在

O(N)\ or \ O(N \log N)

內解決?

Two Pointers

Solution

使用雙指針

\bf{先將陣列排序}
設一個\ l\ 在最左邊,一個\ r\ 在最右邊
arr[l] + arr[r] \begin{cases} < x& 將\ l\ 向後移\\ > x& 將\ r\ 向前移\\ = x& 輸出索引並終止 \end{cases}
array
n = 4,\ x = 7
1
2
5
7
sum = 1 + 7 > 7
sum = 1 + 5 < 7
sum = 2 + 5 = 7

Two Pointers

AC Code

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
 
int main() {
    int n, x;
    cin >> n >> x;
    vector<pair<int, int>> v(n); // first for number, second for index
 
    for (int i = 0; i < n; i++) {
        cin >> v[i].first;
        v[i].second = i + 1;
    }
 
    sort(begin(v), end(v));
 
    int l = 0, r = n - 1;
    while (l < r) {
        if (v[l].first + v[r].first == x) {
            cout << v[l].second << ' ' << v[r].second << '\n';
            return 0;
        } else if (v[l].first + v[r].first > x) {
        	r--;
        } else l++;
    }
 
    cout << "IMPOSSIBLE\n";
}
Complexity:\ O(N \log N)
排序複雜度=O(N \log N),\ 雙指針\ O(N)

Two Pointers

例子

Subarray Sums I

for (int i = 1; i <= n; i++) {
	for (int j = i; j <= n; j++) {
    	for (int k = i; k <= j; k++) {
        	sum += arr[k];
        }
        
        if (sum == x) ans++;
    }
}
複雜度\ O(N^3)\ 一定爛
因為他是求區間,所以不能排序\\
那怎麼用雙指針寫?
給定\ n\ 個正整數,問有幾個區間的和為\ x

Two Pointers

Solution

用兩個快慢不同的指針

l\ 跟\ r\ 都從最左邊開始
sum \begin{cases} < x& r + 1\ 然後\ sum - arr[r]\\ > x& sum + arr[l]\ 然後\ l + 1\\ = x& ans+1\ 然後\ r + 1 \end{cases}
用一個\ sum\ 維護區間 \ [l, r]\ 之和
ans:
|
|
|
array
n = 5,\ x = 7
l
r
sum = 2
2
4
1
2
7
sum = 6
2
4
1
2
7
sum = 7
2
4
1
2
7
sum = 9
2
4
1
2
7
sum = 7
2
4
1
2
7
sum = 14
2
4
1
2
7
sum = 10
2
4
1
2
7
sum = 9
2
4
1
2
7
sum = 7
2
4
1
2
7

Two Pointers

AC Code

#include <iostream>
#include <vector>
using namespace std;
 
int main() {
    int n, x;
    cin >> n >> x;
    vector<int> v(n);
    for (int i = 0; i < n; i++) {
    	cin >> v[i];
    }
 
    int ans = 0;
    int r = 1, sum = v[0];
    for (int l = 0; l < n; l++) {
        while (sum < x && r < n) {
            sum += v[r];
            r++;
        }
 
        if (sum == x) ans++;
        
        sum -= v[l];
    }
    
    if (sum == x) ans++;
 
    cout << ans << '\n';
}
Complexity:\ O(N)

Two Pointers

題單

Binary Search

二分搜

1. 單調性

2. 對答案二分搜

Binary Search

找數字

假設你要找\ arr\ 裡面的\ x
你可能會想......
for (int i = 0; i < n; i++) {
	if (arr[i] == x) {
    	// x found at index i
    }
}
複雜度\ O(n)
有更快的做法嗎?
假設我叫你\ 1 \sim 10\ 中的一個數字\\
並且在你每次猜完之後跟你講太小還是太大
你會從哪個數字開始猜?
5?

Binary Search

找數字

為何先從\ 5?
因為不管是太大還是太小,你都可以篩掉一半的選項
每次猜測都把剩餘選項減半 \rightarrow \log_{2} n\ 次內一定猜得到

二分搜

1
2
3
4
5
6
7
8
9
10
ans = 6
l=1
r = 10
mid = \frac{1 + 10}{2} = 5 < 6
l=5
r = 10
mid = \frac{5 + 10}{2} = 7 > 6
設定左界(1)和右界(10),如果中間大於答案,將右界左移,否則將左界右移
l=5
r = 7
mid = \frac{5 + 7}{2} = 6

Binary Search

找數字

回到找\ arr\ 裡面的\ x
7 < 8,但你不知道要減去哪一半邊的選項
所以二分搜的前提建立在陣列的\bf{單調性}
對於一個數列 a:\\ \\
所以只要確保二分搜的陣列是排序過的就好了
如果拿下面陣列二分搜會發生什麼事?
4
8
7
6
3
n = 5,\ x = 8
array
單調性
對於所有\ i,滿足\ a_i \leq a_{i+1},則為單調遞增數列
對於所有\ i,滿足\ a_i \geq a_{i+1},則為單調遞減數列

Binary Search

判斷

什麼情況會有單調性?
當你要找第一個滿足的數
在他以上的都符合,在他以下的不符合
或者相反,就可以使用二分搜了
例如剛剛猜數字的問題
我們把題目從找到\ x \rightarrow 找到 \geq x\ 的\bf{最小數字}

實作

\textit{左閉右閉}
[l, r] = true
維護一個可能是答案的區間
當\ l = r\ 時終止
我都寫這個
\textit{左閉右開}
[l, r) = true
當\ l + 1 = r\ 時終止
r\ 一定為\ false

Binary Search

實作

自己挑一個喜歡的寫
然後要清楚要縮左界還是右界
如果跑無窮迴圈的話\\ 可以代小數字如\ l=2,\ r=3,\ x=3
初始的\ l,\ r\ 要注意邊界
\textit{左閉右閉}
\textit{左閉右開}
int l = 0, r = n - 1;
while (l < r) {
	int mid = (l + r) / 2;
    if (mid >= x) {
    	r = mid;
    } else {
    	l = mid + 1;
    }
} // 最後 l == r == x
int l = 0, r = n;
while (l + 1 < r) {
	int mid = (l + r) / 2;
    if (mid >= x) {
    	r = mid;
    } else {
    	l = mid;
    }
} // 最後 l == r - 1 == x

Binary Search

實作

內建函式

upper_bound:返回陣列中第一個大於    的值的指標

x

lower_bound:返回陣列中第一個大於等於    的值的指標

x
auto k = lower_bound(arr, arr + n, x) - arr;
if (k < n && arr[k] == x) {
	// x found at index k
}
\textit{陣列}

啊記得陣列要先排序過

set<int> s;
auto it = s.lower_bound(x);
if (it != s.end() && *it == x) {
	// x found in set 
}
\textit{set}

Binary Search

例子

for (int time = 1; time <= sum * t; time++) {
	int prod = 0;
	for (int i = 0; i < n; i++) {
    	prod += time / k[i];
    }
    
    if (prod >= t) {
    	cout << time << '\n';
        break;
    }
}
給你\ n\ 台機器,每台機器可以花\ k_i\ 時間做出一個產品\\ 問最少需要多久才能做出\ t\ 個產品?
複雜度:O(NC)
可以怎麼優化?
C = max(k_i) \times t

Binary Search

Solution

對答案二分搜

什麼東西具有單調性?
\rightarrow 需要花的時間
假設把他用一個陣列表示為\ 000000111111111
我們觀察到\ times\ 大於某個點後一定可以滿足產量
我們要找的就是最早出現的\ 1
所以我們對答案(需要的時間)二分搜
我們就是要對這條線二分搜
t
產量
時間
示意圖僅供參考

Binary Search

AC Code

#include <iostream>
#include <vector>
using namespace std;
using ll = long long;
 
int main() {
    int n, t;
    cin >> n >> t;
 
    vector<int> k(n);
    for (int i = 0; i < n; i++) {
    	cin >> k[i];
    }
 
    ll l = 1, r = 1e18;
    while(l < r) {
        ll mid = (l + r) / 2, sum = 0;
        for (int i : k) {
            sum += mid / i;
            if (sum > t) break;
        }
 
        if(sum >= t) r = mid;
        else l = mid + 1;
    }
 
    cout << l << '\n';
}
Complexity:\ O(N \log C)

Binary Search

例子

將\ n\ 個數字的陣列分成\ k\ 個子陣列\\ 問子陣列之最大和的最小值為多少?
什麼東西具有單調性?
\rightarrow 每個子陣列之和的最大值
k
對這條線二分搜
寫一個\ check()\ 函式計算以當前最大值可以產生多少個子陣列
check(mid) \begin{cases} \leq k& r = mid\\ > k& l = mid + 1 \end{cases}
示意圖僅供參考
子集合最大值
子集合數量

Binary Search

AC Code

#include <iostream>
#include <vector>
using namespace std;
using ll = long long;
 
ll MAX_NUM = 2e5 * 1e9;
int n, k;
vector<int> arr;
 
bool check(ll max_num) {
    int subarr_count = 0;
    ll cur_sum = 0;
 
    for (int x : arr) {
        if (x > max_num) return false;
 
        if (cur_sum + x > max_num) {
            subarr_count++;
            cur_sum = 0;
        }
        cur_sum += x;
    }
    if (cur_sum > 0) subarr_count++;
 
    return subarr_count <= k;
}
 
int main() {
    cin >> n >> k;
    arr.resize(n);
    for (int &i : arr) cin >> i;
 
    ll l = 1, r = MAX_NUM;
    while (l < r) {
        ll mid = (l + r) / 2;
        if (check(mid)) r = mid;
        else l = mid + 1;
    }
 
    cout << l << '\n';
}
Complexity:\ O(N \log C)

Binary Search

題單

Divide & Conquer

分治,分而治之

1. Merge Sort

2. Binary Exponentiation

3. Minimum Euclidean Distance

Divide and Conquer

Merge Sort

2
2
3
3
4
5
6
8

下列兩個已排序陣列按照順序合併的時間複雜度是多少?

2
3
3
5
2
4
6
8
2 = 2
array
2 < 3
3 < 4
3 < 4
4 < 5
5 < 6
用剛剛的雙指針在\ O(N)\ 時間解決
\rightarrow 把它拆成兩半,就跟上面的一樣了
那假設現在問你以下陣列怎麼排序?
2
3
3
5
2
4
6
8

然後你會 Merge Sort 了

如果拆完之後還是沒有排序過的呢?
繼續拆下去直到剩一個

Divide and Conquer

Merge Sort

合併過程
複雜度只需要\ O(N \log N)
分治就是把問題拆成小問題,分而治之
3
3
3
5
2
2
4
8
6
3
3
2
5
2
4
6
8
2
3
3
5
2
4
6
8
2
2
3
3
4
5
6
8

Divide and Conquer

Code

#include <iostream>
#include <vector>
using namespace std;

// [l, r]
void mergeSort(vector<int> &arr, int l, int r) {
	if (l == r) return;
	int m = (l + r) / 2;
	mergeSort(arr, l, m), mergeSort(arr, m + 1, r);
	
	int n1 = m - l + 1, n2 = r - m;
	vector<int> L(n1), R(n2);
	for (int i = 0; i < n1; i++) {
		L[i] = arr[l + i];
	}
	for (int i = 0; i < n2; i++) {
		R[i] = arr[m + 1 + i];
	}

	int i = 0, j = 0, k = l;
	while (i < n1 && j < n2) {
		if (L[i] <= R[j]) arr[k++] = L[i++];
		else arr[k++] = R[j++];
	}

	while (i < n1) arr[k++] = L[i++];
	while (j < n2) arr[k++] = R[j++];
}

int main() {
    int n = 5;
    vector<int> arr = {4, 8, 7, 6, 3};
	
    mergeSort(arr, 0, n - 1);

	for (int a : arr) cout << a << ' ';
}
Complexity:\ O(N \log N)

Divide and Conquer

例子

Exponentiation

將\ a\ 乘\ b\ 次肯定不行(0 \leq b \leq 10^9)
3 ^ 5 = 3 ^ 2 \times 3 ^ 2 \times 3
假設\ a=3,\ b=5,可以觀察到
所以我們只需要把它拆成\ a^{\frac{b}{2}}\ 並且相乘
\begin{cases} b = 0 & a = 1\\ b = 1 & a = a\\ b\ 是奇數& a^b = a^{\frac{b}{2}} \times a^{\frac{b}{2}} \times a \\ b\ 是偶數 & a^b = a^{\frac{b}{2}} \times a^{\frac{b}{2}} \end{cases}
\frac{b}{2}\ 無條件捨去到整數位
就可以在\ \log b\ 的時間內解決了(這東西就是快速冪)
計算\ a^b\ 模\ 10^9 + 7
這兩個一樣

Divide and Conquer

AC Code

#include <iostream>
using namespace std;
using ll = long long;
 
const int mod = 1e9 + 7;

// 遞迴式
ll power(ll x, int y) {
	if (y == 0) return 1;
    if (y == 1) return x;
    ll temp = power(x, y / 2);
    if (y % 2) return temp * temp * x % mod;
    else return temp * temp % mod;
}
 
// 迭代式
ll binpow(ll x, int y) {
	ll ret = 1;
	while (y) {
		if (y & 1) ret = ret * x % mod;
		x = x * x % mod;
		y >>= 1;
	}
	return ret;
}
 
int main() {
	int n;
	cin >> n;
	while (n--) {
		int a, b;
		cin >> a >> b;
		cout << binpow(a, b) << '\n';
	}
}
Complexity:\ O(N \log N)

Binary Search

題單

Bonus

掰掰

Made with Slides.com