luogu1967:[noip2013提高组]货车运输(最小生成树+lca(倍增))或者(最小生成树+树链剖分)

题目传送门

超级好题,完美考核了2个算法:(最小生成树+lca);

当然,也可以用“树链剖分”来替代“lca”。如果想学树链剖分,请拉到最后!


题目大意:

n个点的无向图内,m次询问:x 点到 y 点的路径中,最大值是多少?如果 x与y不连通,输出-1;

解法分析:

 1、在无向图中,求两点的连通性,还要求两点之间道路的最值,应该先建树(最小生成树);

2、多次询问 x与 y之间的道路最值,就是树上的两点的关系,用“最近公共祖先(lca)”来实现;

3、用倍增数组来优化求 lca的过程。


算法介绍:

 1、最小生成树:就是用最少的边(n-1条),把全图连通;

 这里用k算法,方法是:对原图的边进行排序,再取其中的最多(n-1)条边来连接 n个点(就是一棵树(无环)了);

 2、lca:求树上的两个点的最近公共祖先;

这里用倍增数组进行优化跳跃,倍增在这里表现在一个二维数组(如果之前没接触过的孩子,但看本题解比较难懂,加油哦)


上代码:(部分细节,还是在代码注解中)

#include<cstdio>
#include<algorithm>
using namespace std;
const int mx=100005,inf=999999999;//n的最值 
int n,m,len=0;;
struct nod{int x,y,c,gg;}e[mx*5],b[mx*2];
int h[mx],d[mx],f[mx],fa[mx],p[mx][30],w[mx][30];

bool cmp(nod x,nod y) { return x.c>y.c; }//排序的参数 
int ch(int x) { if(f[x]==x) return x; return f[x]=ch(f[x]); }//并查集的板子 
void ins(int x,int y,int c)
{
	len++;b[len].x=x;b[len].y=y;b[len].c=c;b[len].gg=h[x];h[x]=len;
}

void dfs(int x)//对树分层的函数:同时初始化倍增的p和w数组 
{
	for(int i=h[x];i>0;i=b[i].gg)
	{
		int y=b[i].y;
		if(d[y]==0&&p[x][0]!=y)
		{
			d[y]=d[x]+1;//更新y点
			p[y][0]=x; w[y][0]=b[i].c;//w[y][0]指y到父亲的这段路的最小价值
			dfs(y); 
		}
	}
}
 
void ycl()
{
	for(int i=1;i<=n;i++) d[i]=0;//对树分层
	for(int i=1;i<=n;i++) //可能有多棵树,对每棵树,任意选根 
		if(d[i]==0) { d[i]=1;p[i][0]=0; dfs(i);}//p[i][0]等同于 i的父亲节点
	//下面循环是倍增的核心代码,规模从小到大填写p数组和w数组(思维类似合并石子)
	for(int i=1;i<=20;i++)
	{
		for(int x=1;x<=n;x++)//x点的第i跳能到达的地方,记录为p[x][i]; 
		{					//w[x][i]则表示这个过程的最小值 
			p[x][i]=p[p[x][i-1]][i-1];
			w[x][i]=min(w[x][i-1],w[p[x][i-1]][i-1]);
		}//以上两句需要自行理解,我懒得画图讲解了~~ 
	} 
	
	//到此为止,每棵树都已经分层完毕,倍增数组也弄好了!!
	//剩下就是树上的查询工作(lca)了 ~~ 	
}

int lca(int x,int y)//题目求的是 x-y之间的最小值,现在让x和y跳到他们的lca上相遇
{
	if(d[x]<d[y]) swap(x,y);//人为定义方向,让 x 在下, y在上,让x找y
	int ans=inf;
	for(int i=20;i>=0;i--)
	{
		if(d[p[x][i]]>=d[y])//先让 x 跳到 y的同层 
		{
			ans=min(ans,w[x][i]);//注意先维护,再更新
			x=p[x][i];
		}
	} 
	if(x==y) return ans;//同层又相同,说明 y 本身就是 x的 祖先
	
	//如果x、y不同子树,就一起跳上去 
	for(int i=20;i>=0;i--)
	{
		if(p[x][i]!=p[y][i])
		{
			ans=min(ans,w[x][i]); x=p[x][i]; //注意先维护,再更新
			ans=min(ans,w[y][i]); y=p[y][i]; //注意先维护,再更新
			
		}
	}//最终他们跳到lca的下一层停住 
	ans=min(ans,min(w[x][0],w[y][0]));//更新最后一层数据 
	return ans;
} 
int main()
{
	scanf("%d %d",&n,&m);int x,y;
	
	//最小生成树部分 
	for(int i=1;i<=m;i++) scanf("%d %d %d",&e[i].x,&e[i].y,&e[i].c);
	sort(e+1,e+1+m,cmp);//e是原图的边,以c作为关键字排序:从大到小 
	for(int i=1;i<=n;i++) { f[i]=i;h[i]=0; }//初始化,用来构树 
	int kk=0;
	for(int i=1;i<=m;i++)//最多取 n-1 条边构树 
	{
		x=ch(e[i].x); y=ch(e[i].y);
		if(x!=y) //如果x、y不在同一个集合,说明当前边有用 
		{
			ins(e[i].x,e[i].y,e[i].c); 
			ins(e[i].y,e[i].x,e[i].c);//双向边 
			f[x]=y;kk++;if(kk==n-1) break;
		}
	}//本题可能会建成多棵树
	
	//对树进行预处理
	ycl(); 
	
	scanf("%d",&m);//m次询问
	while(m--)
	{
		scanf("%d %d",&x,&y);
		int xx=ch(x);
		int yy=ch(y);
		if(xx!=yy) printf("-1\n");//两点不在同一棵树上,不能到达
		else printf("%d\n",lca(x,y)); //输出 x-y之间的道路最小值	
	} 
	
	return 0;

} 

··········································································以上是LCA的代码·······················································································

···································································以下是树链剖分的代码·······················································································

算法介绍:

 1、构树的过程是一样的:

        最小生成树:就是用最少的边(n-1条),把全图连通;

 这里用k算法,方法是:对原图的边进行排序,再取其中的最多(n-1)条边来连接 n个点(就是一棵树(无环)了);

 2、树链剖分:

        2.1 把一棵树(从上而下)看成很多条链,对于一条特定的链(重链),给他连续的,新的编号;

        2.2 因为连续编号,把树划分为很多编号连续的(重链);

        2.3 以前我们学过线段树,可以高效管理连续的区间;

        2.4 直接用线段树来管理(树链);

        3、树剖的优势:

        因为连续区间已经用线段树来管理了,查找复杂度时间大大缩小。如果上下两个区间不在同一条链内,就做个跳跃吧。

        4、代码思路:

        4.1 构造最小生成树;

        4.2 深搜,求出每个点管理的节点数(包括自己),每个父亲都有一个重儿子;

        4.3 给树上的点赋予新的编号:重儿子们得到连续的编号,其他点也得到新编号;

        4.3 建立线段树,用来管理新编号;

        4.4 求 x-〉y 之间的值,通过线段树来快速查找连续区间,区间之间,就直接跳上去。


上AC代码:(之前错误已经差出来了,感谢小六的同学们!!!)


#include<cstdio>
#include<algorithm>
using namespace std;
const int mx=100005;
struct nodx{int f,tot,son,t,d,c,n;}a[mx];//原图的点 
struct nodb{int x,y,c,gg;}b[mx*6],e[mx*3];//双向边
struct nodt{int l,r,ls,rs,c;}t[mx*2];//线段树的点 (c是最小值)

int n,m,len=0,nx=0,lt=0,la[mx],f[mx];//last数组,f数组用于并查集 

bool cmp(nodb x,nodb y){ return x.c>y.c; }
int ch(int x) { if(x==f[x]) return x; return f[x]=ch(f[x]); }//并查集 
void ins(int x,int y,int c)
{
	len++;b[len].x=x;b[len].y=y;b[len].c=c;b[len].gg=la[x];la[x]=len;
}

void fc(int x)//将树分层,同时记录重儿子 
{
	a[x].tot=1;//管理自己
	a[x].son=0;//重儿子在回溯时更新
	for(int i=la[x];i>0;i=b[i].gg)
	{
		int y=b[i].y;
		if(a[x].f!=y)
		{
			a[y].c=b[i].c;//将边上的值,给下层的点 
			a[y].f=x;a[y].d=a[x].d+1;
			fc(y);
			//回溯:做更新 
			a[x].tot+=a[y].tot;//更新统计数量 
			if(a[y].tot>a[a[x].son].tot) a[x].son=y; //更新重儿子 
		}
	}
}

void bl(int x,int tou)//x表示当前点,tou表示当前链的顶端
{
	a[x].t=tou;a[x].n=++nx;//获取新编号
	if(a[x].son!=0) bl(a[x].son,tou); //优先更新重儿子,使编号连续
	for(int i=la[x];i>0;i=b[i].gg)
	{
		int y=b[i].y;
		if(a[x].f!=y&&a[x].son!=y)//非重.儿子 
		{
			bl(y,y);//新开一条链,头是他自己 
		}
	}
} 

void bt(int l,int r)//建立空的线段树 
{
	 lt++;int x=lt;t[x].l=l;t[x].r=r;t[x].ls=t[x].rs=-1;
	 t[x].c=0;//c是和 
	 if(l<r)
	 {
	 	int m=(l+r)/2;
	 	t[x].ls=lt+1; bt(l,m);
	 	t[x].rs=lt+1; bt(m+1,r);
	 } 
}
void xg(int x,int y,int k)// 改点 
{	// 到叶子了: 
	if(t[x].l==t[x].r) { t[x].c=k; return ;}
	//下探: 
	int m=(t[x].l+t[x].r)/2,ls=t[x].ls,rs=t[x].rs; 
	if(y<=m) xg(ls,y,k);
	else xg(rs,y,k);
	//维护:
	t[x].c=min(t[ls].c,t[rs].c);  
} 
int fmin(int x,int l,int r)
{
	if(t[x].l==l&&t[x].r==r) return t[x].c;
	int m=(t[x].l+t[x].r)/2,ls=t[x].ls,rs=t[x].rs; 
	if(r<=m) return fmin(ls,l,r);
	else if(l>m) return fmin(rs,l,r);
	else return min(fmin(ls,l,m),fmin(rs,m+1,r));
}

int smin(int x,int y)
{
	int tx=a[x].t, ty=a[y].t,ans=999999999;	
	//目标是让两点跳到同一条链上 : 
	while(tx!=ty)
	{
		if(a[tx].d>a[ty].d)  { swap(x,y); swap(tx,ty); }
		ans=min(ans,fmin(1,a[ty].n,a[y].n));//y在链内往上跳
		y=a[ty].f;ty=a[y].t; 
	}
	//以上循环结束后,x与 y 在同一条链内
	if(x==y) return ans;
	if(a[x].d>a[y].d) swap(x,y);
	return min(ans,fmin(1,a[a[x].son].n,a[y].n)); 
}

int main()
{
	scanf("%d %d",&n,&m);int x,y,c; 
	
	//最小生成树部分 ======================= 
	for(int i=1;i<=n;i++) { la[i]=0;f[i]=i; }
	for(int i=1;i<=m;i++)
	{
		scanf("%d %d %d",&x,&y,&c);
		e[i].x=x;e[i].y=y;e[i].c=c;
	}
	sort(e+1,e+m+1,cmp);
	int su=0; 
	for(int i=1;i<=m;i++)
	{
		x=ch(e[i].x); y=ch(e[i].y);
		if(x!=y)
		{
			ins(e[i].x,e[i].y,e[i].c);
			ins(e[i].y,e[i].x,e[i].c);
			f[x]=y;su++;if(su==n-1) break;
		}
	}
	
	//分层:找重儿子 
	for(int i=1;i<=n;i++) a[i].tot=0;
	for(int i=1;i<=n;i++)
	{
		if(a[i].tot==0) { a[i].f=0;fc(i);}
	} 
	
	//建立重链关系,赋予新编号
	for(int i=1;i<=n;i++)
	{
		if(a[i].f==0) bl(i,i);
	} 
	
	bt(1,n);//建立空的线段树  
	for(int i=1;i<=n;i++) xg(1,a[i].n,a[i].c);//将点的信息填入树内 

	scanf("%d",&c);//c 次的询问
	
	for(int i=1;i<=c;i++)
	{
		scanf("%d %d",&x,&y); 
		int tx=ch(x);
		int ty=ch(y);
		if(tx!=ty) printf("-1\n");
		else printf("%d\n",smin(x,y));
	} 
   return 0;
} 










猜你喜欢

转载自blog.csdn.net/liusu201601/article/details/79244399
今日推荐