P3806 【模板】点分治1题解

题干

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

分析

Part1:点分治的意义

 分治都懂吧,将问题划分为若干个子问题,最后合并答案

可以类比归并排序,冒泡排序的复杂度为O(n^2),而归并则是稳定的O(n log2 n)

点分治同理,我们将每个树划分为若干个子树,然后分开来处理,这样可以优化时间复杂度

Part2:如何点分治

P3806 【模板】点分治1题解

观察若有这样一棵树,若我们把1当做根节点,则点分治就毫无意义

因为这时候我们的递归层数为n层

时间复杂度仍然是O(n^2)

但如果看做这样一棵树呢?

P3806 【模板】点分治1题解

 显然,若按照根节点分割开后,剩下的子树为12,45每一颗树的大小缩小为原来的一半

所以递归的层数直接降为 log2 n 层

可以得到现在的复杂度仅有O(n log2 n)

那么我们怎么找到要切割的这个点呢?

显然是树的重心,这样分割完后剩下的子树会尽量地平衡(size的最大会最小),从而达到最优的时间复杂度

Part 3:如何操作

对于一棵树,该树上的所有路径必能分为:

1.经过根节点的路径

2.没经过根节点的路径

情况1:

先考虑第一种情况:我们对每一个根(也就是重心),我们求取他所有的dis[k]表示k到根的距离

那么任意两点的距离(经过根)distance(x,y)=dis[x]+dis[y]

对于一个询问k,若存在dis[x]和dis[y]=k-dis[x]则说明这个条件可以满足

——————————————————————————————————————————————

但仍有一个例外:

P3806 【模板】点分治1题解

假定我们以5为根节点(只画出了左子树嗷),设每一条边长为1

若k=4,那么4和1到5的距离加起来正好为4

但显然1到4这条链上不会有5,所以我们还要统计该点是从那个(根节点的亲儿子)来的

若两个点从同一个儿子过来的,那么就忽略这种情况

——————————————————————————————————————————————---

回到如何检验该询问能否成立

我们将所有的dis从小到大排一个序,设定两个坐标1和dis的长度分别为l,r

若dis[l]+dis[r]>k,就将r减一,这样总和就会变小

反之,将l加一,这样总和就会变大

情况2:

如果该条链不经过根节点,那么必然在他的子树里,我们就要进行分治

先将根节点断开,对于每一棵子树,我们先求得他的根节点也就是重心

之后我们仍然考虑两种情况:

1.经过根节点的路径

2.没经过根节点的路径

懂了吗,情况一就和上面一样,情况二则继续分治

从而有了这样一个divide函数:

void divide(int x) {
    vis[x]=1;
    calc(x);
    for(int i=head[x]; i; i=nxt[i]) {
        int y=to[i];
        if(vis[y]) continue;
        root=0;
        getrt(y,0,size[y]);
        divide(root);
    }
}

 

vis数组表示这个点已经被隔开来,calc函数用于解决在这个根节点上的情况一

之后对于他的每一个孩子,我们都要找到他的重心,然后继续搜索

下面贴一下calc函数,getrt函数,以及其附属函数

calc:

void calc(int x) {
    tot=1;
    a[tot]=x;
    dis[x]=0;
    father[x]=x;
    for(int i=head[x]; i; i=nxt[i]) {
        int y=to[i];
        if(vis[y]) continue;
        get(y,x,val[i],y);
    }
    sort(a+1,a+tot+1,cmp);
    for(int i=1; i<=m; i++) {
        int l=1,r=tot;
        if(ans[i]) continue;
        while(l<r) {
            if(dis[a[l]]+dis[a[r]]>query[i]) r--;
            else 
            if(dis[a[l]]+dis[a[r]]<query[i]) l++;
            else 
            if(father[a[l]]==father[a[r]])
            {
                if(dis[a[r]]==dis[a[r-1]])r--;
                else l++;
            }
            else {
                ans[i]=1;
                break;
            }
        }
    }
}

 dis[i]表示所有点到i的距离

father[i]表示i对应的根的亲儿子

 get函数用于得到所有的dis和father

之后就按照我们上面的思路,排序,查找,如果第i个询问可行,则ans[i]=1

然后是get函数以及cmp函数:

bool cmp(int x,int y) {return dis[x]<dis[y];}
void get(int x,int fa,int now,int rt) {
    tot++;
    a[tot]=x;
    dis[x]=now;
    father[x]=rt;
    for(int i=head[x]; i; i=nxt[i]) {
        int y=to[i];
        if(y==fa||vis[y]) continue;
        get(y,x,now+val[i],rt);
    }
}

 很明显,不用过多的分析。

然后是getrt函数

void getrt(int x,int fa,int total) {
    size[x]=1;
    maxsum[x]=0;
    for(int i=head[x]; i; i=nxt[i]) {
        int y=to[i];
        if(y==fa||vis[y]) continue;
        getrt(y,x,total);
        size[x]+=size[y];
        maxsum[x]=max(size[y],maxsum[x]);
    }
    maxsum[x]=max(maxsum[x],total-size[x]);
    if(!root||maxsum[x]<maxsum[root]) root=x;
}

依旧是很常规,maxsum[i]记录i的最大子树的大小,size记录他子节点以及他本身的个数

然后就完美的解决辽~

代码:

P3806 【模板】点分治1题解
void add(int a,int b,int c) {
    cnt++;
    nxt[cnt]=head[a];
    to[cnt]=b;
    val[cnt]=c;
    head[a]=cnt;
}
void getrt(int x,int fa,int total) {
    size[x]=1;
    maxsum[x]=0;
    for(int i=head[x]; i; i=nxt[i]) {
        int y=to[i];
        if(y==fa||vis[y]) continue;
        getrt(y,x,total);
        size[x]+=size[y];
        maxsum[x]=max(size[y],maxsum[x]);
    }
    maxsum[x]=max(maxsum[x],total-size[x]);
    if(!root||maxsum[x]<maxsum[root]) root=x;
}
bool cmp(int x,int y) {
    return dis[x]<dis[y];
}
void get(int x,int fa,int now,int rt) {
    tot++;
    a[tot]=x;
    dis[x]=now;
    father[x]=rt;
    for(int i=head[x]; i; i=nxt[i]) {
        int y=to[i];
        if(y==fa||vis[y]) continue;
        get(y,x,now+val[i],rt);
    }
}
void calc(int x) {
    tot=1;
    a[tot]=x;
    dis[x]=0;
    father[x]=x;
    for(int i=head[x]; i; i=nxt[i]) {
        int y=to[i];
        if(vis[y]) continue;
        get(y,x,val[i],y);
    }
    sort(a+1,a+tot+1,cmp);
    for(int i=1; i<=m; i++) {
        int l=1,r=tot;
        if(ans[i]) continue;
        while(l<r) {
            if(dis[a[l]]+dis[a[r]]>query[i]) r--;
            else 
            if(dis[a[l]]+dis[a[r]]<query[i]) l++;
            else 
            if(father[a[l]]==father[a[r]])
            {
                if(dis[a[r]]==dis[a[r-1]])r--;
                else l++;
            }
            else {
                ans[i]=1;
                break;
            }
        }
    }
}
void divide(int x) {
    vis[x]=1;
    calc(x);
    for(int i=head[x]; i; i=nxt[i]) {
        int y=to[i];
        if(vis[y]) continue;
        root=0;
        getrt(y,0,size[y]);
        divide(root);
    }
}
int main() {
    ios::sync_with_stdio(false);
    cin>>n>>m;
    for(int i=1; i<n; i++) {
        int x,y,w;
        cin>>x>>y>>w;
        add(x,y,w);
        add(y,x,w);
    }
    for(int i=1; i<=m; i++) {
        cin>>query[i];
        if(!query[i]) ans[i]=1;
    }
    maxsum[0]=n;
    getrt(1,0,n);
    divide(root);
    for(int i=1; i<=m; i++)
        if(ans[i]) cout<<"AYE"<<endl;
        else cout<<"NAY" <<endl;
    return 0;
}
View Code

query存储读入的k

ans用于记录该询问可不可行

在进入搜索前先找一遍重心

AC~

上一篇:leetcode之二叉树中的最大路径和


下一篇:最大子列问题(在线算法)