【学习笔记】图论杂谈(一)

# 壹:【Johnson】

无负权边:直接跑 \(n\)\(dijkstra\)
这里主要讨论带负权边的情况,其主要目的就是把负权边,转化为非负权边,然后 \(v\)\(dijkstra\)
因为无向图必然存在负环,所以这里讨论有向图
显而易见的想法是对于每条边 \(e_i\) 分别加 \(k_i\)
使得原最短路在加权之后还是最短路(限制一),并且图每条边权值非负(限制二)

一:【做法】

一个做法是,建超级原点,跑 \(SPFA\) 计算原点到其他点的最短路 \(f_i\),同时判负环
对于一条边 \(e_i (u->v)\) ,它的权值加上 \(f_u - f_v\) ,可以满足上面两条限制

二:【证明】

1.【限制一】

对于 $u -> a_{1,2...n} -> v $ 的一条路径,设原路径权值和为 \(t\) ,加权后最短路的权值为 \(f_u - f_{a_1} + f_{a_1} -...- f_v + t = f_u - f_v + t\)
可以发现只与变化量 \(u , v\) 有关,所以原最短路不变

2.【限制二】

跑完 \(SPFA\) 之后,一定满足 \(f[u]+w>=f[v]\)\(w+f[u]-f[v]>=0\)

三.【Code】

P5905代码
#include<bits/stdc++.h>
#define Pair pair<int,int>
#define w first
#define v second
#define inf 1e9
using namespace std;
typedef long long LL;
const int N=3010;int n;
vector<Pair> mp[N];
int f[N];
int vis[N],step[N];
bool SPFA(){
	for(int i=1;i<N;i++) f[i]=inf;
	queue<int> q;
	q.push(0);vis[0]=1;
	while(q.size()){
		int u=q.front();q.pop();vis[u]=0;
		for(auto e:mp[u]){
			int v=e.v,w=e.w;
			if(f[v]>f[u]+w){
				f[v]=f[u]+w;
				step[v]=step[u]+1;
				if(step[v]>=n+2) return 1;//n+k保证一定不会WA
				
				if(vis[v]) continue;
				q.push(v);
				vis[v]=1;
			}
		}
	}
	return 0;
}
int dis[N];
int cl(int v,int u){
	if(dis[v]==inf) return dis[v];
	return dis[v]+f[v]-f[u];
}
LL dijkstra(int s){
	priority_queue<Pair,vector<Pair>,greater<Pair> > q;
	for(int i=0;i<N;i++){
		dis[i]=inf;
		vis[i]=0;
	}
	q.push({0,s});dis[s]=0;
	while(q.size()){
		int u=q.top().v;q.pop();
		if(vis[u]) continue;
		vis[u]=1;
		for(auto e:mp[u]){
			int w=e.w,v=e.v;
			if(dis[v]>dis[u]+w){
				dis[v]=dis[u]+w;
				q.push({dis[v],v});
			}
		}
	}
	LL ans=0;
	for(int i=1;i<=n;i++) ans+=1LL*i*cl(i,s);
	return ans;
}
int main(){
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	int m;cin>>n>>m;
	while(m--){
		int u,v,w;cin>>u>>v>>w;
		mp[u].push_back({w,v});
	}
	for(int i=1;i<=n;i++) mp[0].push_back({0,i});
	if(SPFA()){
		cout<<-1<<"\n";
		return 0;
	}
	for(int u=1;u<=n;u++){
		for(auto &e:mp[u]) e.w+=f[u]-f[e.v];	
	}
	for(int u=1;u<=n;u++){
		cout<<dijkstra(u)<<"\n";
	}
	return 0;
}

四:【EX】

当然,如果你不怕被卡,可以直接跑 \(n\)\(SPFA\)

# 贰:【最短路树】

满足以下性质的被称为最短路树:

  • 原图的生成树
  • 根节点到其他节点路径长度是原图中的最短路长度
    可以跑\(dijkstra\),记录前驱生成

一:【最短路DAG】

\(u->v\) 最短路长度为 \(w\) ,把 \(u->v\) 所有长度为 \(w\) 的路径的每条边加入边集当中
对于每个 \(v\) 都做一遍,对边集去重得到的生成图,被称为最短路 \(DAG\)
最短路 \(DAG\) 的每一个生成树都是一颗最短路树
实际构造时,可以先跑一遍最短路
然后再跑一遍,记录前驱

CF545E
#include<bits/stdc++.h>
#define int long long
#define Pair pair<int,int>
#define w first
#define v second
#define inf 1e15
using namespace std;
typedef long long LL;
const int N=3e5+10;
struct node{
	LL w,v,id;
};
vector<node> mp[N];
LL vis[N],dis[N];
void dijkstra(int s){
	for(int i=0;i<N;i++) dis[i]=inf;
	priority_queue<Pair,vector<Pair>,greater<Pair> > q;
	q.push({0,s});dis[s]=0;
	while(q.size()){
		int u=q.top().v;q.pop();
		if(vis[u]) continue;
		vis[u]=1;
		for(auto e:mp[u]){
			int w=e.w,v=e.v;
			if(dis[v]>dis[u]+w){
				dis[v]=dis[u]+w;
				q.push({dis[v],v});
			}
		}
	}
}
vector<node> qq[N];
signed main(){
	int n,m;cin>>n>>m;
	for(int i=1;i<=m;i++){
		int u,v,w;cin>>u>>v>>w;
		mp[u].push_back({w,v,i});
		mp[v].push_back({w,u,i});
	}
	int s;cin>>s;
	dijkstra(s);
	for(int u=1;u<=n;u++){
		for(auto e:mp[u]){
			int v=e.v,w=e.w;
			if(dis[v]==dis[u]+w) qq[v].push_back({w,u,e.id});
		}
	}
	LL ans=0;
	vector<int> xl;
	for(int u=1;u<=n;u++){
		LL mn=inf,tp;
		for(auto e:qq[u]){
			if(e.w<mn){
				mn=e.w;
				tp=e.id;
			}
		}
		if(mn==inf) continue;//root
		ans+=mn;
		xl.push_back(tp);
	}
	cout<<ans<<"\n";
	sort(xl.begin(),xl.end());
	for(auto v:xl) cout<<v<<" ";cout<<"\n";
	return 0;
}
//建最短路DAG
//对于每个点u,在DAG中保留边权最小的入边,构造最短路树

//当然也可以直接建最短路树

二:【删边最短路】

给定一张无向图(有向应该也可以这样求解),求删除每一条边后 \(1->n\) 的最短路
记录一条最短路径 \(st\) ,如果删边不在此最短路径上,则答案不变
如果有多条最短路径,我们认为其他最短路径不为最短路径,即不做考虑,容易证明这样不会对答案造成影响

所以我们只讨论最短路径上边的答案维护
首先剖出1的最短路树 \(T1\)\(n\) 的最短路树 \(T2\)(注意,不是最短路 \(DAG\)

容易猜出一个性质:每删除一条边后,\(1->n\) 的最短路径有且仅有一条边不在最短路树(即 \(T1\)\(T2\) )上

证明
1.【有】如果都在最短路树上,则此路径一定是最短路径 \(st\) ,与题设矛盾
2.【仅有】假设有两条边,则有一条边一定可以被最短路径树上的边给松弛掉
对于每次删边,所以我们可以边 \((u,v,w)\) 满足其不在最短路树上,然后用 \(T1(u)+w+T2(v)\) 更新求最小值,这样做的复杂度为 \(O(m^2)\)

我们可以先枚举 \((u,v,w)\) ,在看有哪些边被删后需要用到 \((u,v,w)\) ,更新其最小值
容易发现,\(u\)\(T1\) 上的祖宗不能被更新,\(v\)\(T2\) 上的祖宗不能被更新,且只需要更新\(st\)上的边
所以我们可以定义\(u'=T1.LCA(u,n),v'=T2.LCA(v,1)\),更新\(u'->v'\)上的边的信息,线段树加速
复杂度\(O(mlogm)\)

总结:猜出性质,建树,枚举\((u,v,w)\),卡范围,线段树维护

P2685 [TJOI2012] 桥
#include<bits/stdc++.h>
#define Pair pair<int,int>
#define w first
#define v second
#define inf 1e9+10
using namespace std;
const int N=1e5+10,M=4*N;int n;
struct Paid{
	int w,v,id;
};
vector<Paid> mp[N];
struct Edge{
	int u,v,w;	
}edg[M];
struct Tree{
	vector<Paid> T[N];
	int dis[N],vis[N];
	Paid pre[N];
	void Dijkstra(int s){
		for(int i=0;i<N;i++){
			dis[i]=inf;
			vis[i]=0;
		}
		priority_queue<Pair,vector<Pair>,greater<Pair> > q;
		q.push({0,s});dis[s]=0;
		while(q.size()){
			int u=q.top().v;q.pop();
			if(vis[u]) continue;
			vis[u]=1;
			
			for(auto e:mp[u]){
				int v=e.v,w=e.w;
				if(dis[v]>dis[u]+w){
					dis[v]=dis[u]+w;
					q.push({dis[v],v});
					pre[v]={w,u,e.id};
				}
			}			
		}
	}
	void make(){
		for(int i=1;i<=n;i++){
			if(pre[i].v==0) continue;
			T[pre[i].v].push_back({pre[i].w,i,pre[i].id});
			T[i].push_back({pre[i].w,pre[i].v,pre[i].id});
		}
	}
	int son[N],top[N],fa[N],dep[N],siz[N],cnt=0;
	void Son(int u,int pa){
		fa[u]=pa;
		dep[u]=dep[pa]+1;
		siz[u]=1;
		for(auto e:T[u]){
			int v=e.v;
			if(v==pa) continue;
			Son(v,u);
			if(siz[v]>siz[son[u]]) son[u]=v;
			siz[u]+=siz[v];
		}
	}
	void Line(int u,int tp){
		top[u]=tp;
		if(!son[u]) return ;
		Line(son[u],tp);
		for(auto e:T[u]){
			int v=e.v;
			if(v==son[u]||v==fa[u]) continue;
			Line(v,v);
		}
	}
	int LCA(int a,int b){
		while(top[a]!=top[b]){
			if(dep[top[a]]<dep[top[b]]) swap(a,b);
			a=fa[top[a]];
		}
		if(dep[a]>dep[b]) swap(a,b);
		return a;
	}
}T1,T2;
int dfn[N];
int st[M];
int nod[M];
int tree[N<<2];
void update(int q,int l,int r,int L,int R,int d){
	if(L>R) return ;
	if(L<=l&&r<=R){
		tree[q]=min(tree[q],d);
		return ;
	}
	int mid=(l+r)>>1;
	if(L<=mid) update(q<<1,l,mid,L,R,d);
	if(mid<R) update(q<<1|1,mid+1,r,L,R,d);
}
int query(int q,int l,int r,int tp){
	if(l==r) return tree[q];
	int ans=tree[q];
	int mid=(l+r)>>1;
	if(tp<=mid) ans=min(ans,query(q<<1,l,mid,tp));
	else ans=min(ans,query(q<<1|1,mid+1,r,tp));
	return ans;
}
int as[M];
int main(){
	int m;cin>>n>>m;
	for(int i=1;i<=m;i++){
		int u,v,w;cin>>u>>v>>w;
		edg[i]={u,v,w};
		mp[u].push_back({w,v,i});
		mp[v].push_back({w,u,i});
	}
	for(int i=1;i<=m;i++) edg[i+m]={edg[i].v,edg[i].u,edg[i].w};
	
	T2.Dijkstra(n);T2.make();
	int cnt=0;
	int now=1;
	while(now!=n){
		st[T2.pre[now].id]=1;
		dfn[now]=++cnt;
		now=T2.pre[now].v;
	}dfn[n]=++cnt;
	T1.Dijkstra(1);
	for(int i=1;i<=n;i++){
		int k=T2.pre[i].v;
		if(!dfn[k]||!dfn[i]) continue;
		T1.pre[k]={T2.pre[i].w,i,T2.pre[i].id};
	}
	T1.make();
	
	T1.Son(1,0);T1.Line(1,1);
	T2.Son(n,0);T2.Line(n,n);
	for(int i=1;i<=2*m;i++){
		if(!st[i]) continue;
		int u=edg[i].u,v=edg[i].v;
		if(T1.dep[u]>T1.dep[v]) swap(u,v);
		nod[i]=u;
	}
	for(int i=0;i<(N<<2);i++) tree[i]=inf;
	for(int i=1;i<=2*m;i++){
		if(st[i]||(i>m&&st[i-m])) continue;
		int u=edg[i].u,v=edg[i].v,w=T1.dis[u]+T2.dis[v]+edg[i].w;
		u=T1.LCA(u,n);v=T1.fa[T2.LCA(v,1)];
		update(1,1,cnt,max(1,dfn[u]),dfn[v],w);
	}
	for(int i=1;i<=m;i++){
		if(!st[i]) as[i]=T1.dis[n];
		else as[i]=query(1,1,cnt,dfn[nod[i]]);
	}
	int mx=0,num=0;
	for(int i=1;i<=m;i++){
		if(as[i]>mx){
			mx=as[i];
			num=1;
		}
		else if(as[i]==mx) num++;
	}
	cout<<mx<<" "<<num<<"\n";
	return 0;
}
//建T2的时候,要保证T1到n的路径,和T2到1的路径重合
//维护信息的时候,把边的信息记录在点上

# 参:【平面图最小割】

平面图:除顶点外处处无边相交的图
这里只考虑网格图,平面图可以很容易被拓展
给定一张网格图,删除一组边,使得不存在一条从左上走到右下的一条路径,且删边权值和最小

一:【对偶图】

每相邻的四个点会围成一个面,我们对于每一个面建点,每相邻两个面连边,边权即为与原图交叉的边的权值
然后我们再人工把网格图外的面切割成两部分,左下建点,右上建点,分别连边
此时我们造出来了一张新图,我们称之为对偶图
例如
对偶图

二:【how to 求最小割】

容易发现,对偶图的最短路即为原图的最小割

三:【EX】

也可以跑网络流

P4001 [ICPC-Beijing 2006] 狼抓兔子
#include<bits/stdc++.h>
#define Pair pair<int,int>
#define w first
#define v second
#define inf 1e9+10
using namespace std;
const int N=999*2*999+10;int n,m;
vector<Pair> mp[N];
int S,T;
int id(int x,int y,int o){
	if(y==0||x==n) return S;
	if(y==m||x==0) return T;
	return (x-1)*(m-1)*2+2*(y-1)+1+o;
}
int dis[N],vis[N];
int main(){
	cin>>n>>m;
	S=0;
	T=(n-1)*(m-1)*2+1;
	for(int i=1;i<=n;i++){
		for(int j=1;j<m;j++){
			int w;cin>>w;
			int a=id(i-1,j,0),b=id(i,j,1);
			mp[a].push_back({w,b});
			mp[b].push_back({w,a});
		}
	}
	for(int i=1;i<n;i++){
		for(int j=1;j<=m;j++){
			int w;cin>>w;
			int a=id(i,j-1,1),b=id(i,j,0);
			mp[a].push_back({w,b});
			mp[b].push_back({w,a});
		}
	}
	for(int i=1;i<n;i++){
		for(int j=1;j<m;j++){
			int w;cin>>w;
			int a=id(i,j,0),b=id(i,j,1);
			mp[a].push_back({w,b});
			mp[b].push_back({w,a});
		}
	}	
	for(int i=0;i<N;i++){
		dis[i]=inf;
		vis[i]=0;
	}
	priority_queue<Pair,vector<Pair>,greater<Pair> > q;
	q.push({0,S});dis[S]=0;
	while(q.size()){
		int u=q.top().v;q.pop();
		if(vis[u]) continue;
		vis[u]=1;
		for(auto e:mp[u]){
			int v=e.v,w=e.w;
			if(dis[v]>dis[u]+w){
				dis[v]=dis[u]+w;
				q.push({dis[v],v});
			}
		}
	}
	cout<<dis[T]<<"\n";
	return 0;
}
//S 纵0横n
//T 纵m横0

# 肆:【Tarjan求连通性问题】

【连通分量定义】
  • 满足某条性质的极大的连通块
【通用求法】
  • \(Tarjan\)算法跑一遍
  • 即构建\(dfs\)优先生成树,构建的过程中,记录\(low=dfn=++cnt\),同时用栈存未被标记节点
  • \(low==dfn\),栈反复弹出直到弹到\(x\)或弹出\(x\),打上标记
  • 以下复杂度都为\(O(n+m)\)
    以下具体情况,具体分析

一:【点双连通分量】

基于无向图

1.【非根节点】

满足\(low==dfn\),此节点即为割点
栈反复弹出直到弹到\(x\)\(x\)不弹出,因为\(x\)属于多个点双)

2.【根节点】

如果\(dfs\)优先生成树中,只有一个子树,则不为割点,否则为割点
当然它有几个子树,它就属于几个点双
因此实际操作的时候,不需要分类讨论,但是求割点的时候需要分类
不过由于一个点可能属于多个点双,所以写法略有不同

P8435 【模板】点双连通分量
#include<bits/stdc++.h>
#define Pair pair<int,int>
#define v first
#define id second
using namespace std;
const int N=5e5+10;int n;
vector<int> mp[N];
int low[N],dfn[N],cnt;int bccidx;
vector<int> st;
vector<int> bl[N];
void BCC(int u,int fa){
	st.push_back(u);
	low[u]=dfn[u]=++cnt;
	if(mp[u].empty()){//独立点判断
		bccidx++;
		bl[bccidx].push_back(u);
		return ;
	}
	for(auto v:mp[u]){
		if(v==fa) continue;
		if(!dfn[v]){
			BCC(v,u);
			low[u]=min(low[u],low[v]);
			if(low[v]>=dfn[u]){//只考虑当前子树
				bccidx++;
				while(1){
					int x=st.back();st.pop_back();
					bl[bccidx].push_back(x);
					if(x==v) break;
				}
				bl[bccidx].push_back(u);
			}
		}
		else low[u]=min(low[u],dfn[v]);
	}
}
void Tarjan(){
	for(int i=1;i<=n;i++){
		if(dfn[i]==0) BCC(i,i);
	}
}
int main(){
	int m;cin>>n>>m;
	for(int i=1;i<=m;i++){
		int a,b;cin>>a>>b;
		if(a==b) continue;//独立点判断前提(去自环)
		mp[a].push_back(b);
		mp[b].push_back(a);
	}
	Tarjan();
	int ans=0;
	for(int i=1;i<=n;i++){
		if(bl[i].size()) ans++;
	}
	cout<<ans<<"\n";
	for(int i=1;i<=n;i++){
		if(bl[i].empty()) continue;
		cout<<bl[i].size()<<" ";
		for(auto v:bl[i]) cout<<v<<" ";cout<<"\n";
	}
	return 0;
}

二:【边双连通分量】

基于无向图
一条边,一个点,最多属于一个边双,所以边双的求法要比点双简单一点
满足\(low==dfn\),此节点到父亲的边为割边(桥)
栈反复弹出直到弹出\(x\)

P8436 【模板】边双连通分量
#include<bits/stdc++.h>
#define Pair pair<int,int>
#define v first
#define id second
using namespace std;
const int N=5e5+10;int n;
vector<Pair> mp[N];
int bcc[N],low[N],dfn[N],cnt;int bccidx;
vector<int> st;
void BCC(int u,int fat){
	st.push_back(u);
	low[u]=dfn[u]=++cnt;
	for(auto e:mp[u]){
		int v=e.v,id=e.id;
		if(id==fat) continue;
		if(!dfn[v]){
			BCC(v,e.id);
			low[u]=min(low[u],low[v]);
		}
		else low[u]=min(low[u],dfn[v]);//无向图不需要考虑横叉边
	}
	if(low[u]==dfn[u]){
		++bccidx;
		while(1){
			int x=st.back();st.pop_back();
			bcc[x]=bccidx;
			if(x==u) break;
		}
	}
}
void Tarjan(){
	for(int i=1;i<=n;i++){
		if(dfn[i]==0) BCC(i,0);
	}
}
vector<int> bl[N];
int main(){
	int m;cin>>n>>m;
	for(int i=1;i<=m;i++){
		int a,b;cin>>a>>b;
		mp[a].push_back({b,i});
		mp[b].push_back({a,i});
	}
	Tarjan();
	for(int i=1;i<=n;i++) bl[bcc[i]].push_back(i);
	int ans=0;
	for(int i=1;i<=n;i++){
		if(bl[i].size()) ans++;
	}
	cout<<ans<<"\n";
	for(int i=1;i<=n;i++){
		if(bl[i].empty()) continue;
		cout<<bl[i].size()<<" ";
		for(auto v:bl[i]) cout<<v<<" ";cout<<"\n";
	}
	return 0;
}
//注意有重边
P4652 [CEOI 2017] One-Way Streets
#include<bits/stdc++.h>
#define Pair pair<int,int>
#define v first
#define id second
using namespace std;
const int N=1e5+10;int n;
struct Edge{
	int u,v,as;
}edg[N];
vector<Pair> mp[N];
int bcc[N],dfn[N],low[N],cnt;int idx;
vector<int> st;
void Tarjan(int u,int fae){
	dfn[u]=low[u]=++cnt;
	st.push_back(u);
	for(auto e:mp[u]){
		int v=e.v;
		if(e.id==fae) continue;
		if(!dfn[v]){
			Tarjan(v,e.id);
			low[u]=min(low[u],low[v]);
		}
		else low[u]=min(low[u],dfn[v]);
	}
	if(low[u]==dfn[u]){
		idx++;
		while(1){
			int x=st.back();st.pop_back();
			bcc[x]=idx;
			if(x==u) break;
		}
	}
}
vector<Pair> sq[N];
void BCC(){
	for(int i=1;i<=n;i++){
		if(dfn[i]==0) Tarjan(i,0);
	}
	for(int u=1;u<=n;u++){
		for(auto e:mp[u]){
			int v=e.v;
			if(bcc[u]==bcc[v]){
				edg[e.id].as=-1;
				continue;
			}
			sq[bcc[u]].push_back({bcc[v],e.id});
		}
	}
}
int sum[N];
int vis[N];
void dfs(int u,int fa){
	if(vis[u]) return ;
	vis[u]=1;
	for(auto e:sq[u]){
		int v=e.v;
		if(v==fa) continue;
		dfs(v,u);
		sum[u]+=sum[v];
		if(sum[v]==0) edg[e.id].as=-1;
		else if(sum[v]>0) edg[e.id].as=u;
		else edg[e.id].as=v; 
	}
}
int main(){
	int m;cin>>n>>m;
	for(int i=1;i<=m;i++){
		int a,b;cin>>a>>b;
		edg[i]={a,b,0};
		mp[a].push_back({b,i});
		mp[b].push_back({a,i});
	}
	BCC();
	for(int i=1;i<=m;i++){
		edg[i].u=bcc[edg[i].u];
		edg[i].v=bcc[edg[i].v];
	}
	int q;cin>>q;
	while(q--){
		int x,y;cin>>x>>y;
		sum[bcc[x]]++;sum[bcc[y]]--;
	}
	for(int i=1;i<=idx;i++){
		if(vis[i]) continue;
		dfs(i,0);
	}
	for(int i=1;i<=m;i++){
		int u=edg[i].u,v=edg[i].v,as=edg[i].as;
		if(as==-1) cout<<"B";
		else if(as==u) cout<<"L";
		else cout<<"R";
	}
	return 0;
}
//搜边双,边双可以按照SCC的方式建边,共两种,所以边双内的边都是B
//然后边双缩点
//x->y x处标记++ y处--
//然后统计子树和,判断正负号

三:【强连通分量】

\(dfn==low\),则反复弹栈,直到x被弹出

P2863 [USACO06JAN] The Cow Prom S
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+10;
vector<int> mp[N];
int n;
int dfn[N],low[N],cnt;
int scc[N];
vector<int> st;int idx;
void Tarjan(int u){
	dfn[u]=low[u]=++cnt;
	st.push_back(u);
	for(auto v:mp[u]){
		if(!dfn[v]){
			Tarjan(v);
			low[u]=min(low[u],low[v]);
		}
		else if(!scc[v]) low[u]=min(low[u],dfn[v]);
	}
	if(low[u]==dfn[u]){
		idx++;
		while(1){
			int x=st.back();st.pop_back();
			scc[x]=idx;
			if(x==u) break;
		}
	}
}
int siz[N];
void SCC(){
	for(int i=1;i<=n;i++){
		if(dfn[i]==0) Tarjan(i);
	}
	for(int i=1;i<=n;i++) siz[scc[i]]++;
}
int main(){
	int m;cin>>n>>m;
	while(m--){
		int a,b;cin>>a>>b;
		mp[a].push_back(b);
	}
	SCC();
	int ans=0;
	for(int i=1;i<=idx;i++) ans+=(siz[i]>1);
	cout<<ans<<"\n";
	return 0;
}

四:【性质】

以上连通分量的性质可以直接由定义出发,不过还有一个性质,即为同连通分量内任意两个点都可以同属一个回路
然后可以基于这些性质进行推论,不做展开

# 伍:【圆方树】

其实并不是什么高端的东西,类似于强连通分量和边双缩点
由于割点属于至少两个点双,所以缩点就不一定有良好性质
我们换个想法,建立圆方树

一:【构造】

一个点双连通分量生成一个新的节点,我们称之为方点,而原图中的点我们称之为圆点
将方点和它对应的圆点连边
我们就用一张图造出了一颗圆方树,然后可以在方点上统一维护信息
构造过程如图示
构造1
构造2
构造3

二:【性质】

  • 相邻点形状不同
  • 度数\(>1\)的圆点,在原图中是割点
  • 方点度数是点双大小
CF487E Tourists
#include<bits/stdc++.h>
#define inf 1e9+10
using namespace std;
const int N=2e5+10;int n;
vector<int> mp[N];
int nw[N];
int dfn[N],low[N],cnt;
vector<int> st;int idx;
vector<int> bl[N];
void Tarjan(int u,int fa){
	dfn[u]=low[u]=++cnt;
	st.push_back(u);
	for(auto v:mp[u]){
		if(v==fa) continue;
		if(!dfn[v]){
			Tarjan(v,u);
			low[u]=min(low[u],low[v]);
			if(low[v]>=dfn[u]){
				idx++;
				while(1){
					int x=st.back();st.pop_back();
					bl[idx].push_back(x);
					if(x==v) break;
				}bl[idx].push_back(u);
			}
		}
		else low[u]=min(low[u],dfn[v]);
	}
}
vector<int> sq[N];
multiset<int> xl[N];
void BCC(){
	for(int i=1;i<=n;i++){
		if(dfn[i]==0) Tarjan(i,0);
	}
	for(int u=n+1;u<=idx;u++){
		for(auto v:bl[u]){
			sq[u].push_back(v);
			sq[v].push_back(u);
		}
	}
}
int fa[N],son[N],siz[N],dep[N];
void Son(int u,int pa){
	fa[u]=pa;
	siz[u]=1;
	dep[u]=dep[pa]+1;
	for(auto v:sq[u]){
		if(v==pa) continue;
		if(u>n) xl[u].insert(nw[v]);
		Son(v,u);
		siz[u]+=siz[v];
		if(siz[v]>siz[son[u]]) son[u]=v;
	}
}
int top[N],tot;//dfn[N]
void Line(int u,int tp){
	dfn[u]=++tot;
	top[u]=tp;
	if(!son[u]) return ;
	Line(son[u],tp);
	for(auto v:sq[u]){
		if(v==son[u]||v==fa[u]) continue;
		Line(v,v);
	}
}
int tree[N<<2];
void push_up(int q){
	tree[q]=min(tree[q<<1],tree[q<<1|1]);
}
void update(int q,int l,int r,int tp,int d){
	if(l==r){
		tree[q]=d;
		return ;
	}
	int mid=l+r>>1;
	if(tp<=mid) update(q<<1,l,mid,tp,d);
	else update(q<<1|1,mid+1,r,tp,d);
	push_up(q);
}
int query(int q,int l,int r,int L,int R){
	if(L<=l&&r<=R){
		return tree[q];
	}
	int ans=inf;
	int mid=l+r>>1;
	if(L<=mid) ans=min(ans,query(q<<1,l,mid,L,R));
	if(mid<R) ans=min(ans,query(q<<1|1,mid+1,r,L,R));
	return ans;
}
int solve(int a,int b){
	int ans=inf;
	while(top[a]!=top[b]){
		if(dep[top[a]]<dep[top[b]]) swap(a,b);
		ans=min(ans,query(1,1,idx,dfn[top[a]],dfn[a]));
		a=fa[top[a]];
	}
	if(dep[a]>dep[b]) swap(a,b);
	ans=min(ans,query(1,1,idx,dfn[a],dfn[b]));
	if(a>n) ans=min(ans,nw[fa[a]]);
	return ans;
}
int main(){
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	int m,q;cin>>n>>m>>q;
	idx=n;
	for(int i=0;i<N;i++) nw[i]=inf;
	for(int i=1;i<=n;i++) cin>>nw[i];
	while(m--){
		int a,b;cin>>a>>b;
		mp[a].push_back(b);
		mp[b].push_back(a);
	}
	BCC();
	Son(1,0);
	Line(1,1);
	for(int i=0;i<(N<<2);i++) tree[i]=inf;
	for(int i=1;i<=n;i++) update(1,1,idx,dfn[i],nw[i]);
	for(int i=n+1;i<=idx;i++) update(1,1,idx,dfn[i],*xl[i].begin());
	while(q--){
		char op;cin>>op;
		int a,b;cin>>a>>b;
		if(op=='C'){
			if(fa[a]){
				xl[fa[a]].erase(xl[fa[a]].find(nw[a]));
				xl[fa[a]].insert(b);
				update(1,1,idx,dfn[fa[a]],*xl[fa[a]].begin());	
			}
			
			
			nw[a]=b;
			update(1,1,idx,dfn[a],nw[a]);
		}
		else{
			cout<<solve(a,b)<<"\n";
		}
	}
	return 0;
}
//建圆方树,方点用multiset维护周围圆点最小值
//修改时用圆点更新周围方点multiset
//然后树链剖分加速询问
//但是如果出现菊花图,原点周围可能出现非常非常多的方点,会被卡掉
//这里有一个Trick,只维护修改父节点位置的方点,其他位置不管,在solve函数的最后一步取min即可

//这里进行一个拓展:如果把一个点不能被经过多次 改成 一条边不能经过多次该怎么做
//我的想法是,边双缩点,树剖加速,实际上更简单了

# 陆:【Boruvka】

一:【实现】

即为多路合并\(Prim\)
每次遍历所有连通块,对于每个连通块,求其与其他连通块连边的权值最小值,进行合并
如果以边来遍历,复杂度\(O(mlogn)\)
以点来遍历,复杂度一般为\(O(nlog^2n)\),因为一般需要数据结构加速

二:【应用】

这个算法稠密图没Prim效率高,稀疏图没Kruskal效率高
但在特殊问题下是杀招
这类特殊条件形如 给你一个完全图,完全图上的边权可以通过端点的点权经过某种计算得出,求最小生成树
这类问题,\(kruskal-O(n^2 log n^2),Prim-O(n^4)\),都解决不了
但是如果用\(Boruvka\)的话,以点来遍历,数据结构(比如线段树)维护,复杂度 \(O(nlog^2n)\)

P3366 【模板】最小生成树
#include<bits/stdc++.h>
#define Pair pair<int,int>
#define w first
#define v second
#define inf 2e9+10;
using namespace std;
const int N=5010;
vector<Pair> mp[N];
int lk[N],mn[N];
int fa[N];
int find(int x){
	if(x==fa[x]) return x;
	return fa[x]=find(fa[x]);
}
int main(){
	int n,m;cin>>n>>m;
	while(m--){
		int a,b,c;cin>>a>>b>>c;
		mp[a].push_back({c,b});
		mp[b].push_back({c,a});
	}
	for(int i=1;i<=n;i++) fa[i]=i;
	int ans=0;
	while(1){
		for(int i=1;i<=n;i++){
			lk[i]=-1;
			mn[i]=inf;
		}
		for(int u=1;u<=n;u++){
			int x=find(u);
			for(auto e:mp[u]){
				int y=find(e.v),w=e.w;
				if(x==y) continue;
				if(w<mn[x]){
					lk[x]=y;mn[x]=w;
				}
			}
		}
		int flag=1;
		for(int u=1;u<=n;u++){
			if(lk[u]==-1) continue;
			int x=find(u),y=find(lk[u]);
			if(x==y) continue;
			fa[x]=y;
			ans+=mn[x];
			flag=0;
		}
		if(flag) break;
	}
	set<int> q;
	for(int i=1;i<=n;i++) q.insert(find(i));
	if(q.size()>1) cout<<"orz\n";
	else cout<<ans<<"\n";
	return 0;
}
CF888G
#include<bits/stdc++.h>
#define inf 2e9+10
#define Pair pair<int,int>
#define w first
#define v second
using namespace std;
typedef long long LL;
const int N=2e5+10,S=N*70;
int ak[S][2],cnt,siz[S];int tail[S];
int root[N];
int a[N];
void insert(int &rt,int x,int tp){
	if(!rt) rt=++cnt;
	int now=rt;
	siz[now]++;
	for(int i=30;i>=0;i--){
		int k=x>>i&1;
		if(ak[now][k]==0) ak[now][k]=++cnt;
		now=ak[now][k];
		siz[now]++;
	}
	tail[now]=tp;
}
int merge(int a,int b){
	if(!a||!b) return a+b;
	ak[a][0]=merge(ak[a][0],ak[b][0]);
	ak[a][1]=merge(ak[a][1],ak[b][1]);
	if(ak[a][0]+ak[a][1]==0) siz[a]=1;
	else siz[a]=siz[ak[a][0]]+siz[ak[a][1]];
	tail[a]=tail[b];
	return a;
}
Pair query(int las,int now,int x){
	LL ans=0;
	for(int i=30;i>=0;i--){
		int k=x>>i&1;
		if(siz[ak[now][k]]-siz[ak[las][k]]>0){
			now=ak[now][k];
			las=ak[las][k];
		}
		else{
			ans+=1ll<<i;
			now=ak[now][!k];
			las=ak[las][!k];
		}
	}
	return {ans,tail[now]};
}
int lk[N],mn[N];
int fa[N];
int find(int x){
	if(x==fa[x]) return x;
	return fa[x]=find(fa[x]);
}
signed main(){
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	int n;cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	sort(a+1,a+1+n);n=unique(a+1,a+1+n)-a-1;
	for(int i=1;i<=n;i++){
		insert(root[0],a[i],i);
		insert(root[i],a[i],i);		
	}
	LL ans=0;
	for(int i=1;i<=n;i++) fa[i]=i;
	while(1){
		for(int i=1;i<=n;i++){
			lk[i]=-1;
			mn[i]=inf;
		}
		for(int u=1;u<=n;u++){
			int x=find(u);
			Pair tmp=query(root[x],root[0],a[u]);
			int w=tmp.w,y=find(tmp.v);
			if(x==y) continue;
			if(w<mn[x]){
				mn[x]=w;
				lk[x]=y;
			}
		}
		bool flag=1;
		for(int u=1;u<=n;u++){
			int x=find(u);
			if(lk[x]==-1) continue;
			int y=find(lk[x]),w=mn[x];
			if(x==y) continue;
			flag=0;
			fa[y]=x;
			root[x]=merge(root[x],root[y]);
			ans+=w;
		}
		if(flag) break;
	}
	cout<<ans<<"\n";
	return 0;
}
//全局建01trie,每个连通块再建一个01trie,做差就是此连通块外的01trie
//对于一个连通块,暴力记录到其他连通块的边权最小值
//多路合并

# 柒:【kruskal重构树】

一:【构造】

\(kruskal\)执行的过程中,对于一条边\((u,v,w)\)
\(u\)在重构树上的根节点为\(u',v\)\(v'\)
新建一个节点\(x,x\)的左右儿子分别为\(u',v',x\)的权值为\(w\)
\(kruskal\)跑完之后,\(kruskal\)重构树构造完成

二:【性质】

  • 大根堆
  • 两点简单路径最大边权的最小值为 它们在重构树上的\(LCA\)的权值
  • \(u\)走权值\(<=k\)的边可达的点集一定是 \(kruskal\)重构树上某个包含\(u\)的子树

三:【应用】

  • 性质二:求某两个点简单路径最大边权最小值
  • 性质三:求\(u\)走权值\(<=k\)的边可达的点集
P1967 [NOIP 2013 提高组] 货车运输
#include<bits/stdc++.h>
using namespace std;
const int N=5e4+10;
struct Edge{
	int u,v,w;
	bool operator<(Edge b){
		return w>b.w;
	}
}edg[N];
int bcj[N];
int nw[N];
vector<int> mp[N];int idx;
int find(int x){
	if(x==bcj[x]) return x;
	return bcj[x]=find(bcj[x]);
}
int ind[N];
int dep[N],siz[N],son[N],fa[N],top[N];
void Son(int u,int pa){
	fa[u]=pa;
	dep[u]=dep[pa]+1;
	siz[u]=1;
	for(auto v:mp[u]){
		if(v==pa) continue;
		Son(v,u);
		siz[u]+=siz[v];
		if(siz[v]>siz[son[u]]) son[u]=v;
	}
}
void Line(int u,int tp){
	top[u]=tp;
	if(!son[u]) return ;
	Line(son[u],tp);
	for(auto v:mp[u]){
		if(v==son[u]||v==fa[u]) continue;
		Line(v,v);
	}
}
int LCA(int a,int b){
	while(top[a]!=top[b]){
		if(dep[top[a]]<dep[top[b]]) swap(a,b);
		a=fa[top[a]];
	}
	if(dep[a]>dep[b]) swap(a,b);
	return a;
}
int main(){
	int n,m;cin>>n>>m;idx=n;
	for(int i=1;i<=m;i++){
		int u,v,w;cin>>u>>v>>w;
		edg[i]={u,v,w};
	}
	sort(edg+1,edg+1+m);
	for(int i=1;i<=2*n;i++) bcj[i]=i;
	for(int i=1;i<=m;i++){
		int u=edg[i].u,v=edg[i].v,w=edg[i].w;
		u=find(u);v=find(v);
		if(u==v) continue;
		nw[++idx]=w;
		bcj[u]=bcj[v]=idx;
		mp[idx].push_back(u);ind[u]++;
		mp[idx].push_back(v);ind[v]++;
	}
	for(int i=1;i<=idx;i++){
		if(ind[i]!=0) continue;
		Son(i,0);
		Line(i,i);
	}
	int q;cin>>q;
	while(q--){
		int a,b;cin>>a>>b;
		if(find(a)!=find(b)) cout<<-1<<"\n";
		else cout<<nw[LCA(a,b)]<<"\n";
	}
	return 0;
}

# 捌:【Kosaraju】(施工中)

一个求\(SCC\)的算法
有些题目可以采取\(bitset\)优化,这些是\(tarjan\)做不了的
比如\(HDU6072\)
这里不做展开 我懒得学以后再补

# 玖:【次小生成树】(施工中)

# 拾:【拟阵】(施工中)

学不懂(晕)

# ※:【练习题】

posted @ 2026-01-05 08:51  Ming3398  阅读(31)  评论(1)    收藏  举报