BZOJ 3669 & luogu 2387 魔法森林

传送门

前置知识:dijkstra(kruskal,LinkCutTree)dijkstra或(kruskal,Link-Cut-Tree)

思路:

因为有两个条件,所以我们可以类似dp那么先固定一个,再跑spfa求瓶颈路。
同时很明显可以二分优化。(当然,这还不够优秀)
时间复杂度大致为O(log2mmn)(mn)O(log_2m*m*n)(m*n都会炸了)
下面提供两种正解:

  1. 动态加点dijkstradijkstra
  2. kruskalkruskal求瓶颈生成树

1.dijkstradijkstra

我们以b为第一关键字排序边,然后维护最小生成树。
对于a呢,因为多加一条边,所以只有边的两个端点可以再贡献1~n的瓶颈路。
每次加完边后,答案就为刚加边的b值+1~n的瓶颈路长度。

代码by ZZY大佬:

// luogu-judger-enable-o2
#include<cstdio>
#include<cstring>
#include<queue>
#include<algorithm>
#include<ctype.h>
using namespace std;
struct node3
{
	int id,d;
	friend bool operator <(node3 x,node3 y)
	{
		return x.d>y.d;//实际上是小根堆
	}
};
priority_queue<node3> f;//堆
	int n,m,len=0,ans=2147483647;
	struct node1{int x,y,z1,z2;} d[100010];
	struct node2{int x,y,z,next;} a[200010];
	int last[50010],dis[50010];
	bool bz[50010];
inline char getc()//fread优化
{
	static char buf[1<<18],*fs,*ft;
	return(fs==ft&&(ft=(fs=buf)+fread(buf,1,1<<18,stdin)),fs==ft)?EOF:*fs++;
}
inline int read()
{
	char ch=getc(),f=1;
	int x=0;
	for(;!isgraph(ch);ch=getc());
	if(ch=='-') f=-1,ch=getc();
	for(;isdigit(ch);ch=getc())
		x=((x+(x<<2))<<1)+(ch^0x30);
	return x*f;
}
bool cmp(node1 x,node1 y)
{
	return x.z1<y.z1;
}
void ins(int x,int y,int z)
{
	a[++len]=(node2){x,y,z,last[x]}; last[x]=len;
}
int dijkstra(int st1,int st2)//不完全的dijkstra,同学们可以手打堆来保证堆顶的dis最优。这样每个点就只用进一次堆了
{
	f.push((node3){st1,dis[st1]});
	f.push((node3){st2,dis[st2]});
	while(!f.empty())
	{
		int x=f.top().id;
		f.pop();
		for(int i=last[x];i;i=a[i].next)
		{
			int y=a[i].y;
			if(dis[y]>max(dis[x],a[i].z))
			{
				dis[y]=max(dis[x],a[i].z);
				if(!bz[y]) f.push((node3){y,dis[y]}),bz[y]=true;
			}
		}
		bz[x]=false;
	}
	return dis[n];
}
int main()
{
	n=read(),m=read();
	for(int i=1;i<=m;i++)
		d[i].x=read(),d[i].y=read(),d[i].z1=read(),d[i].z2=read();
	sort(d+1,d+m+1,cmp);
	memset(dis,63,sizeof(dis));
	dis[1]=0;
	bool flag=false;
	for(int i=1;i<=m;i++)
	{
		if(d[i].z1>=ans) break;
		if(d[i].x==d[i].y) continue;
		ins(d[i].x,d[i].y,d[i].z2);
		ins(d[i].y,d[i].x,d[i].z2);
		if(d[i].x==1||d[i].y==1) flag=true;
		if(flag) ans=min(ans,d[i].z1+dijkstra(d[i].x,d[i].y));//
	}
	printf("%d",ans<(int)1e9?ans:-1);
}

提问: 1. 为什么disdis不用每次都memsetmemset
          ~~~~~~~~~~ 2. 如果加的这题边对瓶颈路没有贡献会不会影响答案。
回答:
1.每次memsetmemset整个图都要重跑,复杂度太大。为了保证正确性,加两个端点,使得这条边对图有贡献就行。
2.分两种情况讨论:
(1):这条边对瓶颈路有贡献
这样的话,显然直接加就行。
(2):这条边对瓶颈路无贡献
这种情况说明这条边没有之前的边优秀,因为每加一条边就更新一下答案,所以之前就已经更新了较优值,对结果无影响。

2.我们要用生成树做。

因为b的上限越大,边越多。(废话)
所以我们可以按边的b sort,再求a的瓶颈生成树。

引理:一个无向图的最小生成树一定为瓶颈生成树。(但瓶颈生成树不一定为最小生成树)
证明:可用反证法。假设最小生成树不为瓶颈生成树,设最小生成树的最大边长度为a,瓶颈路生成树的最大边长度为b,则有a>b,那么瓶颈生成树的每一条边都小于a,我们把长度为a的边(端点为x,y)断开,换成瓶颈生成树中连接x,y所属联通块的边,那么生成树的大小更小,与最小生成树的定义矛盾,故原命题成立。

所以我们用kruskalkruskal维护a的最小生成树就行。
本题需要动态加边,所以要用LCT.
因为我们在找1~n的瓶颈路长度时,不能保证1 ~ n的路径的深度严格递增,所以要换根(makeroot()makeroot()),LCT就是有根树。
又因为要算边权,但把边权放给子节点的方法,由于换根而不能做。所以我们定义多m个点,把边权压到这些点上,其他点无权。那么我们就把求边权转化为了求点权。

#include<cstdio>
#include<cctype>
#include<cstring>
#include<algorithm>
#define g getchar()
#define lc tr[x].son[0]
#define rc tr[x].son[1]
using namespace std;
const int N=15e4+10,M=1e5+10;//N的范围需注意 
void qr(int &x)
{
	char c=g;bool v=x=0;
	while(!(isdigit(c)||c=='-'))c=g;//isdigit返回bool值表示是否为十进制中的字符.isdigit(c)同'0'<=c&&c<='9' 
	if(c=='-')v=1,c=g;
	while(isdigit(c))x=x*10+c-'0',c=g;
	if(v)x=-x;
}
struct edge{int x,y,a,b;}a[M];//边 
bool cmp(edge x,edge y){return x.b<y.b;}
int d[N];
struct node{int f,son[2],p;bool v;}tr[N];//p为子树内d的最大值的标号 
void update(int x)
{
	tr[x].p=x;
	if(lc&&d[tr[x].p]<d[tr[lc].p])tr[x].p=tr[lc].p;
	if(rc&&d[tr[x].p]<d[tr[rc].p])tr[x].p=tr[rc].p;
}
void fz(int x)//翻转 
{
	tr[x].v=0;swap(lc,rc);
	tr[lc].v^=1;tr[rc].v^=1;
}
bool rt(int x){return tr[tr[x].f].son[0]!=x&&tr[tr[x].f].son[1]!=x;}//是否为splay的根 
void wh(int x)
{
	if(!rt(x))wh(tr[x].f);
	if(tr[x].v)fz(x);
}
void rotate(int x,int w)
{
	int f=tr[x].f,ff=tr[f].f,r,R;
	r=tr[x].son[w];R=f;
	tr[R].son[1-w]=r;
	if(r)tr[r].f=R;
	r=x;R=ff;
		 if(tr[R].son[0]==f)tr[R].son[0]=r;
	else if(tr[R].son[1]==f)tr[R].son[1]=r;
	tr[r].f=R;
	r=f;R=x;
	tr[R].son[w]=r;
	tr[r].f=R;
	update(f);
	update(x);
}
void splay(int x)
{
	wh(x);
	while(!rt(x))
	{
		int f=tr[x].f;
		if(rt(f))rotate(x,tr[f].son[0]==x);
		else
		{
			int ff=tr[f].f,a=(tr[f].son[0]==x),b=(tr[ff].son[0]==f);
			if(a^b)rotate(x,a),rotate(x,b);
			else rotate(f,a),rotate(x,a);
		}
	}
}
void access(int x){for(int y=0;x;x=tr[y=x].f)splay(x),rc=y,update(x);}
void makeroot(int x){access(x);splay(x);tr[x].v^=1;}
int find_root(int x)
{
	access(x);splay(x);
	while(lc)x=lc;
	return x;
}
void split(int x,int y){makeroot(y);access(x);splay(x);}
void link(int x,int y){makeroot(x);tr[x].f=y;access(x);}
void cut(int x,int y){split(x,y);rc=tr[y].f=0;update(x);}
int query(int x,int y){split(x,y);return tr[x].p;}
int fa[N],s[N];//s为子树大小
int findfa(int x){return fa[x]==x?x:fa[x]=findfa(fa[x]);} 
void merge(int x,int y)
{
	x=findfa(x);y=findfa(y);
	if(s[x]<s[y]){fa[x]=y;s[y]+=s[x];}
	else 		 {fa[y]=x;s[x]+=s[y];}
}
int main()
{
	int n,m,x,y,z,b;
	qr(n);qr(m);
	for(int i=1;i<=n;i++)fa[i]=i,s[i]=1;
	for(int i=1;i<=m;i++)
		qr(x),qr(y),qr(z),qr(b),a[i]=(edge){x,y,z,b};
	sort(a+1,a+m+1,cmp);
	for(int i=1;i<=m;i++)d[n+i]=a[i].a;
//因为这是一棵无根树,所以不能把边权压到子节点,我们需要多定义m个点,把边权压到这些点上,其他点的权为0就行 
	int ans=N;
	for(int i=1;i<=m;i++)
	{
		x=a[i].x;y=a[i].y;bool v=1; 
		if(findfa(x)==findfa(y))//再加边就会形成回路。如果最大权的边较大,就换下
		{
			z=query(x,y);
			if(d[z]>a[i].a)cut(a[z-n].x,z),cut(z,a[z-n].y);
			else v=0;
		}
		else merge(x,y);
		if(v)link(a[i].x,i+n),link(i+n,a[i].y);
		if(findfa(1)==findfa(n))
			ans=min(ans,a[i].b+d[query(1,n)]);
	}
	if(ans==N)puts("-1");
	else printf("%d
",ans);
	return 0;
}
原文地址:https://www.cnblogs.com/zsyzlzy/p/12373894.html