「学习笔记」线性分治

注:此博客写于 2017.12

嘛,分治应该是一个比较有意思的一个东西。蒟蒻Cyani也来学一发姿势辣。。

前置技能

分治为什么更好?

按照我的理解:分治的复杂度更优,往往基于减少一些 重复 的运算。这就需要我们去把大问题划分为若干个小问题,这些小问题的公共部分就可以利用一些技巧求解。

分治的复杂度计算。

主定理!

一般地,一个分治的时间复杂度可以表示为:

$$\large T(n) = a \times T(n/b) + f(n)$$

画出递归树,或者暴力展开就会发现:

$$\large T(n) = O(n ^ {\log _ b a} + \sum _ {i=0} ^ {\log _ b n - 1} a ^ i f( \frac n {b ^ i}))$$

其中左边是递归底层的总复杂度,右边是非底层的复杂度。要计算 $T(n)$ 的渐进复杂度,就要比较 $f(n)$ 与 $O(n ^ {\log _ b a})$ 的阶。

情况一, $f(n) < O(n ^ {\log _ b a})$

可以证明得到:

$$\large T(n) = O(n ^ {\log _ b a})$$

可以发现,递归树的每一层都比上一层高出指数级。即叶子部分是时间复杂度的主体。

情况二, $f(n) = O(n ^ {\log _ b a} \times \log ^ k n )$

可以证明得到:

$$\large T(n) = O(n ^ {\log _ b a} \times log ^ {k+1} n)$$

可以发现,递归树每一层代价都相同。总共有 $O(\log n)$ 层,复杂度要乘以 $log$ 。

情况三, $f(n) > O(n ^ {\log _ b a})$

可以证明得到:

$$\large T(n) = O(f(n))$$

可以发现,递归的主要代价都在树根。

CQD分治

一般形式

定义过程 $Solve(l,r)$。假设运行 $Solve(l,r)$, 可以得到 $f[l..r]$ 的值/处理 $[l,r]$ 的询问。

$Solve(l, r)$

  • $Solve(l, mid)$

  • 考虑 $Solve(l, mid)$ 中的操作/询问,对 $[mid+1,r]$ 中的影响。

  • $Solve(mid+1, r)$

离线算法。

常见应用:k维偏序

问题的一般形式:$k$ 维空间中有 $n$ 个点。问最长的一个点的序列 $p1,p2,..$ ,使得 $pi$ 的所有坐标都比 $pi+1$ 小。

一维偏序:直接排序。

二维偏序:排序+树状数组。

三维偏序:树套树(?),利用CDQ分治转化为二维偏序,利用排序+树状数组解决。

首先按照 $z$ 轴排序。因为所有的修改操作都在查询操作之前。所以就与 $z$ 轴无关了。

一些题目

BZOJ4553-[TJOI2016]序列 有一个长度为 $n$ 的序列。有些位置上的数可能变化为另一个给定的数。但最多只会变化一个位置的上。问最长的一个子序列,使得不管怎样变化,这个子序列始终是非递减的。

三维偏序,DP。 计算每个位置能变成的最大值 $r[i]$ ,最小值 $l[i]$。当且仅当 $j

计算几何,分治。 做法类似于一个经典问题:平面最近点对。可以做到复杂度 $O(n \log n)$(利用抽屉原理证明)。此题也是类似。

考虑首先按照 $x$ 轴分治,递归处理两边。再考虑横跨的情况。假设当前的最优解为 $ans$ ,那么就要取与 $x[mid]$ 差不超过 $ans/2$ 的所有点。然后单调扫描,可以找到与当前点 $y$ 相差不超过 $ans/2$ 的所有点。根据抽屉原理,这个边长为 $ans$ 的正方形中,不会有超过 $8$ 点。所以这一部分的时间复杂度是 $O(n)$ 的。如果用归并排序的方法,就可以做到 $O(n \log n)$。

BZOJ2287-[POJChallenge]消失之物 与普通的01背包类似。不过需要你求出,当某个物品不存在时,放置的方案总数。

经典的分治背包。 考虑按照物品分治,第 $i$ 个物品存在的时间为 $[1,i-1],[i+1,n]$。 然后分治的过程中DP即可。复杂度 $O(n^2 \log n)$ 。

BZOJ4025-二分图 给定一个图,每条边有一个存在的时间范围。问每一时刻,是否是一个二分图。

按时间分治,并查集。 显然可以按时间分治。而判断而二分图就可以用经典的带权并查集,需要按秩合并。

注意处理权值的方法。似乎有必要贴代码?

#include<bits/stdc++.h>
#define rep(i,a,b) for (int i=(a); i<=int(b); i++)
using namespace std;

inline int read() {
    char ch = getchar(); int x = 0, op = 1;
    while (ch < '0' || '9' < ch) { if (ch == '-') op = -1; ch = getchar(); }
    while ('0' <= ch && ch <= '9') { x = x * 10 + ch - '0'; ch = getchar(); }
    return op * x;
}

const int maxn = 200009;
struct node { int u, v, s, t; } a[maxn];
int pa[maxn], rk[maxn], val[maxn], ans[maxn], n, m, T, top;
int stk[maxn]; vector<int> p;

int getpa(int x) {
    while (pa[x] != x) x = pa[x];
    return x;
}

int getdist(int x) {
    int res = 0;
    while (pa[x] != x) {
        res ^= val[x]; x = pa[x];
    }
    return res;
}

void link(int x, int y) {
    int tmp = getdist(x) ^ getdist(y) ^ 1;
    x = getpa(x); y = getpa(y);
    if (rk[x] < rk[y]) swap(x, y);
    if (rk[x] == rk[y]) { rk[x]++; stk[++top] = -x; }
    pa[y] = x; val[y] = tmp; stk[++top] = y;
}

void del(int bot) {
    int x;
    while (top != bot) {
        x = stk[top--];
        if (x > 0) pa[x] = x;
        else rk[-x]--;
    }
}

void solve(int l, int r, vector<int> &p) {
    int mid = (l + r) >> 1, u, v, bot = top;
    vector<int> L, R;
    rep (i, 0, p.size()-1)
        if (a[p[i]].s <= l && r <= a[p[i]].t) {
            u = a[p[i]].u; v = a[p[i]].v;
            if (getpa(u) == getpa(v)){
                if (!(getdist(u) ^ getdist(v))) {
                    del(bot); return;
                }
            }
            else link(u, v);
        }
        else {
            if (!(a[p[i]].t < l || mid < a[p[i]].s)) L.push_back(p[i]);
            if (!(a[p[i]].t < mid+1 || r < a[p[i]].s)) R.push_back(p[i]);
        }
    if (l == r) { ans[l] = 1; del(bot); return; }
    solve(l, mid, L); solve(mid+1, r, R);  del(bot);
}

int main() {
    n = read(); m = read(); T = read();
    rep (i, 1, m) {
        a[i].u = read(); a[i].v = read();
        a[i].s = read()+1; a[i].t = read();
        p.push_back(i);
    }
    rep (i, 1, n) { pa[i] = i; rk[i] = 1; }
    solve(1, T, p);
    rep (i, 1, T) puts(ans[i] ? "Yes" : "No");
    return 0;
}

BZOJ4456-[ZJOI2016]旅行者 有一个 $n \times m \leq 20000$ 的网格图。给定每条边的代价。$Q$ 个询问,每个询问需要回答从 $(sx,sy)$ 到 $(tx,ty)$ 的最短路。

分治最短路。 考虑按照矩形的较短边来分治,所以只需要考虑跨过分界线的答案即可。求出分界线上的单源最短路,然后处理在这个矩形内的询问就好了。

看上去复杂度不优?最重要的还是利用主定理分析时间复杂度,%%%jry_2。

如果每一次按照长边分治:(以下S表示矩形的当前面积,n表示矩形的总面积)

$$T(S) = 2T(S/2) + O(S \sqrt n \log S)$$

解得:(将 $O(\sqrt n)$ 提出)

$$T(n) = O( n \sqrt n \log ^2 n)$$

每次按照短边分治:

$$T(S) = 2T(S/2) + O(S \sqrt S \log S)$$

解得:

$$T(n) = O(n \sqrt n \log n)$$

按照短边分治可以少一个 $\log$ 。

整体二分

对于某些具有单调性的问题,我们可以二分答案。然后用各种方法,DP,贪心,数据结构来判断。

但是,

如果有多个询问,那么里面再套数据结构。复杂度好像就不太对了。事实上,很容易发现,多次二分答案时,很多数据结构的操作都是重复的。

所以就可以整体进行二分。

一般形式

定义分治过程 $Solve(Q, l, r)$。其中 $Q$ 是询问/操作的序列,$[l,r]$ 是询问的答案范围。

$Solve(Q, l, r)$

  • 如果 $l = r$ ,那么把所有的答案设置为 $l$ 。退出。

  • 取 $mid = (l + r) / 2$ ,是当前判断的答案。

  • 考虑 $Q$ 中的每一个操作/询问(注意一定要按照时间排序!)

    • 如果是操作,那么执行对 $[l,mid]$ 有影响的操作。同时划分到 $Ql or Qr$。
    • 如果是询问,按照是否达到要求的贡献,划分到 $Ql or Qr$。注意划分到 $Qr$ 时,要减去现有的贡献。
  • $Solve(Ql, l, mid), Solve(Qr, mid+1, r)$

一些题目

BZOJ3110-[ZJOI2013]K大数查询 有 $n$ 个位置,$m$ 个操作。操作有两种,每次操作如果是 $(1,a,b,c)$ ,表示在第 $a$ 个位置到第 $b$ 个位置,每个位置加入一个数 $c$。如果是 $(2,a,b,c)$ ,表示询问从第 $a$ 个位置到第 $b$ 个位置,第 $c$ 大的数是多少。

经典的整体二分。 在整体二分的过程中,用线段树维护区间内权值 $[l,mid]$ 的数的个数。根据与 $K$ 的关系,来划分到更小的答案范围。

注意有时间限制,所以所有的操作都需要按照时间排序。代码如下:

#include<bits/stdc++.h>
#define rep(i,a,b) for (int i=(a); i<=int(b); i++)
#define lc (o << 1)
#define rc (o << 1 | 1)
#define Mid ((l + r) >> 1)
using namespace std;
typedef long long LL;
inline int read();
const int maxn = 100009;
struct segment { LL sum; int add, clr; } T[maxn<<2];
struct ask {
    int op, id, l, r, k; LL val;
    bool operator < (const ask &a) const {
        return k < a.k || k == a.k && id < a.id;
    }
} q[maxn];
int n, m, ans[maxn];
void init(int o) { T[o].sum = T[o].add = 0; T[o].clr = 1; }
void pushdown(int o, int l, int r) {
    if (T[o].clr) { init(lc); init(rc); T[o].clr = 0; }
    if (T[o].add) {
        T[lc].sum += 1LL * (Mid - l + 1) * T[o].add;
        T[rc].sum += 1LL * (r - Mid) * T[o].add;
        T[lc].add += T[o].add; T[rc].add += T[o].add;
        T[o].add = 0;
    }
}
void pushup(int o) { T[o].sum = T[lc].sum + T[rc].sum; }
void update(int o, int l, int r, int x, int y, int z) {
    if (l == x && y == r) {
        T[o].sum += 1LL * (r - l + 1) * z;
        T[o].add += z;
        return;
    }
    pushdown(o, l, r);
    if (x <= Mid) update(lc, l, Mid, x, min(Mid, y), z);
    if (Mid+1 <= y) update(rc, Mid+1, r, max(Mid+1, x), y, z);
    pushup(o);
}
LL query(int o, int l, int r, int x, int y) {
    if (l == x && y == r) return T[o].sum;
    pushdown(o, l, r);
    if (y <= Mid) return query(lc, l, Mid, x, y);
    if (Mid+1 <= x) return query(rc, Mid+1, r, x, y);
    return query(lc, l, Mid, x, Mid) + query(rc, Mid+1, r, Mid+1, y);
}
void solve(int l, int r, int ql, int qr) {
    if (l > r || ql > qr) return;
    if (l == r) {
        rep (i, ql, qr) if (q[i].op == 2) ans[q[i].id] = l;
        return;
    }
    int mid = (l + r) >> 1, tmp = ql-1; LL sum; init(1);
    rep (i, ql, qr)
        if (q[i].op == 1) {
            if (q[i].val > mid) { q[i].k = 1; update(1, 1, n, q[i].l, q[i].r, 1); }
            else { tmp++; q[i].k = 0; }
        }
        else {
            sum = query(1, 1, n, q[i].l, q[i].r);
            if (sum >= q[i].val) q[i].k = 1;
            else { tmp++; q[i].k = 0; q[i].val -= sum; }
        }
    sort(q+ql, q+qr+1);
    solve(l, mid, ql, tmp); solve(mid+1, r, tmp+1, qr);
}
int main() {
    n = read(); m = read();
    rep (i, 1, m) {
        q[i].id = i; q[i].op = read();
        q[i].l = read(); q[i].r = read();
        q[i].val = read();
        if (q[i].op == 1) q[i].val = q[i].val+n;
    }
    memset(ans, 0x3f, sizeof ans);
    solve(0, 2*n, 1, m);
    rep (i, 1, m) if (ans[i] <= 1e9)
        printf("%d\n", ans[i]-n);
    return 0;
}

猜你喜欢

转载自www.cnblogs.com/cyanic/p/9159433.html
今日推荐