知识点 - 点分治

知识点 - 点分治

解决问题类型:

树上路径问题的工具,举个例子:

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

实现

原理

img

如图,假设我们选出一个根 R o o t Root Root ,那么答案路径肯定是要么被一个子树所包含,要么就是跨过 R o o t Root Root,在黑子树中选择一部分路径,在红子树中选择一部分路径,然后从 R o o t Root Root 处拼起来形成一条答案路径

分类讨论不会写的,这辈子都不可能写分类讨论的

仔细想一下,发现情况1(被一个子树包含)中,答案路径上的一点变为根 R o o t Root Root,就成了情况2(在两棵子树中)

[外链图片转存失败(img-ljAdxBxY-1568865920016)(https://a-failure.github.io/img/study/dianfenzhi6.png)]

如图, R o o t Root Root 为根的子树中存在答案(蓝色实边路径),可以看成以 R o o t 2 Root2 Root2 为根的两棵子树存在答案,所以只用处理情况2就行了,可以用分治的方法,这应该是点分治的基本原理

先从找好一个根开始

选根(选重心)

首先根不能随便选,选根不同会影下面遍历的效率的,如图:

[外链图片转存失败(img-DJGd2xth-1568865920018)(https://a-failure.github.io/img/study/dianfenzhi3.png)]

显然选 y y y 为根比选 x x x 为根不优,选 x x x 最多递归2层,选 y y y 最多递归4层

显然可以发现找树的重心(重心所有的子树的大小都不超过整个树大小的一半)是最优的

我们可以根据每个点子树大小确定根,当根的最大的子树最小时肯定是重心

一个简单的树形 d p dp dp 就能搞定

void GET_ROOT(int x,int fa)//x为当前点,fa为父亲节点
{
    f[x]=0,siz[x]=1;//f表示这个点最大子树的大小,siz是这个点子树大小的和
    for(int i=h[x];i;i=c[i].x)//枚举儿子
      {
        int y=c[i].y;
        if(use[y]||y==fa) continue;//use表示之前遍历过了,这里没啥用
        GET_ROOT(y,x);//往下遍历
        f[x]=max(f[x],siz[y]);//更新f
        siz[x]+=siz[y];
      }
    f[x]=max(f[x],Siz-siz[x]);//Siz表示在现在这棵子树中点的总数,开始时Siz=n,除了枚举的儿子所在的子树外,还有一棵子树是上面的那一堆,容斥原理
    if(f[x]<f[rt]) rt=x;//更新root
}

因为之后的分治过程还需要对子树单独找重心,所以代码中有 u s e use use ,但是开始对整棵树无影响

求距离

找到根了,现在我们可以 d f s dfs dfs 一遍重心的子树,求出重心到子树各个点的距离

然后可以枚举子树里的两个点,如果两个点到重心的距离和为 k k k (题目要找距离为 k k k 的点对),那么答案 + 1 +1 +1

这是第二种情况,第一种情况就让距离根为 k k k 的点跟重心配对就行了,因为重心到重心的距离为 0 0 0

如何统计答案呢?

统计答案

肯定不能直接枚举啊… n 2 n^2 n2的复杂度啊喂

考虑枚举一个点,另一个点可以通过二分来求解, s o r t sort sort一下让距离有序,这样要找距离为 k k k -枚举点的距离的点的个数,因为相同距离的点现在是连续的,所以可以二分出左右边界 l , r , a n s + = r − l + 1 l,r, ans+=r-l+1 l,rans+=rl+1

int look(int l,int x)//找左边界
{
    int ans=0,r=cnt;
    while(l<=r)
    {
        int mid(l+r>>1);
        if(d[mid]<x) l=mid+1;
        else ans=mid,r=mid-1;
    }
    return ans;
}
int look2(int l,int x)//找右边界
{
    int ans=0,r=cnt;
    while(l<=r)
    {
        int mid(l+r>>1);
        if(d[mid]<=x) ans=mid,l=mid+1;
        else r=mid-1;
    }
    return ans;
}
void GET_NUM(int x,int fa,int D)//求重心到子树每个点的距离
{
    for(int i=h[x];i;i=c[i].x)
      {
        int y=c[i].y;
        if(use[y]||y==fa) continue;
        d[++cnt]=D+c[i].dis;
        GET_NUM(y,x,d[cnt]);
      }
}
int GET_ANS(int x)//求答案
{
    d[cnt=1]=0;
    GET_NUM(x,0,0);
    sort(d+1,d+1+cnt);//排下序
    int l=1,ans=0;
    while(l<cnt&&d[l]+d[cnt]<k) ++l;
    while(l<cnt&&k-d[l]>=d[l])
    {
        int D1(look(l+1,k-d[l])),D2(look2(l+1,k-d[l]));//枚举一个点,因为和是k,所以可以直接求出另一点到重心的距离,二分一下就行了
        if(D2>=D1) ans+=D2-D1+1;
        ++l;
    }
    return ans;
}
void dfs(int x)//分治函数
{
    use[x]=1,ans+=GET_ANS(x);//统计这个点的答案
    for(int i=h[x];i;i=c[i].x)
      {
        int y=c[i].y;
        if(use[y]) continue;//防止找父亲
        Siz=siz[y],rt=0;//下一个重心肯定在当前点的子树里
        GET_ROOT(y,x),dfs(rt);//找下一个重心
      }
}

也可以通过移动两个指针来实现只要不是枚举两个点就行了

这样我们就快乐的A掉了这道题

[外链图片转存失败(img-WBpnyRw6-1568865920019)(https://a-failure.github.io/img/qaq/happy.jpg)]

了吗?

求一遍发现答案不对诶…似乎多了几种情况?如图

: [外链图片转存失败(img-0Vkwnq7F-1568865920019)(https://a-failure.github.io/img/study/dianfenzhi4.png)]

假设 k = 4 k=4 k=4 ,图中 A A A R o o t Root Root 的距离为 2 2 2 B B B R o o t Root Root 的距离为 2 2 2,合起来是 4 4 4,这时候答案 + 1 +1 +1,但是显然这两个点最短路径不是 4 4 4

这是因为ta们在同一子树中,到重心的路径有重叠部分

统计答案 二连击

处理方法:

1.可以求距离的时候把点染色,不同子树不同颜色,那么求答案的时候就得枚举每个符合答案的每个点看是否不在一个子树里

2.可以求当前点儿子的答案,统计儿子答案时各个点的距离加上儿子到根的距离,即把符合在一个子树条件的情况统计出来,最后这个点的答案减去儿子答案就行了

[外链图片转存失败(img-lI5YJNgM-1568865920020)(https://a-failure.github.io/img/study/dianfenzhi5.png)]

图中求 R o o t Root Root 儿子 s o n son son 的答案,因为加上儿子到重心的距离,所以 A A A 的距离还是 2 2 2 B B B 的距离还是 2 2 2,这样就把不符合条件的答案去掉了

int GET_ANS(int x,int D)//改一下这里就行了
{
    d[cnt=1]=D;
    GET_NUM(x,0,D);
    sort(d+1,d+1+cnt);
    int l=1,ans=0;
    while(l<cnt&&d[l]+d[cnt]<k) ++l;
    while(l<cnt&&k-d[l]>=d[l])
    {
        int D1(look(l+1,k-d[l])),D2(look2(l+1,k-d[l]));
        if(D2>=D1) ans+=D2-D1+1;
        ++l;
    }
    return ans;
}
void dfs(int x)//分治函数也改一下
{
    use[x]=1,ans+=GET_ANS(x,0);//当前点答案
    for(int i=h[x];i;i=c[i].x)
      {
        int y=c[i].y;
        if(use[y]) continue;//防止找父亲
        ans-=GET_ANS(y,1);//去掉满足在一个子树条件的不合法答案
        Siz=siz[y],rt=0;
        GET_ROOT(y,x),dfs(rt);//找下一个重心
      }
}
复杂度

每次处理找树的重心,保证递归层数不超过 l o g n logn logn d f s dfs dfs求距离复杂度是 O ( n ) O(n) O(n) ,这里处理答案是 l o g n logn logn ,所以这个题总复杂度是 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)的…?反正过几万的数据是绰绰有余的(逃

补充:经 Aufun 大爷嘲讽教导后,因为有的题可以用桶排序,所以复杂度可以降到 O ( l o g n ) O(logn) O(logn)

当然有的题桶开不下必须 s o r t sort sort 了QAQ我一直用sort竟然没被卡

复杂度:

操作时间复杂度为 l o g n logn logn

例题

题目描述

给定一棵有n个点的树

询问树上距离为k的点对是否存在。

代码

// luogu-judger-enable-o2
#include <iostream>
#include <cstdio>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;
#define rep(i,j,k) for(int i = (int)j;i <= (int)k;i ++)
#define debug(x) cerr<<#x<<":"<<x<<endl
#define pb push_back

typedef long long ll;
const int MAXN = (int)1e5+7;
const int INF  = (int)0x3f3f3f3f;

inline int read() { int c = 0, f = 1; char ch = getchar();
    while (ch < '0' || ch > '9') {if (ch == '-') f = -1;ch = getchar();}
    while (ch >= '0' && ch <= '9') {c = c * 10 + ch - '0';ch = getchar();}
    return c * f;
}

struct Node {
    int v,w;
    Node(int v = 0,int w = 0):v(v),w(w){}
};

vector<Node> G[MAXN];
vector<int> dis;
bool vis[MAXN];
int N,M,K,root,sz[MAXN],maxv[MAXN],mx,num[10000007];
ll ans;
bool flag = 0;

inline void dfs_sz(int u,int fa) {
    maxv[u] = 0;sz[u] = 1;
    rep(i,0,G[u].size()-1) {
        int v = G[u][i].v;
        if (v == fa || vis[v]) continue;
        dfs_sz(v,u);
        sz[u] += sz[v];
        maxv[u] = max(maxv[u],sz[v]);
    }
}

inline void dfs_root(int u,int fa,int rt) {
    maxv[u] = max(maxv[u],sz[rt]-sz[u]);

    if (mx > maxv[u]) {
        root = u;
        mx = maxv[u];
    }else return;

    rep(i,0,G[u].size()-1) {
        int v = G[u][i].v;
        if (v == fa|| vis[v]) continue;
        dfs_root(v,u,rt);
    }
}

inline void dfs_dis(int u,int fa,int weight) {
    //if (weight > K) return;
    dis.pb(weight);
    rep(i,0,G[u].size()-1) {
        int v = G[u][i].v;
        if (vis[v] || fa == v) continue;
        dfs_dis(v,u,weight+G[u][i].w);
    }
}

inline void cal_dis(int u,int weight) {
    dis.clear();
    dfs_dis(u,-1,weight);
    int l = 0,r = dis.size()-1;
    rep(i,0,r) {
        rep(j,i+1,r) {
            if (!weight) {
                num[dis[i] + dis[j]] ++;
            }
            else   {
                num[dis[i] + dis[j]] --;

            }
        }
    }
}

inline void DFS(int u) {
    mx = N;
    dfs_sz(u,-1);
    dfs_root(u,-1,u);
    int rt = root; //因为在后续的递归中root的值会变化,所以这里必须使用临时变量储存起来
    vis[rt] = 1;
    cal_dis(rt,0);

    rep(i,0,G[rt].size()-1) {
        int v = G[rt][i].v;
        if (vis[v]) continue;
        cal_dis(v,G[rt][i].w);
        DFS(v);
    }
}

void init(int n) {
    rep(i,0,n) G[i].clear();
    memset(vis,0,sizeof(bool)*(n+1));
    ans = 0;
}

int main()
{
    scanf("%d %d",&N,&M);
    init(N);
    rep(i,1,N-1) {
        int u,v,w;
        u = read(),v = read(),w = read();
        G[u].pb(Node(v,w));
        G[v].pb(Node(u,w));
    }
    DFS(1);

    rep (i,1,M) {
        scanf("%d",&K);
        if (num[K]) puts("AYE");
        else        puts("NAY");
    }
}
/*
5 4
1 2 1
1 3 2
1 4 5
4 5 10

2 1
1 2 1
*/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值