点分治与动态点分治

前言

点分治一般是用于解决树上路径问题。

前置知识

树的重心:把重心这个点割掉后,使所形成的最大的联通块大小最小的点。

可以证明重心子树的大小最大不会超过 \(n\over 2\)

重心可以通过 \(dfs\) 一遍求出。

//maxsiz[x] 表示割掉点x后所形成的的最大的联通块的大小
void dfs(int x,int fa)
{
	siz[x] = 1; 
	for(int i = head[x]; i; i = e[i].net)
	{
		int to = e[i].to;
		if(to == fa) continue;
		dfs(to,x);
		siz[x] += siz[to];
		max_siz[x] = max(max_siz[x],siz[to]);
 	} 
 	max_siz[x] = max(max_siz[x],n-siz[x]);
 	if(max_siz[x] < max_siz[root]) root = x;
} 

点分治

先来看到例题吧

给定一棵树和一个整数 \(k\),求树上边数等于 \(k\) 的路径有多少条

我们的暴力做法就是枚举每条两个点,然后在判断他们两个的距离是否为 \(k\)

大概是 O(\(n^3\)) 的复杂度,优化一下的话可以跑到 O(\(n^2 log n\))

\(n\) 的范围一大,还是会 \(TLE\), 考虑优化一下复杂度 。

我们现在主要想解决的是在以 \(s\) 为根的子树中符合条件的路径个数。

不难发现路径一共可以分为三类:

情况一

\(s\) 出发到他的子树中一个点 \(t\) 所形成的路径, 如图中的黄色路径:。

这个很好统计,可以直接由 \(s\) \(dfs\) 一遍即可, 复杂度 \(O(m)\) 。( \(m\) 为路径个数)

情况二:

不在 $s $ 的同一个子树中的两个点 \(u,v\) 所形成的路径,如图。

显然 \(u-v\) 的路径是肯定要经过点 \(s\) 的,那么 \(u-v\) 的路径也就可以拆成 \(u-s\)\(s-v\) 的两条路径。

这两条路径我们在解决情况 \(1\) 的时候已经求出来了,剩下的就是考虑怎么把他们拼接起来。

\(d_u\) 表示 \(u\)\(s\) 的距离,我们现在要解决的是 \(d_u + d_v = k\)\(u\)\(v\) 不在 \(s\) 的同一颗子树中的情况。

我们可以把 \(d_u\) 按从小到大排一下序,根据单调性,利用双指针就可以很好的解决这个问题。

但我们还要注意的是,需要排除两条在同一颗子树中的路径的干扰。

具体的做法就是对每一条路径记录他位于 \(s\) 的那一颗子树中。

这个可以和 \(d_u\) 一起在情况 \(1\)\(dfs\) 中一并求出来。

假设路径条数为 \(m\) ,那么排序的复杂度为 \(O(mlogm)\), 双指针的复杂度为 \(O(m)\), 所以总的复杂度为 \(O(mlogm)\)

那么第二种情况我们就解决出来了。

情况三:

位于 \(s\) 的同一颗子树中的 \(u,v\) 两点形成的路径,如图:

这个显然是当前问题的子问题,递归继续求解即可。

三种情况我们已经考虑完了,现在分析一下时间复杂度的问题。

假设递归的深度为 \(k\), 每做一次的复杂度最坏为 \(O(nlogn)\) (主要来自于情况2的排序)。

那么总的时间复杂度就是 \(O(knlogn)\), 所以为了保证复杂度,我们要使递归深度尽可能的小。

根据我们上面提到的关于重心的知识,每次分治的时候选取子树的重心,这样可以保证递归深度为 \(logn\)

所以总的复杂度最坏为 \(O(nlog^2n)\) (实际上是很难达到这个上界的)

在做点分治的时候,我们需要把两个子树的信息合并,我们暴力合并的复杂度过高,有的情况下会使用启发式合并的方法来合并。

一般点分治的题套路都是一样的,都是分治递归求解,唯一的不同点就在于怎么合并子树的信息。

解决了合并子树信息这个问题,剩下的就是套模板的事情了。

例题代码:

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 1e4+10;
int n,m,tot,cnt,sum_siz,root,u,v,w;
int head[N],siz[N],max_siz[N],dis[N],k[N];
bool vis[N],ans[N];
struct bian
{
    int to,net,w;
}e[N<<1];
struct node
{
    int d,wh;
    node() {};
    node(int x, int y){d = x; wh = y;}
}a[N];
inline int read()
{
    int s = 0,w = 1; char ch = getchar();
    while(ch < '0' || ch > '9'){if(ch == '-') w = -1; ch = getchar();}
    while(ch >= '0' && ch <= '9'){s =s * 10+ch - '0'; ch = getchar();}
    return s * w;
}
void add(int x,int y,int w)
{
    e[++tot].w = w;
    e[tot].to = y;
    e[tot].net = head[x];
    head[x] = tot;
}
bool comp(node a,node b)
{
    return a.d < b.d;
}
void get_root(int x,int fa)//找重心
{
    max_siz[x] = 0; siz[x] = 1; 
    for(int i = head[x]; i; i = e[i].net)
    {
        int to = e[i].to;
        if(to == fa || vis[to]) continue;
        get_root(to,x);
        siz[x] += siz[to];
        max_siz[x] = max(max_siz[x],siz[to]);
    }
    max_siz[x] = max(max_siz[x],sum_siz-siz[x]);
    if(max_siz[x] < max_siz[root]) root = x;
}
void get_dis(int x,int fa,int who)//找到重心的距离, who 记录他是谁的子树
{
    a[++cnt] = node(dis[x],who);
    for(int i = head[x]; i; i = e[i].net)
    {
        int to = e[i].to;
        if(to == fa || vis[to]) continue;
        dis[to] = dis[x] + e[i].w;
        get_dis(to,x,who);
    }
}
int search(int d)
{
    int L = 1, R = cnt, res = 0;
    while(L <= R)
    {
        int mid = (L+R)>>1;
        if(a[mid].d >= d)
        {
            res = mid;
            R = mid - 1;
        }
        else L = mid + 1;
    }
    return res;
}
void calc(int x,int d)
{
    dis[x] = 0; cnt = 0; 
    for(int i = head[x]; i; i = e[i].net)
    {
        int to = e[i].to;
        if(vis[to]) continue;
        dis[to] = dis[x] + e[i].w; 
        get_dis(to,x,to);
    }
    a[++cnt] = node(0,0); 
    sort(a + 1, a + cnt + 1, comp);//排一下序
    for(int i = 1; i <= m; i++)
    {
        if(ans[i]) continue;
        int l = 1, r = cnt;
        while(l <= cnt && a[l].d + a[r].d < k[i]) l++;
        while(l <= cnt && !ans[i])
        {
            if(k[i] - a[l].d < a[l].d) break;
            int id = search(k[i] - a[l].d);
            while(a[id].d + a[l].d == k[i] && a[id].wh == a[l].wh) id++;
            if(a[l].d + a[id].d == k[i]) ans[i] = 1;
            l++;
        }
    }
}

void slove(int x)
{
    calc(x,0); vis[x] = 1;//先算 x 点的贡献
    for(int i = head[x]; i; i = e[i].net)
    {
        int to = e[i].to;
        if(vis[to]) continue;
        max_siz[0] = n; sum_siz = siz[to]; root = 0;
        get_root(to,0); slove(root);//递归处理子树
    }
}
int main()
{
    n = read(); m = read();
    for(int i = 1; i <= n-1; i++)
    {
        u = read(); v = read(); w = read();
        add(u,v,w); add(v,u,w);
    }
    for(int i = 1; i <= m; i++) k[i] = read();
    max_siz[0] = sum_siz = n; root = 0;
    get_root(1,0); slove(root);//找到一开始整颗树的重心
    for(int i = 1; i <= m; i++)
    {
        if(ans[i]) puts("AYE");
        else puts("NAY");
    }
    return 0;
}

例2: P4178 Tree

题目描述

给定一棵 \(n\) 个节点的树,每条边有边权,求出树上两点距离小于等于 \(k\) 的点对数量。

和模板题差不多,只要在合并子树的时候稍微改一下即可。

code:

#pragma GCC optimize(2) 

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 4e4+10;
int n,m,tot,cnt,root,sum_siz,u,v,w,k,ans;
int head[N],siz[N],max_siz[N],dis[N],a[100010];
bool vis[N];
struct node
{
    int to,net,w;
}e[N<<1];
inline int read()
{
    int s = 0,w = 1; char ch = getchar();
    while(ch < '0' || ch > '9'){if(ch == '-') w = -1; ch = getchar();}
    while(ch >= '0' && ch <= '9'){s =s * 10+ch - '0'; ch = getchar();}
    return s * w;
}
void add(int x,int y,int w)
{
    e[++tot].w = w;
    e[tot].to = y;
    e[tot].net = head[x];
    head[x] = tot;
}
void get_root(int x,int fa)
{
    max_siz[x] = 0; siz[x] = 1; 
    for(int i = head[x]; i; i = e[i].net)
    {
        int to = e[i].to;
        if(to == fa || vis[to]) continue;
        get_root(to,x);
        siz[x] += siz[to];
        max_siz[x] = max(max_siz[x],siz[to]);
    }
    max_siz[x] = max(max_siz[x],sum_siz-siz[x]);
    if(max_siz[x] < max_siz[root]) root = x;
}
void get_dis(int x,int fa)
{
	a[++cnt] = dis[x];
    for(int i = head[x]; i; i = e[i].net)
    {
        int to = e[i].to;
        if(to == fa || vis[to]) continue;
        dis[to] = dis[x] + e[i].w;
        get_dis(to,x);
    }
}
int calc(int x,int d)
{
	int res = 0;
    dis[x] = d; cnt = 0;
	a[++cnt] = dis[x]; 
    for(int i = head[x]; i; i = e[i].net)
    {
        int to = e[i].to;
        if(vis[to]) continue;
        dis[to] = dis[x] + e[i].w;
        get_dis(to,x);
    }
    sort(a+1,a+cnt+1);
    int L = 1, R = cnt;
    while(L <= R)
   {
        if(a[L] + a[R] <= k)
	{
	    res += R-L;
            L++;
	}
	else R--;
    }
    return res;
}
void slove(int x)
{
    ans += calc(x,0);
    vis[x] = 1;
    for(int i = head[x]; i; i = e[i].net)
    {
        int to = e[i].to;
        if(vis[to]) continue;
        ans -= calc(to,e[i].w);
        max_siz[0] = n; sum_siz = siz[to]; root = 0;
        get_root(to,0); slove(root);
   }
}
int main()
{
    n = read(); 
    for(int i = 1; i <= n-1; i++)
    {
        u = read(); v = read(); w = read();
        add(u,v,w); add(v,u,w);
    }
    k = read();
    max_siz[0] = sum_siz = n; root = 0;
    get_root(1,0); slove(root);
    printf("%d\n",ans);
    return 0;
}

动态点分治

动态点分治又叫点分树(个人觉得点分树更形象一些),主要解决的树上的一下带修改问题。

动态点分治还是基于点分治的那套理论,每次选重心分治。

但考虑到修改操作,我们不可能每次修改都做一遍。

我们可以建点分树来解决这个问题,具体来说就是:

设当前的分治中心为 \(x\), 由他子树中的重心 \(y\)\(x\) 连边,不难发现这样会构成一棵树,这棵树也被叫做点分树。

不难发现当我们要修改 \(x\) 这个节点的信息的时候,发现他会影响到的是 \(x\) 到根节点路径上的点的信息。

查询的话同样会用到 \(x\) 到根节点路径上点的信息。

由于我们树的高度不超过 \(logn\), 所以直接暴力修改查询即可。

我们就可以拿数据结构来维护每个点的信息,巴拉巴拉。

代码,咕咕咕

posted @ 2021-02-21 19:28  genshy  阅读(206)  评论(0编辑  收藏  举报