• [NOI2015] 软件包管理器【树链剖分+线段树区间覆盖】


    Online JudgeLuogu-P2146

    Label:树链剖分,线段树区间覆盖

    题目大意

    (n)个软件包(编号0~n-1),他们之间的依赖关系用一棵含(n-1)条边的树来描述。一共两种操作:

    • install x:表示安装软件包x
    • uninstall x:表示卸载软件包x

    安装(x)时,必须得先安装x到根节点路径上所有的软件包;而卸载(x)时,必须得先卸载x子树中所有已经安装的软件包。对于每个操作,输出该操作前后,安装状态产生变化的节点的数量

    对于100%的数据,(n,q<=100000)

    输入

    输入文件的第1行包含1个整数n,表示软件包的总数。软件包从0开始编号。

    随后一行包含n−1个整数,相邻整数之间用单个空格隔开,分别表示1,2,3,⋯,n−2,n−1号软件包依赖的软件包的编号。

    接下来一行包含1个整数q,表示询问的总数。之后q行,每行1个询问。询问分为两种:

    install x:表示安装软件包x

    uninstall x:表示卸载软件包x

    你需要维护每个软件包的安装状态,一开始所有的软件包都处于未安装状态。

    对于每个操作,你需要输出这步操作会改变多少个软件包的安装状态,随后应用这个操作(即改变你维护的安装状态)。

    输出

    输出文件包括q行。

    输出文件的第i行输出1个整数,为第i步操作中改变安装状态的软件包数。

    样例

    Input#1

    7
    0 0 0 1 1 5
    5
    install 5
    install 6
    uninstall 1
    install 4
    uninstall 0
    

    Output#1

    3
    1
    3
    2
    3
    

    题解

    将这两种操作对应到树上。

    1.安装时:

    将x到根节点的路径上所有点的安装状态赋值为已安装:1

    2.卸载时:

    将x子树内的点的安装状态赋值为未安装:0

    发现两个操作都需要对一段连续区间进行赋值,注意不是修改,不过都可以利用线段树完成,至于线段树上区间的赋值和修改的具体差别在文末具体bb阐述。

    第一个操作可以利用树链剖分跳重链,在O((log^2N))的时间内完成跳跃以及修改一条链上的点的状态。

    第二个操作,直接根据dfs序,对子树对应的区间进行区间赋值即可,单次操作的复杂度是(O(logN))

    综上整个算法的时间复杂度为(O(q cdot log^2N))

    完整代码如下:

    #include<bits/stdc++.h>
    using namespace std;
    const int N=1e5+10;
    inline int read(){
        int x=0;char c=getchar();
        while(c<'0'||c>'9')c=getchar();
        while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
        return x;
    }
    struct edge{
    	int to,nxt;
    }e[N];
    int head[N],cnt;
    inline void link(int u,int v){e[++cnt]=(edge){v,head[u]};head[u]=cnt;}
    
    
    struct node{
    	int l,r,w,lazy;
    }b[N<<2];
    void build(int o,int l,int r){
    	b[o].l=l,b[o].r=r,b[o].lazy=-1;
    	if(l==r)return;
    	int mid=l+r>>1;
    	build(o<<1,l,mid),build(o<<1|1,mid+1,r);
    }
    void down(int o){
    	int g=b[o].lazy;
    	if(g==-1)return;
    	b[o<<1].w=(b[o<<1].r-b[o<<1].l+1)*g;
    	b[o<<1|1].w=(b[o<<1|1].r-b[o<<1|1].l+1)*g;
    	b[o<<1].lazy=b[o<<1|1].lazy=g;
    	b[o].lazy=-1;
    }
    void update(int o,int l,int r,int d){
    	if(b[o].l==l&&b[o].r==r){
    		b[o].w=(b[o].r-b[o].l+1)*d;
    		b[o].lazy=d;
    		return;
    	}
    	down(o);
    	int mid=b[o].l+b[o].r>>1;
    	if(r<=mid)update(o<<1,l,r,d);
    	else if(l>=mid+1)update(o<<1|1,l,r,d);
    	else{
    		update(o<<1,l,mid,d);update(o<<1|1,mid+1,r,d);
    	}
    	b[o].w=b[o<<1].w+b[o<<1|1].w;
    }
    
    
    int n,q;
    int sz[N],dep[N],fa[N],top[N],son[N];
    int li[N],ri[N],tot;
    void predfs(int x,int f){
    	sz[x]=1;fa[x]=f;dep[x]=dep[f]+1;
    	for(int i=head[x];i;i=e[i].nxt){
    		int y=e[i].to;
    		predfs(y,x);
    		sz[x]+=sz[y];
    		if(!son[x]||sz[y]>sz[son[x]])son[x]=y;
    	}
    }
    void redfs(int x,int tp){
    	top[x]=tp;li[x]=++tot;
    	if(son[x])redfs(son[x],tp);
    	for(int i=head[x];i;i=e[i].nxt){
    		int y=e[i].to;
    		if(y!=son[x])redfs(y,y);
    	}
    	ri[x]=tot;
    }
    void boom(int x,int y,int d){
    	while(top[x]!=top[y]){		
    		if(dep[top[x]]<dep[top[y]])swap(x,y);
    		update(1,li[top[x]],li[x],d);
    		x=fa[top[x]];
    	}
    	if(dep[x]>dep[y])swap(x,y);
    	update(1,li[x],li[y],d);
    }
    int main(){
    	n=read();
    	for(int i=2,u;i<=n;i++){
    		u=read();u++;
    		link(u,i);
    	}
    	predfs(1,0);redfs(1,1);
    	build(1,1,n);
    	q=read();
    	while(q--){
    		char op[13];scanf("%s",op);
    		int x=read(),base=b[1].w;
    		x++;	
    		if(op[0]=='i'){			
    			boom(1,x,1);
    			printf("%d
    ",b[1].w-base);
    		}
    		else{	
    			update(1,li[x],ri[x],0);
    			printf("%d
    ",base-b[1].w);	
    		}
    	}
    }
    

    End

    借这道题整理一下几个常见问题及模板:

    一、树链剖分

    part1

    树剖五件套:(sz[],son[],fa[],dep[],top[])(分别对应子树大小,重儿子编号,父亲节点编号,深度,所属重链的顶端节点编号)

    注意每个点必落在一条重链上,当只有一个点时,链退化成点,那个唯一的点,就是该条重链的top

    part2

    实现上主要分三个部分

    predfs(root);//主要处理sz,son,fa,dep这四个数组
    redfs(root,root);//主要处理top,题目需要时处理li[],ri[](dfs序)
    calc(u,v);//统计路径(u,v)上的值、或者是对路径(u,v)进行修改
    

    这里有一个细节,处理dfs序的时候必须得在(redfs)时进行,因为我们得保证一条重链上的(li[])连续的,这样才能映射到线段树上,对连续的一段链进行区间修改。当然,这样处理dfs序仍然能表示一个节点的子树->([li[x],ri[x]])

    下面给出三个部分的模板代码

    predfs

    void predfs(int x,int f){
    	sz[x]=1;fa[x]=f;dep[x]=dep[f]+1;
    	for(int i=head[x];i;i=e[i].nxt){
    		int y=e[i].to;
    		predfs(y,x);
    		sz[x]+=sz[y];
    		if(!son[x]||sz[y]>sz[son[x]])son[x]=y;
    	}
    }
    

    redfs

    void redfs(int x,int tp){
    	top[x]=tp;li[x]=++tot;
    	if(son[x])redfs(son[x],tp);
    	for(int i=head[x];i;i=e[i].nxt){
    		int y=e[i].to;if(y!=son[x])redfs(y,y);
    	}
    	ri[x]=tot;
    }
    

    calc

    不同的题目,主要的变化就在第三部分,下面是几个常见问题的板子

    1.求LCA

    int LCA(int x,int y){
    	while(top[x]!=top[y]){
    		if(dep[top[x]]<dep[top[y]])swap(x,y);
    		x=fa[top[x]];
        }
    	return dep[x]<dep[y]?x:y;
    }
    

    2.查询、修改一段路径

    void boom(int x,int y,int d){
    	while(top[x]!=top[y]){		
    		if(dep[top[x]]<dep[top[y]])swap(x,y);
    		update(1,li[top[x]],li[x],d);//修改时:ans+=query();
    		x=fa[top[x]];
    	}
    	if(dep[x]>dep[y])swap(x,y);
    	update(1,li[x],li[y],d);//修改时:ans+=query();
    }
    

    part3

    安利一篇树剖的blog

    二、树状数组、线段树常见操作

    Ⅰ.树状数组

    相关blog:树状数组(含进阶版)

    普通的树状数组支持:

    0.单点修改+单点查询(普通数组就可以了2333)

    1.单点更新+区间求和

    2.区间更新+单点求值(利用差分)

    3.单点更新+求前缀的最值(必须满足修改的值呈现单调性


    Ⅱ.线段树

    相关操作比较多就不一一阐述了。这里区分一下本题覆盖修改(可理解为增加某个值)的操作。

    part1:懒标记、节点的不同意义

    覆盖操作和修改操作都需要打一个懒标记,但意义和用法有所不同。

    对于更为常见的修改操作,(lazy)用来给儿子下传需要增加的累计值。节点(o)表示([b[o].l,b[o].r])这整个区间的整合信息;

    而对于覆盖操作,(lazy)用来标记该给这个区间赋的值,它与之前这棵树长什么样没有关系。对于非叶子节点的节点(o)来说它本身没有具体含义。

    part2:具体实现

    我们知道,在普通的区间修改操作时:

    build:一开始建树时(lazy)赋值为0,表示需要下传给儿子的累计值为0;

    update:对于区间([ql,qr])的修改操作,将被包含的节点的值(w)增加某个值,懒标记(lazy)增加某个值(具体操作视问题而定);

    down:对于某节点(o)的下传操作,将其左右儿子的(w)增加某个值,懒标记(lazy)增加某个值。

    而对于区间覆盖来说,如果一开始建树时不能直接赋值为0(假如有某个赋值为0的操作,那就重了),我们可以将其赋值为一个接下来操作中不会覆盖到的值,举个例子:假如你只对区间覆盖一个自然数,那么你就可以将(lazy)初始赋值为-1。而接下来对应操作中也不应该是增加,而是赋值

    part3:同时包含区间覆盖+区间修改的问题

    可以参考这篇blog.

    其实本质不变,开两个标记,根据情况分类讨论一下即可。

  • 相关阅读:
    杭电 2546 饭卡 (01背包问题)
    杭电2602 Bone Collector
    01背包问题的详细概述
    杭电1003 最大子串(第二次AC) 当作DP的训练吧
    杭电1087 Super Jumping! Jumping! Jumping!(初见DP)
    Virtualenv虚拟环境的创建、激活、及退出
    修改maven本地仓库路径
    PPT如何去除背景音乐(史上最全方案)
    NVIDIA显卡驱动安装步骤
    Python之NumPy(axis=0 与axis=1)区分
  • 原文地址:https://www.cnblogs.com/Tieechal/p/11468990.html
Copyright © 2020-2023  润新知