# 分治法 `第13週社課` 顧名思義,分而治之 分治法最核心的思想就是把大問題分割成小問題,小的問題解決之後再來組合出大的問題的答案 ### 簡單的小例子:Merge Sort merge sort 是一個分治法的經典例子,他把大區間分成小區間,小區間排序好之後再用排序好的小區間湊出大區間 :::spoiler code ```cpp #include <bits/stdc++.h> using namespace std; constexpr int MAX_N = 10005; int a[MAX_N], tmp[MAX_N]; void MGS(int L, int R) { if (L == R) return; int M = (L + R) >> 1, i = L, j = M + 1, k = L; MGS(L, M); MGS(M + 1, R); while (i <= M && j <= R) { if (a[i] > a[j]) tmp[k++] = a[j++]; else tmp[k++] = a[i++]; } while (i <= M) tmp[k++] = a[i++]; while (j <= R) tmp[k++] = a[j++]; for (i = L; i <= R; ++i) a[i] = tmp[i]; } int main() { int n, i; scanf("%d", &n); for (i = 0; i < n; ++i) scanf("%d", a + i); MGS(0, n - 1); for (i = 0; i < n; ++i) printf("%d ", a[i]); } ``` 執行結果 ![](https://i.imgur.com/DvayVTA.png) 實際上 merge sort 還可以套進很多題目裡面當作製造單調性的解題手段(相容性很高) ::: ### 好玩的演算法:Karatsuba 這是一個把多項式乘法從$O(n^2)$變成$O(n^{1.5})$的演算法 :::spoiler explain 考慮: $$ \left\{ \begin{aligned} \large{f(x)=x^n\Big(f_1(x)\Big)+f_2(x)} \\ \large{g(x)=x^n\Big(g_1(x)\Big)+g_2(x)} \end{aligned} \right. $$ $$\large{ \Rightarrow f(x)g(x)= x^{2n}\Big (f_1(x)g_1(x) \Big)+ x ^ n \Big (f_1(x)g_2(x)+f_2(x)g_1(x)\Big) + \Big (f_2(x)g_2(x)}\Big )$$ 你可以用求出$\large\Big(f_1(x)+f_2(x)\Big)\Big(g_1(x)+g_2(x)\Big)$、$\large \Big(f_1(x)g_1(x)\Big)$、$\large \Big(f_2(x)g_2(x)\Big)$組合出上面你需要的東西,遞迴下去就解了 ::: :::spoiler code 這隻code其實是之前寫的,品質不太好而且可能有bug ```cpp #include <bits/stdc++.h> #include <cmath> using namespace std; // からつば algorithm float log_2 (float a) {return log(a) / log(2);} int power (int a, int b) {for (int i = 0;i < b;i++) {a *= 2;} return a;} void debug_vec(vector<int> a) {for (int i = 0;i < a.size();i++) {cout << a[i] << ' ';}} vector<int> add_up(vector<int> a, vector<int> b) { for (int i = 0;i < a.size();i++) a[i] += b[i]; return a; } vector<int> minus_up(vector<int> a, vector<int> b) { for (int i = 0;i < a.size();i++) a[i] -= b[i]; return a; } vector<int> merge_up(vector<int> a, vector<int> b) { for (int i = 0;i < b.size();i++) a.push_back(b[i]); return a; } vector<int> times(vector<int> a, int t) { for (int i = 0;i < a.size();i++) a[i] *= t; return a; } vector<int> karatsuba(vector<int> f, vector<int> g) { int half_f = f.size() / 2, half_g = g.size() / 2; if (half_f == 1) { vector<int> q = { f[0] * g[0], f[0] * g[1] + f[1] * g[0], f[0] * g[0] }; return q; } if (half_f == 2) { vector<int> q = { 0, f[0] * g[0], f[0] * g[1] + f[1] * g[0], f[1] * g[1] + f[0] * g[2] + f[2] * g[0], f[0] * g[3] + f[3] * g[0] + f[1] * g[2] + f[2] * g[1], f[1] * g[3] + f[3] * g[1] + f[2] * g[2], f[2] * g[3] + f[3] * g[2], f[3] * g[3] }; return q; } vector<int> a(f.begin(), f.begin() + half_f), b(f.begin() + half_f, f.end()), c(g.begin(), g.begin() + half_g), d(g.begin() + half_g, g.end()); vector<int> p = karatsuba(add_up(a, b), add_up(c, d)); f = karatsuba(a, c); g = karatsuba(b, d); p = minus_up(p, add_up(f, g)); int i; for (i = 0;i < half_f;i++) f[half_f + i] += p[i]; for (i = 0;i < half_f;i++) g[i] += p[half_f + i]; return merge_up(f, g); } int main() { // int i, j, m, n, p, q; // scanf("%d%d", &m, &n) // p = power(2, log_2(m + n - 0.5)); // vector<int> A(p, 0), B(p, 0); // for (i = m - 1;i > ;) vector<int> result = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; vector<int>A = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; vector<int>B = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, -1}; auto C = [](int a, int b) { long long sum = 1; for (int i = 0;i < b;i++) sum *= a - i; for (int i = 2;i <= b;i++) sum /= i; return sum; }; for (int i = 0;i <= 7;i++) { result = add_up(result, times(A, C(8, i + 1))); debug_vec(A);cout << endl; A = karatsuba(A, B); } debug_vec(result); return 0; } // 64 96 115 107 102 110 180 167 95 72 91 68 25 4 0 ``` ::: ### 平面最近點對 分左邊和右邊,用左右最小距離合併問題(把中線左右D的點做距離) :::spoiler code ```cpp struct point{ long long x[3]; }; bool cmp_x(point a, point b) { return a.x[0] < b.x[0]; } bool cmp_y(point a, point b) { return a.x[1] < b.x[1]; } bool cmp_x_y(point a, point b) { if (a.x[0] < b.x[0]) { return true; } else { if (a.x[0] == b.x[0]) { return a.x[1] < b.x[1]; } else { return false; } } } inline long long dist_2(point a, point b) { long long dif_x = a.x[0] - b.x[0]; long long dif_y = a.x[1] - b.x[1]; return (dif_x * dif_x + dif_y * dif_y); } pair<point, point> closest_pair_rec(point *Px, int low, int high) { pair<point, point> result; if (high - low + 1 <= 3) { if (n == 2) { result.first = Px[low]; result.second = Px[low + 1]; } else { if (dist_2(Px[low], Px[low + 1]) < dist_2(Px[low + 1], Px[low + 2])) { result.first = Px[low]; result.second = Px[low + 1]; } else { result.first = Px[low + 1]; result.second = Px[low + 2]; } } return result; } int mid = (low + high) / 2; pair<point, point> p1 = closest_pair_rec(Px, low, mid); pair<point, point> p2 = closest_pair_rec(Px, mid + 1, high); long long d_min = min(dist_2(p1.first, p1.second), dist_2(p2.first, p2.second)); long long x_mid = Px[mid].x[0]; auto *Sy = new point[high - low + 1]; int w = 0; for (int i = low; i <= high; ++i) { if ((Px[i].x[0] - x_mid) * (Px[i].x[0] - x_mid) <= d_min) { Sy[w++] = Px[i]; } } sort(Sy, Sy + w, cmp_y); pair<point, point> p3; bool isInMid = false; for (int i = 0; i < w - 1; ++i) { for (int j = i + 1; j < w; ++j) { if (dist_2(Sy[i], Sy[j]) < d_min) { long long d_new = dist_2(Sy[i], Sy[j]); if (d_new < d_min) { d_min = d_new; if(Sy[i].x[0] < Sy[j].x[0]){ p3.first = Sy[i]; p3.second = Sy[j]; } else{ p3.first = Sy[j]; p3.second = Sy[i]; } isInMid = true; } } } } delete[] Sy; if (isInMid) { return p3; } else { if (d_min == dist_2(p1.first, p1.second)) { return p1; } else { return p2; } } } pair<point, point> two_d(point *set) { sort(set, set + n, cmp_x_y); auto *Px = new point[n]; for (int i = 0; i < n; ++i) { Px[i] = set[i]; } pair<point, point> result = closest_pair_rec(Px, 0, n - 1); delete[] Px; return result; } ``` ::: ### 常數優化 對於一個分治的遞迴,在小case可以換成暴力解法,會比遞迴下去更優 舉例:在計算最近點對時,如果遞迴到$n\leq 8$時,可以直接$O(n^2)$計算($n$代表有幾個點) 實作上設定一個遞迴下界,用評測系統對下界三分搜(因為他是U型函數)