2023.4.21【图论】点分治

2023.4.21【图论】点分治

(点分治其实是广泛地统计树上全局路径问题的算法,此处采用luogu模板题的题面)

题目描述

给定一棵有 n 个点的树,询问树上距离为 k 的点对是否存在。

  • 对于 100% 的数据,保证 1n1041m1001k1071u,vn1w104

算法描述

点分治,又名淀粉质,是计算树上路径问题的算法,与树剖不同的是,它计算的是对于整棵树的所有路径的情况,树剖难以完成。此题有m100个询问,我们考虑每一条路径,发现当我们枚举一个点x时,一条路径要么是经过x的,要么是与x不相交的。对于那些与x不相交的路径,我们可以向下分治来讨论。

我们发现,对于过x,但是同时经过x的父亲的路径,在x的父亲一层会考虑,所以我们只讨论x的子树以内的路径,遍历每一棵x的子树(直接连接的儿子),计算子树中每一个数到x的距离dis,将它们推进一个桶judge中,为了方便处理,我们将这个子树的点的dis在遍历时推进一个队列,我们扫描队列中的每个元素,如果judge[kidis]是存在的,那么说明存在一条长为k的路径,从x先前的子树向上到x,再向下转到当前子树中的点,这个询问就有答案,鉴于询问数m100,直接枚举每个ki,然后用O(1)判断每个ki是否可以成立即可(这个地方开桶有一种空间换时间的思想)。对于每次清空judge数组,不能直接memset,要记录你更改了哪些值,再改回去就好了。

这是计算(calc)函数:

inline void calc(int x)
{
    top = 0;
    for(int i = head[x];i;i = e[i].next)
    {
        int to = e[i].v;
        if(vis[to]) continue;
        dis[to] = e[i].w;
        rem[0] = 0;//rem[0]是计数器,rem是队列
        getdis(to,x);
        for(int j = 1;j <= rem[0];j++)
            for(int k = 1;k <= m;k++)
                if(query[k] - rem[j] >= 0)
                    if(judge[query[k] - rem[j]] == 1)
                        rt[k] = 1;
        for(int j = 1;j <= rem[0];j++)
            if(rem[j] <= T)
                q[++top] = rem[j],judge[rem[j]] = 1;
    }
    for(int i = 1;i <= top;i++) judge[q[i]] = 0;
}

时间复杂度O(nm logn)

讲解视频:https://www.bilibili.com/video/BV1GJ411x7h7

上条链不就被卡死了qwq

这里需要打破传统的树形问题的遍历顺序,而是每次处理一棵子树时,以这棵子树的重心为根进行处理,这样就能保证把子树切开后,大小一定会小于原来的12,所以就能保证复杂度严格,但是遍历顺序改变了,所以遍历过的点要将一个vis标签打为1,在所有函数的遍历中,如果visson=1,就不访问son

找重心部分:(一个点为根子树大小的最大值最小)

inline void dfs(int x,int last)
{
    siz[x] = 1;
    int now = 0;
    for(int i = head[x];i;i = e[i].next)
    {
        int to = e[i].v;
        if(to == last || vis[to]) continue;
        dfs(to,x);
        siz[x] += siz[to];
        now = max(now,siz[to]);
    }
    now = max(now,sum - siz[x]);
    if(now < Minsiz) Minsiz = now,root = x;
}

注意到一个细节:函数里面的“整棵树大小”用了一个sum变量,是因为每一次在子树中递归时总大小不一样,因为这次是确定了以x为根,下一次进入到儿子son中,就可以直接用sizson作为sum。由于以son为根dfs时只会改变son子树的siz,所以对于x的其他子树没有影响。

Code

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5,T = 1e7 + 5;
struct Edge{
    int v,w,next;
}e[N * 5];
int head[N],n,m,vis[N],dis[N],query[N],judge[N * 100],rt[N],siz[N],rem[N],root = 0,Minsiz = 0x3f3f3f3f,tot = 0,sum,q[N],top = 0;
inline void add(int x,int y,int z)
{
    ++tot;
    e[tot].v = y;
    e[tot].w = z;
    e[tot].next = head[x];
    head[x] = tot;
}
inline void dfs(int x,int last)
{
    siz[x] = 1;
    int now = 0;
    for(int i = head[x];i;i = e[i].next)
    {
        int to = e[i].v;
        if(to == last || vis[to]) continue;
        dfs(to,x);
        siz[x] += siz[to];
        now = max(now,siz[to]);
    }
    now = max(now,sum - siz[x]);
    if(now < Minsiz) Minsiz = now,root = x;
}
inline void getdis(int x,int last)
{
    rem[++rem[0]] = dis[x];
    for(int i = head[x];i;i = e[i].next)
    {
        int to = e[i].v;
        if(to == last || vis[to]) continue;
        dis[to] = dis[x] + e[i].w;
        getdis(to,x);
    }
}
inline void calc(int x)
{
    top = 0;
    for(int i = head[x];i;i = e[i].next)
    {
        int to = e[i].v;
        if(vis[to]) continue;
        dis[to] = e[i].w;
        rem[0] = 0;
        getdis(to,x);
        for(int j = 1;j <= rem[0];j++)
            for(int k = 1;k <= m;k++)
                if(query[k] - rem[j] >= 0)
                    if(judge[query[k] - rem[j]] == 1)
                        rt[k] = 1;
        for(int j = 1;j <= rem[0];j++)
            if(rem[j] <= T)
                q[++top] = rem[j],judge[rem[j]] = 1;
    }
    for(int i = 1;i <= top;i++) judge[q[i]] = 0;
}
inline void solve(int x)
{
    judge[0] = 1;
    vis[x] = 1;
    calc(x);
    for(int i = head[x];i;i = e[i].next)
    {
        int to = e[i].v;
        if(vis[to]) continue;
        sum = siz[to];
        Minsiz = 0x3f3f3f3f;
        dfs(to,0);
        solve(root);
    }
}
int main()
{
    memset(siz,0,sizeof(siz));
    int x,y,z;
    cin>>n>>m;
    for(int i = 1;i <= n - 1;i++)
    {
        cin>>x>>y>>z;
        add(x,y,z);
        add(y,x,z);
    }
    for(int i = 1;i <= m;i++) cin>>query[i];
    sum = n;
    dfs(1,0);
    memset(judge,0,sizeof(judge));
    memset(rt,0,sizeof(rt));
    memset(vis,0,sizeof(vis));
    solve(root);
    for(int i = 1;i <= m;i++)
        if(rt[i])
            cout<<"AYE"<<endl;
        else
            cout<<"NAY"<<endl;
    return 0;
}

我们关注到一个细节,就是这道题目的询问是满足m200的,复杂度是O(nmlogn)的,那么如果我们将m这个数字提到105呢?如果我们再修改一下呢?

点分树闪亮登场

题目描述plus

有一颗节点个数为n的树,树边长都是1,点有点权,现有q个操作,格式如下:

0 x k查询与x树上距离不小于k的点权和。

1 x y将点x的点权修改为y

强制在线,1n,q105

算法描述plus

点分树,是将点分治的过程离线下来变成一棵树的过程,考虑一般的暴力:统计子树信息,修改后一层一层跳父亲,会有O(n2)的复杂度,就是因为树的深度可能达到n,所以点分树的O(logn)的深度能允许其进行这样的操作。有时我们统计一些特殊信息,例如树上路径,联通块,寻找关键点等,不用在意两个点的父子关系究竟是怎么样的,点分树的构造方法很简单:进行一次点分治,由当前root子树当中的子root,在点分树中是当前root的儿子。

有以下结论:点分树满足两点的LCA必在原树这两点的路径上。

显然地,两个点的路径长等于它们分别到LCA路径长的和,我们就可以对于每个点,用一个动态开点线段树开桶,维护子树中节点到这个点的距离为k的点权。(在一般情形中,这个数据结构要根据题目具体情况来添加),然后每次修改一个点,只需要修改它在点分树上的O(logn)个祖先和它自己的数据结构即可,每次查询一个点,只需要从自己开始向上跳,统计到祖先cur距离小于等于kdis(x,cur)的点权即可,因为经过curx的路径一定是到curcurx两部分组成的。

然后就WA了。

显然地,统计祖先节点的贡献时会把原来来自x方向子树中的节点算进去,但是x到这些节点路径并不经过cur,所以我们再开一个线段树,记录当前点子树中的点到其父亲的(在原树中的)距离桶,这样就可以每次在统计祖先节点时减去这个方向的结果,就可以得到真正的答案。刚学点分树的人可能和我一样懵,认为不需要重开一个,事实上这个地方的两个距离桶都是在原树上的距离,和点分树的结构没有关系,不能通过一个推出另一个,所以要开两个桶。

总时间复杂度O((n+q)log2n)

tips:对于大多数数据范围为105的题目,这个时间复杂度都有些勉强(当然可以优化自行百度),像LCA这种可以用树剖实现,倍增的常数有些大。而且点分树的实现很复杂,动辄200+行,所以一定要将各个部分想办法分别调好再拼起来。

Code

#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5,inf = 0x3f3f3f3f;
struct Edge{
	int v,next;
}e[N * 2];
int head[N],val[N],n,m,tot = 0,vis[N],siz[N],sum,Minsiz = inf,root,dfa[N],dep[N],fa[N],rt1[N],rt2[N],a[N * 50],lc[N * 50],rc[N * 50],dis[N],top[N],son[N];
inline void pushup(int pos)
{
	a[pos] = a[lc[pos]] + a[rc[pos]];
}
inline void modify(int l,int r,int x,int k,int pos)
{
	if(l == r)
	{
		a[pos] += k;
		return;
	}
	int mid = (l + r) >> 1;
	if(x <= mid)
	{
		if(!lc[pos]) lc[pos] = ++tot;
		modify(l,mid,x,k,lc[pos]);
	}
	else
	{
		if(!rc[pos]) rc[pos] = ++tot;
		modify(mid + 1,r,x,k,rc[pos]);
	}
	pushup(pos);
}
inline int query(int l,int r,int L,int R,int pos)
{
	if(pos == 0) return 0;
	if(L <= l && r <= R)
		return a[pos];
	int ret = 0,mid = (l + r) >> 1;
	if(L <= mid)
		ret += query(l,mid,L,R,lc[pos]);
	if(R > mid)
		ret += query(mid + 1,r,L,R,rc[pos]);
	return ret;
}
inline void add(int x,int y)
{
	++tot;
	e[tot].v = y;
	e[tot].next = head[x];
	head[x] = tot; 
}
inline void dfs0(int x,int last)
{
	fa[x] = last;
	dep[x] = dep[last] + 1;
	siz[x] = 1;
	son[x] = 0;
	for(register int i = head[x];i;i = e[i].next)
	{
		int to = e[i].v;
		if(to == last) continue;
		dfs0(to,x);
		siz[x] += siz[to];
		if(siz[to] > siz[son[x]]) son[x] = to;
	}
}
inline void dfs1(int x,int last)
{
	if(son[x])
	{
		top[son[x]] = top[x];
		dfs1(son[x],x);
	}
	for(int i = head[x];i;i = e[i].next)
	{
		int to = e[i].v;
		if(to == last || to == son[x]) continue;
		top[to] = to;
		dfs1(to,x);
	}
}
inline int getlca(int x,int y)
{
	while(top[x] != top[y])
	{
		if(dep[top[x]] > dep[top[y]]) x = fa[top[x]];
		else y = fa[top[y]];
	}
	return (dep[x] < dep[y]) ? x : y;
}
inline void dfs(int x,int last)
{
	siz[x] = 1;
	int now = 0;
	for(register int i = head[x];i;i = e[i].next)
	{
		int to = e[i].v;
		if(to == last || vis[to]) continue;
		dfs(to,x);
		siz[x] += siz[to];
		now = max(now,siz[to]);
	}
	now = max(now,sum - siz[x]);
	if(now < Minsiz) Minsiz = now,root = x;
}
inline int getd(int x,int y)
{
	return dep[x] + dep[y] - 2 * dep[getlca(x,y)];
}
inline void solve(int x)
{
	vis[x] = 1;
	rt1[x] = ++tot;
	rt2[x] = ++tot;
	for(register int i = head[x];i;i = e[i].next)
	{
		int to = e[i].v; 
		if(vis[to]) continue;
		Minsiz = inf;
		sum = siz[to];
		dfs(to,0);
		dfa[root] = x;
		solve(root);
	} 
}
inline int q(int x,int k)
{
	int ret = 0,pre = 0,cur = x;
	while(cur)
	{
		if(getd(cur,x) > k)
		{
			pre = cur;
			cur = dfa[cur];
			continue;
		}
		ret += query(0,n - 1,0,k - getd(cur,x),rt1[cur]);
		if(pre)
			ret -= query(0,n - 1,0,k - getd(cur,x),rt2[pre]);
		pre = cur;cur = dfa[cur];
	}
	return ret;
}
inline void md(int x,int k)
{
	int cur = x;
	while(cur)
	{
		modify(0,n - 1,getd(cur,x),k - val[x],rt1[cur]);
		if(dfa[cur])
			modify(0,n - 1,getd(dfa[cur],x),k - val[x],rt2[cur]);
		cur = dfa[cur];
	}
}
inline int read()
{
	int s = 0,w = 1;
	char k = getchar();
	while(k > '9' || k < '0')
	{
		if(k == '-') w = -w;
		k = getchar();
	}
	while(k <= '9' && k >= '0')
	{
		s = s * 10 + k - '0';
		k = getchar();
	}
	return s * w;
}
inline void write(int x)
{
	if(!x) return;
	write(x / 10);
	putchar((x % 10) + '0');
}
int main()
{
	memset(fa,0,sizeof(fa));
	n = read();m = read();
	for(register int i = 1;i <= n;i++) val[i] = read();
	int x,y,z;
	for(register int i = 1;i <= n - 1;i++)
	{
		x = read();y = read();
		add(x,y);
		add(y,x);
	}
	dep[0] = 0;
	dfs0(1,0);
	fa[1] = 1;
	top[1] = 1;
	dfs1(1,0);
	memset(siz,0,sizeof(siz));
	tot = 0;
	sum = n;
	dfs(1,0);
	solve(root);
	for(register int i = 1;i <= n;i++) md(i,val[i] << 1);
	int lastans = 0;
	for(register int i = 1;i <= m;i++)
	{
		x = read();y = read();z = read();
		y ^= lastans;z ^= lastans;
		if(x == 0)
		{
			lastans = q(y,z);
			if(lastans == 0)
				putchar('0');
			else
				write(lastans);
			putchar('\n');
		}
		else
			md(y,z),val[y] = z;
	}
	return 0;
}
posted @   The_Last_Candy  阅读(40)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示