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(之后会用到同样的思想)

因为序列的递推式为

\[S(i) = \begin{cases} 3 & i=0 \\ S(i-1)+1+2+i+S(i-1) & \text{otherwise.} \end{cases} \]

所以,可以直接考虑四件事:

\(\text{divide(x, t)}\) 表示到了第 \(x\) 个点,到了第几个区间。

\[\begin{cases} (x, t-1) & x \le S(t) \\ m & x = S(t - 1) + 1 \\ o & x \le S(t - 1) + 3 + t \\ (x - S(t - 1) - (t + 3), t - 1) & \text{otherwise.} \end{cases} \]

然后代码很好写了:

#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}\)

证明一下:

\[\begin{aligned} S(n) &= 2S(n - 1) + 1 \end{aligned} \]

两边同时 \(+1\) 得:

\[\begin{aligned} S(n) + 1 = 2S(n - 1) + 2 \end{aligned} \]

\(T(n) = S(n + 1)\) 得:

\[\begin{aligned} T(n) &= 2[S(n-1)+1] \\ &= 2T(n-1) \end{aligned} \]

所以

\[\begin{aligned} T(n) &= 2^n \end{aligned} \]

所以

\[\begin{aligned} S(n) &= T(n) - 1 \\ &= 2^n-1 \end{aligned} \]

所以当 \(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;
}
posted @ 2026-07-22 10:08  AKCoder  阅读(12)  评论(0)    收藏  举报