天天看點

BZOJ 3669 & luogu 2387 魔法森林前置知識: d i j k s t r a 或 ( k r u s k a l , L i n k − C u t − T r e e ) dijkstra或(kruskal,Link-Cut-Tree) dijkstra或(kruskal,Link−Cut−Tree)思路:

傳送門

前置知識: d i j k s t r a 或 ( k r u s k a l , L i n k − C u t − T r e e ) dijkstra或(kruskal,Link-Cut-Tree) dijkstra或(kruskal,Link−Cut−Tree)

思路:

因為有兩個條件,是以我們可以類似dp那麼先固定一個,再跑spfa求瓶頸路。

同時很明顯可以二分優化。(當然,這還不夠優秀)

時間複雜度大緻為 O ( l o g 2 m ∗ m ∗ n ) ( m ∗ n 都 會 炸 了 ) O(log_2m*m*n)(m*n都會炸了) O(log2​m∗m∗n)(m∗n都會炸了)

下面提供兩種正解:

  1. 動态加點 d i j k s t r a dijkstra dijkstra
  2. 用 k r u s k a l kruskal kruskal求瓶頸生成樹

1. d i j k s t r a dijkstra dijkstra

我們以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. 為什麼 d i s dis dis不用每次都 m e m s e t memset memset

           ~~~~~~~~~~            2. 如果加的這題邊對瓶頸路沒有貢獻會不會影響答案。

回答:

1.每次 m e m s e t memset memset整個圖都要重跑,複雜度太大。為了保證正确性,加兩個端點,使得這條邊對圖有貢獻就行。

2.分兩種情況讨論:

(1):這條邊對瓶頸路有貢獻

這樣的話,顯然直接加就行。

(2):這條邊對瓶頸路無貢獻

這種情況說明這條邊沒有之前的邊優秀,因為每加一條邊就更新一下答案,是以之前就已經更新了較優值,對結果無影響。

2.我們要用生成樹做。

因為b的上限越大,邊越多。(廢話)

是以我們可以按邊的b sort,再求a的瓶頸生成樹。

引理:一個無向圖的最小生成樹一定為瓶頸生成樹。(但瓶頸生成樹不一定為最小生成樹)

證明:可用反證法。假設最小生成樹不為瓶頸生成樹,設最小生成樹的最大邊長度為a,瓶頸路生成樹的最大邊長度為b,則有a>b,那麼瓶頸生成樹的每一條邊都小于a,我們把長度為a的邊(端點為x,y)斷開,換成瓶頸生成樹中連接配接x,y所屬聯通塊的邊,那麼生成樹的大小更小,與最小生成樹的定義沖突,故原命題成立。

是以我們用 k r u s k a l kruskal kruskal維護a的最小生成樹就行。

本題需要動态加邊,是以要用LCT.

因為我們在找1~n的瓶頸路長度時,不能保證1 ~ n的路徑的深度嚴格遞增,是以要換根( m a k e r o o t ( ) 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\n",ans);
	return 0;
}
           

繼續閱讀