[TreeDivideAndConquer]点分治

羊禄
2023-12-01

Pre :树分治

树分治有两种,一种是基于点的分治,一种是基于边的分治,由于我觉得边分治用处不大,所以我们只讨论点分治。

点分治

路径

点分治是在树上的基于重心的分治方法,最初被用来处理有关树上路径计数的问题(见漆子超论文),因此我们从路径统计问题开始谈起。
首先不管是什么路径,在树上一定都有一个最高点(也就是说一个路径 (u,v) 一定是从 v 往上爬,爬到LCA(u,v),再向下走到 v 的)。树上的任何一个点都可能成为某一条路径的最高点,我们将所有路径按最高点分类来处理。
假如我们当前处理的点是u,我们将点 u 的子树提取出来,所有以u为最高点的路径一定都是从 u 的两个不同子树中选取两个点作为端点,这时候我们自然产生了一种想法:假如我们对于每个u都能承受枚举 u 子树里所有点的复杂度,那么问题就会变的简化许多,因为我们至少已经得到了路径的最高点(就是u)和路径的一个端点(枚举得到)。
但是显然如果我们直接对原树每个点都这样做一次,最坏情况下复杂度是平方级别的,这时候我们就需要运用点分治来解决这一问题。

重心

为了引入点分治,我们首先介绍重心。
重心是一棵树中到所有点的距离和最小的那个点,同时也是一棵无根树中选取为根能够使得最大子树最小的那个点。
至于重心的求法,我们从它的定义出发做一遍动态规划就可以求出了。

重心分解

回到刚才的问题,我们希望选取出一个点 u ,处理出以这个点为最高点的路径,然后继续递归处理以它的儿子为最高点的路径,并使得我们能够承受枚举u子树里所有点的复杂度。
注意到我们很多时候并不关心某条路径的最高点具体是哪个点,我们只需要它被它的最高点统计过一次就行了,换言之,我们可以通过适当改变树的形态,来降低我们之前做法的复杂度(很多时候我们直接做都是被一条链卡掉的)。
点分治就成功运用了重心的独特性质来做到这一点:
假如我们当前统计子树 u 里的所有路径,我们提取出子树u的重心 cg 作为这棵树的根,统计出以 cg 为最高点的路径,然后继续递归子树。
点分治的复杂度分析基于这样一个定理:
每次提取出树的重心作为这棵树的根,删掉重心,原树被分解为若干子树,继续分解子树,至多分解 log(n) 次树就会被分解完。
可以用主定理证明,对于点分治的每一层来说,假如处理一个大小为 n 的子树的复杂度为O(f(n)),那么整个点分治的复杂度为 O(f(n)log(n))

BZOJ3365 [Usaco2004 Feb]Distance Statistics 路程统计

大意:
给一棵树,求两两之间距离不超过 K 的点对数
解:
这就是最简单的路径统计问题。我们运用点分治的思想,对于每棵子树提取出重心为根,然后考虑最高点在新根的所有路径。
对于一个根,我们遍历它的子树,将子树里所有点按到根的距离排序,对于一个点i,合法的路径的另一个端点必然在排序后的某一个前缀里,因此我们利用单调性扫一扫就好。

类似的简单题还有BZOJ2599, BZOJ2152

重心重构树到动态点分治

点分治最早用来解决路径统计问题,但点分治的威力绝不仅仅于此。
点分治最核心的思想,就在于重心分解,而重心分解从另一个角度来看,其实是将原树的结构做了重新的调整。
我们提取出分治时每一层的重心,将层与层之间的重心重新连边,我们就得到了原树重构之后的一棵新树,容易知道新树恰好包含了原树的每一个点,树高不超过 logn 并且保留了原树中的一些子树关系(例如原树的一棵子树对应到重构树里还是一棵子树,只不过父子关系发生了很大变化;原树一条根到子树的路径被拆解为根到重心,重心到子树的两条路径),之前的路径统计其实是在这棵隐式的重构树上暴力,我们并不需要把这棵重构树显式地建立出来。
有了重心重构树的想法,我们能够解决的问题就绝不仅仅限于路径统计了。

BZOJ3672 [Noi2014]购票

大意:
比较复杂不方便直接说,建议看一遍原题
解:
首先我们写出该题的动态规划方程,发现我们直接暴力转移是会超时的,观察之后发现要斜率优化,进而注意到对于一个点 v ,决策点是它向上若干连续的祖先,也就是对一条路径上的点求一个凸壳,直接做依然超时,我们考虑引入重心重构树的想法。
重心重构之后,假如我们原来要处理的子树是u,现在在重构树中对应于子树 cg ,重构树中的 cg 有若干子树,其中有一棵包含了 u ,其它都是cg在原树中的子树,注意到 u 所在的子树的dp值与 cg 其他子树无关,我们先递归处理之,得到该子树信息之后,我们考虑这棵子树对 cg 其它子树的影响:将 cg u 的路径提取出来暴力求一个凸壳(这些点都在之前先求出来的子树里),再暴力更新cg其它子树的每一个点。最后递归处理 cg 其它子树即可,由于重心重构树的性质,我们这么暴力更新有复杂度保证。
有很多人说这么做类似于 CDQ ,感觉颇有道理,把子树类比成区间似乎就有了共通的地方。不过本来都是分治算法,很正常。

//By Hachiman
#include <bits/stdc++.h>
using namespace std;
#define rep(i, l, r) for (int i = (l); i <= (r); i++)
#define per(i, r, l) for (int i = (r); i >= (l); i--)
#define REP(i, n) for (int i = 0; i < (n); i++)
#define PER(i, n) for (int i = (n)-1; ~i; i--)
#define MS(_, __) memset(_, __, sizeof(_))
#define MP make_pair
#define PB push_back
#define FT first
#define SC second
#define Hachiman
#ifdef Hachiman
#   define debug(...) fprintf(stderr, __VA_ARGS__)
#else
#   define debug(...)
#endif
#define RND(l, r) rand()%((r)-(l)+1)+(l)
typedef long long ll;
typedef long double ld;
typedef unsigned int ui;
typedef pair<ll, ll> PII;
const ll INF = 0x7fffffffffffffff;
const double eps = 1e-8;
template<typename T> inline void read(T &x){
    x = 0; T f = 1; char ch = getchar();
    while (!isdigit(ch)) { if (ch == '-') f = -1; ch = getchar(); }
    while (isdigit(ch)) { x = x * 10 + ch - '0'; ch = getchar(); }
    x *= f;
} 
template<typename T> inline void ckmax(T &a, T b){ if (a < b) a = b; }
template<typename T> inline void ckmin(T &a, T b){ if (a > b) a = b; }
template<typename T> inline bool CKmax(T &a, T b){ if (a < b) { a = b; return 1;} return 0; }
template<typename T> inline bool CKmin(T &a, T b){ if (a > b) { a = b; return 1;} return 0; }

const int MaxN = 200100;

struct Point{
    ll x, y;
    Point(){}
    Point(ll X, ll Y):x(X), y(Y){}
}CH[MaxN];
struct Edge{
    int v; ll w; bool ban; Edge *nxt;
}pool[MaxN<<1], *tail = pool, *g[MaxN];
int TOP;
double slope[MaxN];
int n, type, fa[MaxN], lst[MaxN], root;
ll p[MaxN], q[MaxN], lim[MaxN], f[MaxN], dis[MaxN], mx[MaxN], sz[MaxN], mn;

inline double cal_slope(const Point &u, const Point &v){ return (1.*u.y-v.y)/(1.*u.x-v.x); }
inline void ins(int u){
    Point p = Point(dis[u], f[u]);
    double s = TOP ? cal_slope(p, CH[TOP]) : INF;
    for (; TOP >= 2 && s >= slope[TOP]; s = cal_slope(p, CH[--TOP]));
    slope[++TOP] = s; CH[TOP] = p;
}
inline int qry(double k){ 
    //printf("k=%.1lf\n",k); 
    if (slope[TOP] > k) return TOP;
    int l = 1, r = TOP;
    while (l + 1 < r){
        int mid = l+r >> 1;
        if (slope[mid] > k) l = mid; else r = mid;
    }
    return l;
}
inline bool cmp(int u, int v){ return dis[u]-lim[u] > dis[v]-lim[v]; }
inline void addedge(int u, int v, int w){
    tail->v = v; tail->w = w; tail->nxt = g[u]; g[u] = tail++;
}
inline void dfs_dis(int u){
    for (Edge *p = g[u]; p; p = p->nxt){
        dis[p->v] = dis[u] + p->w; dfs_dis(p->v);
    }
}
inline void dfs_size(int u){
    sz[u] = 1; mx[u] = 0;
    for (Edge *p = g[u]; p; p = p->nxt) if (!p->ban){
        dfs_size(p->v);
        sz[u] += sz[p->v]; ckmax(mx[u], sz[p->v]);
    }
}
inline void find_heavy(int u, int rt, int &gc, ll nowmn){
    ckmax(mx[u], sz[rt]-sz[u]);
    if (CKmin(nowmn, mx[u])) gc = u;
    for (Edge *p = g[u]; p; p = p->nxt) if (!p->ban) find_heavy(p->v, rt, gc, nowmn);
}
inline void dfs_lst(int u){
    lst[++lst[0]] = u;
    for (Edge *p = g[u]; p; p = p->nxt) if (!p->ban) dfs_lst(p->v);
}
inline void Tree_Divide_and_Conquer(int u, int size){
    if (size == 1) return;
    int root; ll mn = n; dfs_size(u); find_heavy(u, u, root, mn);
    for (Edge *p = g[root]; p; p = p->nxt) p->ban = 1;
    Tree_Divide_and_Conquer(u, size - sz[root] + 1);
    lst[0] = 0; for (Edge *p = g[root]; p; p = p->nxt) dfs_lst(p->v);
    sort(lst + 1, lst + 1 + lst[0], cmp); 
    TOP = 0;
    for (int i = 1, anc = root; i <= lst[0]; i++){ int now = lst[i];
        for (; anc != fa[u] && dis[anc]+lim[now]>=dis[now]; anc = fa[anc]) ins(anc);
        if (TOP){
            Point res = CH[qry(1.*p[now])];
            ckmin(f[now], res.y + p[now]*(dis[now]-res.x)+q[now]);
        }
    }
    for (Edge *p = g[root]; p; p = p->nxt) Tree_Divide_and_Conquer(p->v, sz[p->v]);
}
int main(int argc, char *argv[]){
    read(n); read(type);
    rep(i, 2, n){ ll len;
        read(fa[i]), read(len), read(p[i]), read(q[i]), read(lim[i]);
        addedge(fa[i], i, len);
    }
    dfs_dis(1);
    MS(f, 0x3f); f[1] = 0;
    Tree_Divide_and_Conquer(1, n);
    rep(i, 2, n) printf("%lld\n", f[i]);
    return 0;
}

由静到动:动态点分治

之前的问题我们都没有对树上的信息作任何的修改,因此我们不需要显式地将重心重构树建出来,但在更高层次的问题中,树上点/边的信息,甚至是树的形态都会发生改变,这时候重心重构的思想依然是重要的武器,但我们不得不将重心重构树建立出来,并用新的手段维护重构树上的信息了。
这种做法,就是动态点分治。

BZOJ1095 [ZJOI2007]Hide 捉迷藏

大意:
给一棵树,每个点要么黑要么白,给若干操作,改变棋子的颜色或是询问相距最远的两个黑点的距离
解:
假如说没有改颜色的话那么我们直接点分治就能解决这个问题了,但现在结点的信息在改变,我们需要显式地建立出重心重构树并维护重构树上的信息。
对于每个点,我们需要维护的是它和它子树里所有黑点到它的父亲(注意如果不特别指出我所说的都是重构树里的父子关系)的距离,以及它每个儿子的子树里到它最远的黑点的距离,我们用两个堆来维护上述两个信息(再次注意到由于重构树树高 log(n) ,我们存这么多信息也只会用 nlog(n) 的空间,因为一个点只会在它的所有祖先里被存下来,非常好)再用一个堆维护全局路径最大值,至于修改,找到那个点暴力往上爬把祖先一路改掉就行了。

//%%%SRwudi,%%%CZGJ
//By Hachiman
//Dynamic Tree Divide and Conquer
#include <bits/stdc++.h>
using namespace std;
#define rep(i, l, r) for (int i = (l); i <= (r); i++)
#define per(i, r, l) for (int i = (r); i >= (l); i--)
#define REP(i, n) for (int i = 0; i < (n); i++)
#define PER(i, n) for (int i = (n)-1; ~i; i--)
#define EREP(p, x) for (Edge *p = g[x]; p; p = p->nxt)
#define NEREP(p, x) for (Ede *&p = cur[x]; p; p = p->nxt)
#define MS(_) memset(_, 0, sizeof(_))
#define MP make_pair
#define PB push_back
#define FT first
#define SC second
#ifdef Hachiman
#   define debug(...) fprintf(stderr, __VA_ARGS__)
#else
#   define debug(...)
#endif
#define RND(l, r) rand()%((r)-(l)+1)+(l)
typedef long long ll;
typedef long double ld;
typedef unsigned int ui;
typedef pair<int, int> PII;
const int INF = 0x7fffffff;
const double eps = 1e-8;
const int MOD = 1000000007;
template<typename T> inline void read(T &x){
    x = 0; T f = 1; char ch = getchar();
    while (!isdigit(ch)) { if (ch == '-') f = -1; ch = getchar(); }
    while (isdigit(ch)) { x = x * 10 + ch - '0'; ch = getchar(); }
    x *= f;
} 
template<typename T> inline void ckmax(T &a, T b){ if (a < b) a = b; }
template<typename T> inline void ckmin(T &a, T b){ if (a > b) a = b; }
template<typename T> inline bool CKmax(T &a, T b){ if (a < b) { a = b; return 1;} return 0; }
template<typename T> inline bool CKmin(T &a, T b){ if (a > b) { a = b; return 1;} return 0; }

const int MaxN = 100010;

struct Heap{
    priority_queue<int> ins, del;
    inline void Insert(int x){ ins.push(x); }
    inline void Erase(int x){ del.push(x); }
    inline void Clear(){ for (; !del.empty() && del.top() == ins.top(); del.pop(), ins.pop()); }
    inline void Pop(){ Clear(); ins.pop();  }
    inline int First(){ Clear(); return ins.top(); }
    inline int Second(){ int tmp = First(); Pop(); int res = First(); Insert(tmp); return res;  }
    inline int Size(){ return ins.size() - del.size(); }
}heap[2][MaxN], ans;
struct Edge{
    int v; bool ban; Edge *nxt, *ptr;
}pool[MaxN<<1], *tail = pool, *g[MaxN];

int n, m, cnt;
int fa[MaxN], mx[MaxN], Log[MaxN<<1], dep[MaxN], sz[MaxN], pos[MaxN], seq[MaxN<<1][20], Time = 0;
bool state[MaxN];

inline void addedge(int u, int v){
    tail->v = v; tail->nxt = g[u]; tail->ptr = tail+1; g[u] = tail++;
    tail->v = u; tail->nxt = g[v]; tail->ptr = tail-1; g[v] = tail++;
}
inline void dfs_size(int x, int fa){
    sz[x] = 1; mx[x] = 0;
    EREP(p, x) if (!p->ban && p->v != fa){
        dfs_size(p->v, x);
        sz[x] += sz[p->v]; ckmax(mx[x], sz[p->v]);
    }   
}
inline void find_heavy(int x, int rt, int fa, int &cg, int nowmn){
    ckmax(mx[x], sz[rt]-sz[x]);
    if (CKmin(nowmn, mx[x])) cg = x;
    EREP(p, x) if (!p->ban && p->v != fa) find_heavy(p->v, rt, x, cg, nowmn);
}
inline void dfs_dis(int x, int fa, int dep, Heap &q){
    q.Insert(dep);
    EREP(p, x) if (!p->ban && p->v != fa) dfs_dis(p->v, x, dep+1, q);
}
inline void ins(Heap &q){ if (q.Size()>=2) ans.Insert(q.First()+q.Second()); }
inline void del(Heap &q){ if (q.Size()>=2) ans.Erase(q.First()+q.Second()); }
inline int TreeDivideAndConquer(int x){
    int cg, mn = n; dfs_size(x, 0); find_heavy(x, x, 0, cg, mn);
    heap[1][cg].Insert(0);
    EREP(p, cg) if (!p->ban){ 
        p->ban = p->ptr->ban = true;
        Heap q; dfs_dis(p->v, 0, 1, q); int son = TreeDivideAndConquer(p->v);
        fa[son] = cg; heap[0][son] = q;
        heap[1][cg].Insert(heap[0][son].First());
    }
    ins(heap[1][cg]);
    return cg;  
}
inline void EulerTour(int x, int fa){
    seq[pos[x] = ++Time][0] = dep[x] = dep[fa] + 1;
    EREP(p, x) if (p->v != fa) EulerTour(p->v, x), seq[++Time][0] = dep[x];
}
inline void Mutiplication(){
    EulerTour(1, 0);
    rep(i, 2, Time) Log[i] = Log[i>>1]+1;
    rep(j, 1, Log[Time]) for (int i = 1; i+(1<<j)-1<=Time; i++)
        seq[i][j] = min(seq[i][j-1], seq[i+(1<<(j-1))][j-1]);   
}
inline int LCA_Depth(int x, int y){
    x = pos[x]; y = pos[y]; if (x > y) swap(x, y);
    int L = Log[y-x+1];
    return min(seq[x][L], seq[y-(1<<L)+1][L]);
}
inline int Distance(int x, int y){ return dep[x]+dep[y]-2*LCA_Depth(x, y); }
inline void Turnon(int x){
    del(heap[1][x]); heap[1][x].Erase(0); ins(heap[1][x]);
    for (int i = x; fa[i]; i = fa[i]){
        del(heap[1][fa[i]]);
        if (heap[0][i].Size()) heap[1][fa[i]].Erase(heap[0][i].First());
        heap[0][i].Erase(Distance(fa[i], x));
        if (heap[0][i].Size()) heap[1][fa[i]].Insert(heap[0][i].First());
        ins(heap[1][fa[i]]);
    }
}
inline void Turnoff(int x){
    del(heap[1][x]); heap[1][x].Insert(0); ins(heap[1][x]);
    for (int i = x; fa[i]; i = fa[i]){
        del(heap[1][fa[i]]);
        if (heap[0][i].Size()) heap[1][fa[i]].Erase(heap[0][i].First());
        heap[0][i].Insert(Distance(fa[i], x));
        if (heap[0][i].Size()) heap[1][fa[i]].Insert(heap[0][i].First());
        ins(heap[1][fa[i]]);
    }
}
int main(int argc, char *argv[]){
    read(n); cnt = n;
    rep(i, 1, n-1){ int u, v;
        read(u); read(v); addedge(u, v);
    }
    TreeDivideAndConquer(1);
    Mutiplication();
    rep(i, 1, n) state[i] = true;
    read(m);
    for (; m; m--){ char op;
        scanf(" %c", &op);
        if (op == 'G') printf("%d\n", cnt <= 1 ? cnt-1 : ans.First());
        else{ int x;
            scanf("%d", &x);
            if (state[x] == true){ --cnt; state[x] = false; Turnon(x); }
            else{ ++cnt; state[x] = true; Turnoff(x); } 
        }
    }
    return 0;   
}

BZOJ3435 [Wc2014]紫荆花之恋

大意:
给定一棵树,每次添加一个节点并询问当前有多少点对满足 dis(i,j)<=Ri+Rj 强制在线
解:
假如说不添加结点的话直接点分治就好了,现在树的形态在发生改变,这样问题就更为棘手,因为重心重构树的形态也会发生改变。
为了维护重心重构树的性质,我们引入替罪羊树的思想,先大胆直接添加叶子,一旦发现一棵子树过于不平衡,我们就暴力重构之。
然后在重构树每个点上用一个平衡树维护子树所有点的信息就可以了。

 类似资料: