Dash Speed【好题,分治,并查集按秩合并】

Dash Speed

Online Judge:NOIP2016十联测,Claris#2 T3

Label:好题,分治,并查集按秩合并,LCA

题目描述

比特山是比特镇的飙车圣地。在比特山上一共有 n 个广场,编号依次为 1 到 n,这些广场之间通过 n − 1 条双向车道直接或间接地连接在一起,形成了一棵树的结构。

因为每条车道的修建时间以及建筑材料都不尽相同,所以可以用两个数字 li, ri 量化地表示一条车道 的承受区间,只有当汽车以不小于 li 且不大于 ri 的速度经过这条车道时,才不会对路面造成伤害。

Byteasar 最近新买了一辆跑车,他想在比特山飙一次车。Byteasar 计划选择两个不同的点 S, T ,然 后在它们树上的最短路径上行驶,且不对上面任意一条车道造成伤害。

Byteasar 不喜欢改变速度,所以他会告诉你他的车速。为了挑选出最合适的车速,Byteasar 一共会 向你询问 m 次。请帮助他找到一条合法的道路,使得路径上经过的车道数尽可能多。

输入

第一行包含两个正整数 n, m,表示广场的总数和询问的总数。

接下来 n − 1 行,每行四个正整数 ui, vi, li, ri,表示一条连接 ui 和 vi 的双向车道,且承受区间为[li, ri]。

接下来 m 行,每行一个正整数 qi,分别表示每个询问的车速。

输出

输出 m 行,每行一个整数,其中第 i 行输出车速为 qi 时的最长路径的长度,如果找不到合法的路 径则输出 0。

样例

Input#1

5 3		
3 2 2 4
1 5 2 5
4 5 2 2
1 2 3 5
1
2
3

Output#1

0
2
3

样例解释:
当车速为 1 时,不存在合法的路径。
当车速为 2 时,可以选择 1-5-4 这条路径,长度为 2。
当车速为 3 时,可以选择 3-2-1-5 这条路径,长度为 3

题解

Subtask1:暴力

对于每个询问直接枚举一个端点,然后dfs一遍,找最大距离。

时间复杂度为(O(Mcdot N^2)),期望得分20。

Subtask2:树退化成链且l=1

由于l没有限制了,只要当前边的r大于等于限制lim,则可以通过。考虑离线询问,以r为关键字排序所有连边及询问,用并查集维护连通块,每个连通块记录两个值(ma,mi),表示该连通块内节点的最大/最小深度,则当前连通块构成的链长度为(ma-mi)

时间复杂度为(O(NlogN+M)),结合Subtask1期望得分40。

Subtask3:树退化成链的所有情况

比起Subtask2,这里还要考虑l的限制。

枚举当前车速,则假如一条边限制车速为{(l,r)},则在车速为(l)时加入这条连边,在车速为(r+1)时删除这条连边,对于每个车速,答案为当前所有连边形成的最大链长

维护可以利用线段树。具体维护内容如下,对于线段树上的每个节点,设其对应树边范围为((l,r)),维护三个值((li,ri,S))(li)表示(l)向右能延伸的最大连续长度,(ri)表示(r)向左能延伸的最大连续长度,(S)表示该区间内的最大链长。

时间复杂度为(O(NlogN+M)),结合Subtask1期望得分60。

前面三种情况的切分代码如下:

#include<bits/stdc++.h>
using namespace std;
#define R register

bool nc1;
const int N=70010;
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,l,r;
}e[N<<1];
int ecnt,head[N];
inline void link(int u,int v,int l,int r){
    e[++ecnt]=(edge){v,head[u],l,r};
    head[u]=ecnt;
}
int n,m;
namespace SUB1_bl{//o(M*(N^2)):n,m<=20
    int now=0,lim;
    void dfs(int x,int f,int dis){
        if(dis>now)now=dis;
        for(int i=head[x];i;i=e[i].nxt){
            int y=e[i].to,l=e[i].l,r=e[i].r;
            if(y==f||lim<l||lim>r)continue;
            dfs(y,x,dis+1);
        }
    }
    void solve(){
        while(m--){
            lim=read();now=0;
            for(R int i=1;i<=n;++i)dfs(i,0,0);
            printf("%d
",now);
        }
    }
}
struct EE{
    int x,d;
}E[N];
vector<int>que[N];
int res[N];
inline bool cmpsub2(EE a,EE b){return a.d<b.d;}
namespace SUB2_linkandL{//O(NlogN+M) 所有l=1且形态为链 
    int ma[N],mi[N],CNT,tmp[N],bcj[N],ans=0;
    int find(int k){return bcj[k]==k?k:bcj[k]=find(bcj[k]);}
    void solve(){
        for(R int x=1;x<=n;x++){
            ma[x]=mi[x]=bcj[x]=x;
            for(R int i=head[x];i;i=e[i].nxt){
                int y=e[i].to;
                if(y==x+1){
                    E[++CNT]=(EE){x,e[i].r};
                    tmp[CNT]=e[i].r;
                    break;
                }
            }
        }
        sort(E+1,E+CNT+1,cmpsub2);
        sort(tmp+1,tmp+CNT+1);
         
        for(R int i=1;i<=m;++i){
            int qnum=read();
            qnum=lower_bound(tmp+1,tmp+CNT+1,qnum)-tmp;
            que[qnum].push_back(i);
        }
         
        for(R int i=CNT;i>=1;i--){
            int u=E[i].x,v=u+1,A=find(u),B=find(v);
            if(A==B)continue;
            bcj[A]=B;
            ma[B]=max(ma[B],ma[A]);
            mi[B]=min(mi[B],mi[A]);
            ans=max(ans,ma[B]-mi[B]);
            for(int j=0;j<que[i].size();++j)res[que[i][j]]=ans;
        }
        for(R int i=1;i<=m;++i)printf("%d
",res[i]);
     
    }
}
typedef pair<int,int> pii;
vector<pii>g[N];
namespace SUB3_LINK{//链的所有情况 
	struct SGT{
		int l,r;
		int S,li,ri;
		/*S:区间内最大连续链长 
		  li:左端点向右的最大连续距离
		  ri:右端点向左的最大连续距离 
		*/
	}b[N<<2];
	void build(int o,int l,int r){
		b[o].l=l,b[o].r=r,b[o].S=b[o].li=b[o].ri=0;
		if(l==r)return;
		int mid=l+r>>1;
		build(o<<1,l,mid),build(o<<1|1,mid+1,r);
	}
	void up(int o){
		int ls=o<<1,rs=o<<1|1;
		int sizL=b[ls].r-b[ls].l+1,sizR=b[rs].r-b[rs].l+1;
		b[o].S=max(max(b[ls].S,b[rs].S),b[ls].ri+b[rs].li);
		b[o].li=b[ls].li;
		if(b[ls].li==sizL)b[o].li+=b[rs].li;
		b[o].ri=b[rs].ri;
		if(b[rs].ri==sizR)b[o].ri+=b[ls].ri;
	}
	void update(int o,int pos,int z){
		if(b[o].l==b[o].r){
			b[o].S=b[o].li=b[o].ri=z;
			return;
		}
		int mid=b[o].l+b[o].r>>1;
		if(pos<=mid)update(o<<1,pos,z);
		else update(o<<1|1,pos,z);
		up(o);
	}
	void solve(){
		for(R int x=1;x<n;x++){
            for(R int i=head[x];i;i=e[i].nxt){
                int y=e[i].to;
                if(y==x+1){
                    g[e[i].l].push_back(make_pair(x,1));
                    g[e[i].r+1].push_back(make_pair(x,0));
                    break;
                }
            }
        }
        for(R int i=1;i<=m;i++){
			int x=read();
			que[x].push_back(i);
		}
		build(1,1,n-1);//n-1条线段 
		for(R int i=1;i<=n;i++){
			for(R int j=0;j<g[i].size();j++)update(1,g[i][j].first,g[i][j].second);
			for(R int j=0;j<que[i].size();j++)res[que[i][j]]=b[1].S;
		} 
        for(R int i=1;i<=m;i++)printf("%d
",res[i]);
	}
}
bool nc2;
int main(){
    n=read(),m=read();
    bool islink=1,Lequal1=1;
    for(int i=1;i<n;++i){
        int u=read(),v=read(),l=read(),r=read();
        link(u,v,l,r);link(v,u,l,r);
        if(u!=v+1&&v!=u+1)islink=0;
        if(l!=1)Lequal1=0;
    }
	if(n<=20){SUB1_bl::solve();return 0;}
	if(islink&&Lequal1){SUB2_linkandL::solve();return 0;}
	if(islink){SUB3_LINK::solve();return 0;}
}

Subtask4:l=1的所有情况

只用考虑r的限制。

之前NOIP真题有做过类似的,好像是这题NOIP2013 货车运输。货车运输这题是给你每条路的承重量,多个询问,问你从u到v最多能带多少货物。做法是离线询问,从限制最宽(承重量最大)的边开始加,然后对于新生成的树启发式合并,维护两点连通情况,顺便更新答案。

两点连通情况不难维护,但本题需要维护的是新树的直径

仍然基于Subtask2的想法,但是现在新树直径不是链长了。对于当前边((x,y)),如果已经同属于一棵树了就continue掉,反之对于分别属于的两棵树进行合并,并对新合并的树求直径。

每个连通块(树),维护两个东西{(A[],B[])},分别表示该连通块(树)的直径的两个端点

对于合并后的新树,不难知道,新直径只可能有(C_4^2=6)种情况,即在原来的四个端点中选两个作为新树的端点,比较形成的直径长度,取6种中最长的一个更新。如何快速求两点距离呢?其实它就等于原树(所有边都连上时)上的距离,那么用树剖/倍增直接求LCA然后求原树上的距离即可。

时间复杂度为(O(NlogN+M)),结合Subtask1,Subtask3期望得分80。

Subtask5:所有情况

现在还要考虑上l的限制了:(

由于车速处在([1,n])间,考虑预处理完所有(ans[i=1..n])表示车速为i时能走的最长路径,然后直接在线回答询问。

考虑根据车速来分治。对于当前区间([l,r]),设道路限速为([ql,qr])。则将所有满足(ql<=l,r<=qr)的道路存在这个速度区间里(表示这个道路限度完全包含这个速度区间)。像下面这样:

//当前存的道路限速为[ql,qr],当前车速区间为[l,r],节点编号为o
//u,v是道路的两个端点
int head[N<<2],U[N*20],V[N*20],nxt[N*20],ecnt;
//大概N<<2个节点,大概会存N*logN次边
void Insert(int o,int l,int r,int ql,int qr,int u,int v){
	if(ql<=l&&r<=qr){
		U[++ecnt]=u;V[ecnt]=v;
		nxt[ecnt]=head[o];head[o]=ecnt;//前向星存边 
		return;
	}
	int mid=l+r>>1;
	if(ql<=mid)Insert(o<<1,l,mid,ql,qr,u,v);
	if(qr>mid)Insert(o<<1|1,mid+1,r,ql,qr,u,v);
}

先来分析一下单单这样存边的复杂度,m条边每条边都存一次,递归层数为(logN),所以(O(MlogN))。顺便注意一下存边数组的大小。

存完边后开始分治连边。

//当前节点为o,速度区间为[l,r],ret表示此时的树上最大链长
void solve(int o,int l,int r,int ret){
	int pos=cur;//cur和pos的具体用处下面会讲到
	for(R int i=head[o];i;i=nxt[i])merge(U[i],V[i],ret);
    //merge(u,v,ret)表示连接uv这条边,并更新当前ret
	if(l==r){
		ans[l]=ret;//当递归到叶子时得到ans
		Retrace(pos);//Retrace:回溯
		return;
	}
	int mid=l+r>>1;
	solve(o<<1,l,mid,ret);
	solve(o<<1|1,mid+1,r,ret);
	Retrace(pos);//分治到另一半时还得把当前这一半的影响给回溯了
}

merge函数的具体实现跟Substack4里讲的基本一样,利用并查集维护连通关系(但不能路径压缩,因为之后还要回溯)和当前树内的两个直径端点A[i],B[i],然后直接枚举(C_4^2=6)种情况,去更新新树的直径大小及端点。

代码如下:

int Find(int x){return fa[x]==x?x:Find(fa[x]);}
inline void Dia(int a,int b,int &P1,int &P2,int &ma){//考虑(a,b)作为新树直径的情况 
	int tmp=dep[a]+dep[b]-2*dep[LCA(a,b)];//这个LCA在前面预处理出来,倍增或树剖
	if(tmp>ma)ma=tmp,P1=a,P2=b;//更新直径长度及两个端点	
}
inline void merge(int x,int y,int &ret){//在(x,y)间连边,形成新树 
	x=Find(x),y=Find(y);
	int P1,P2,ma=-1,tmp;
    //六种情况
	Dia(A[x],B[x],P1,P2,ma);
	Dia(A[x],A[y],P1,P2,ma);
	Dia(A[x],B[y],P1,P2,ma);
	Dia(B[x],A[y],P1,P2,ma);
	Dia(B[x],B[y],P1,P2,ma);
	Dia(A[y],B[y],P1,P2,ma);	
	if(ma>ret)ret=ma;
	//!!!!
	if(rk[x]==rk[y]){//当两树秩相等时,将Treey连到Treex上,Treex的秩++ 
		rk[x]++;
		op[++cur]=(Op){0,x,0};
	}
	if(rk[x]<rk[y])swap(x,y);	
    
	op[++cur]=(Op){1,y,0};
	op[++cur]=(Op){2,x,A[x]};
	op[++cur]=(Op){3,x,B[x]};
	fa[y]=x,A[x]=P1,B[x]=P2;
    //!!!!
}

看到下面重点标记的部分,这一部分其实就是按秩合并两棵树,并且为之后的回溯操作做准备

按秩合并的部分不难理解,就是将高度(秩)较小的树连在高度(秩)较大的树上。为回溯做准备的代码可以结合下面回溯代码去理解。

op这个结构体记录回溯操作,用类似的形式去存储回溯操作,cur就表示当前栈内元素(需要回溯的操作)。现在就可以解释之前分治函数solve()中这句代码的意义了int pos=cur;//cur和pos的具体用处下面会讲到,它存储了递归子树前栈顶的位置,现在我去分治另一半时只用回到pos位置即可。

struct Op{//记录回溯操作 
	int t,x,y;
}op[N<<2];
inline void Retrace(int pos){//回溯到pos位置 
	while(cur>pos){
		if(op[cur].t==0)rk[op[cur].x]--;
		if(op[cur].t==1)fa[op[cur].x]=op[cur].x;
		if(op[cur].t==2)A[op[cur].x]=op[cur].y;//还原该树直径端点1 
		if(op[cur].t==3)B[op[cur].x]=op[cur].y;//还原该树直径端点2 
		cur--;
	}
}

这样整个做法就结束了,具体实现时注意细节,包括数组大小/数组名混淆/不能路径压缩这些。

上面的分治做法看似非常暴力,来分析一下复杂度,边存储了(MlogN)次,每条边都至多merge()一次,每次合并产生的需要回溯操作不多,而每次操作都至多出栈一次,整个算法的时间复杂度大致为(O(Nlog^2N))

完整代码如下:

#include<bits/stdc++.h>
#define R register
using namespace std;
const int N=70010;
int n,m,cur,ans[N];
int sz[N],f[N],dep[N],son[N],top[N];//树剖 
int fa[N],rk[N],A[N],B[N];//并查集 

int head[N<<2],U[N*20],V[N*20],nxt[N*20],ecnt;
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 Op{//记录回溯操作 
	int t,x,y;
}op[N<<2];
vector<int>g[N];
void dfs(int x){
	sz[x]=1;
	for(int i=0;i<g[x].size();++i){
		int y=g[x][i];if(y==f[x])continue;
		f[y]=x,dep[y]=dep[x]+1;
		dfs(y),sz[x]+=sz[y];
		if(sz[y]>sz[son[x]])son[x]=y;
	}
}
void redfs(int x,int tp){
	top[x]=tp;
	if(son[x])redfs(son[x],tp);
	for(int i=0;i<g[x].size();++i){
		int y=g[x][i];if(y==f[x]||y==son[x])continue;
		redfs(y,y);
	}
}
inline int LCA(int x,int y){
	while(top[x]!=top[y]){
		if(dep[top[x]]<dep[top[y]])swap(x,y);
		x=f[top[x]];
	}
	return dep[x]<dep[y]?x:y;
}
int Find(int x){return fa[x]==x?x:Find(fa[x]);}
inline void Dia(int a,int b,int &P1,int &P2,int &ma){//考虑(a-b)作为新树直径的情况 
	int tmp=dep[a]+dep[b]-2*dep[LCA(a,b)];
	if(tmp>ma)ma=tmp,P1=a,P2=b;	
}
inline void merge(int x,int y,int &ret){//在(x,y)间连边,形成新树 
	x=Find(x),y=Find(y);
	int P1,P2,ma=-1,tmp;
	Dia(A[x],B[x],P1,P2,ma);
	Dia(A[x],A[y],P1,P2,ma);
	Dia(A[x],B[y],P1,P2,ma);
	Dia(B[x],A[y],P1,P2,ma);
	Dia(B[x],B[y],P1,P2,ma);
	Dia(A[y],B[y],P1,P2,ma);	
	if(ma>ret)ret=ma;
	
	if(rk[x]==rk[y]){//当两树秩相等时,将Treey连到Treex上,Treex的秩++ 
		rk[x]++;
		op[++cur]=(Op){0,x,0};
	}
	if(rk[x]<rk[y])swap(x,y);	
	op[++cur]=(Op){1,y,0};
	op[++cur]=(Op){2,x,A[x]};
	op[++cur]=(Op){3,x,B[x]};
	fa[y]=x,A[x]=P1,B[x]=P2;
}
inline void Retrace(int pos){//回溯 
	while(cur>pos){
		if(op[cur].t==0)rk[op[cur].x]--;
		if(op[cur].t==1)fa[op[cur].x]=op[cur].x;
		if(op[cur].t==2)A[op[cur].x]=op[cur].y;//还原该树直径端点1 
		if(op[cur].t==3)B[op[cur].x]=op[cur].y;//还原该树直径端点2 
		cur--;
	}
}
void Insert(int o,int l,int r,int ql,int qr,int u,int v){
	if(ql<=l&&r<=qr){
		U[++ecnt]=u;V[ecnt]=v;
		nxt[ecnt]=head[o];head[o]=ecnt;//存边 
		return;
	}
	int mid=l+r>>1;
	if(ql<=mid)Insert(o<<1,l,mid,ql,qr,u,v);
	if(qr>mid)Insert(o<<1|1,mid+1,r,ql,qr,u,v);
}
void solve(int o,int l,int r,int ret){
	int pos=cur;
	for(R int i=head[o];i;i=nxt[i])merge(U[i],V[i],ret);
	if(l==r){
		ans[l]=ret;
		Retrace(pos);
		return;
	}
	int mid=l+r>>1;
	solve(o<<1,l,mid,ret);
	solve(o<<1|1,mid+1,r,ret);
	Retrace(pos);
}
int main(){
	n=read(),m=read();
	for(R int i=1;i<n;++i){
		int u=read(),v=read(),l=read(),r=read();
		g[u].push_back(v);
		g[v].push_back(u);
		Insert(1,1,n,l,r,u,v);
	}
	dfs(1);redfs(1,1);
	for(R int i=1;i<=n;++i)fa[i]=A[i]=B[i]=i;
	solve(1,1,n,0);
	while(m--)printf("%d
",ans[read()]);
}
原文地址:https://www.cnblogs.com/Tieechal/p/11648640.html