20251025 - 分治
在解决比较复杂的问题时,我们可以把原问题划分成若干个结构相似,规模更小的子问题。
解决子问题后,用子问题的答案合并出原问题的答案。
这就是分治的思想。
PS:如果各子问题是不独立的,则分治法要重复地解公共的子问题,也就做了许多不必要的工作。此时虽然也可用分治法,但一般用 DP 比较好。
归并排序
假设要排序的区间为 \((1, 10)\),那么可以先看作排 \((1, 5)\),\((6, 10)\),\((1, 5)\) 可以划分成 \((1, 2)\) 和 \((3, 5)\),\((1, 2)\) 又可以划分成 \((1, 1)\) 和 \((2, 2)\)。欸,一个数还不好排序吗?直接不变就好了!
假如把两个有序的数组合并,可以用双指针来做,每次比较指向的元素,即(mid 代表中间位置的元素,c 代表合并之后的结果):
int i = l, j = mid + 1, k = l;
while (i <= mid && j <= r) {
if (a[i] <= a[j]) c[k++] = a[i++], res += j - mid - 1;
else c[k++] = a[j++];
}
while (i <= mid) c[k++] = a[i++], res += j - mid - 1;
while (j <= r) c[k++] = a[j++];
观察到,这个过程层数不超过 \(\log n\) 层,每一次是 \(O(n)\) 的,所以复杂度就是 \(O(n \log n)\)。
PS:C++ 有一个叫做
inplace_merge函数,可以直接做归并(注意没有排序)。
Moo B(之后会用到同样的思想)
因为序列的递推式为
所以,可以直接考虑四件事:
设 \(\text{divide(x, t)}\) 表示到了第 \(x\) 个点,到了第几个区间。
然后代码很好写了:
#include <bits/stdc++.h>
using namespace std;
#define AKCoder
#ifdef AKCoder
template<class t,class u>ostream& operator<<(ostream& os,const pair<t,u>& p){return os<<"("<<p.first<<","<<p.second<<")";}
#define debug(x) cout<<#x<<" = "<<(x)<<'\n'
#else
#define debug(x);
#endif
#define ll long long
#define ull unsigned long long
#define db double
#define all(x) (x).begin(), (x).end()
#define inf (1 << 30)
#define lnf (1LL << 60)
typedef pair<int, int> PII;
constexpr int N = 1e5 + 7;
constexpr int P = 998244353;
int n;
ll f[N];
char divide(int x, int t) {
if (!t) return (x == 1) ? 'm' : 'o';
if (x <= f[t - 1]) return divide(x, t - 1);
else if (x == f[t - 1] + 1) return 'm';
else if (x <= f[t - 1] + 3 + t) return 'o';
else return divide(x - f[t - 1] - (t + 3), t - 1);
}
int main() {
scanf("%d", &n);
f[0] = 3;
int t = 1;
for (int i = 1; ; i++, t++) {
f[i] = f[i - 1] + 3 + i + f[i - 1];
if (f[i] >= n) break;
}
printf("%c\n", divide(n, t));
return 0;
}
例题
A - 幂次方
首先先将指数分解,递归下去,再把指数的指数分解,以此类推。
注意输出的问题。
void divide(int x) {
for (int i = 14; i >= 0; i--) {
if (x >= (1 << i)) {
if (i == 1) printf("2");
else if (i == 0) printf("2(0)");
else {
printf("2(");
divide(i);
printf(")");
}
x -= (1 << i);
if (x) putchar('+');
}
}
}
B - 逆序对
二维偏序?
在归并排序的时候,是把两个有序数组合并。
当选到左边的时候,这时候右边的小于他的数量就是右边选了多少个。
当然,利用扫描线的思想,先把一维排序,再将第二维用 BIT 去维护。
将两者结合起来就变成了 CDQ。
```cpp
ll divide(int l, int r) {
if (l == r) return 0;
int mid = (l + r) / 2;
ll res = divide(l, mid) + divide(mid + 1, r);
int i = l, j = mid + 1, k = l;
while (i <= mid && j <= r) {
if (a[i] <= a[j]) c[k++] = a[i++], res += j - mid - 1;
else c[k++] = a[j++];
}
while (i <= mid) c[k++] = a[i++], res += j - mid - 1;
while (j <= r) c[k++] = a[j++];
for (int p = l; p <= r; p++) a[p] = c[p];
return res;
}
C - Permutation Transformation
就是一个分治构建树,建树比较麻烦,直接记录父节点就好了。
void divide(int l, int r, int f) {
if (l > r) return;
int maxx = 0, maxd = 0;
for (int i = l; i <= r; i++) if (a[i] > maxx) {
maxx = a[i];
maxd = i;
}
from[maxx] = f;
divide(l, maxd - 1, maxx);
divide(maxd + 1, r, maxx);
}
D - 地毯填补问题
先划分棋盘,从 \(2^k \times 2^k\) 划分成 \(2^{k-1} \times 2^{k-1}\)。
这样子就可以变成更小的子问题。
可以分四种情况,找到空的格子在那个位置。
剩下的怎么填呢?
在空位的周围三个填上,也就可以看作有一个新的公主占领了。
再铺上地毯。
void divide(int x1, int y1, int x2, int y2, int x, int y) {
if (x2 - x1 == 1 && y2 - y1 == 1) {
if (x1 == x && y1 == y) printf("%d %d 1\n", x2, y2);
if (x1 == x && y2 == y) printf("%d %d 2\n", x2, y1);
if (x2 == x && y1 == y) printf("%d %d 3\n", x1, y2);
if (x2 == x && y2 == y) printf("%d %d 4\n", x1, y1);
return;
}
int midx = (x1 + x2) / 2, midy = (y1 + y2) / 2;
if (x <= midx && y <= midy) {
divide(x1, y1, midx, midy, x, y);
printf("%d %d 1\n", midx + 1, midy + 1);
divide(midx + 1, y1, x2, midy, midx + 1, midy);
divide(midx + 1, midy + 1, x2, y2, midx + 1, midy + 1);
divide(x1, midy + 1, midx, y2, midx, midy + 1);
}
if (x <= midx && y > midy) {
divide(x1, midy + 1, midx, y2, x, y);
printf("%d %d 2\n", midx + 1, midy);
divide(x1, y1, midx, midy, midx, midy);
divide(midx + 1, y1, x2, midy, midx + 1, midy);
divide(midx + 1, midy + 1, x2, y2, midx + 1, midy + 1);
}
if (x > midx && y <= midy) {
divide(midx + 1, y1, x2, midy, x, y);
printf("%d %d 3\n", midx, midy + 1);
divide(midx + 1, midy + 1, x2, y2, midx + 1, midy + 1);
divide(x1, y1, midx, midy, midx, midy);
divide(x1, midy + 1, midx, y2, midx, midy + 1);
}
if (x > midx && y > midy) {
divide(midx + 1, midy + 1, x2, y2, x, y);
printf("%d %d 4\n", midx, midy);
divide(x1, y1, midx, midy, midx, midy);
divide(x1, midy + 1, midx, y2, midx, midy + 1);
divide(midx + 1, y1, x2, midy, midx + 1, midy);
}
}
E - Equivalent Strings
直接分治用 substr 去搞就好了。
bool divide(string a, string b) {
int len = (int)a.size();
if (len & 1) return a == b;
string s1 = a.substr(0, len / 2);
string s2 = a.substr(len / 2, len / 2);
string s3 = b.substr(0, len / 2);
string s4 = b.substr(len / 2, len / 2);
return (divide(s1, s4) && divide(s2, s3)) || (divide(s1, s3) && divide(s2, s4));
}
F - Merge Sort
发现偶数长度的一定不行,因为划分都是两两划分的。
然后递归到某一个位置的时候刚好不能用了,这时就可以直接让他无序,最简单的方法就是交换。
注意左闭右开。
void divide(int l, int r) {
if (!k || l + 1 >= r) {
return;
}
k -= 2;
int mid = (l + r) / 2;
swap(a[mid - 1], a[mid]);
divide(l, mid), divide(mid, r);
}
G - Secret Cow Code S
分治,然后按照题目做就行了。
char divide(ll n, ll l) {
if (n <= len) return s[n];
ll lc = l, nc = n - (l + 1);
if (!nc) nc = lc;
while (lc >= nc) lc /= 2;
return divide(nc, lc);
}
H - Code For 1
还记得 Moo 吗?
这里的递推式是 \(S(n) = S(n - 1) + 1 + S(n - 1)\),其实就是 \(2^{\lfloor log_2 n\rfloor + 1}\)。
证明一下:
两边同时 \(+1\) 得:
令 \(T(n) = S(n + 1)\) 得:
所以
所以
所以当 \(n_1\) 取 \(\log_2 n + 1\) 时,\(S(n_1) > n\)。
然后就没了。
bool divide(ll x, ll t) {
ll k = 1, nx = x;
for (; nx > 1; nx >>= 1) k <<= 1;
if (t < k) return divide(x >> 1, t);
else if (t > k) return divide(x >> 1, t - k);
else return x & 1;
}
I - Max GEQ Sum
我们可以用单调栈求出当 \(i\) 为最大值的区间。
然后判断其中的最大子段和是不是 \(\le a_i\)。
然后这个可以用类似小白逛公园的思想来搞。
struct Info {
ll msum, msl, msr, mss;
Info(ll x = 0) : msum(x), msl(x), msr(x), mss(x) {}
};
struct Node {
Info val;
} seg[N * 4];
Info operator + (const Info &A, const Info &B) {
Info C;
C.msum = A.msum + B.msum;
C.msl = max(A.msl, A.msum + B.msl);
C.msr = max(B.msr, B.msum + A.msr);
C.mss = max({A.mss, B.mss, A.msr + B.msl});
return C;
}
void update(int id) {
seg[id].val = seg[id * 2].val + seg[id * 2 + 1].val;
}
void build(int id, int l, int r) {
if (l == r) {
seg[id].val = Info(a[l]);
} else {
int mid = (l + r) / 2;
build(id * 2, l, mid);
build(id * 2 + 1, mid + 1, r);
update(id);
}
}
Info query(int id, int l, int r, int ql, int qr) {
if (ql == l && qr == r) return seg[id].val;
int mid = (l + r) / 2;
if (qr <= mid) return query(id * 2, l, mid, ql, qr);
else if (ql > mid) return query(id * 2 + 1, mid + 1, r, ql, qr);
else return query(id * 2, l, mid, ql, mid) + query(id * 2 + 1, mid + 1, r, mid + 1, qr);
}
void solve() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
build(1, 1, n);
vector<int> L(n + 1), R(n + 1);
stack<int> stk;
for (int i = 1; i <= n; i++) {
while (!stk.empty() && a[stk.top()] < a[i]) {
R[stk.top()] = i - 1;
stk.pop();
}
stk.push(i);
}
while (!stk.empty()) {
R[stk.top()] = n;
stk.pop();
}
for (int i = n; i >= 1; i--) {
while (!stk.empty() && a[stk.top()] < a[i]) {
L[stk.top()] = i + 1;
stk.pop();
}
stk.push(i);
}
while (!stk.empty()) {
L[stk.top()] = 1;
stk.pop();
}
for (int i = 1; i <= n; i++) {
int l = L[i], r = R[i];
auto now = query(1, 1, n, l, r);
if (now.mss > a[i]) return puts("NO"), void();
}
puts("YES");
}
就说是不是分治吧。
J - 平面最近点对(加强版)
先按照 \(x\) 排序。
考虑 \((l,r)\) 的最近点对。
很容易求出 \((l, mid)\) 和 \((mid + 1, r)\) 的最近点对。
然后到两边找到横坐标距离不超过 \((l, mid)\) 和 \((mid + 1, r)\) 的最近点对。
然后按照 \(y\) 排序。
然后暴力枚举,如果距离 \(> y\),直接跳出循环。
以人类的智慧发现最多只有 \(8\) 个点。
ldb divide(int l, int r) {
if (l == r) return 1e18;
int mid = (l + r) / 2, midx = a[mid].x;
ldb d = min(divide(l, mid), divide(mid + 1, r));
inplace_merge(a + l, a + mid + 1, a + r + 1, [&](const Node &A, const Node &B) {
return A.y < B.y;
});
int tot = 0;
for (int i = l; i <= r; i++) {
if (abs(a[i].x - midx) < d) c[++tot] = a[i];
}
for (int i = 1; i <= tot; i++) {
for (int j = i + 1; j <= tot && abs(c[j].y - c[i].y) < d; j++) {
d = min(d, dist(c[i].x, c[i].y, c[j].x, c[j].y));
}
}
return d;
}

浙公网安备 33010602011771号