分治算法详解:从CSP-S到IOI
分治算法是算法竞赛中最重要的思想之一,从基础的二分查找到高级的CDQ分治、点分治,贯穿整个OI/ACM生涯。本文将系统讲解分治算法的各个方面。
一、分治思想基础
1.1 什么是分治
分治(Divide and Conquer) 是一种算法设计范式,其核心思想是:
- 分解(Divide):将原问题分解为若干个规模较小的子问题
- 解决(Conquer):递归地求解各个子问题
- 合并(Combine):将子问题的解合并为原问题的解
1.2 分治算法的特征
- 子问题独立性:子问题之间相互独立,不重叠
- 最优子结构:原问题的最优解包含子问题的最优解
- 递归结构:问题可以递归分解
1.3 时间复杂度分析
分治算法的时间复杂度通常可以用递推式表示:
T(n) = aT(n/b) + f(n)其中:
- a 是子问题个数
- n/b 是子问题规模
- f(n) 是分解与合并的时间
根据 主定理(Master Theorem):
- 若 f(n) = O(n^(log_b(a)-ε)),则 T(n) = Θ(n^(log_b(a)))
- 若 f(n) = Θ(n^(log_b(a))),则 T(n) = Θ(n^(log_b(a)) * log(n))
- 若 f(n) = Ω(n^(log_b(a)+ε)),则 T(n) = Θ(f(n))
二、归并排序与逆序对
2.1 归并排序
归并排序是分治思想的经典应用。
void merge_sort(int arr[], int l, int r, int temp[]) { if (l >= r) return;
int mid = (l + r) / 2; merge_sort(arr, l, mid, temp); merge_sort(arr, mid + 1, r, temp);
// 合并两个有序数组 int i = l, j = mid + 1, k = l; while (i <= mid && j <= r) { if (arr[i] <= arr[j]) { temp[k++] = arr[i++]; } else { temp[k++] = arr[j++]; } } while (i <= mid) temp[k++] = arr[i++]; while (j <= r) temp[k++] = arr[j++];
for (int i = l; i <= r; i++) { arr[i] = temp[i]; }}时间复杂度:T(n) = 2T(n/2) + O(n) = O(n log n) 空间复杂度:O(n)
2.2 逆序对问题
逆序对是指序列中满足 i < j 但 a[i] > a[j] 的数对 (i, j)。
利用归并排序可以在 O(n log n) 时间内统计逆序对数量:
long long merge_and_count(int arr[], int l, int r, int temp[]) { if (l >= r) return 0;
int mid = (l + r) / 2; long long count = 0;
count += merge_and_count(arr, l, mid, temp); count += merge_and_count(arr, mid + 1, r, temp);
// 合并时统计跨越中点的逆序对 int i = l, j = mid + 1, k = l; while (i <= mid && j <= r) { if (arr[i] <= arr[j]) { temp[k++] = arr[i++]; } else { temp[k++] = arr[j++]; count += (mid - i + 1); // 关键:统计逆序对 } } while (i <= mid) temp[k++] = arr[i++]; while (j <= r) temp[k++] = arr[j++];
for (int i = l; i <= r; i++) { arr[i] = temp[i]; }
return count;}三、快速排序与快速选择
3.1 快速排序
快速排序通过选择枢轴(pivot)将数组分为两部分。
int partition(int arr[], int l, int r) { int pivot = arr[r]; int i = l - 1;
for (int j = l; j < r; j++) { if (arr[j] <= pivot) { i++; swap(arr[i], arr[j]); } } swap(arr[i + 1], arr[r]); return i + 1;}
void quick_sort(int arr[], int l, int r) { if (l >= r) return;
int p = partition(arr, l, r); quick_sort(arr, l, p - 1); quick_sort(arr, p + 1, r);}平均时间复杂度:O(n log n) 最坏时间复杂度:O(n²)(可通过随机化避免)
3.2 快速选择(第K小元素)
快速选择算法可以在平均 O(n) 时间内找到第k小的元素。
int quick_select(int arr[], int l, int r, int k) { if (l == r) return arr[l];
int p = partition(arr, l, r); int cnt = p - l + 1; // 左侧元素个数
if (k == cnt) { return arr[p]; } else if (k < cnt) { return quick_select(arr, l, p - 1, k); } else { return quick_select(arr, p + 1, r, k - cnt); }}四、二分查找及其变种
4.1 基础二分查找
int binary_search(int arr[], int n, int target) { int l = 0, r = n - 1; while (l <= r) { int mid = l + (r - l) / 2; if (arr[mid] == target) { return mid; } else if (arr[mid] < target) { l = mid + 1; } else { r = mid - 1; } } return -1;}4.2 查找第一个/最后一个位置
// 查找第一个 >= target 的位置int lower_bound(int arr[], int n, int target) { int l = 0, r = n; while (l < r) { int mid = l + (r - l) / 2; if (arr[mid] < target) { l = mid + 1; } else { r = mid; } } return l;}
// 查找第一个 > target 的位置int upper_bound(int arr[], int n, int target) { int l = 0, r = n; while (l < r) { int mid = l + (r - l) / 2; if (arr[mid] <= target) { l = mid + 1; } else { r = mid; } } return l;}五、整数二分与实数二分
5.1 整数二分模板
// 模板1:找满足条件的最小值int binary_search_min(int l, int r) { while (l < r) { int mid = l + (r - l) / 2; if (check(mid)) { r = mid; } else { l = mid + 1; } } return l;}
// 模板2:找满足条件的最大值int binary_search_max(int l, int r) { while (l < r) { int mid = l + (r - l + 1) / 2; // 注意这里要+1 if (check(mid)) { l = mid; } else { r = mid - 1; } } return l;}5.2 实数二分
double binary_search_real(double l, double r) { const double eps = 1e-8; while (r - l > eps) { double mid = (l + r) / 2; if (check(mid)) { r = mid; } else { l = mid; } } return l;}
// 或者固定迭代次数double binary_search_real_iter(double l, double r) { for (int i = 0; i < 100; i++) { // 100次足够精确 double mid = (l + r) / 2; if (check(mid)) { r = mid; } else { l = mid; } } return l;}六、二分答案
二分答案是一种重要的算法技巧,适用于”最大化最小值”或”最小化最大值”问题。
6.1 经典例题:最小化最大值
问题:有n本书,m个人抄写,每个人抄写连续的几本,求最小化最大抄写量。
#include <bits/stdc++.h>using namespace std;
int n, m;int books[505];
// 检查是否可以在最大抄写量为maxPages时完成bool check(int maxPages) { int people = 1, current = 0; for (int i = 0; i < n; i++) { if (books[i] > maxPages) return false; if (current + books[i] > maxPages) { people++; current = books[i]; if (people > m) return false; } else { current += books[i]; } } return true;}
int main() { cin >> n >> m; int l = 0, r = 0; for (int i = 0; i < n; i++) { cin >> books[i]; r += books[i]; l = max(l, books[i]); }
while (l < r) { int mid = l + (r - l) / 2; if (check(mid)) { r = mid; } else { l = mid + 1; } }
cout << l << endl; return 0;}七、CDQ分治
CDQ分治是陈启峰提出的一种分治算法,主要用于解决动态问题和多维偏序问题。
7.1 三维偏序问题
问题:给定n个三元组(a, b, c),统计每个三元组被多少个三元组”支配”(即存在三元组(x, y, z)满足x≤a, y≤b, z≤c)。
#include <bits/stdc++.h>using namespace std;
const int MAXN = 100005;
struct Point { int a, b, c; int ans, cnt; // ans: 被支配次数, cnt: 相同点数量 int id;} p[MAXN], tmp[MAXN];
int n, k;int tree[MAXN * 2];int result[MAXN];
bool cmp_a(const Point& x, const Point& y) { if (x.a != y.a) return x.a < y.a; if (x.b != y.b) return x.b < y.b; return x.c < y.c;}
bool cmp_b(const Point& x, const Point& y) { if (x.b != y.b) return x.b < y.b; return x.c < y.c;}
void add(int x, int val) { for (; x <= k; x += x & -x) { tree[x] += val; }}
int query(int x) { int res = 0; for (; x; x -= x & -x) { res += tree[x]; } return res;}
void cdq(int l, int r) { if (l >= r) return;
int mid = (l + r) / 2; cdq(l, mid); cdq(mid + 1, r);
// 归并排序,按b排序 sort(p + l, p + mid + 1, cmp_b); sort(p + mid + 1, p + r + 1, cmp_b);
int j = l; for (int i = mid + 1; i <= r; i++) { while (j <= mid && p[j].b <= p[i].b) { add(p[j].c, p[j].cnt); j++; } p[i].ans += query(p[i].c); }
// 清空树状数组 for (int i = l; i < j; i++) { add(p[i].c, -p[i].cnt); }}
int main() { cin >> n >> k; for (int i = 1; i <= n; i++) { cin >> p[i].a >> p[i].b >> p[i].c; p[i].cnt = 1; p[i].id = i; }
sort(p + 1, p + n + 1, cmp_a);
// 去重 int tot = 0; for (int i = 1; i <= n; i++) { if (i > 1 && p[i].a == p[i-1].a && p[i].b == p[i-1].b && p[i].c == p[i-1].c) { p[tot].cnt++; } else { p[++tot] = p[i]; } }
cdq(1, tot);
for (int i = 1; i <= tot; i++) { result[p[i].ans + p[i].cnt - 1] += p[i].cnt; }
for (int i = 0; i < n; i++) { cout << result[i] << "\n"; }
return 0;}7.2 CDQ优化动态规划
CDQ分治可以优化某些DP转移,将O(n²)优化到O(n log n)。
八、点分治
点分治是处理树上路径问题的强大工具。
8.1 树上路径统计
问题:给定一棵树和距离K,求距离不超过K的点对数量。
#include <bits/stdc++.h>using namespace std;
const int MAXN = 10005;
vector<pair<int, int>> adj[MAXN]; // {邻接点, 边权}bool vis[MAXN];int sz[MAXN], maxSz[MAXN];int n, k;long long ans = 0;
void getSize(int u, int fa) { sz[u] = 1; maxSz[u] = 0; for (auto [v, w] : adj[u]) { if (v == fa || vis[v]) continue; getSize(v, u); sz[u] += sz[v]; maxSz[u] = max(maxSz[u], sz[v]); }}
int getRoot(int u, int fa, int total) { maxSz[u] = total - sz[u]; int root = u; for (auto [v, w] : adj[u]) { if (v == fa || vis[v]) continue; int subRoot = getRoot(v, u, total); if (maxSz[subRoot] < maxSz[root]) { root = subRoot; } } return root;}
vector<int> dist;
void getDist(int u, int fa, int d) { dist.push_back(d); for (auto [v, w] : adj[u]) { if (v == fa || vis[v]) continue; getDist(v, u, d + w); }}
long long calc(int u, int initDist) { dist.clear(); getDist(u, -1, initDist); sort(dist.begin(), dist.end());
long long res = 0; int l = 0, r = dist.size() - 1; while (l < r) { if (dist[l] + dist[r] <= k) { res += r - l; l++; } else { r--; } } return res;}
void solve(int u) { getSize(u, -1); int root = getRoot(u, -1, sz[u]);
vis[root] = true; ans += calc(root, 0);
for (auto [v, w] : adj[root]) { if (vis[v]) continue; ans -= calc(v, w); solve(v); }}
int main() { cin >> n; for (int i = 1; i < n; i++) { int u, v, w; cin >> u >> v >> w; adj[u].push_back({v, w}); adj[v].push_back({u, w}); } cin >> k;
solve(1); cout << ans << endl;
return 0;}时间复杂度:O(n log² n)
九、线段树分治
线段树分治用于处理带时间维度的问题,特别是动态图问题。
9.1 动态图连通性
#include <bits/stdc++.h>using namespace std;
const int MAXN = 100005;
struct DSU { int fa[MAXN], sz[MAXN]; stack<pair<int, int>> stk;
void init(int n) { for (int i = 0; i <= n; i++) { fa[i] = i; sz[i] = 1; } }
int find(int x) { return fa[x] == x ? x : find(fa[x]); }
bool merge(int x, int y) { x = find(x); y = find(y); if (x == y) return false; if (sz[x] < sz[y]) swap(x, y); stk.push({y, sz[x]}); fa[y] = x; sz[x] += sz[y]; return true; }
void undo() { auto [y, oldSz] = stk.top(); stk.pop(); int x = fa[y]; sz[x] = oldSz; fa[y] = y; }};
struct Edge { int u, v, l, r; // 边(u,v)在时间[l,r)内存在};
vector<Edge> edges;vector<int> queries[MAXN * 4];DSU dsu;
void insert(int o, int l, int r, int ql, int qr, int id) { if (ql <= l && r <= qr) { queries[o].push_back(id); return; } int mid = (l + r) / 2; if (ql < mid) insert(o * 2, l, mid, ql, qr, id); if (qr > mid) insert(o * 2 + 1, mid, r, ql, qr, id);}
void solve(int o, int l, int r) { int cnt = 0; for (int id : queries[o]) { if (dsu.merge(edges[id].u, edges[id].v)) { cnt++; } }
if (l + 1 == r) { // 处理时刻l的查询 // ... } else { int mid = (l + r) / 2; solve(o * 2, l, mid); solve(o * 2 + 1, mid, r); }
// 撤销操作 for (int i = 0; i < cnt; i++) { dsu.undo(); }}十、FFT与NTT
快速傅里叶变换(FFT)和数论变换(NTT)用于快速计算多项式乘法。
10.1 FFT实现
#include <bits/stdc++.h>using namespace std;
const double PI = acos(-1);
struct Complex { double r, i; Complex(double r = 0, double i = 0) : r(r), i(i) {} Complex operator+(const Complex& c) const { return Complex(r + c.r, i + c.i); } Complex operator-(const Complex& c) const { return Complex(r - c.r, i - c.i); } Complex operator*(const Complex& c) const { return Complex(r * c.r - i * c.i, r * c.i + i * c.r); }};
void fft(Complex a[], int n, int inv) { if (n == 1) return;
Complex a0[n/2], a1[n/2]; for (int i = 0; i < n/2; i++) { a0[i] = a[i * 2]; a1[i] = a[i * 2 + 1]; }
fft(a0, n/2, inv); fft(a1, n/2, inv);
Complex wn(cos(2 * PI / n), inv * sin(2 * PI / n)); Complex w(1, 0);
for (int i = 0; i < n/2; i++) { Complex t = w * a1[i]; a[i] = a0[i] + t; a[i + n/2] = a0[i] - t; w = w * wn; }}
vector<int> multiply(vector<int>& A, vector<int>& B) { int n = 1; while (n < A.size() + B.size()) n *= 2;
Complex a[n], b[n]; for (int i = 0; i < n; i++) { a[i] = i < A.size() ? Complex(A[i], 0) : Complex(0, 0); b[i] = i < B.size() ? Complex(B[i], 0) : Complex(0, 0); }
fft(a, n, 1); fft(b, n, 1);
for (int i = 0; i < n; i++) { a[i] = a[i] * b[i]; }
fft(a, n, -1);
vector<int> result(n); for (int i = 0; i < n; i++) { result[i] = (int)(a[i].r / n + 0.5); }
return result;}10.2 NTT实现
const long long MOD = 998244353;const long long G = 3;
long long qpow(long long a, long long b, long long mod) { long long res = 1; while (b) { if (b & 1) res = res * a % mod; a = a * a % mod; b >>= 1; } return res;}
void ntt(long long a[], int n, int inv) { if (n == 1) return;
long long a0[n/2], a1[n/2]; for (int i = 0; i < n/2; i++) { a0[i] = a[i * 2]; a1[i] = a[i * 2 + 1]; }
ntt(a0, n/2, inv); ntt(a1, n/2, inv);
long long wn = qpow(G, (MOD - 1) / n, MOD); if (inv == -1) wn = qpow(wn, MOD - 2, MOD);
long long w = 1; for (int i = 0; i < n/2; i++) { long long t = w * a1[i] % MOD; a[i] = (a0[i] + t) % MOD; a[i + n/2] = (a0[i] - t + MOD) % MOD; w = w * wn % MOD; }}十一、分治优化DP
11.1 决策单调性优化
当DP转移满足决策单调性时,可以使用分治优化。
// dp[i] = min(dp[j] + cost(j+1, i)) for j < i// 且决策点单调不减
long long dp[MAXN], cost[MAXN][MAXN];int n;
void solve(int l, int r, int optL, int optR) { if (l > r) return;
int mid = (l + r) / 2; int opt = -1;
for (int k = optL; k <= min(mid - 1, optR); k++) { long long val = dp[k] + cost[k + 1][mid]; if (opt == -1 || val < dp[mid]) { dp[mid] = val; opt = k; } }
solve(l, mid - 1, optL, opt); solve(mid + 1, r, opt, optR);}
int main() { // 初始化cost数组 // ...
dp[0] = 0; for (int i = 1; i <= n; i++) dp[i] = INF;
solve(1, n, 0, n - 1);
cout << dp[n] << endl; return 0;}时间复杂度:从O(n²)优化到O(n log n)
十二、经典分治题目
12.1 最接近点对
问题:平面上有n个点,求距离最近的两个点。
#include <bits/stdc++.h>using namespace std;
struct Point { double x, y;};
bool cmp_x(const Point& a, const Point& b) { return a.x < b.x;}
bool cmp_y(const Point& a, const Point& b) { return a.y < b.y;}
double dist(const Point& a, const Point& b) { return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));}
double closest_pair(vector<Point>& points, int l, int r) { if (r - l <= 3) { double minDist = 1e18; for (int i = l; i < r; i++) { for (int j = i + 1; j < r; j++) { minDist = min(minDist, dist(points[i], points[j])); } } sort(points.begin() + l, points.begin() + r, cmp_y); return minDist; }
int mid = (l + r) / 2; double midX = points[mid].x;
double d = min(closest_pair(points, l, mid), closest_pair(points, mid, r));
// 合并 vector<Point> strip; for (int i = l; i < r; i++) { if (abs(points[i].x - midX) < d) { strip.push_back(points[i]); } }
for (int i = 0; i < strip.size(); i++) { for (int j = i + 1; j < strip.size() && strip[j].y - strip[i].y < d; j++) { d = min(d, dist(strip[i], strip[j])); } }
return d;}
int main() { int n; cin >> n; vector<Point> points(n); for (int i = 0; i < n; i++) { cin >> points[i].x >> points[i].y; }
sort(points.begin(), points.end(), cmp_x); cout << fixed << setprecision(6) << closest_pair(points, 0, n) << endl;
return 0;}12.2 天际线问题
使用分治法求城市天际线轮廓。
vector<pair<int, int>> merge_skylines(vector<pair<int, int>>& left, vector<pair<int, int>>& right) { vector<pair<int, int>> result; int h1 = 0, h2 = 0; int i = 0, j = 0;
while (i < left.size() && j < right.size()) { int x, h; if (left[i].first < right[j].first) { x = left[i].first; h1 = left[i].second; i++; } else if (left[i].first > right[j].first) { x = right[j].first; h2 = right[j].second; j++; } else { x = left[i].first; h1 = left[i].second; h2 = right[j].second; i++; j++; }
h = max(h1, h2); if (result.empty() || result.back().second != h) { result.push_back({x, h}); } }
while (i < left.size()) result.push_back(left[i++]); while (j < right.size()) result.push_back(right[j++]);
return result;}总结
分治算法是算法设计中的基础思想,掌握分治需要:
- 理解分治思想:分解、求解、合并
- 掌握基础算法:归并排序、快速排序、二分查找
- 学会二分答案:最大化最小值、最小化最大值
- 深入高级技巧:CDQ分治、点分治、线段树分治
- 熟悉优化方法:FFT/NTT、分治优化DP
通过系统学习和大量练习,你将能够熟练运用分治算法解决各类竞赛问题。从CSP-S的基础二分到IOI的高级分治,分治思想将伴随你整个算法竞赛生涯。
推荐练习题目:
- 洛谷P1226(快速幂)
- 洛谷P1908(逆序对)
- 洛谷P3810(三维偏序)
- 洛谷P4178(点分治)
- 洛谷P3803(FFT/NTT)
- Codeforces 各种Div2 D/E题
坚持练习,你一定能精通分治算法!