El pueblo unido jamas serà vencido!

树分治乱学

点分治

\(\mathrm{\color{black}{w}\color{red}{ind\_whisper}}\) 轻松切点分治然后还写了\(\huge{\rightarrow\text{博客}\leftarrow}\) orz.

0. 什么是点分治

\(\mathrm{\color{black}{w}\color{red}{ind\_whisper}}\) 说 : 所谓点分治,就是把所有的点分开来治

那么,什么是点分治? 点分治 就是 淀粉汁 也就是为了勾芡而调配的水淀粉,一种乳白色悬浊液,那么,一道色香味俱全的糖醋排骨就做好啦!(大雾).

通过对于整个树形结构的遍历,一次性解决大量对于树链的询问.

对于一棵树,其任意两点之间都有唯一一条简单路径,于是总的路径数就是 \(\frac{n(n - 1)}{2}\) , 是 \(n^2\) 级别的,显然直接枚举每条路径然后维护是很劣的.

那么,在遍历一棵树的时候,把树尽可能均匀分成两部分,那么整体就是 \(\mathrm{O}(n \log n)\) 了.

1.如何点分治

为了更为平均,引入 树的重心.

对于树上的每一个点,算其所有子树中最大的子树节点数,这个值最小的点就是这棵树的重心.

而重心有一个性质 : 以树的重心为根时,所有子树的大小都 不超过整棵树大小的一半.

求重心也很简单, \(\mathrm{O}(n)\) 的一次 dfs 即可.

那么问题就来了,大量的求重心复杂度会很高吗 ?

实际是不会的.只要总是找到重心,子树就能尽量均分,这样就可以实现找重心总体 \(\mathrm{O}(n \log n)\)

可以发现,分治 + 求重心的过程都是 \(\mathrm{O}(n \log n)\) 的,于是总体复杂度就是 \(\mathrm{O}(n \log n)\).

然后分的部分解决后,考虑如何处理路径问题.

首先,对于目前分治到的一个点 \(u\) (这时候这个点是重心,作为根节来用),可以求出 \(u\) 到其子树内所有的点的路径的信息,然后递归得到 \(u\) 子树内路径的信息,最后回溯到 \(u\) 时,合并这些信息和 \(u\) 父亲的信息,拼接在一起得到跨越分治到的根的信息.

2.模板题

\(\texttt{Link}\)

Code :

int head[N],ecnt = -1;
struct Edge {
    int nxt,to,w;
}e[N << 1];
inline void AddEdge(int st,int ed,int w) {
    e[++ecnt] = (Edge) {head[st],ed,w},head[st] = ecnt;
    e[++ecnt] = (Edge) {head[ed],st,w},head[ed] = ecnt;
}

int n,m;
int q[105];
bool res[105],vis[N];
bool buc[10000005];

int dist[N],tmpd[N];
int siz[N],f[N];

int totsiz,rt,cnt;

#define Max(a,b) ((a) > (b) ? (a) : (b))
void calcsiz(int u,int _f) {
    siz[u] = 1,f[u] = 0;
    fe(i,u) {
        int v = e[i].to;
        if(v == _f || vis[v]) continue;
        calcsiz(v,u);
        f[u] = Max(f[u],siz[v]);
        siz[u] += siz[v];
    }
    f[u] = Max(f[u],totsiz - siz[u]);
    if(f[u] < f[rt]) rt = u;
}

void calcdist(int u,int _f) {
    tmpd[++cnt] = dist[u];
    fe(i,u) {
        int v = e[i].to;
        if(v == _f || vis[v]) continue;
        dist[v] = dist[u] + e[i].w;
        calcdist(v,u);
    }
}

std::queue<int> tag;

void solve(int u,int _f) {
    buc[0] = 1;
    tag.push(0);
    vis[u] = 1;
    fe(i,u) {
        int v = e[i].to;
        if(v == _f || vis[v]) continue;
        dist[v] = e[i].w;
        calcdist(v,u);
        ff(j,1,cnt) ff(k,1,m) if(q[k] >= tmpd[j])
            res[k] |= buc[q[k] - tmpd[j]];
        ff(j,1,cnt) if(tmpd[j] < 10000000)
            tag.push(tmpd[j]),buc[tmpd[j]] = 1;
        cnt = 0;
    }
    while(!tag.empty())
        buc[tag.front()] = 0,tag.pop();
    fe(i,u) {
        int v = e[i].to;
        if(v == _f || vis[v]) continue;
        totsiz = siz[v];
        rt = 0;
        f[rt] = INF;
        calcsiz(v,u);
        calcsiz(rt,rt);
        solve(rt,u);
    }
}

int mian() {
	init_IO();
	mems(head,-1);
	n = read(),m = read();
	ff(i,1,n - 1) {
	    int st = read(),ed = read(),w = read();
	    AddEdge(st,ed,w);
	}
	ff(i,1,m) q[i] = read();
	rt = 0,f[rt] = INF;
	totsiz = n;
	calcsiz(1,1);
	calcsiz(rt,rt);
	solve(rt,rt);
	ff(i,1,m) if(res[i])
	    putC('A'),putC('Y'),putC('E'),enter;
	else
	    putC('N'),putC('A'),putC('Y'),enter;
	end_IO();
	return 0;
}

P4178 Tree

从查找有没有改为了前缀和,树状数组即可.

但是我的点分治好慢啊.

\(\texttt{Link}\)

Code :

int head[N],ecnt = -1;
struct Edge {
    int nxt,to,w;
}e[N << 1];
inline void AddEdge(int st,int ed,int w) {
    e[++ecnt] = (Edge) {head[st],ed,w},head[st] = ecnt;
    e[++ecnt] = (Edge) {head[ed],st,w},head[ed] = ecnt;
}

int n;
int q,ans;
bool vis[N];

int dist[N],tmpd[N];
int siz[N],f[N];

int totsiz,rt,cnt;

#define Max(a,b) ((a) > (b) ? (a) : (b))
void calcsiz(int u,int _f) {
    siz[u] = 1,f[u] = 0;
    fe(i,u) {
        int v = e[i].to;
        if(v == _f || vis[v]) continue;
        calcsiz(v,u);
        f[u] = Max(f[u],siz[v]);
        siz[u] += siz[v];
    }
    f[u] = Max(f[u],totsiz - siz[u]);
    if(f[u] < f[rt]) rt = u;
}

void calcdist(int u,int _f) {
    tmpd[++cnt] = dist[u];
    fe(i,u) {
        int v = e[i].to;
        if(v == _f || vis[v]) continue;
        dist[v] = dist[u] + e[i].w;
        calcdist(v,u);
    }
}

#define lb(x) (x & (-x))
int T[20005];
void M(int p,int val) {
    while(p <= 20001) {
        T[p] += val;
        p += lb(p);
    }
}

int Q(int p) {
    int ret = 0;
    while(p) {
        ret += T[p];
        p -= lb(p);
    }
    return ret;
}

std::queue<int> tag;

void solve(int u,int _f) {
    M(1,1);
    tag.push(1);
    vis[u] = 1;
    fe(i,u) {
        int v = e[i].to;
        if(v == _f || vis[v]) continue;
        dist[v] = e[i].w;
        calcdist(v,u);
        ff(j,1,cnt) if(q >= tmpd[j])
            ans += Q(q - tmpd[j] + 1);
        ff(j,1,cnt) if(tmpd[j] <= q) {
            M(tmpd[j] + 1,1);
            tag.push(tmpd[j] + 1);   
        }
        cnt = 0;
    }
    while(!tag.empty()) {
        M(tag.front(),-1);
        tag.pop();
    }
    fe(i,u) {
        int v = e[i].to;
        if(v == _f || vis[v]) continue;
        totsiz = siz[v];
        rt = 0;
        f[rt] = INF;
        calcsiz(v,u);
        calcsiz(rt,rt);
        solve(rt,u);
    }
}

边分治

和点分治相似,同样是先选择边再均分整棵树,但是可以发现,对于一个二叉树,这个过程进行的最平均,不然可能会被卡.

那么考虑新建一些不影响统计过程的结点,然后分配一下其余点使其成为二叉树.

点分树

带修好难啊.

以后再补吧.

总结

  • 重链剖分解决指定少量链问题

  • 点分治解决全体链性质问题

  • 树形 DP 非常泛用,最重要的是如何设计状态

  • dsu on tree 有时候比较难想

  • 长链剖分线性复杂度真的强不过需要先设计出 DP 方案

posted @ 2022-01-28 15:42  AstatineAi  阅读(36)  评论(0编辑  收藏  举报