图的连通性

\(\text{By DaiRuiChen007}\)

一、割点与桥

割点和桥 - OI Wiki

I. 定义

在一张无向图 \(\mathbf G\) 上,如果删除某一条无向边 \((u,v)\) 会使得图中的极大连通分量(连通块)的数量增加,则称 \((u,v)\)\(\mathbf G\) 中的一个桥(割边)

在一张无向图 \(\mathbf G\) 上,如果删除某一个点 \(u\) 和所有与它相连的边会使得图中的极大连通分量(连通块)的数量增加,则称 \(u\)\(\mathbf G\) 中的一个割点

II. 求割点

用类似 tarjan 的算法求解割点,对于每个节点,记录其 dfn 序,用 \(low_u\) 表示在搜索树上以 \(u\) 为根的子树中,经过至多一条非树边后能到达的最小的 dfn 序值

对于某一个节点 \(u\),其为割点的充分必要条件是:搜索树上的存在一个 \(u\) 的儿子 \(v\) 满足 \(low_v\ge dfn_u\),也就是说 \(v\) 无论如何不经过 \(u\) 都不能到达 \(u\) 的祖先,如果删除点 \(u\),则搜索树上以 \(v\) 为根的子树会额外形成一个极大连通分量,则 \(u\) 必然是一个割点

特别地,如果 \(u\) 是搜索树的根节点,则满足条件的儿子 \(v\) 至少需要 \(2\)

参考代码如下:

inline void tarjan(int p,int lim) {
	low[p]=dfn[p]=++dfncnt;
	int siz=0;
	for(int v:edge[p]) {
		if(!dfn[v]) {
			tarjan(v,1);
			low[p]=min(low[p],low[v]);
			if(low[v]>=dfn[p]) {
				++siz;
				if(siz>=lim) isCutVertex[p]=true;
			}
		} else low[p]=min(low[p],dfn[v]);
	}
}
signed main() {
    for(int i=1;i<=n;++i) if(!dfn[i]) tarjan(i,2);
}

III. 求桥

用类似 tarjan 的算法求解割点,对于每个节点,记录其 dfn 序,用 \(low_u\) 表示在搜索树上以 \(u\) 为根的子树中,经过至多一条非树边后能到达的最小的 dfn 序值

对于某一条边 \((u,v)\),其为桥的充分必要条件是:搜索树上 \(v\)\(u\) 的儿子且满足 \(low_v> dfn_u\),也就是说 \(v\) 无论如何不经过 \((u,v)\) 都不能到达 \(u\) 的祖先,如果删除边 \((u,v)\),则搜索树上以 \(v\) 为根的子树会额外形成一个极大连通分量,则 \((u,v)\) 必然是一个桥

在记录 \(low_u\) 的时候,要特别注意不能从其搜索树上的父亲处转移最小 dfn 序,所以递归的时候要记录父亲防止错误转移

准确来说,我们的 \(low_u\) 不能从搜索树上的父亲到儿子的树边转移,不过如果图上出现重边,则我们会将 \((u,v)\) 之间的非树边当成树边切断转移

所以,在有重边的图上,我们不能通过记录父亲来防止 \((u,v)\) 连接,我们要记录搜索树上边的编号判断是否通过 \((u,v)\),通过链式前向星存边,将边的下标从 \(0/2\) 开始编号,这样对于每一条边,其反向边的编号就是其本身编号异或 \(1\)

参考代码如下(链式前向星从 \(2\) 开始编号)

inline void tarjan(int p,int uid) {
	dfn[p]=low[p]=++dfncnt;
	for(int i=head[p];i;i=edge[i].lst) {
		int v=edge[i].des;
		if(!dfn[v]) {
			tarjan(v,i);
			low[p]=min(low[p],low[v]);
			if(low[v]>dfn[p]) edge[i].isBridge=edge[i^1].isBridge=true;
		} else if(i!=(uid^1)) low[p]=min(low[p],dfn[v]);
	}
}
signed main() {
    for(int i=1;i<=n;++i) if(!dfn[i]) tarjan(i,0);
}

二、强连通与强连通分量

强连通分量 - OI Wiki

I. 定义

一张有向图 \(\mathbf G\) 是强连通的,当且仅当 \(\mathbf G\) 中任意两个点都联通,而 \(\mathbf G\) 的强连通分量(SCC,Strongly Connected Components)是指 \(\mathbf G\) 的一个极大连通子图

II. 求强连通分量

我们维护每个节点的 dfn 序

\(low_u\) 维护每个节点能够到达的最小节点的 dfn 序,对于非树边直接更新当前节点的 \(low\) 值即可

不难发现每个 SCC 在搜索树中都是一个连通块,并且有且仅有一个节点 \(u\) 满足 \(dfn_u=low_u\),这个节点应该是这个 SCC 中第一个搜索到的节点,所以我们可以通过维护一个栈来将当前 SCC 中所有的节点取出

不过在搜索树中,可能有某些边影响将当前节点连向某个已经在其他 SCC 中的节点,我们只用栈内节点的 dfn 序更新 \(low\)

参考代码如下:

inline void tarjan(int p) {
	low[p]=dfn[p]=++dfncnt;
	st[++top]=p;
	ins[p]=true;
	for(int v:edge[p]) {
		if(!dfn[v]) {
			tarjan(v);
			low[p]=min(low[p],low[v]);
		} else if(ins[v]) low[p]=min(low[p],low[v]);
	}
	if(dfn[p]==low[p]) {
		++scccnt;
		int k;
		do {
			k=st[top--];
			ins[k]=false;
			bel[k]=scccnt;
		} while(k!=p);
	}
}
signed main() {
    for(int i=1;i<=n;++i) if(!dfn[i]) tarjan(i);
}

三、典例分析

I. [洛谷2860]Redundant Paths G

\(\text{Link}\)

思路分析

显然,在一个 E-DCC(Edge-Double Connnected Components,边双连通分量)中的点都满足题意,所以我们可以对原图进行缩点,将每个 E-DCC 缩成一个点,最终会得到一棵树,我们可以考虑对于树上每个 E-DCC 如果其度数 \(\ge 2\),则不需要加边,我们可以每两个度数为 \(1\) 的 E-DCC连一条边,不妨设这一类的 E-DCC 共有 \(k\) 个,则答案为 \(\left\lceil\dfrac{k}{2}\right\rceil\)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=5001,MAXM=1e4+1;
struct node {
	int des,lst;
	bool isBridge;
}	edge[MAXM<<1];
int low[MAXN],head[MAXN],dfn[MAXN],dfncnt,leaf,tot=1;
int bel[MAXN],edcccnt;
vector <int> g[MAXN]; 
inline void link(int u,int v) {
	edge[++tot]=(node){v,head[u]};
	head[u]=tot;
}
inline void tarjan(int p,int id) {
	low[p]=dfn[p]=++dfncnt;
	for(int i=head[p];i;i=edge[i].lst) {
		int v=edge[i].des;
		if(!dfn[v]) {
			tarjan(v,i);
			low[p]=min(low[p],low[v]);
			if(low[v]>dfn[p]) edge[i].isBridge=edge[i^1].isBridge=true;
		} else if(i!=(id^1)) low[p]=min(low[p],dfn[v]);
	}
}
inline void build(int p) {
	bel[p]=edcccnt;
	for(int i=head[p];i;i=edge[i].lst) {
		if(edge[i].isBridge||bel[edge[i].des]) continue;
		build(edge[i].des);
	}
}
inline void dfs(int p,int f) {
	if(g[p].size()==1) ++leaf;
	for(int v:g[p]) {
		if(v==f) continue;
		dfs(v,p);
	}
}
signed main() {
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;++i) {
		int u,v;
		scanf("%d%d",&u,&v);
		link(u,v);
		link(v,u);
	}
	for(int i=1;i<=n;++i) if(!dfn[i]) tarjan(i,0);
	for(int i=1;i<=n;++i) if(!bel[i]) ++edcccnt,build(i);
	for(int i=1;i<=n;++i) {
		for(int j=head[i];j;j=edge[j].lst) {
			if(bel[i]==bel[edge[j].des]) continue;
			g[bel[i]].push_back(bel[edge[j].des]);
		}
	}
	dfs(1,0); 
	printf("%d\n",(leaf+1)/2);
	return 0;
}

II. [洛谷3469] - BLO-Blockade

\(\text{Link}\)

思路分析

简单数数题,考虑对于每一个割点考虑,如果某个点 \(u\) 不是割点,那么答案只可能是点 \(u\) 与其他某个点断连,故答案为 \(2(n-1)\)

如果某个点 \(u\) 是一个割点,那么考虑切断所有与 \(u\) 相连的边,得到若干个连通块,它们的大小分别为 \(siz_1\sim siz_m\),则其答案为:

\[\sum_{i=m}^msiz_i\times(n-siz_i)
\]

回顾割点的求法搜索树上 \(u\) 的满足 \(low_v\ge dfn_u\) 的子节点 \(v\) 的子树成为一个连通块,\(u\) 本身和搜索树上不在满足的其他点构成一个连通块

代码呈现

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1e5+1;
vector <int> edge[MAXN];
int dfn[MAXN],low[MAXN],dfncnt,siz[MAXN],ans[MAXN],n,m;
bool cut[MAXN];
inline void tarjan(int p,int lim) {
	dfn[p]=low[p]=++dfncnt;
	siz[p]=1;
	int cnt=0,sum=0;
	for(int v:edge[p]) {
		if(!dfn[v]) {
			tarjan(v,1);
			siz[p]+=siz[v];
			low[p]=min(low[p],low[v]);
			if(low[v]>=dfn[p]) {
				++cnt;
				ans[p]+=(n-siz[v])*siz[v];
				sum+=siz[v];
				if(cnt>=lim) cut[p]=true;
			}
		} else low[p]=min(low[p],dfn[v]);
	}
	if(!cut[p]) ans[p]=2*(n-1);
	else ans[p]+=(n-1)+(n-sum-1)*(sum+1);
}
signed main() {
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=m;++i) {
		int u,v;
		scanf("%lld%lld",&u,&v);
		edge[u].push_back(v);
		edge[v].push_back(u);
	}
	tarjan(1,2);
	for(int i=1;i<=n;++i) printf("%lld\n",ans[i]);
	return 0;
}

III. [洛谷3627] - 抢掠计划

\(\text{Link}\)

思路分析

首先对于每个 SCC 进行缩点,然后在缩点后的 DAG 上用 SPFA 跑全源最长路即可

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e5+1;
struct node {
	int des,val;
};
vector <node> edge[MAXN],g[MAXN];
int dfn[MAXN],low[MAXN],dfncnt,st[MAXN],top,scccnt,bel[MAXN],w[MAXN],val[MAXN],dis[MAXN];
bool ins[MAXN],ist[MAXN],inq[MAXN];
inline void tarjan(int p) {
	dfn[p]=low[p]=++dfncnt;
	st[++top]=p;
	ins[p]=true;
	for(auto e:edge[p]) {
		int v=e.des;
		if(!dfn[v]) {
			tarjan(v);
			low[p]=min(low[v],low[p]);
		} else if(ins[v]) low[p]=min(low[p],low[v]);
	}
	if(low[p]==dfn[p]) {
		++scccnt;
		int k;
		do {
			k=st[top--];
			ins[k]=false;
			bel[k]=scccnt;
			val[scccnt]+=w[k];
		} while(k!=p);
	}
}
signed main() {
	int n,m,s,p;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;++i) {
		int u,v;
		scanf("%d%d",&u,&v);
		edge[u].push_back((node){v,0});
	}
	for(int i=1;i<=n;++i) scanf("%d",&w[i]);
	for(int i=1;i<=n;++i) if(!dfn[i]) tarjan(i);
	for(int i=1;i<=n;++i) {
		for(auto e:edge[i]) {
			if(bel[i]==bel[e.des]) continue;
			g[bel[i]].push_back((node){bel[e.des],val[bel[e.des]]});
		}
	}
	scanf("%d%d",&s,&p);
	for(int i=1;i<=p;++i) {
		int x;
		scanf("%d",&x);
		ist[x]=true;
	}
	queue <int> q;
	memset(dis,-0x3f,sizeof(dis));
	q.push(bel[s]);
	dis[bel[s]]=val[bel[s]];
	inq[bel[s]]=true;
	while(!q.empty()) {
		int p=q.front();q.pop();
		inq[p]=false;
		for(auto e:g[p]) {
			int v=e.des;
			if(dis[v]<dis[p]+e.val) {
				dis[v]=dis[p]+e.val;
				if(!inq[v]) {
					q.push(v);
					inq[v]=true;
				}
			}
		}
	}
	int res=0;
	for(int i=1;i<=n;++i) if(ist[i]) res=max(res,dis[bel[i]]);
	printf("%d\n",res);
	return 0;
}

IV. [洛谷3119] - Grass Cownoisseur

\(\text{Link}\)

思路分析

跟上一题相似,对 SCC 缩点,为了解决一次逆向行走的限制,我们可以在分层图上求解,第一层的图表示没有逆行,第二层的图表示逆行之后,对于原图中的一条有向边 \(u\to v\),我们可以连接第一层的 \(v\) 和第二层的 \(u\),然后再在分层图上 SPFA 计算最长路即可

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+1;
struct node {
	int des,val;
};
vector <node> edge[MAXN],g[MAXN<<1];
int dfn[MAXN],low[MAXN],dfncnt,st[MAXN],top,scccnt,bel[MAXN],val[MAXN],dis[MAXN<<1];
bool ins[MAXN],inq[MAXN<<1];
inline void tarjan(int p) {
	dfn[p]=low[p]=++dfncnt;
	st[++top]=p;
	ins[p]=true;
	for(auto e:edge[p]) {
		int v=e.des;
		if(!dfn[v]) {
			tarjan(v);
			low[p]=min(low[v],low[p]);
		} else if(ins[v]) low[p]=min(low[p],low[v]);
	}
	if(low[p]==dfn[p]) {
		++scccnt;
		int k;
		do {
			k=st[top--];
			ins[k]=false;
			bel[k]=scccnt;
			++val[scccnt];
		} while(k!=p);
	}
}
signed main() {
	int n,m,s=1;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;++i) {
		int u,v;
		scanf("%d%d",&u,&v);
		edge[u].push_back((node){v,0});
	}
	for(int i=1;i<=n;++i) if(!dfn[i]) tarjan(i);
	for(int i=1;i<=n;++i) {
		for(auto e:edge[i]) {
			if(bel[i]==bel[e.des]) continue;
			g[bel[i]].push_back((node){bel[e.des],val[bel[i]]});
			g[bel[i]+scccnt].push_back((node){bel[e.des]+scccnt,val[bel[i]]});
			g[bel[e.des]].push_back((node){bel[i]+scccnt,val[bel[e.des]]});
		}
	}
	queue <int> q;
	memset(dis,0,sizeof(dis));
	q.push(bel[s]);
	dis[bel[s]]=0;
	inq[bel[s]]=true;
	while(!q.empty()) {
		int p=q.front();q.pop();
		inq[p]=false;
		for(auto e:g[p]) {
			int v=e.des;
			if(dis[v]<dis[p]+e.val) {
				dis[v]=dis[p]+e.val;
				if(!inq[v]) {
					q.push(v);
					inq[v]=true;
				}
			}
		}
	}
	printf("%d\n",max(dis[bel[s]+scccnt],dis[bel[s]]));
	return 0;
}

V. [洛谷2515] - 软件安装

\(\text{Link}\)

思路分析

一道 tarjan 和树形 dp 的复合题,考虑对于每个点 \(i\),将 \(d_i\)\(i\) 连一条边之后,原图会形成一个基环外向树森林,由于一个形成环的依赖关系必须同时全选,考虑对于每个基环树的环缩成一个点,然后建一个虚拟根节点连向所有入度为 \(0\) 的 SCC,然后在树上做一遍简单背包即可

树上依赖性背包大概和选课差不多

时间复杂度 \(\Theta(nm^2)\)

代码呈现

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int MAXN=101,MAXM=501,INF=1e18;
int val[MAXN],wgh[MAXN],d[MAXN],deg[MAXN],n,m,res;
int inv[MAXN],inw[MAXN];
int dp[MAXN][MAXM];
vector <int> edge[MAXN],g[MAXN];
inline void dfs(int p) {
	if(wgh[p]<=m) dp[p][wgh[p]]=val[p];
	for(int v:g[p]) {
		dfs(v);
		for(int i=m;i>=0;--i) {
			for(int j=i;j>=0;--j) {
				dp[p][i]=max(dp[p][i],dp[v][j]+dp[p][i-j]);
			}
		}
	}
	dp[p][0]=0;
}
int dfn[MAXN],low[MAXN],dfncnt,st[MAXN],top,bel[MAXN],scccnt;
bool ins[MAXN];
inline void tarjan(int p) {
	dfn[p]=low[p]=++dfncnt;
	st[++top]=p;
	ins[p]=true;
	for(int v:edge[p]) {
		if(!dfn[v]) {
			tarjan(v);
			low[p]=min(low[v],low[p]);
		} else if(ins[v]) low[p]=min(low[p],low[v]);
	}
	if(dfn[p]==low[p]) {
		++scccnt;
		int k;
		do {
			k=st[top--];
			ins[k]=false;
			bel[k]=scccnt;
			wgh[scccnt]+=inw[k];
			val[scccnt]+=inv[k];
		} while(k!=p);
	}
}
signed main() {
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=n;++i) scanf("%lld",&inw[i]);
	for(int i=1;i<=n;++i) scanf("%lld",&inv[i]);
	for(int i=1;i<=n;++i) {
		scanf("%lld",&d[i]);
		if(d[i]) edge[d[i]].push_back(i);
	}
	for(int i=1;i<=n;++i) if(!dfn[i]) tarjan(i);
	for(int i=1;i<=n;++i) {
		for(int j:edge[i]) {
			if(bel[i]==bel[j]) continue;
			g[bel[i]].push_back(bel[j]);
			++deg[bel[j]];
		}
	}
	for(int i=1;i<=scccnt;++i) if(!deg[i]) g[0].push_back(i);
	memset(dp,-0x3f,sizeof(dp));
	dfs(0);
	for(int i=0;i<=m;++i) res=max(res,dp[0][i]);
	printf("%lld\n",res);
	return 0;
}

VI. [洛谷4334] - Policijia

\(\text{Link}\)

思路分析

tarjan 好题,考虑对于原图建立出 dfs 树并预处理 \(dfn\)\(low\) 数组,在 dfs 树上解决问题,注意 \(low_u\) 不能经过 \(u\) 的父亲

  1. 删边操作

考虑删除边 \((u,v)\) 后使 \(a,b\) 连通的条件,如果 \((u,v)\) 是非树边(返祖边),一定对答案无影响,故假设 \((u,v)\) 是树边,且 \(u\)\(v\) 的父亲

  • \(a,b\) 都在 \(v\) 的子树中
  • \(a,b\) 都不在 \(v\) 的子树中
  • \(a,b\) 有且仅有一个在 \(v\) 的子树中,且 \((u,v)\) 不为桥

根据 dfn 序和桥的判定解决即可

  1. 删点操作

考虑删除点 \(c\) 后,\(a,b\) 所在的 \(c\) 的某棵子树(如果在的话)是否与外界不连通

由于 \(a,b\) 所在的子树中的位置对于答案没有影响,所以如果 \(a,b\)\(c\) 的子树中,我们就用 \(x,y\) 来表示 \(c\) 的儿子中子树包含 \(a,b\) 的节点

那么删除 \(c\) 后,\(a,b\) 依然联通的可能条件如下

  • \(a,b\) 均不在 \(c\) 的子树中
  • 仅有 \(a\)\(c\) 的子树中,且删去 \(c\)\(x\) 依然与外界联通,仅有 \(b\) 同理
  • \(a,b\) 均在 \(c\) 的子树中,且删去 \(c\)\(x,y\) 依然均与外界联通

通过维护每个节点在 dfs 树上的深度并且结合倍增,可以在 \(\Theta(\log n)\) 的时间内求出 \(x,y\)

然后用割点的性质判定即可

时间复杂度 \(\Theta\left( (n+q)\log n\right )\)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+1;
int low[MAXN],dfn[MAXN],dep[MAXN],lid[MAXN],rid[MAXN],dfncnt,fa[MAXN][21];
vector <int> edge[MAXN];
inline void tarjan(int p,int f) {
	dep[p]=dep[f]+1;
	low[p]=dfn[p]=lid[p]=++dfncnt;
	fa[p][0]=f;
	for(int i=1;i<=20;++i) fa[p][i]=fa[fa[p][i-1]][i-1];
	for(int v:edge[p]) {
		if(v==f) continue; 
		if(!dfn[v]) {
			tarjan(v,p);
			low[p]=min(low[p],low[v]);
		} else low[p]=min(low[p],dfn[v]);
	}
	rid[p]=dfncnt;
}
inline int jump(int u,int d) {
	for(int i=20;i>=0;--i) {
		if(d>=(1<<i)) {
			u=fa[u][i];
			d-=(1<<i);
		}
	}
	return u;
}
inline bool subtree(int x,int r) {
	return lid[r]<=lid[x]&&rid[x]<=rid[r];
}
inline bool CutEdge(int x,int y,int a,int b) {
	if(dep[x]<dep[y]) swap(x,y);
	if(fa[x][0]!=y) return false;
	if(subtree(a,x)==subtree(b,x)) return false;
	return low[x]>dfn[y];
}
inline bool CutNode(int x,int a,int b) {
	if((!subtree(a,x))&&(!subtree(b,x))) return false;
	if(subtree(a,x)&&subtree(b,x)) {
		a=jump(a,dep[a]-dep[x]-1);
		b=jump(b,dep[b]-dep[x]-1);
		if(a==b) return false;
		return low[a]>=dfn[x]||low[b]>=dfn[x];
	} else {
		if(subtree(a,x)) {
			a=jump(a,dep[a]-dep[x]-1);
			return low[a]>=dfn[x];
		} else {
			b=jump(b,dep[b]-dep[x]-1);
			return low[b]>=dfn[x];
		}
	}
}
signed main() {
	int n,m,q;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;++i) {
		int u,v;
		scanf("%d%d",&u,&v);
		edge[u].push_back(v);
		edge[v].push_back(u);
	}
	tarjan(1,1);
	scanf("%d",&q);
	while(q--) {
		int opt;
		scanf("%d",&opt);
		if(opt==1) {
			int x,y,a,b;
			scanf("%d%d%d%d",&a,&b,&x,&y);
			puts(CutEdge(x,y,a,b)?"no":"yes");
		}
		if(opt==2) {
			int x,a,b;
			scanf("%d%d%d",&a,&b,&x);
			puts(CutNode(x,a,b)?"no":"yes");
		}
	}
	return 0;
}

VII. [洛谷5234] - 越狱老虎桥

\(\text{Link}\)

思路分析

显然,任何一条不是桥的边都不会对答案造成影响(因为在一个 E-DCC 中,任意删去一条边都不可能使图不连通)

所以我们可以对原图建立 dfs 搜索树并且求出桥,将所有树上非桥的边的边权都设为 \(+\infty\)

假设连了某一条边 \((u,v)\) ,使得答案为 \(k\),则所有边权 \(<k\) 的边都应该在简单路径 \((u,v)\)

考虑对答案二分,如果需要检查某个 \(k\) 是否可能成为答案,我们只需要找到一条路径 \((u,v)\) 覆盖所有边权 \(<k\) 的边即可

因此我们可以对每条边权 \(<k\) 的边设为 \(1\) 否则设为 \(0\),然后用这个边权求一遍树的直径,只需要检查直径长度是否等于边权 \(<k\) 的边的数量即可

时间复杂度 \(\Theta(n\log T)\)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e5+1,INF=1e9;
struct node {
	int des,val,lst;
} edge[MAXN<<1],tr[MAXN<<1];
int low[MAXN],dfn[MAXN],head1[MAXN],head2[MAXN],cnt[MAXN],dp[MAXN],tot=0,ans=0,dfncnt,totedge=1,tottree=0,n,m;
inline void addedge(int u,int v,int w) {
	edge[++totedge]=(node){v,w,head1[u]};
	head1[u]=totedge;
}
inline void addtree(int u,int v,int w) {
	tr[++tottree]=(node){v,w,head2[u]};
	head2[u]=tottree;
}
inline void tarjan(int p,int uid) {
	low[p]=dfn[p]=++dfncnt;
	for(int i=head1[p];i;i=edge[i].lst) {
		int v=edge[i].des;
		if(!dfn[v]) {
			tarjan(v,i);
			low[p]=min(low[p],low[v]);
			if(low[v]<=dfn[p]) addtree(p,v,INF);
			else addtree(p,v,edge[i].val);
		} else if(i!=(uid^1)) low[p]=min(low[p],dfn[v]);
	}
	return ;
}
inline void dfs(int p,int l) {
	vector <int> vec;
	if(!head2[p]) dp[p]=cnt[p];
	for(int i=head2[p];i;i=tr[i].lst) {
		int v=tr[i].des;
		cnt[v]=cnt[p];
		if(tr[i].val<l) ++cnt[v],++tot;
		dfs(v,l);
		vec.push_back(dp[v]);
	}
	sort(vec.begin(),vec.end(),greater<int>());
	if(vec.size()>0) dp[p]=vec[0],ans=max(vec[0]-cnt[p],ans);
	if(vec.size()>1) ans=max(ans,vec[0]+vec[1]-cnt[p]*2);
}
inline bool check(int x) {
	memset(cnt,0,sizeof(cnt));
	memset(dp,0,sizeof(dp));
	ans=tot=0;
	dfs(1,x);
	return ans==tot;
}
signed main() {
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;++i) {
		int u,v,w;
		scanf("%d%d%d",&u,&v,&w);
		addedge(u,v,w);
		addedge(v,u,w);
	}
	tarjan(1,0);
	int l=0,r=INF,res=-1;
	while(l<=r) {
		int mid=(l+r)>>1;
		if(check(mid)) l=mid+1,res=mid;
		else r=mid-1;
	}
	if(res==INF) res=-1;
	printf("%d\n",res);
	return 0;
}