洛谷-题解 SP913【QTREE2 - Query on a tree II】

写在前面:由于太,希望各位大佬能来博客灌水,提出建议。
洛谷题目地址:https://www.luogu.org/problemnew/show/SP913
此题洛谷博客地址:https://www.luogu.org/blog/f9107l/solution-sp913

明显的LCA问题,用了树剖。思路是所有会一点树剖的人都能看懂的。

先不谈树剖的构造,先看问题如何解决,然后再用原来树剖思路构造并延伸。

  1. 首先对于第一问,明显的LCA,求边权和,由于不需要修改,所以用前缀和即可(线段树的话时间有点紧,代码也太长了)。这样我们就需要在构造时维护一个前缀和,维护链上的边权和。
  2. 对于第二问,还是LCA,但是有些棘手。可以分成两种情况,一种是从x到最近公共祖先,一种是y到最近公共祖先,再用数组记录结点编号,这就有不少细节问题了。

好了,问题都整理了一下,但貌似仍然不好写,所以要了解树剖的关键性质。

  1. 假设x和y的最近公共祖先为u,那么x到u(包括u)一共有
    (deep[x]-deep[u]+1)个点(deep为结点深度)。同理y到u也有(deep[x]-deep[u]+1)个点(也包括u),这样对于第二问的许多判断起了重大作用
  2. 我们每次在dfs2中先遍历此节点的重儿子,就是让每条链上的相邻结点的dfs序也是相邻的(依次递增)这样第一问的前缀和,第二问的结点编号就可以以结点dfs序记录。

所以接下来如何构造树剖就知道了,只是在原来基础上多了前缀和和编号数组(因为代码能力有限,又用了一个数组记录重儿子与此点相连边的序号)

代码如下,仅供参考因为太烂了

#include<cstdio>
#include<iostream>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<queue>
#include<map> 
using namespace std;
#define re register int
inline int read(){
    int x=0,ff=1;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-')ff=-1;c=getchar();}
    while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c^48);c=getchar();}
    return x*ff;
}
struct tree{
    int to,next,kk;
}a[20005];
int h[20005],tot;
int n,fa[20005],ss[20005],o[20005],d[20005],op[20005],s[20005];
int cnt,id[20005],as[20005],sl[20005],oo[20005];
void add(int x,int y,int z){
    a[++tot].next=h[x];a[tot].to=y;a[tot].kk=z;h[x]=tot;
}
void dfs1(int i,int la,int dp){
    fa[i]=la;d[i]=dp;s[i]=1;
    int x=-1;
    for(int j=h[i];j;j=a[j].next){
        if(a[j].to==la)continue;
        dfs1(a[j].to,i,dp+1);
        s[i]+=s[a[j].to];
        if(s[a[j].to]>x){
            x=s[a[j].to];o[i]=a[j].to;oo[i]=j;
        }
    }
}
void dfs2(int i,int tp){
    op[i]=tp;id[i]=++cnt;as[cnt]=i;
    if(!o[i])return;
    dfs2(o[i],tp);
    ss[id[i]]=ss[id[o[i]]]+a[oo[i]].kk;
    for(int j=h[i];j;j=a[j].next){
        if(a[j].to==fa[i])continue;
        sl[cnt+1]=a[j].kk;
        if(a[j].to==o[i])continue;
        dfs2(a[j].to,a[j].to);
    }
}
//上面的构造都基本是模板,略微的更改都已提到了
void lca(int x,int y){
    int ans=0;
    while(op[x]!=op[y]){
        if(d[op[x]]<d[op[y]])swap(x,y);
        ans+=sl[id[op[x]]]+ss[id[op[x]]]-ss[id[x]];x=fa[op[x]];//性质2+经典前缀和
    }
    if(d[x]>d[y])swap(x,y);
    ans+=ss[id[x]]-ss[id[y]];
    printf("%d\n",ans);
}
void find(int x,int y,int z){
    int yy=y,xx=x;
    while(op[xx]!=op[yy]){
        if(d[op[xx]]<d[op[yy]])swap(xx,yy);
        xx=fa[op[xx]];
    }
    if(d[xx]>d[yy])swap(xx,yy);
    if(d[x]-d[xx]+1>=z){
        while(1){
            if(z<=d[x]-d[op[x]]+1)break;
            z-=d[x]-d[op[x]]+1;x=fa[op[x]];
            //上面两个语句都根据性质1
        }
        printf("%d\n",as[id[x]-z+1]);//性质2
    }
    else {
        z-=d[x]-d[xx];//性质1
        int u=d[y]-d[xx]+1;//性质1
        while(1){
            if(op[y]==op[xx])break;
            if(z>u-(d[y]-d[op[y]]+1))break;
            u-=(d[y]-d[op[y]]+1);
            //上面两句也是性质1
            y=fa[op[y]];
        }
        if(op[y]==op[xx]){
            printf("%d\n",as[z-(u-(d[y]-d[xx]+1))+id[xx]-1]);//性质2
            return;
        }
        printf("%d\n",as[z-(u-(d[y]-d[op[y]]+1))+id[op[y]]-1]);//性质2
    }
}
//find过程中有一些小细节,自己画图就知道了。
signed main(){
    int t=read();
    while(t--){
    n=read();int x,y,z;
    memset(h,0,sizeof(h));tot=0;
    memset(ss,0,sizeof(ss));
    memset(s,0,sizeof(s)); 
    memset(sl,0,sizeof(sl));
    cnt=0;
    memset(fa,0,sizeof(fa));
    memset(o,0,sizeof(o));
    for(int i=1;i<n;i++){
        x=read();y=read();z=read();
        add(x,y,z);add(y,x,z);
    }
    cnt=0;dfs1(1,0,1);dfs2(1,1);
    char ch[105];
    scanf("%s",ch);
    while(ch[1]!='O'){
        if(ch[1]=='I'){
            x=read();y=read();
            lca(x,y);
        }
        else {
            x=read();y=read();z=read();
            find(x,y,z);
        }
        scanf("%s",ch);
    }}
    return 0;
}

此题最后的坑点是多组数据,别忘了清空记录重儿子的数组

转载于:https://www.cnblogs.com/ffrxy01bt/p/11232441.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值