• 点分治与动态点分治


    前言

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

    前置知识

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

    可以证明重心子树的大小最大不会超过 (nover 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
    ",ans);
        return 0;
    }
    

    动态点分治

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

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

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

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

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

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

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

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

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

    代码,咕咕咕

  • 相关阅读:
    Spring IOC(控制反转)思想笔记
    实战SpringBoot Admin
    包及权限配置&java存储机理绘制
    极验验证(滑动验证)的使用
    Java基础知识之this关键字知识讲解
    bean生命周期
    笔记-13-多线程 Thread方法 线程安全 生产者和消费者 死锁和阻塞 练习
    JAVA 进行图片中文字识别(准确度高)!!!
    Java 面试题关于包装类
    HashMap底层实现原理及面试常见问题
  • 原文地址:https://www.cnblogs.com/genshy/p/13599788.html
Copyright © 2020-2023  润新知