图论算法合集

图论

奇怪的树

prufer序列

树转序列

可以使用一个 prufer 数列唯一地表示一个带标号的树。

这个序列的生成算法是,每次选择编号最小的叶节点删除,并记录与其相邻的那个点。剩下两个点时结束。

显然,我们用堆维护可以做到 \(O(n\log n)\)

这个的实现是简单的,而我们可以通过另外一个算法做到线性构造。

我们发现构造过程中,叶子的数量非严格单调降。

那么我们每次删一个叶子,就检查它相邻的点是否是新的叶子,然后比较其与刚才删除的叶子的编号大小,如果它更小,那么继续删它。否则就是检查他们编号之间是否存在叶子,存在就删那个最小的,否则再来删这个新叶子。也是好实现的。拿一个指针存储的话,这个指针是单调递增的。复杂度是 \(O(n)\) 的。

性质

1. 剩下的两个点有一个是最大编号的点。

2. 节点出现次数为其度数 \(-1\)

序列转树

拿最小的叶子去和序列最前面的点连接。然后减掉这个两个点的各一个度数,然后就会出现新的叶子。堆维护复杂度 \(O(n\log n)\) ,指针维护复杂度 \(O(n)\)

然后就可以做 板子题 了。

#include<bits/stdc++.h>
#define ll long long
#define db double
#define file(a) freopen(#a".in","r",stdin),freopen(#a".out","w",stdout)
#define sky fflush(stdout)
#define gc getchar
#define pc putchar
namespace IO{
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			T p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p/=10;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
};
using IO::read;
const int N=5e6+3;
inline ll Value(int *f,int n){
	ll ans=0;
	for(int i=1;i<=n;++i){
		ans^=1ll*i*f[i];
	}
	return ans;
}
int n,ty;
int fa[N],pru[N];
int deg[N];
int main(){
	file(a);
	read(n,ty);
	if(ty==1){
		for(int i=1;i<n;++i){
			read(fa[i]);
			++deg[fa[i] ];
			++deg[i];
		}
		int p=0,u=0;
		for(int i=1;i<=n;++i){
			if(deg[i]==1){
				u=p=i;break;
			}
		}
		for(int i=1;i<=n-2;++i){
			int v=fa[u];
			--deg[u];--deg[v];
			pru[i]=v;
			if(deg[v]==1 and v<p){
				u=v;
			}else{
				while(deg[p]!=1) ++p;
				u=p;
			}
		}
		printf("%lld\n",Value(pru,n-2) );
	}else{
		int p=0,u=0;
		for(int i=1;i<=n-2;++i){
			read(pru[i]);
			++deg[pru[i] ];
		}
		for(int i=1;i<=n;++i){
			++deg[i];
			if(!p and deg[i]==1){
				u=p=i;
			}
		}
		for(int i=1;i<=n-2;++i){
			int v=pru[i];
			fa[u]=v;
			--deg[u];--deg[v];
			if(deg[v]==1 and v<p){
				u=v;
			}else{
				while(deg[p]!=1) ++p;
				u=p;
			}
		}
		fa[u]=n;
		printf("%lld\n",Value(fa,n-1) );
	}
	return 0;
}

应用

Cayley 公式

\(n\) 阶完全图总共有 \(n^{n-2}\) 种生成树。

证明是直接 prufer 序列,每个位置有 \(n\) 中可能,可得。

然后有扩展版本:

\(n\) 个点 \(m\) 条边的带标号无向图,其中有 \(k\) 个连通块,第 \(i\) 个连通块有 \(s_i\) 个点,用 \(k-1\) 条边使之连通的方案数是 \(n^{k-2}\prod_{i=1}^k s_i\)

证明:

设每个连通块度数为 \(d_i\)。那么如果 \(d_i\) 给定,方案数为:

\[\binom{k-2}{d_1-1,d_2-1,\dots,d_k-1} \]

现在再枚举一下度数,并且把连接位置加进去:

\[\sum_{d_i\ge 1 ,\sum_{i=1}^kd_i=2k-2}\binom{k-2}{d_1-1,d_2-1,\dots,d_k-1}\prod_{i=1}^ks_i^{d_i} \]

通过多项式定理,上式等价于:

\[(\sum_{i=1}^ks_i)^{k-2}\prod_{i=1}^ks_i \]

即:

\[n^{k-2}\prod_{i=1}^ks_i \]

矩阵树定理

我们在线代专题讨论过这个问题,点这里

笛卡尔树

这里

虚树

这里

树哈希

仅提供哈希思路,考虑对树的括号序列进行哈希。在每个节点合并子树哈希时对子树哈希值排序后在合并。

通过判断哈希可以判断树同构。

例题

斯坦纳树

在一个无向图里面给出一个点集 \(S\),让你选一个无向图的子图,使得 \(S\) 都在子图中出现且连通,并要求边权和最小。

显然选出的这个玩意是个树。我们称之为 斯坦纳树

板子

实际上就是状压DP。

\(f_{i,S}\) 表示,以 \(i\) 为根的树中,已经选出 \(S\) 集合的最小代价。

于是有转移方程:

\[f_{i,S}=\min\{f_{i,S},f_{i,S'}+f_{i,S/S'}(S'\subset S)\}\\ f_{i,S}=\min\{f_{i,S},f(j,S')+w(i,j)(j\in to_i)\} \]

#include<bits/stdc++.h>
#define ll long long
#define db double
#define file(a) freopen(#a".in","r",stdin),freopen(#a".out","w",stdout)
#define sky fflush(stdout)
#define gc getchar
#define pc putchar
namespace IO{
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			T p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p/=10;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
};
using IO::read;
const int N=100+1,S=1024,M=500+3;
const int inf=0x3f3f3f3f;
int n,m,k;
struct Edge{
	int u,v,w;
}to[M<<1];
int head[N],nxt[M<<1];
int Etot=-1;
int p[11];
inline void link(int u,int v,int w){
	nxt[++Etot]=head[u];
	head[u]=Etot;
	to[Etot]={u,v,w};
}
int f[S][N];
namespace Dijkstra{
	struct node{
		int x,val;
		inline friend bool operator < (node x,node y){
			return x.val>y.val;
		}
	};
	std::priority_queue<node>q;
	bool vis[N];
	inline void solve(int *dis){
		while(!q.empty() ) q.pop();
		for(int i=1;i<=n;++i){
			if(dis[i]!=inf) q.push({i,dis[i]});
		}
		memset(vis,0,sizeof(vis) );
		while(!q.empty() ){
			int u=q.top().x;q.pop();
			if(vis[u]) continue;
			vis[u]=1;
			for(int i=head[u];~i;i=nxt[i]){
				int v=to[i].v,w=to[i].w;
				if(dis[v]>dis[u]+w){
					dis[v]=dis[u]+w;
					q.push({v,dis[v]});
				}
			}
		}
	}
};
int main(){
	file(a);
	read(n,m,k);
	memset(head,-1,sizeof(head) );
	memset(f,0x3f,sizeof(f) );
	for(int i=1;i<=m;++i){
		int u,v,w;read(u,v,w);
		link(u,v,w);link(v,u,w);
	}
	for(int i=1;i<=k;++i){
		read(p[i]);
		f[(1<<(i-1) )][p[i] ]=0;
	}
	for(int i=1;i<=n;++i){
		f[0][i]=0;
	}
	for(int s=0,lim=(1<<k);s<lim;++s){
		//fprintf(stderr,"%d\n",s);
		for(int i=1;i<=n;++i){
			for(int t=s;t;t=(t-1)&s){
				f[s][i]=std::min(f[s][i],f[t][i]+f[s^t][i]);
			}
		}
		Dijkstra::solve(f[s]);
		/*
		for(int i=1;i<=n;++i){
			fprintf(stderr,"%d ",f[s][i]);
		}fprintf(stderr,"\n");
		*/
	}
	int ans=inf;
	for(int i=1;i<=n;++i){
		ans=std::min(ans,f[(1<<k)-1][i]);
	}
	printf("%d\n",ans);
	return 0;
}

生成树

最小生成树

这里

在此学习一下非 Kruskal 的最下生成树算法。

Prim

随意选一个节点,然后每次加入距离已加入节点距离最小的一个,然后把这条边连上。

\(n-1\) 次即可,用堆可达到 \(O((n+m)\log n)\) (斐波那契堆可以把 \(m\) 去掉)。

Boruvka

先假设图上没有任何边,然后把所有边遍历一遍,分别更新每个连通块对外连出的最小的边。一直做这个东西直到所有连通块都不存在对外连出的边。

对于每个原图的连通块,每做一次使得连通块减半,复杂度 \(O(m\log n)\) (前提是边权互不相同)。

次小生成树

每条没被选择的边 \((u,v,w)\),都可以通过替换最小生成树上的 \(u,v\) 间的路径上的最大值得到。然后枚举每条边做这个事情,答案取 \(\min\) 即可。

但是这个是不严格的次小,如何做到严格次小。

考虑维护一下路径严格次大值,如果最大值和这个边相等,那么用严格次大值。

瓶颈生成树

最大边权最小的生成树。

可知最小生成树一定是瓶颈生成树,但是瓶颈生成树不一定是最小生成树。可以反证。

最小瓶颈路

\(x,y\) 间的路径中,最大边权最小的那条路径。等价于最小生成树上路径中边权的最大值。

Kruskal重构树 求即可。

最短路生成树

例题

其上所有点到某一点的距离都是最短路。以那个点跑一遍最短路,记录一下从哪里松弛而来即可。如需要保证其边权和尽可能小,那么相等的松弛也进行记录(前提是没有 \(0\) 权边,否则会出现环)。

如果保留所有 \(dis(u)+w(u,v)=dis(v)\) 的边,则得到 最短路DAG

最小直径生成树

直径最小的生成树。

定义图的绝对中心,其到所有点最短距离的最大值最小。这个中心可以在边或者点上。

假设绝对中心 \(M\), 在边 \((u,v)\) 上,与 \(u\) 距离 \(a\), 到 \(x\) 的最短距离为 \(\min\{d(u,x)+a,d(v,x)+w(u,v)-a\}\).

显然看出是两个关于 \(a\) 一次函数取 \(\min\)\(a\) 看作横坐标,绝对中心到所有点最短距离的最大值看做纵坐标),且两者的斜率互为相反数。然后考虑所有的点 \(x\) ,发现就是很多斜率为 \(\pm 1\) 的纵截距为 \(d(u,x)\) 或者在 \(a=w(u,v)\) 处取到 \(d(v,x)\) 的一次函数。

我们只看其最上层,发现是像山一样呐。最低的地方是绝对中心的位置。

首先如果这个绝对中心在点上,就是直接和最远点的距离。

否则考虑每个 \(d(u,x),d(v,x)\) 对。如果我们按照 \(d(u,x)\) 降序枚举 \(x\) ,那么每次新加入的 \(d(v,x)\) 如果作为所有之前加入的 \(d(v,x)\) 最大值,那么它的递减部分会完全位于最高层,并且会由当前的 \(d(u,x)\) 与之前的最大的那一组的 \(d(v,x)\) 的递减部分截出一个谷。每截出一个谷就取一个 \(\min\)

然后你会发现最开始那个在点上的特判就是在特判最两边的两个山脚。

保存方案就是更新的时候求个交点,记一下 \((u,v,a)\) 即可。

求绝对中心

#include<bits/stdc++.h>
#define ll long long
#define db double
#define file(a) freopen(#a".in","r",stdin),freopen(#a".out","w",stdout)
#define sky fflush(stdout)
#define gc getchar
#define pc putchar
namespace IO{
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			T p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p/=10;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
};
using IO::read;
const int N=200+3;
const int inf=0x3f3f3f3f;
const int M=N*(N-1)/2;
int n,m;
int d[N][N];
int tmp;
int rk[N][N];
struct Edge{
	int u,v,w;
}e[M];
int main(){
	file(a);
	read(n,m);
	for(int i=1;i<=n;++i){
		for(int j=1;j<=n;++j){
			d[i][j]=inf;
		}
		d[i][i]=0;
	}
	for(int i=1;i<=m;++i){
		int u,v,w;read(u,v,w);
		e[i]={u,v,w};
		d[u][v]=std::min(d[u][v],w);
		d[v][u]=std::min(d[v][u],w);
	}
	for(int k=1;k<=n;++k){
		for(int i=1;i<=n;++i){
			if(i==k) continue;
			for(int j=1;j<=n;++j){
				if(i==j or k==j) continue;
				d[i][j]=std::min(d[i][j],d[i][k]+d[k][j]);
			}
		}
	}
	for(int i=1;i<=n;++i){
		for(int j=1;j<=n;++j){
			rk[i][j]=j;
		}
		tmp=i;
		std::sort(rk[i]+1,rk[i]+1+n,[](int x,int y){
			return d[tmp][x]<d[tmp][y];
		});
	}
	int ans=inf;
	for(int i=1;i<=n;++i) ans=std::min(ans,d[i][rk[i][n] ]*2);
	for(int i=1;i<=m;++i){
		int u=e[i].u,v=e[i].v,w=e[i].w;
		for(int j=n-1,mx=n;j;--j){
			if(d[v][rk[u][j] ]>d[v][rk[u][mx] ]){
				ans=std::min(ans,d[v][rk[u][mx] ]+d[u][rk[u][j] ]+w);
				mx=j;
			}
		}
	}
	printf("%.1lf\n",ans/2.0);
	return 0;
}

求最小直径生成树,就以其为生成树的直径的中点,弄一个最短路树即可。

求最小直径生成树

#include<bits/stdc++.h>
#define ll long long
#define db double
#define file(a) freopen(#a".in","r",stdin),freopen(#a".out","w",stdout)
#define sky fflush(stdout)
#define gc getchar
#define pc putchar
namespace IO{
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			T p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p/=10;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
};
using IO::read;
const int N=500+3,M=N*(N-1)/2;
const ll INF=1e18;
int n,m;
struct Edge{
	int u,v;ll w;
}to[M<<1];
int head[N],nxt[M<<1];
int Etot=-1;
inline void link(int u,int v,ll w){
	nxt[++Etot]=head[u];
	head[u]=Etot;
	to[Etot]={u,v,w};
}
ll d[N][N];
int rk[N][N],tmp;
int path[N];
namespace Dijkstra{
	struct node{
		int x;ll val;
		inline bool friend operator < (node x,node y){
			return x.val>y.val;
		}
	};
	std::priority_queue<node>q;
	ll dis[N];bool vis[N];
	inline void solve(int s){
		for(int i=1;i<=n+1;++i){
			dis[i]=INF;
			vis[i]=0;
		}
		dis[s]=0;
		q.push({s,0});
		while(!q.empty() ){
			int u=q.top().x;q.pop();
			if(vis[u]) continue;
			vis[u]=1;
			for(int i=head[u];~i;i=nxt[i]){
				int v=to[i].v;ll w=to[i].w;
				if(dis[v]>dis[u]+w){
					path[v]=i;
					dis[v]=dis[u]+w;
					q.push({v,dis[v]});
				}
				//这里如果加了替换,又有 0 权边,则会出现环
			}
		}
	}
};
int main(){
	file(a);
	read(n,m);
	memset(head,-1,sizeof(head) );
	for(int i=1;i<=n;++i){
		for(int j=1;j<=n;++j){
			if(i==j) d[i][j]=0;
			else d[i][j]=INF;
		}
	}
	for(int i=1;i<=m;++i){
		int u,v;ll w;
		read(u,v);read(w);w*=2;
		link(u,v,w);link(v,u,w);
		d[u][v]=std::min(d[u][v],w);
		d[v][u]=std::min(d[v][u],w);
	}
	for(int k=1;k<=n;++k){
		for(int i=1;i<=n;++i){
			if(i==k) continue;
			for(int j=1;j<=n;++j){
				if(i==j or j==k) continue;
				d[i][j]=std::min(d[i][j],d[i][k]+d[k][j]);
			}
		}
	}
	for(int i=1;i<=n;++i){
		for(int j=1;j<=n;++j){
			rk[i][j]=j;
		}
		tmp=i;
		std::sort(rk[i]+1,rk[i]+1+n,[](int x,int y){
			return d[tmp][x]<d[tmp][y];
		});
	}
	ll ans=INF;
	int pl=-1,pr=-1;ll dl=INF,dr=INF;
	for(int i=1;i<=n;++i){
		if(ans>d[i][rk[i][n] ]+d[i][rk[i][n-1] ]){
			ans=d[i][rk[i][n] ]+d[i][rk[i][n-1] ];
			pl=i;pr=-1;dl=0;dr=INF;
		}
	}
	for(int i=0;i<=Etot;i+=2){
		int u=to[i].u,v=to[i].v;ll w=to[i].w;
		for(int j=n-1,mx=n;j;--j){
			if(d[v][rk[u][j] ]>d[v][rk[u][mx] ]){
				if(ans>d[v][rk[u][mx] ]+d[u][rk[u][j] ]+w){
					ans=d[v][rk[u][mx] ]+d[u][rk[u][j] ]+w;
					pl=u;pr=v;
					dl=(d[v][rk[u][mx] ]+d[u][rk[u][j] ]+w)/2-d[u][rk[u][j] ];
					dr=w-dl;
				}
				mx=j;
			}
		}
	}
	printf("%lld\n",ans/2);
	if(pr!=-1){
		link(n+1,pl,dl);link(pl,n+1,dl);
		link(n+1,pr,dr);link(pr,n+1,dr);
		Dijkstra::solve(n+1);
		printf("%d %d\n",pl,pr);
		for(int i=1;i<=n;++i){
			if(i==pl and to[path[i] ].u==pr) continue;
			if(i==pr and to[path[i] ].u==pl) continue;
			if(to[path[i] ].u!=n+1) printf("%d %d\n",to[path[i] ].u,i);
		}
	}else{
		Dijkstra::solve(pl);
		for(int i=1;i<=n;++i){
			if(i==pl) continue;
			printf("%d %d\n",to[path[i] ].u,i);
		}
	}
	return 0;
}

上面这两的复杂度瓶颈都是 \(O(n^3)\)Floyd。使用 johnson全源最短路 可以优化至 \(O(n^2\log n)\)。(而且这俩题都是非负边权,这里的 johnson全源最短路 等价于 \(n\)Dijkstra

最小树形图

有向图的最小生成外向树。

板子

常用的是 \(O(nm)\)朱刘算法 (也叫 Edmonds) ,当然你 Tarjan 大爷弄了个 \(O((n+m)\log m)\) 的做法。

朴素做法

先讲朴素的。

发现如果图是个 DAG ,那么直接每个点选取最小入边即可。如果是个环,那我们断开最大边即可。

对于一般的图来说,我们先按照 DAG 的做法去选,如果没出现环那么就OK了。但是它特殊的地方就在于可能出现环。考虑把环缩成一个点来处理。先不说这个,那么该如何处理环?从别的地方连一条入边过来,再把原先的入边断开即可。这个只能求出其最小树形图的值。如果没有要求根,那么我们建立一个虚根向其他点连一条极大的边即可。

#include<bits/stdc++.h>
#define ll long long
#define db double
#define file(a) freopen(#a".in","r",stdin),freopen(#a".out","w",stdout)
#define sky fflush(stdout)
#define gc getchar
#define pc putchar
namespace IO{
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			T p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p/=10;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
};
using IO::read;
const int M=1e4+3;
const int N=100+3;
const int inf=0x3f3f3f3f;
int n,m,rt;
struct Edge{
	int u,v,w;
}e[M];
int cir[N],mi[N];
int vis[N],fa[N];
inline int Edmonds(){
	int ans=0;
	while(1){
		int tot=0;
		for(int i=1;i<=n;++i){
			mi[i]=inf;
			cir[i]=fa[i]=vis[i]=0;
		}
		for(int i=1;i<=m;++i){
			int u=e[i].u,v=e[i].v,w=e[i].w;
			if(u!=v and mi[v]>w){
				fa[v]=u;mi[v]=w;
			}
		}
		mi[rt]=0;
		for(int i=1;i<=n;++i){
			if(mi[i]==inf) return -1;
			ans+=mi[i];
		}
		for(int i=1;i<=n;++i){
			if(i==rt) continue;
			int x=i;
			while(x!=rt and vis[x]!=i and !cir[x]){
				vis[x]=i;x=fa[x];
			}
			if(x!=rt and !cir[x]){
				cir[x]=++tot;
				for(int y=fa[x];y!=x;y=fa[y]) cir[y]=tot; 
			}
		}
		if(!tot) return ans;
		for(int i=1;i<=n;++i) if(!cir[i]) cir[i]=++tot;
		for(int i=1;i<=m;++i){
			e[i].w=e[i].w-mi[e[i].v];
			e[i].u=cir[e[i].u];
			e[i].v=cir[e[i].v];
		}
		n=tot;rt=cir[rt];
	}
}
int main(){
	file(a);
	read(n,m,rt);
	for(int i=1;i<=m;++i){
		int u,v,w;read(u,v,w);
		e[i]={u,v,w};
	}
	printf("%d",Edmonds() );
	return 0;
}

重点不在于最小树形图,而在于 Edmonds 算法的思想,将图缩点转为树或 DAG 以方便求解一些问题。

Tarjan的优化

最小割树

我觉得这个应该算在网络流里面,这里先提一嘴,等下面讲网络流的时候再具体说。

板子

离谱的图

连通性相关

基础Tarjan

双连通分量

圆方树

2-SAT

对于多个条件:\(a\)\(0/1\) ,或者 \(b\)\(0/1\)。判断是否有解并输出方案。

我们可以把上面的条件转换为,当 \(a\)\(0/1\)\(b\)\(0/1\) 。也就是两者至少满足一个的另外一种表达。那么我们首先把真假的各个状态拆点,然后把转换后的条件转为有向图的边,那么一个点所能到达的点,就代表这个点成立时,这些条件也要成立。那么无解等价于同一个变量的两种状态可以互达。

那么解的构造就是选择不能到达另一状态的那种。

这个过程我们使用 Tarjan 维护,构建强连通分量。然后一个小提示,Tarjan 构建强连通分量的顺序是反拓扑序。

#include<bits/stdc++.h>
#define ll long long
#define db double
#define file(a) freopen(#a".in","r",stdin),freopen(#a".out","w",stdout)
#define sky fflush(stdout)
#define gc getchar
#define pc putchar
namespace IO{
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			T p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p/=10;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
};
using IO::read;
const int N=1e6+3,M=1e6+3;
int n,m;
int Etot=-1;
struct Edge{
	int u,v;
}to[M<<1];
int head[N<<1],nxt[M<<1];
inline void link(int u,int v){
	nxt[++Etot]=head[u];
	head[u]=Etot;
	to[Etot]={u,v};
}
int idx;
int dfn[N<<1],st[N<<1],top,low[N<<1];
bool ink[N<<1];
int col[N<<1],Ctot;
void dfs(int u){
	low[u]=dfn[u]=++idx;
	st[++top]=u;
	ink[u]=1;
	for(int i=head[u];~i;i=nxt[i]){
		int v=to[i].v;
		if(!dfn[v]){
			dfs(v);
			low[u]=std::min(low[u],low[v]);
		}else if(ink[v]){
			low[u]=std::min(low[u],dfn[v]);
		}
	}
	if(low[u]==dfn[u]){
		++Ctot;
		while(1){
			col[st[top] ]=Ctot;
			ink[st[top] ]=0;
			if(st[top]==u){
				--top;
				break;
			}
			--top;
		}
	}
}
int main(){
	file(a);
	read(n,m);
	memset(head,-1,sizeof(head) );
	for(int i=1;i<=m;++i){
		int x,a,y,b;read(x,a,y,b);
		if(a==1 and b==1){
			link(x+n,y);link(y+n,x);
		}
		if(a==0 and b==1){
			link(y+n,x+n);link(x,y);
		}
		if(a==1 and b==0){
			link(x+n,y+n);link(y,x);
		}
		if(a==0 and b==0){
			link(x,y+n);link(y,x+n);
		}
	}
	for(int i=1;i<=n*2;++i){
		if(!dfn[i]) dfs(i);
	}
	for(int i=1;i<=n;++i){
		if(col[i]==col[i+n]){
			printf("IMPOSSIBLE\n");
			return 0;
		}
	}
	printf("POSSIBLE\n");
	for(int i=1;i<=n;++i){
		if(col[i]<col[i+n]) printf("1 ");
		else printf("0 ");
	}pc('\n');
	return 0;
}

最短路

普通最短路暂且就不说了吧。

回忆一些特殊的。

Johnson 全源最短路

提一嘴,这里建超级源点是为了懒得判连通性。全部一起跑判环什么的都会快很多。

那什么的差分约束是同理的。

k短路

A* 求K短路

这个不太好,这个过程我们考虑用可持久化可并堆维护。

欧拉图

指具有欧拉回路的图。半欧拉图指有欧拉路径的图。

欧拉回路:通过图中每条边恰好一次的回路。

欧拉路径:通过图中每条边恰好一次的路径。

无向图的所有非零度点连通,且度数皆为偶数时,无向图为欧拉图。如果有 \(0\)\(2\) 个奇数度点,那么为半欧拉图。

对于有向图而言,非零度点强连通,且每个点的出入度相同那么为欧拉图。如果每个非零度点弱连通,最多一个点入度出度差为 \(1\),最多一个点出度入度差为 \(1\)(两者同时存在或同时不存在),其他出入度相同,则为半欧拉图。

考虑一个做法去求解一条欧拉回路。

求解欧拉回路(通路)

一个算法是避桥法,每次尽量避开桥边,但是每次求一遍要跑一次 Tarjan ,复杂度比较寄 \(O(m^2+nm)\)

做欧拉路径的话很简单,只需要做一个类似当前弧优化的东西,记录一下上次这个点遍历到的边,然后直接遍历即可。(总之就是不走重复的边,回路会自动走回起点,而路径的话我们找到出入度差为 \(1\) 或者奇数度点的那个跑一遍即可)。

板子

#include<bits/stdc++.h>
#define ll long long
#define db double
#define file(a) freopen(#a".in","r",stdin),freopen(#a".out","w",stdout)
#define sky fflush(stdout)
#define gc getchar
#define pc putchar
namespace IO{
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			T p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p/=10;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
};
using IO::read;
const int N=1e5+3,M=2e5+3;
int n,m;
std::vector<int>G[N];
int cur[N],deg_in[N],deg_ot[N];
inline bool IsEuler(int &S){
	int cnt1=0,cnt2=0;
	bool flag=1;
	for(int i=1;i<=n;++i){
		if(deg_ot[i]-deg_in[i]==1){
			S=i;++cnt1;
		}
		if(deg_in[i]-deg_ot[i]==1){
			++cnt2;
		}
		if(deg_in[i]!=deg_ot[i]) flag=0;
	}
	if((!flag) and (!(cnt1==1 and cnt2==1) ) ) return 0;
	return 1;
}
int st[M],top;
void dfs(int u){
	for(int i=cur[u],len=G[u].size();i<len;i=cur[u]){
		cur[u]=i+1;
		dfs(G[u][i]);
	}
	st[++top]=u;
}
int main(){
	file(a);
	read(n,m);
	for(int i=1;i<=m;++i){
		int u,v;read(u,v);
		G[u].push_back(v);
		++deg_ot[u];
		++deg_in[v];
	}
	for(int i=1;i<=n;++i){
		std::sort(G[i].begin(),G[i].end() );
	}
	int S=1;
	if(!IsEuler(S) ){
		printf("No\n");
		return 0;
	}
	dfs(S);
	while(top){
		printf("%d ",st[top--]);
	}pc('\n');
	return 0;
}

哈密顿图

这个和欧拉图是一个相对标的概念,只不过是每个点只经过一次。

如果 \(n\) 点的无向简单图,若任意不相邻节点 \(u,v\) 满足 \(deg_u+deg_v\ge n-1\) ,那么存在哈密顿路径。但是至少得有 \(2\) 个点。

如果 \(n\) 点的无向简单图,若任意不相邻节点 \(u,v\) 满足 \(deg_u+deg_v\ge n\) ,那么存在哈密顿路径。但是至少得有 \(3\) 个点。

这个要求的话,遍历一遍就是。

最小环

权值和最小的环。暴力就是随便断一条边,以这条边的端点为源点求最短路。最小环权值就是端点最短路加这条边权。

二分图

就是一个图有两个点集,点集内部没有边相连。

这个性质比较多并且有意思,和网络流有较为深厚的关系。

我们对二分图遍历并进行黑白染色,显然两个集合颜色分明。那么一个显然的推论是其不存在奇环。这个可以判定一个图是否为二分图。

下面部分和这玩意讲的东西同类型,就放一个链接。还有最大匹配什么的放到网络流再去说吧!

序理论

最大匹配

点集不交的选择一些边,得到的最大值。这个可以网络流求解。

最小点覆盖

用尽量少的点覆盖所有边。在数值上,二分图最小点覆盖等于二分图最大匹配。

最大匹配看作最大流再看作最小割,那么相当于一条边至少得割一个点来满足,因此可得。

关于最大流最小割什么的,我们下面网络流会马上讲。

带权同理。

最大独立集

最多的点使得这些点之间无边相连。

就是去掉尽量少的点使得点之间没有边,答案是节点数减二分图最大匹配(最小点覆盖)。

带权同理。

平面图、对偶图

平面图、对偶图

平面图转为对偶图,然后从平面图一点到另一点的最小割等价于在对偶图上加任意一条线把图分割开,这条线要满足从起点到终点必定经过这条线。

玄学的网络流

好啊,上讲了一堆没学过网络流就不一定懂的东西,现在正式讲一下网络流。

最大流

【模板】网络最大流

【模板】最大流 加强版 / 预流推进

就是一个网络(图),每条边有流量限制(或者说容量),其中有一个源点 \(S\),和汇点 \(T\)

每条边有流量,其不超过容量。我们称容量减流量为残余流量。而残余网络就是残余流量大于 \(0\) 的边构成的原图的一个子图。

而最大流求的是从源点不断给出流量,汇点能接受到的流量最大值。

然后我们要找一种方法求它。这个东西方法巨多,一个一个来。

好,首先,我们称残余网络上一条源点到汇点的路径为增广路。并且人为的为每条边建一条反边,流量为流过其正边的流量大小。这个东西的存在是给我们反悔的余地。

来,第一个算法。

EK动能算法

一直从 \(S\) 开始 BFS 找到一条增广路,维护从哪个点转移过来,以及路径经过的残余流量的最小值。然后把这条路的流量修改,并且加入答案。BFS 到达汇点即结束,整个过程结束的标志是源汇点在残余网络上不连通。

复杂度 \(O(nm^2)\) 。挺慢的。

这里有一篇证明

#include<bits/stdc++.h>
#define ll long long
#define db double
#define file(a) freopen(#a".in","r",stdin),freopen(#a".out","w",stdout)
#define sky fflush(stdout)
#define gc getchar
#define pc putchar
namespace IO{
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			T p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p/=10;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
};
using IO::read;
const int M=5e3+3,N=2e2+3;
int n,m,S,T;
int Etot=-1;
struct Edge{
	int u,v,f;
}to[M<<1];
int head[N],nxt[M<<1];
inline void link(int u,int v,int f){
	nxt[++Etot]=head[u];
	head[u]=Etot;
	to[Etot]={u,v,f};
}
inline void flow_link(int u,int v,int f){
	link(u,v,f);
	link(v,u,0);
}
const int inf=INT_MAX;
int pre[N];
int fmi[N];
inline bool BFS(){
	static std::queue<int>q;
	while(!q.empty() ) q.pop();
	memset(fmi,0,sizeof(fmi) );
	fmi[S]=inf;
	q.push(S);
	while(!q.empty() ){
		int u=q.front();q.pop();
		for(int i=head[u];~i;i=nxt[i]){
			int v=to[i].v,f=to[i].f;
			if(!fmi[v] and f){
				pre[v]=i;
				fmi[v]=std::min(fmi[u],f);
				q.push(v);
			}
		}
		if(fmi[T]) break;
	}
	return !!fmi[T];
}	
inline ll EK(){
	ll ans=0;
	while(BFS() ){
		for(int u=T;u!=S;u=to[pre[u] ].u){
			to[pre[u] ].f-=fmi[T];
			to[pre[u]^1].f+=fmi[T];
		}
		ans+=fmi[T];
	}
	return ans;
}
int main(){
	file(a);
	read(n,m,S,T);
	memset(head,-1,sizeof(head) );
	for(int i=1;i<=m;++i){
		int u,v,f;read(u,v,f);
		flow_link(u,v,f);
	}
	printf("%lld\n",EK() );
	return 0;
}

dinic算法

BFS 建立分层图(按照距离 \(S\) 的最短距离),每次 DFS 增广只增广深度大 \(1\) 的,这样会使得增广路最短(因此避免 DFS 深度过深)。

发现可以同时增广好几条路!得到一个优化。

同样有直接DFS的做法,但是可能效率比较低,这个做法叫 FF

然后就是一个快到起飞的优化。一条边在一次增广中被增广完就不用管了(因为到汇点的流被流满了)。当前弧优化还是牛啊!

复杂度 \(O(n^2m)\)。对于每个点只有一条出边或只有一条入边,且所以边容量为 \(1\) 的网络,我们称之为单位网络。dinic 在单位网络上的复杂度是 \(O(m\sqrt n)\) 。(二分图最大匹配就是典型的单位网络)

这里是证明

这个算法是我们一般来说网络流时用得最多的一个。

#include<bits/stdc++.h>
#define ll long long
#define db double
#define file(a) freopen(#a".in","r",stdin),freopen(#a".out","w",stdout)
#define sky fflush(stdout)
#define gc getchar
#define pc putchar
namespace IO{
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			T p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p/=10;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
};
using IO::read;
const int M=5e3+3,N=2e2+3;
int n,m,S,T;
int Etot=-1;
struct Edge{
	int u,v,f;
}to[M<<1];
int head[N],nxt[M<<1];
inline void link(int u,int v,int f){
	nxt[++Etot]=head[u];
	head[u]=Etot;
	to[Etot]={u,v,f};
}
inline void flow_link(int u,int v,int f){
	link(u,v,f);
	link(v,u,0);
}
const int inf=INT_MAX;
int dep[N];
inline bool BFS(){
	static std::queue<int>q;
	while(!q.empty() ) q.pop();
	memset(dep,0,sizeof(dep) );
	dep[S]=1;
	q.push(S);
	while(!q.empty() ){
		int u=q.front();q.pop();
		for(int i=head[u];~i;i=nxt[i]){
			int v=to[i].v,f=to[i].f;
			if(!dep[v] and f){
				dep[v]=dep[u]+1;
				q.push(v);
			}
		}
	}
	return !!dep[T];
}	
int cur[N];
ll dfs(int u,int flow){
	if(u==T or (!flow) ) return flow;
	ll ans=0;
	for(int i=cur[u];~i;i=nxt[i]){
		cur[u]=i;
		int v=to[i].v,f=to[i].f;
		if(f and dep[v]==dep[u]+1){
			int tmp=dfs(v,std::min(f,flow) );
			if(tmp>0){
				to[i].f-=tmp;
				to[i^1].f+=tmp;
				ans+=tmp;
				flow-=tmp;
				if(!flow) break;
			}
		}
	}
	return ans;
}
inline ll dinic(){
	ll ans=0;
	while(BFS() ){
		for(int i=1;i<=n;++i) cur[i]=head[i];
		while(ll d=dfs(S,inf) ){
			ans+=d;
		}
	}
	return ans;
}
int main(){
	file(a);
	read(n,m,S,T);
	memset(head,-1,sizeof(head) );
	for(int i=1;i<=m;++i){
		int u,v,f;read(u,v,f);
		flow_link(u,v,f);
	}
	printf("%lld\n",dinic() );
	return 0;
}

你发现有些算法严格难写于某些算法,真算复杂度还劣于某些算法,这种我就懒得学了(摆.png)

Push-Relabel 预留推进算法

该算法忽略流量守恒,并定义超额流为入流与出流的差,记作 \(e(u)\)。若 \(e(u)>0\) ,则称 \(u\) 节点溢出。

预留推进算法通过维护高度函数 \(h(u)\),并且溢出节点只向高度低于自身的节点推送超额流。如果不存在低于其的点,则重贴标记,更新这个点的高度。

高度函数

我们这里的高度函数这样规定:

首先源点的高度为点的个数,汇点高度为 \(0\) ,残余网络上每条边 \((u,v)\) 满足 \(h(u)\le h(v)+1\)

因此,\(h(u)>h(v)+1\) 时,则残余网络上没有这条边。

算法对高度差为一的点进行推送。

推送(Push)

对溢出的节点 \(u\) ,找到残余网络上的相邻的点,高度差为一的点 \(v\),并对 \((u,v)\) 进行推送。

我们尽可能将超额流从 \(u\) 推送到 \(v\) 。我们只保证边的容量不会超过,而不考虑其是否使得 \(v\) 溢出。

重贴标记(Relabel)

如果 \(u\) 溢出而不存在高度低于它的临边,那么对其重贴标记。抬高高度。

\(h(u)\rightarrow \min_{(u,v)\in E_f}\{h(v)+1\}\)

具体流程

首先把源点的流推出来,高度函数调高,并且将 \(e(s)\) 初始化。

接下来扫整张图,检查是否有可以进行 Push 或者 Relabel 操作的。

不断进行这个操作,最后除去源汇点其他点不发生溢出,且流量为最大流。此时流量守恒。(有些溢出的流量会通过反向边回到源点)

HLPP 最高标号预留推进算法

就是上面那个东西的优化。因此不必给上面的代码。

基本流程与上面类似,但是加入了一些优化。(有测试表明很多时间花费在大量的重贴标记,考虑减少这个的次数)

首先是类似上面的初始化,我们先进行一遍BFS,用到 \(t\) 的最短路长度作为 \(high\) 数组,特别地,\(high(s)=n\) 。同时检查图是否连通。然后推送 \(s\) 的流量,将其超额流设为负数。

之后每次选取最高的点进行推送,若推送完仍然溢出那么重贴标记。持续进行直到不存在溢出节点。

然后还有一个 GAP 优化,如果一个点 \(u\) 被重贴标记,那么 \(high(u)\) 数量减一。如果因此没有了高度为 \(high(u)\) 的点,那么高度大于他的大可全部设为 \(n+1\) ,因为已经不能再进行推送。(出现断层,而断层以上的都没用啦,而且重贴为 \(n+1\) 的这些点都非溢出节点)

然后,我们不需要维护高度大于 \(n\) 的点。因为它必定无法到达汇点。这种必定回到源点。但是这样的话图上的流不守恒,按照需求选择是否使用这个优化。(无限制下最大 \(2n-1\)

#include<bits/stdc++.h>
#define ll long long
#define db double
#define file(a) freopen(#a".in","r",stdin),freopen(#a".out","w",stdout)
#define sky fflush(stdout)
#define gc getchar
#define pc putchar
namespace IO{
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			T p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p/=10;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
};
using IO::read;
const int N=1200+3,M=120000+3;
const int inf=0x3f3f3f3f;
struct Edge{
	int u,v,f;
}to[M<<1];
int head[N],nxt[M<<1];
int n,m,S,T;
int Etot=-1;
inline void link(int u,int v,int f){
	nxt[++Etot]=head[u];
	head[u]=Etot;
	to[Etot]={u,v,f};
}
inline void flow_link(int u,int v,int f){
	link(u,v,f);link(v,u,0);
}
int ex[N];
int high[N];
std::queue<int>q;
inline bool BFS(){
	while(!q.empty() ) q.pop();
	for(int i=1;i<=n;++i){
		high[i]=inf;
	}
	high[T]=0;
	q.push(T);
	while(!q.empty() ){
		int u=q.front();q.pop();
		for(int i=head[u];~i;i=nxt[i]){
			int v=to[i].v;
			if(to[i^1].f and high[v]>high[u]+1){
				high[v]=high[u]+1;
				q.push(v);
			}
		}
	}
	return high[S]!=inf;
}
int level;
int gap[N<<1];
std::vector<int>box[N<<1];
inline int select(){
	while(!box[level].size() ){
		--level;
		if(level==-1) break;
	}
	return ((level==-1)?0:box[level].back() );
}
inline bool Push(int u){
	for(int i=head[u];~i;i=nxt[i]){
		int v=to[i].v,f=to[i].f;
		if(!f or high[u]!=high[v]+1) continue;
		int tmp=std::min(f,ex[u]);
		if(v!=S and v!=T and !ex[v]){
			level=std::max(level,high[v]);
			box[high[v] ].push_back(v);
		}
		ex[u]-=tmp;ex[v]+=tmp;
		to[i].f-=tmp;to[i^1].f+=tmp;
		if(!ex[u]) return 0;
	}
	return 1;
}
inline void Relabel(int u){
	high[u]=inf;
	for(int i=head[u];~i;i=nxt[i]){
		if(to[i].f) high[u]=std::min(high[u],high[to[i].v]);
	}
	++high[u];
	if(high[u]<n){//这里看情况改
		box[high[u] ].push_back(u);
		level=std::max(level,high[u]);
		++gap[high[u] ];
	}
}
inline int HLPP(){
	if(!BFS() ) return 0;
	for(int i=1;i<=n;++i){
		if(high[i]!=inf) ++gap[high[i] ];
	}
	level=0;
	for(int i=head[S];~i;i=nxt[i]){
		int v=to[i].v,f=to[i].f;
		if(!f) continue;
		if(v!=S and v!=T and !ex[v]){
			level=std::max(level,high[v]);
			box[high[v] ].push_back(v);
		}
		ex[S]-=f;ex[v]+=f;
		to[i].f=0;to[i^1].f=f;
	}
	high[S]=n;
	int u;
	while((u=select() ) ){
		box[level].pop_back();
		if(Push(u) ){
			if(!(--gap[high[u] ]) ){
				for(int i=1;i<=n;++i){
					if(i!=S and i!=T and high[i]>high[u]){
						high[i]=n+1;
					}
				}
			}
			Relabel(u);
		}
	}
	/*
	for(int i=1;i<=n;++i){
		if(i==S or i==T) continue;
		if(ex[i]) fprintf(stderr,"auto_machine\n");
	}
	*/
	return ex[T];
}
int main(){
	file(a);
	read(n,m,S,T);
	memset(head,-1,sizeof(head) );
	for(int i=1;i<=m;++i){
		int u,v,f;read(u,v,f);
		flow_link(u,v,f);
	}
	printf("%d\n",HLPP() );
	return 0;
}

费用流

在流量最大的前提下费用最小。即最小费用最大流。

按照费用最短路 SPFA 建立分层图,其他和求最大流差不多。

反向边的费用为负。注意开个访问数组防止走环。

复杂度 \(O(nmf)\) 的,\(f\) 是最大流。但是如果数据随机,那么跑个几千几万的没太大问题吧。

#include<bits/stdc++.h>
#define ll long long
#define db double
#define file(a) freopen(#a".in","r",stdin),freopen(#a".out","w",stdout)
#define sky fflush(stdout)
#define gc getchar
#define pc putchar
namespace IO{
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			T p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p/=10;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
};
using IO::read;
const int N=5000+3,M=5e4+3;
const int inf=0x3f3f3f3f;
int n,m,S,T;
struct Edge{
	int u,v,f,c;
}to[M<<1];
int head[N],nxt[M<<1];
int Etot=-1;
inline void link(int u,int v,int f,int c){
	nxt[++Etot]=head[u];
	head[u]=Etot;
	to[Etot]={u,v,f,c};
}
inline void flow_link(int u,int v,int f,int c){
	link(u,v,f,c);
	link(v,u,0,-c);
}
int dis[N];
bool inq[N];
std::queue<int>q;
inline bool SPFA(){
	while(!q.empty() ){
		inq[q.front()]=0;
		q.pop();
	}
	for(int i=1;i<=n;++i){
		dis[i]=inf;
	}
	dis[S]=0;inq[S]=1;
	q.push(S);
	while(!q.empty() ){
		int u=q.front();q.pop();
		inq[u]=0;
		for(int i=head[u];~i;i=nxt[i]){
			int v=to[i].v,f=to[i].f,c=to[i].c;
			if(f and dis[v]>dis[u]+c){
				dis[v]=dis[u]+c;
				if(!inq[v]){
					inq[v]=1;
					q.push(v);
				}
			}
		}
	}
	return dis[T]!=inf;
}
int cur[N];
int ret;
bool vis[N];
int dfs(int u,int flow){
	if(u==T) return flow;
	if(!flow) return 0;
	vis[u]=1;
	int ans=0;
	for(int i=cur[u];~i;i=nxt[i]){
		int v=to[i].v,f=to[i].f,c=to[i].c;
		cur[u]=i;
		if(!vis[v] and f and dis[v]==dis[u]+c){
			int tmp=dfs(v,std::min(flow,f) );
			if(tmp>0){
				ret+=c*tmp;
				to[i].f-=tmp;
				to[i^1].f+=tmp;
				ans+=tmp;
				flow-=tmp;
				if(!flow) break;
			}
		}
	}
	vis[u]=0;
	return ans;
}
inline int Dinic(){
	int ans=0;
	while(SPFA() ){
		for(int i=1;i<=n;++i) cur[i]=head[i];
		while(int d=dfs(S,inf) ){
			ans+=d;
		}
	}
	return ans;
}
int main(){
	file(a);
	read(n,m,S,T);
	memset(head,-1,sizeof(head) );
	for(int i=1;i<=m;++i){
		int u,v,f,c;read(u,v,f,c);
		flow_link(u,v,f,c);
	}
	printf("%d ",Dinic() );
	printf("%d\n",ret);
	return 0;
}

Primal_Dual 原始对偶

使得支持 Dijkstra 找增广路,把 \(n\) 变为 \(\log n\) 或者 \(\log m\)。(你愿意的话可以写斐波那契堆,可以把最为雷哥的 \(m\) 搞掉)

初始的时候先SPFA跑一个初始势能。

然后我们由于每跑完一次增广路,都会改变残余网络的形态,考虑怎么通过修改势能使得边权再次为正。

答案是直接势能加上上一次的最短路。如果这条边在最短路上,改完后边权为 \(0\) ,否则肯定大于等于 \(0\) 。即所有边非负。(这个最短路的值不需减去自身势能)

#include<bits/stdc++.h>
#define ll long long
#define db double
#define file(a) freopen(#a".in","r",stdin),freopen(#a".out","w",stdout)
#define sky fflush(stdout)
#define gc getchar
#define pc putchar
namespace IO{
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			T p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p/=10;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
};
using IO::read;
const int N=5e3+3,M=5e4+3;
struct Edge{
	int u,v,f,c;
}to[M<<1];
int head[N],nxt[M<<1];
int Etot=-1;
inline void link(int u,int v,int f,int c){
	nxt[++Etot]=head[u];
	head[u]=Etot;
	to[Etot]={u,v,f,c};
}
inline void flow_link(int u,int v,int f,int c){
	link(u,v,f,c);
	link(v,u,0,-c);
}
const int inf=0x3f3f3f3f;
int n,m,S,T;
int h[N];
namespace SPFA{
	bool inq[N];
	std::queue<int>q;
	inline bool solve(){
		while(!q.empty() ) q.pop();
		for(int i=1;i<=n;++i){
			h[i]=inf;
		}
		h[S]=0;inq[S]=1;
		q.push(S);
		while(!q.empty() ){
			int u=q.front();q.pop();
			inq[u]=0;
			for(int i=head[u];~i;i=nxt[i]){
				int v=to[i].v,c=to[i].c;
				if(to[i].f and h[v]>h[u]+c){
					h[v]=h[u]+c;
					if(!inq[v]){
						q.push(v);
						inq[v]=1;
					}
				}
			}
		}
		return h[T]!=inf;
	}
};
int dis[N];
namespace Dijkstra{
	bool vis[N];	
	struct node{
		int p,x;
		inline friend bool operator < (node x,node y){
			return x.x>y.x;
		}
	};
	std::priority_queue<node>q;
	inline bool solve(){
		while(!q.empty() ) q.pop();
		for(int i=1;i<=n;++i){
			dis[i]=inf;
			vis[i]=0;
		}
		dis[S]=0;
		q.push({S,0});
		while(!q.empty() ){
			int u=q.top().p;q.pop();
			if(vis[u]) continue;
			vis[u]=1;
			for(int i=head[u];~i;i=nxt[i]){
				int v=to[i].v,c=to[i].c+h[u]-h[v];
				if(to[i].f and dis[v]>dis[u]+c){
					dis[v]=dis[u]+c;
					q.push({v,dis[v]});
				}
			}
		}
		return dis[T]!=inf;
	}
};
int ret;
int cur[N];
bool vis[N];
int dfs(int u,int flow){
	if(!flow) return 0;
	if(u==T) return flow;
	int ans=0;
	vis[u]=1;
	for(int i=cur[u];~i;i=nxt[i]){
		int v=to[i].v,f=to[i].f,c=to[i].c+h[u]-h[v];
		cur[u]=i;
		if(f and !vis[v] and dis[v]==dis[u]+c){
			int tmp=dfs(v,std::min(f,flow) );
			if(tmp>0){
				ans+=tmp;
				ret+=to[i].c*tmp;
				to[i].f-=tmp;
				to[i^1].f+=tmp;
				flow-=tmp;
				if(!flow) break;
			}
		}
	}
	vis[u]=0;
	return ans;
}
inline int Primal_Dual(){
	ret=0;
	int ans=0;
	if(!SPFA::solve() ){
		return 0;
	}
	while(Dijkstra::solve() ){
		for(int i=1;i<=n;++i) cur[i]=head[i];
		while(int d=dfs(S,inf) ){
			ans+=d;
		}
		for(int i=1;i<=n;++i) h[i]+=dis[i];
	}
	return ans;
}
int main(){
	file(a);
	read(n,m,S,T);
	memset(head,-1,sizeof(head) );
	for(int i=1;i<=m;++i){
		int u,v,f,c;read(u,v,f,c);
		flow_link(u,v,f,c);
	}
	printf("%d ",Primal_Dual() );
	printf("%d\n",ret);
	return 0;
}

因为用了STL的原因,开O2的话会快一些。

最小割

我们可以知道:最小割=最大流。

不做严格证明,不难接受。

然后没什么东西了。

那么接下来就可以做网络流 \(24\) 题了。

网络流24题

最大权闭合子图

最大权闭合子图

设最小割使得我们构成的图分为 \(S,T\) 两个部分,\(s\) 所在的部分为 \(S\) ,\(t\) 所在部分为 \(T\)

可以发现 \(S/\{s\}=\{s相邻未被割的点\}\cup \{t相邻被割的点\}=\{未被割的正点\}\cup\{被割的负点\}\)

因此答案 \(=正点权之和-被割正点和-被割负点绝对值和=正点和-最小割\)

上下界网络流

Stoer-Wagner

图匹配

二分图最大匹配

要么网络流,要么匈牙利。之前网络流讲过了,这里说一下匈牙利算法。

枚举每个没配对的点,尝试对其进行配对。对其相邻点进行判断,若对方未配对则直接配对,否则尝试更换对方的配对者的配对方案,更改成功则与之配对。

蛮暴力的,可以证明复杂度 \(O(m\sqrt n)\)

#include<bits/stdc++.h>
#define ll long long
#define db double
#define file(a) freopen(#a".in","r",stdin),freopen(#a".out","w",stdout)
#define sky fflush(stdout)
#define gc getchar
#define pc putchar
namespace IO{
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			T p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p/=10;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
	template<class T>
	inline void print(T x){
		if(x<0) {x=-x;pc('-');}
		static char st[40];
		static int top;
		top=0;
		do{st[++top]=x-x/10*10+'0';}while(x/=10);
		while(top) {pc(st[top--]);}
	}
	template<class T,class ...A>
	inline void print(T s,A ...a){
		print(s);print(a...);
	}
};
using IO::read;
using IO::print;
const int N=500+3,M=5e4+3;
int n,m,e;
struct Edge{
	int u,v;
}to[M];
int head[N],nxt[M];
int Etot=-1;
inline void link(int u,int v){
	nxt[++Etot]=head[u];
	head[u]=Etot;
	to[Etot]={u,v};
}
bool vis[N];
int match[N];
bool find(int u){
	for(int i=head[u];~i;i=nxt[i]){
		int v=to[i].v;
		if(vis[v]) continue;
		vis[v]=1;
		if((!match[v]) or find(match[v]) ){
			match[v]=u;
			return 1;
		}
	}
	return 0;
}
int main(){
	file(a);
	read(n,m,e);
	memset(head,-1,sizeof(head) );
	for(int i=1;i<=e;++i){
		int u,v;read(u,v);
		link(u,v);
	}
	int ans=0;
	for(int i=1;i<=n;++i){
		ans+=find(i);
		memset(vis,0,sizeof(vis) );
	}
	printf("%d\n",ans);
	return 0;
}

二分图最大权匹配

二分图最大权匹配

可以补点之后跑最大费用最大流。最大费用最大流就是最短路换成最长路。

复杂度 \(O(nmf)\) ,是 \(O(n^4)\) 级别的。会TLE。

然后如果用匈牙利 KM 算法做的话可以在 \(O(n^3)\) 的复杂度内完成。

仍然是补点啊,求最大权完美匹配。

一般图最大匹配

一般图最大权匹配

网络流24题及一些杂题的解题记录

1.餐巾计划问题

拆点,左边视作第 \(i\) 天给出的脏的纸,右边为干净的用掉的纸。由于第 \(i\) 天使用掉 \(a[i]\) 个纸,所以源点向左边的点连边,右边的点向汇点连边,流量 \(a[i]\) ,费用 \(0\)。考虑脏纸的转移,我们假定它洗完的纸都是洗好就用,洗完不用的就暂且不洗,那么左边的第 \(i\) 个点可以向右边的第 \(i+t\) 个点转移,\(t\) 为洗的时间,流量 \(+\infty\),费用 \(f\)。还可以直接买,源点向右边连边,流量 \(+\infty\) ,费用 \(p\) 。脏纸还可以放着不管,左边 \(i\) 向左边 \(i+1\) 连边,流量 \(+\infty\) ,费用 \(0\)

跑最小费用最大流,最小费用为答案。显然一定有解。

2.[CTSC1999]家园 / 星际转移问题

\(m\) 艘船周期性靠站的问题直接做有点麻烦。发现 \(n,m,k\) 全都很小,且用时最大为 \((n+2)\times (n+2)\)(一个非常不紧凑的上界),考虑对于每个点时间拆点。然后转移就是对于每个船向下一站转移,流量 \(h[i]\),费用 \(1\)。同样可以向自己时间加一的点,流量 \(+\infty\),费用 \(1\)。记得在最开始的地方建一个虚拟起点连向 \(0\),然后用边来限制最大流。(突然发现其实只要进 \(0\) 的时候流不给 \(+\infty\) 而是给 \(k\) 就可以了)

这里费用流的作用不是直接求解答案,而是找到一组最优方案。被增广过的边中端点时间最大的那个为答案。\(-1\) 那个点不需时间拆点。

3.飞行员配对方案问题

大水题,直接二分图最大匹配即可。

4.软件补丁问题

裸状压,隐式建图(空间开不下),直接跑 Dijkstra 即可。

网络流里没有流,对吧。

5.太空飞行计划问题

求答案是简单的,直接上最大权闭合子图即可。比较雷的地方是要求输出方案。

我们本质上是找到未被割掉的正点,和被割掉的负点。输出从 \(S\) 在残余网络上能到达的点即可(网络流算法进行完之后分成的两部分就是由最小割隔开)。需要注意的是 \(T\) 所属的那个部分并不一定保证连通。

也就是总结一下二分图最小割。结束时能被分到层的右侧点被割,不能被分到层的左侧点被割。

6.试题库问题

大水题,仍然是直接匹配。

7.最小路径覆盖问题

[CTSC2008]祭祀river

可以和这个题放在一起理解。前者是的路径不带有重复的点,后者可以有。

前者可以看做,最开始有 \(n\) 个单点链,每次选边作为连接。显然每个点都是出度入度小于等于 \(1\) 的。所以可以把一个点拆成出点和入点,每个点限制一点流量跑最大流。因为每连接一条边就少用一条链,总点数减去最大流即为答案。

后者两个理解方式,一个是用传递闭包把路径拆开,使得同一段点可以用多次。另一种理解方式是直接用狄尔沃斯定理,最小链覆盖(可带重复点)等于最长反链长度,最长反链长度就是拓扑图传递闭包后的得到的偏序图的最小路径覆盖,所以传递闭包后,拆点做一个最大流,答案还是总点数减去最大流。

8.魔术球问题

先看这个题。

[SCOI2010] 连续攻击游戏

拆点成装备和属性,然后从小到大依次匹配属性 \(i\),匹配不了就停下。这样的原因是网络流只能知道最多匹配上多少,而不是连续匹配多少。这里用匈牙利进行是最优的做法。

可以考虑舍去相关柱子的内容,大可看做最开始一堆散的珠子,然后每次按标号依次连接到其他的珠子上。就是问在链不超过 \(n\) 的情况下,能串多少个珠。把加起来为完全平方数的珠子连边(小的连向大的),然后问题变成最小路径覆盖小于等于 \(n\) 的情况下,最多能取前多少个珠子。

类似连续攻击游戏那个题一样一个点一个点跑匈牙利即可。

9.最长不下降子序列问题

第一问是简单的,三种做法,要么直接树状数组,要么通过狄尔沃斯定理转成关于 \(i<j,a_i>a_j\) 的偏序的最小路径覆盖。我比较懒,就直接 DP 了。

\(f_i\) 表示以 \(a_i\) 结尾的最长不降子序列长度最大值。我们入流只从 \(f_i=1\) 的入,出流只从 \(f_i=s\) 的出,然后拆点来使得每个点只选择一次,一层用来获得流量,一层用来转移到 \(f=f_i+1\) 的和它满足偏序关系的点。跑最大流即可。两层直接连边来限制被选择次数。

第三问给特定点加上无限流量的边即可。记得特判一下 \(s=1\) 的情况,不然会流无限的最大流。

10.航空路线问题

和上面那个题比较像,还是拆点限制到达次数。流只从 \(1\) 进只从 \(n\) 出。

方案输出沿着被增广的路 dfs 即可。特判一下 \(1\)\(n\) 连的边,这个流量要改为 \(2\) ,不然会被误判无解。

11.方格取数问题

棋盘黑白染色,然后最大权独立集即可。(总权值减去最小权点覆盖)

12.机器人路径规划问题

13.圆桌问题

14.骑士共存问题

棋盘黑白染色,直接求最大独立集。

15.火星探险问题

16.最长k可重线段集问题

17.最长k可重区间集问题

18.汽车加油行驶问题

19.孤岛营救问题

20.深海机器人问题

21.数字梯形问题

22.分配问题

23.运输问题

24.负载平衡问题

[NOI2009] 植物大战僵尸

就是说,如果要攻击位于 \((x,y)\) 的植物,那么能攻击或者位于 \((x,y),(x,y+1),...,(x,n)\) 的植物需要被在其之前消灭。直接上最大权闭合子图即可。

House Planning

posted @ 2022-10-20 14:30  cbdsopa  阅读(73)  评论(1编辑  收藏  举报