[复健]蓝书精刷 0x60 图论

0x61 最短路

单源最短路径

Dijkstra 算法

算法流程:每次找出未被标记的、\(dis_i\) 最小的节点 \(x\),然后标记节点 \(x\),扫描所有出边并进行松弛操作,重复以上操作直到所有节点被标记.

该算法基于贪心思想,只适用于所有边的长度都是非负数的图.

朴素版复杂度为 \(O(n^2)\),瓶颈在于寻找全局最小值的过程.

若用二叉堆维护,用 \(O(\log n)\) 的时间获取最小值并删除,共需 \(O(n \log n)\);用 \(O(\log n)\) 的时间执行一条边的松弛,共需 \(O(m \log n)\),因此总时间复杂度为 \(O((m+n) \log n)\).

int dis[N];
bool vis[N];
std::priority_queue<std::pair<int,int>> q;

void dijkstra(int s) {
	for(int i=1;i<=n;i++) dis[i]=inf;
	dis[s]=0;q.push(std::make_pair(0,s));
	while(!q.empty()) {
		auto cur=q.top();q.pop();
		int u=cur.second;
		if(vis[u]) continue;
		vis[u]=true;
		for(int i=head[u];i;i=next[i]) {
			int v=ver[i],w=val[i];
			if(dis[v]<dis[u]+w) continue;
			dis[v]=dis[u]+w;
			q.push(std::make_pair(-dis[v],v));
		}
	}
}

Bellman-Ford 算法

给定一张有向图,若对于图中的某一条边 \((x,y,z)\),有 \(dis_y \leq dis_x +z\) 成立,则称该边满足三角形不等式。若所有边都满足三角形不等式,那么 \(dis\) 就是所求最短路。

算法流程:扫描所有边,若该边不满足三角形不等式,则进行松弛操作,直到所有边都满足三角形不等式.

该算法基于迭代思想,至多需要 \(n-1\) 轮扫描,因此复杂度为 \(O(nm)\).

SPFA 算法

实际上是基于队列优化的 Bellman-Ford 算法.

用队列保存进行过松弛操作的节点,实际上未进行松弛操作的节点也必定不可能导致出边进行松弛,同时记录当前节点是否在队中即可,时间复杂度为 \(O(km)\),其中 \(k\) 是一个比较小的常数.

int dis[N];
bool vis[N];
std::queue<int> q;

void spfa(int s) {
	for(int i=1;i<=n;i++) dis[i]=inf;
	dis[s]=0;vis[s]=true;q.push(s);
	while(!q.empty()) {
		int u=q.front();q.pop();
		vis[u]=false;
		for(int i=head[u];i;i=next[i]) {
			int v=ver[i],w=val[i];
			if(dis[v]>dis[u]+w) {
				dis[v]=dis[u]+w;
				if(vis[v]) continue;
				vis[v]=true;q.push(v);
			}
		}
	}
}

P1948 [USACO08JAN] Telephone Lines S

解法一:分层图最短路

由于 \(k\) 很小,因此可以考虑分层图最短路。

而直接建图空间复杂度是无法接受的,因此我们用 spfa+dp 的方式进行分层图最短路。

为方便理解,我们在原题的情景下进行描述。

可以仿照动态规划的思想,用 \(dp_{x,p}\) 表示从 \(1\sim x\),途中已经指定 \(p\) 条边免费时,路径上最贵的花费的最小值是多少。

显然有对于一条边 \((u,v)\),有 \(dp_{v,p}=\max(dp_{u,p},w(u,v))\)\(dp_{v,p+1}=dp_{u,p}\).

前者表示对于该边,不使用免费机会,后者表示使用。

而因为该状态定义方式是具有后效性的,因此用迭代的思想 spfa 算法进行 dp,直至所有状态都无法被更新。

最坏时间复杂度 \(O(knm)\).

实际上由于随机数据的原因,本题用实现较好的分层图最短路也可以通过。

#include <queue>
#include <cstdio>
#include <algorithm>

const int N=1e3+10;
const int M=1e4+10;
const int K=1e4+10;
const int inf=1e9;

int head[N],ver[M<<1],val[M<<1],next[M<<1],tot;

void add(int u,int v,int w) {
	ver[++tot]=v;val[tot]=w;
	next[tot]=head[u];head[u]=tot;
}

int n,m,k;
int dp[N][K],dis[N][K];
bool vis[N][K];
std::queue<std::pair<int,int>> q;

void spfa() {
	for(int i=1;i<=n;i++) for(int j=0;j<=k;j++) dp[i][j]=inf;
	q.push(std::make_pair(1,0));
	vis[1][0]=true;dp[1][0]=0;
	while(!q.empty()) {
		auto cur=q.front();q.pop();
		int u=cur.first,p=cur.second;
		vis[u][p]=false;
		for(int i=head[u];i;i=next[i]) {
			int v=ver[i],w=val[i];
			if(dp[v][p]>std::max(dp[u][p],w)) {
				dp[v][p]=std::max(dp[u][p],w);
				dis[v][p]=dis[u][p]+1;
				if(!vis[v][p]) {
					q.push(std::make_pair(v,p));
					vis[v][p]=true;
				}
			}
			if(p<k && dp[v][p+1]>dp[u][p]) {
				dp[v][p+1]=dp[u][p];
				dis[v][p+1]=dis[u][p]+1;
				if(!vis[v][p+1]) {
					q.push(std::make_pair(v,p+1));
					vis[v][p+1]=true;
				}
			}
		}
	}
}

int main() {
    freopen("assignment.in","r",stdin);
    freopen("assignment.out","w",stdout);
	scanf("%d%d%d",&n,&m,&k);
	for(int i=1,u,v,w;i<=m;i++) {
		scanf("%d%d%d",&u,&v,&w);
		add(u,v,w);add(v,u,w);
	}
	spfa();
	if(dp[n][k]==inf) puts("-1");
	else if(dis[n][k]<=k) puts("0"); 
	else printf("%d\n",dp[n][k]);
	return 0;
}

解法二:二分答案+双端队列 bfs

观察到我们要使第 \(k+1\) 大的值最小,考虑二分,因此重点在于 check 如何编写。

问题化归为:是否存在一种合法的方案使得第 \(k+1\) 大的值不超过 \(mid\).

而我们只需要将路径上大于 \(mid\) 的边看作长度为 \(1\) 的边,反之看作长度为 \(0\) 的边,最后求解 \(1 \sim n\) 的最短路是否不超过 \(k\) 即可,这是典型的 01bfs.

时间复杂度 \(O((n+m)\log n)\).

#include <queue>
#include <cstdio>
#include <cstring>
#include <iostream>

const int N=1e3+10;
const int M=1e4+10;
const int K=1e4+10;
const int inf=1e9;

int n,m,k;
int ver[M],val[M],next[M];
int head[N],tot;

void add(int u,int v,int w) {
	ver[++tot]=v;val[tot]=w;
	next[tot]=head[u];head[u]=tot;
}

int dis[1005];
bool vis[1005];
std::deque<int> q;

void _01bfs(int s,int mid) {
	for(int i=1;i<=n;i++) dis[i]=inf;
	for(int i=1;i<=n;i++) vis[i]=false;
	dis[s]=0;q.push_back(s);
	while(!q.empty()) {
		int u=q.front();q.pop_front();
		if(vis[u]) continue;
		vis[u]=true;
		for(int i=head[u];i;i=next[i]) {
			int v=ver[i];int w=(val[i]>mid);
			if(vis[v] || dis[v]<=dis[u]+w) continue;
			dis[v]=dis[u]+w;
			w?q.push_back(v):q.push_front(v);
		}
	}
}

bool check(int x) {
	_01bfs(1,x);
	if(dis[n]<=k) return true;
	return false;
}

int main() {
	freopen("assignment.in","r",stdin);
	freopen("assignment.out","w",stdout);
	int l=0,r=0;
	scanf("%d%d%d",&n,&m,&k);
	for(int i=1;i<=m;i++) {
		int u,v,w;
		scanf("%d%d%d",&u,&v,&w);
		add(u,v,w);add(v,u,w);
		r=std::max(r,w);
	}
	_01bfs(1,r);
	if(dis[n]==inf) return puts("-1"),0;
	while(l<r) {
		int mid=(l+r)>>1;
		if(check(mid)) r=mid;
		else l=mid+1;
	}
	printf("%d\n",l);
	return 0;
}

P1073 [NOIP 2009 提高组] 最优贸易

本题等价于求一个有向图上,从 \(1 \sim n\) 的路径上选出两个点 \(p,q\)(先经过 \(p\) 再经过 \(q\)),使得 \(val_q-val_p\) 最大。

那么可以先在原图上跑出 \(d_i\) 表示从 \(1 \sim i\) 的路径上的最小权值。

再在反图上跑出 \(f_i\) 表示从 \(1 \sim i\) 的路径上的最大权值。

最后的答案就是最大的 \(f_i-d_i\).

#include <queue>
#include <cstdio>
#include <algorithm>

const int N=1e5+10;
const int M=5e5+10;
const int inf=1e9;

int head[N],ver[M<<1],next[M<<1],tot;

void add(int u,int v) {
	ver[++tot]=v;next[tot]=head[u];head[u]=tot;
}

int n,m;
int val[N],d[N],f[N];
bool vis[N];

std::priority_queue<std::pair<int,int>> q;

void dijkstra_1() {
	for(int i=1;i<=n;i++) d[i]=inf;
	d[1]=val[1];q.push(std::make_pair(-d[1],1));
	while(!q.empty()) {
		auto cur=q.top();q.pop();
		int u=cur.second;
		if(vis[u]) continue;
		vis[u]=true;
		for(int i=head[u];i;i=next[i]) {
			int v=ver[i];
			if(d[u]<d[v]) {
				d[v]=std::min(d[u],val[v]);
				q.push(std::make_pair(-d[v],v));
			}
		}
	}
}

void dijkstra_2() {
	for(int i=1;i<=n;i++) vis[i]=false;
	for(int i=1;i<=n;i++) f[i]=-inf;
	f[n]=val[n];q.push(std::make_pair(f[n],n));
	while(!q.empty()) {
		auto cur=q.top();q.pop();
		int u=cur.second;
		if(vis[u]) continue;
		vis[u]=true;
		for(int i=head[u];i;i=next[i]) {
			int v=ver[i];
			if(f[u]>f[v]) {
				f[v]=std::max(f[u],val[v]);
				q.push(std::make_pair(f[v],v));
			}
		}
	}
}

int u[M],v[M],w[M];

int main() {
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&val[i]);
	for(int i=1;i<=m;i++) {
		scanf("%d%d%d",&u[i],&v[i],&w[i]);
		add(u[i],v[i]);
		if(w[i]==2) add(v[i],u[i]);
	}
	dijkstra_1();
	for(int i=1;i<=n;i++) head[i]=0;
	tot=0;
	for(int i=1;i<=m;i++) {
		add(v[i],u[i]);
		if(w[i]==2) add(u[i],v[i]);
	}
	dijkstra_2();
	int ans=-inf;
	for(int i=1;i<=n;i++) ans=std::max(ans,f[i]-d[i]);
	printf("%d\n",ans);
	return 0;
}

P3008 [USACO11JAN] Roads and Planes G

注意到所有强连通分量都只有正边权,因此在强连通分量内用 dijkstra,外用拓扑排序。

时间复杂度 \(O(n+m+m \log n)\).

#include <queue>
#include <cstdio>
#include <algorithm>

const int N=2.5e4+10;
const int M=1e5+10;
const int inf=1e9;

int head[N],ver[M<<1],val[M<<1],next[M<<1],tot;

void add(int u,int v,int w) {
	ver[++tot]=v;val[tot]=w;
	next[tot]=head[u];head[u]=tot;
}

int chead[N],cver[M<<1],cnext[M<<1],ctot;
int c[N],deg[N],cnt;

void cadd(int u,int v) {
	cver[++ctot]=v;
	cnext[ctot]=chead[u];
	chead[u]=ctot;
}

void dfs(int u) {
	cadd(c[u],u);
	for(int i=head[u];i;i=next[i]) {
		int v=ver[i];
		if(c[v]) continue;
		c[v]=c[u];
		dfs(v);
	}
}

int n,m1,m2,s;
int dis[N];
bool vis[N];

std::queue<int> q1;
std::priority_queue<std::pair<int,int>> q2;

void sol() {
	for(int i=1;i<=n;i++) dis[i]=inf;
	for(int i=1;i<=cnt;i++) if(!deg[i]) q1.push(i);
	dis[s]=0;if(deg[c[s]]) q1.push(c[s]);
	while(!q1.empty()) {
		int id=q1.front();q1.pop();
		for(int i=chead[id];i;i=cnext[i]) {
			int v=cver[i];
			q2.push(std::make_pair(-dis[v],v));
		}
		while(!q2.empty()) {
			auto cur=q2.top();q2.pop();
			int u=cur.second;
			if(vis[u]) continue;
			vis[u]=true;
			for(int i=head[u];i;i=next[i]) {
				int v=ver[i],w=val[i];
				if(c[u]==c[v]) {
					if(dis[u]+w<dis[v]) {
						dis[v]=dis[u]+w;
						q2.push(std::make_pair(-dis[v],v));
					}
				} else {
					--deg[c[v]];if(!deg[c[v]] && c[v]!=c[s]) q1.push(c[v]);
					if(dis[u]!=inf && dis[v]>dis[u]+w) dis[v]=dis[u]+w;
				}
			}
		}
	}
}

int main() {
	scanf("%d%d%d%d",&n,&m1,&m2,&s);
	for(int i=1,u,v,w;i<=m1;i++) {
		scanf("%d%d%d",&u,&v,&w);
		add(u,v,w);add(v,u,w);
	}
	for(int i=1;i<=n;i++) {
		if(!c[i]) {
			c[i]=++cnt;
			dfs(i);
		}
	}
	for(int i=1,u,v,w;i<=m2;i++) {
		scanf("%d%d%d",&u,&v,&w);
		add(u,v,w);++deg[c[v]];
	}
	sol();
	for(int i=1;i<=n;i++) {
		if(dis[i]==inf) puts("NO PATH");
		else printf("%d\n",dis[i]);
	}
	return 0;
}

任意两点间最短路径

可以把每一个点作为起点,求解 \(n\) 次单源最短路径,时间复杂度 \(O(nm\log n)\).

Floyd 算法

\(dp_{k,i,j}\) 表示经过若干个编号不超过 \(k\) 的节点从 \(i\)\(j\) 的最短路长度。我们有:\(dp_{k,i,j}=\min(dp_{k-1,i,j},dp_{k-1,i,k}+dp_{k-1,k,j})\).

初始化为 \(dp_{0,i,j}=a_{i,j}\).

注意到,本质是动态规划,\(k\) 是阶段,因此可以滚动数组优化,其中 \(i,j\) 是附加状态,应该置于内层,\(k\) 置于外层,时间复杂度 \(O(n^3)\).

for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) dp[i][j]=inf;
for(int i=1;i<=n;i++) dp[i][i]=0;
for(int i=1,u,v,w;i<=m;i++) {
	scanf("%d%d%d",&u,&v,&w);
	dp[u][v]=std::min(dp[u][v],w);
	dp[v][u]=std::min(dp[v][u],w);
}
for(int k=1;k<=n;k++) {
	for(int i=1;i<=n;i++) {
		for(int j=1;j<=n;j++) dp[i][j]=std::min(dp[i][j],dp[i][k]+dp[k][j]);
	}
}

P10927 Sightseeing trip

无向图最小环问题,可以用 Floyd 算法解决。

当外层循环 \(k\) 刚开始时,\(d_{i,j\) 保存着经过编号不超过 \(k-1\) 的节点从 \(i\)\(j\) 的最短路长度。

于是 \(\min_{1 \leq i<j<k}(d_{i,j}+a_{i,k}+a_{k,j})\) 就是满足由编号不超过 \(k\) 的节点且经过节点 \(k\) 的最小环长度。

而由于对称性,即可得到全图的最小环,只需将 \(k\) 取遍 \(1 \sim n\).

若需要记录路径则记录 \(i,j\) 之间的中间节点然后递归处理即可。

#include <cstdio>
#include <algorithm>

const int N=100+10;
const int M=1e4+10;
const int inf=1e9;

int path[N],pos[N][N],tot;

void get_path(int u,int v) {
	if(!pos[u][v]) return;
	get_path(u,pos[u][v]);
	path[++tot]=pos[u][v];
	get_path(pos[u][v],v);
}

int n,m,ans=inf;
int a[N][N],dp[N][N];

int main() {
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) dp[i][j]=a[i][j]=inf;
	for(int i=1;i<=n;i++) dp[i][i]=a[i][i]=0;
	for(int i=1,u,v,w;i<=m;i++) {
		scanf("%d%d%d",&u,&v,&w);
		a[u][v]=dp[u][v]=std::min(a[u][v],w);
		a[v][u]=dp[v][u]=std::min(a[v][u],w);
	}
	for(int k=1;k<=n;k++) {
		for(int i=1;i<k;i++) {
			for(int j=i+1;j<k;j++) {
				if(ans>(long long)dp[i][j]+a[i][k]+a[k][j]) {
					ans=dp[i][j]+a[i][k]+a[k][j];
					tot=0;path[++tot]=i;
					get_path(i,j);
					path[++tot]=j;path[++tot]=k;
				}
			}
		}
		for(int i=1;i<=n;i++) {
			for(int j=1;j<=n;j++) {
				if(dp[i][j]>dp[i][k]+dp[k][j]) {
					dp[i][j]=dp[i][k]+dp[k][j];
					pos[i][j]=k;
				}
			}
		}
	}
	if(!tot) printf("No solution.");
	else {
		for(int i=1;i<=tot;i++) {
			printf("%d",path[i]);
			i==tot?0:putchar(' ');
		}
	}
	return 0;
}

对于有向图的最小环问题,可以枚举起点 \(s=1\sim n\),执行堆优化的 dijkstra 算法求解单源最短路,\(s\) 一定是第一个从堆中取出的节点,扫描完 \(s\) 的所有出边完成松弛操作时,令 \(d_s=+ \infty\),然后继续求解,当 \(s\) 第二次被从堆中取出时,\(d_s\) 就是经过 \(s\) 的最小环长度。时间复杂度 \(O(nm\log n)\).

传递闭包

在交际网络种,给定若干个元素和若干对二元关系,且关系具有传递性。通过传递性推导出尽量多的元素之间的关系的问题被称作传递闭包。

建立邻接矩阵 \(d\),其中 \(d_{i,j}=1\) 表示 \(i,j\) 满足传递关系,反之表示不满足。其中 \(d_{i,i}\) 始终为 \(1\),用 Floyd 算法可以解决。

给定 \(n\) 个元素和 \(m\) 个不等式。判断不等式之间是否具有矛盾。若无矛盾,判断这些不等式能否确定每一对变量之间的关系,若能,求出最少能用前几个不等式就确定。

这是一道有向的传递闭包问题,稍作修改。不妨设 \(i<j\)\(d_{i,j}=1\),其余情况均有 \(d_{i,j}=0\).

使用 Floyd 算法后,若存在 \(i,j\) 使得 \(d_{i,j}=d_{j,i}=1\),则说明不等式之间存在矛盾。若 \(d_{i,j}=d_{j,i}=0\),则说明无法确定。

反之二分最少需要的不等式数量,并判断即可,时间复杂度 \(O(n^3 \log m)\).

0x62 最小生成树

给定一张边带权的无向图 \(G=(V,E),\ n=|V|,\ m=|E|\).

\(V\) 中全部 \(n\) 个顶点和 \(E\)\(n-1\) 条边构成的无向连通子图被称为 \(G\) 的一棵生成树。边的权值之和最小的生成树被称为无向图 \(G\) 的最小生成树。

定理:任意一棵最小生成树一定包含无向图中权值最小的边。

可以用反证法证明:若不包含,则将该边加入到生成树时,可以与其他顶点构成一个环,环上任意一条边权值都大于该边,将任意边替换成该边结果都会更优,与假设矛盾。

推论:给定一张无向图,从 \(E\) 中选出 \(k<n-1\) 条边构成一个生成森林。若再从剩下的 \(m-k\) 条边中选 \(n-1-k\) 条边添加到生成森林中,使其称为 \(G\) 的生成树,并且选出的边的权值之和最小,则该生成树一定包含 \(m-k\) 条边中链接生成森林的两个不连通节点的权值最小的边。

Kruscal 算法

该算法就是基于以上推论。

时间复杂度 \(O(m \log m)\).

#include <cstdio>
#include <algorithm>

const int N=5e3+10;
const int M=2e5+10;

int fa[N];

void init(int n) {
	for(int i=1;i<=n;i++) fa[i]=i;
}

int find(int x) {
	if(fa[x]==x) return x;
	return fa[x]=find(fa[x]);
}

struct edge {
	int u,v,w;
}G[M];

int n,m;

int main() {
	scanf("%d%d",&n,&m);
	init(n);
	for(int i=1;i<=m;i++) scanf("%d%d%d",&G[i].u,&G[i].v,&G[i].w);
	std::sort(G+1,G+1+m,[](edge a,edge b) {
		return a.w<b.w;
	});
	int ans=0,cnt=1;
	for(int i=1;i<=m && cnt<=n;i++) {
		int u=G[i].u,v=G[i].v;
		if(find(u)==find(v)) continue;
		fa[fa[v]]=fa[u];ans+=G[i].w;++cnt;
	}
	if(cnt<n) puts("orz");
	else printf("%d\n",ans);
	return 0;
}

P10928 走廊泼水节

在前面定理时有提到,要使边 \((u,v,w)\) 仍在生成树中,那么需要满足此时链接 \(S_u\)\(S_v\) 的所有边的权值都大于 \((u,v,w)\),同时由贪心可得,越早将边添加到图中,结果更优,此时的权值更小,为 \(w+1\),由乘法计数原理可得贡献为 \((S_u \times S_v-1) \times (w+1)\).

int fa[N],siz[N];

int find(int x) {
	if(fa[x]==x) return x;
	return fa[x]=find(fa[x]);
}

struct edge {
	int u,v,w;
}G[N];

int n;

void sol() {
	scanf("%d",&n);
	for(int i=1;i<=n;i++) fa[i]=i;
	for(int i=1;i<=n;i++) siz[i]=1;
	for(int i=1;i<n;i++) scanf("%d%d%d",&G[i].u,&G[i].v,&G[i].w);
	std::sort(G+1,G+n,[](edge a,edge b) {
		return a.w<b.w;
	});
	int ans=0;
	for(int i=1;i<n;i++) {
		int u=find(G[i].u),v=find(G[i].v),w=G[i].w;
		if(u==v) continue;
		ans+=(w+1)*(siz[u]*siz[v]-1);
		fa[v]=u;siz[u]+=siz[v];
	}
	printf("%d\n",ans);
}

Prim 算法

最初,\(1\) 号节点处在生成树中,我们设 \(S\) 为未在生成树中的节点,\(T\) 为在生成树中的节点。

那么若 \(u \in S\),则设 \(d_u=\min_{v \in T}{w(u,v)}\),反之设 \(d_u\) 为将 \(u\) 加入生成树中时 \(d_u\) 的值。

那么由贪心思想可得,每次选出 \(S\)\(d_u\) 最小的 \(u\) 节点加入生成树一定不劣。

若用堆优化,时间复杂度为 \(O(m \log n)\),主要用于稠密图,特别是完全图。

#include <queue>
#include <cstdio>
#include <algorithm>

const int N=5e3+10;
const int M=2e5+10;
const int inf=1e9;

int head[N],ver[M<<1],next[M<<1],val[M<<1],tot;

void add(int u,int v,int w) {
	ver[++tot]=v;val[tot]=w;
	next[tot]=head[u];head[u]=tot;
}

int n,m;
int d[N];
bool vis[N];
int a[N][N];

std::priority_queue<std::pair<int,int>> q;

void prim() {
	for(int i=2;i<=n;i++) d[i]=std::min(inf,a[1][i]);
	vis[1]=true;
	for(int i=2;i<=n;i++) q.push(std::make_pair(-d[i],i));
	while(!q.empty()) {
		auto cur=q.top();q.pop();
		int u=cur.second;
		if(vis[u] || d[u]==inf) continue;
		vis[u]=true;
		for(int i=head[u];i;i=next[i]) {
			int v=ver[i],w=val[i];
			if(vis[v]) continue;
			if(w<d[v]) {
				d[v]=w;
				q.push(std::make_pair(-d[v],v));
			}
		}
	}
}

int main() {
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) a[i][j]=inf;
	for(int i=1,u,v,w;i<=m;i++) {
		scanf("%d%d%d",&u,&v,&w);
		add(u,v,w);add(v,u,w);
		a[u][v]=a[v][u]=std::min(a[u][v],w);
	}
	prim();
	bool fl=true;
	for(int i=1;i<=n;i++) if(!vis[i]) fl=false;
	if(!fl) return puts("orz"),0;
	int ans=0;
	for(int i=1;i<=n;i++) ans+=d[i];
	printf("%d\n",ans);
	return 0;
}

UVA1537 Picnic Planning

首先判断一下去掉根节点后连通块的数量,解存在的充要条件是连通块数量不大于 \(k\).

然后对于整一张图跑出最小生成树,其中要求根节点只能与一个节点相连,再根据剩余的情况把其余的与根节点相连的边进行替换,每次可以换一个环上的边。结果不会更优的充要条件是环上所有的边都小于等于当前待替换边。

#include <map>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

const int N=50;
const int inf=250;

int n,m,k,cnt,root,ans;
int g[N][N];
bool vis[N][N];

int col[N];

void dfs(int u) {
	col[u]=cnt;
	for(int i=1;i<=n;i++) {
		if(i==root) continue;
		if(g[u][i]<inf && !col[i]) dfs(i);
	}
}

std::string pp="Park";
std::map<std::string,int> mp;

int minto[N];

void init() {
	n=m=k=cnt=root=ans=0;
	std::map<std::string,int> _mp;
	std::swap(mp,_mp);
	memset(g,0x7f,sizeof(g));
	memset(vis,0,sizeof(vis));
	std::fill(minto,minto+N-1,inf);
	memset(col,0,sizeof(col));
	scanf("%d",&m);
	std::string name1,name2;
	int w=0;
	for(int i=1;i<=m;i++) {
		std::cin>>name1>>name2;
		scanf("%d",&w);
		int u=0,v=0;
		if(!mp[name1]) mp[name1]=u=++n;
		else u=mp[name1];
		if(!mp[name2]) mp[name2]=v=++n;
		else v=mp[name2];
		g[u][v]=g[v][u]=std::min(g[u][v],w);
	}
	scanf("%d",&k);
	cnt=0;root=mp[pp];
	for(int i=1;i<=n;i++) {
		if(!col[i]) {
			if(i==root) continue;
			++cnt;dfs(i); 
		}
	}
	for(int i=1;i<=n;i++) {
		if(i==root) continue;
		if(minto[col[i]]==inf || g[root][minto[col[i]]]>g[root][i]) minto[col[i]]=i;
	}
}

int dis[N],pre[N],maxn[N];
bool in[N],linked[N];

void upd(int u,int fa) {
	pre[u]=fa;
	if(u!=root) maxn[u]=g[u][fa];
	if(u!=root) maxn[u]=std::max(maxn[u],maxn[fa]);
	in[u]=true;
	for(int i=1;i<=n;i++) {
		if(!vis[u][i] || in[i]) continue;
		upd(i,u);
	}
}

void sol() {
	ans=0;
	memset(vis,0,sizeof(vis));
	memset(dis,0x7f,sizeof(dis));
	memset(pre,0,sizeof(pre));
	memset(in,0,sizeof(in));
	memset(linked,0,sizeof(linked));
	memset(maxn,0,sizeof(maxn));
	in[root]=true;dis[root]=0;
	for(int i=1;i<=cnt;i++) {
		dis[minto[i]]=g[root][minto[i]];
		pre[minto[i]]=root;linked[minto[i]]=true;
	}
	for(int i=1;i<n;i++) { // Prim
		int minn=0;
		for(int j=1;j<=n;j++) {
			if(in[j]) continue;
			if(dis[minn]>dis[j] || !minn) minn=j;
		}
		in[minn]=true;ans+=dis[minn];
		vis[pre[minn]][minn]=true;
		for(int j=1;j<=n;j++) {
			if(in[j] || g[minn][j]>=dis[j]) continue;
			dis[j]=g[minn][j];
			pre[j]=minn;
		}
	}
	for(int i=1;i<=n;i++) vis[i][pre[i]]=vis[pre[i]][i]=true;
	for(int i=cnt+1;i<=k;i++) {
		memset(in,0,sizeof(in));
		upd(root,0);
		int minn=0;
		for(int j=1;j<=n;j++) {
			if(j==root || linked[j]) continue;
			if(!minn || g[root][minn]-maxn[minn]>g[root][j]-maxn[j]) minn=j;
		}
		if(g[root][minn]>maxn[minn]) break;
		vis[root][minn]=vis[minn][root]=linked[minn]=true; 
		ans+=g[root][minn];
		int u=minn;
		while(u!=root) {
			if(g[pre[u]][u]==maxn[minn]) {
				vis[pre[u]][u]=vis[u][pre[u]]=false;
				ans-=maxn[minn];
				break;
			}
			u=pre[u];
		}
	}
	printf("Total miles driven: %d\n",ans);
}

int t;

int main() {
	init();
	sol();
	return 0;
}

0x63 树的直径与最近公共祖先

最近公共祖先(LCA)

树上倍增法

先跳较深节点使得两个节点到同一深度,若此时两个节点重合则得出答案,反之一起往上跳,保证两个节点不重合,最后得到父亲节点即可。

#include <cstdio>
#include <algorithm>

const int N=5e5+10;
const int M=5e5+10;
const int W=20;

int h[N],nxt[M<<1],ver[M<<1],idx;

void add(int u,int v) {
	ver[++idx]=v;nxt[idx]=h[u];h[u]=idx;
}

int n,m,rt;
int dep[N];
int f[N][W];

void dfs(int u,int fath) {
	dep[u]=dep[fath]+1;
	f[u][0]=fath;
	for(int j=1;j<W;j++) f[u][j]=f[f[u][j-1]][j-1];
	for(int i=h[u];i;i=nxt[i]) {
		int v=ver[i];
		if(v==fath) continue;
		dfs(v,u);
	}
}

int lca(int x,int y) {
	if(dep[x]<dep[y]) std::swap(x,y);
	for(int i=W-1;i>=0;i--) {
		if(dep[f[x][i]]>=dep[y]) x=f[x][i];
	}
	if(x==y) return x;
	for(int i=W-1;i>=0;i--) {
		if(f[x][i]!=f[y][i]) {
			x=f[x][i];
			y=f[y][i];
		}
	}
	return f[x][0];
}

int main() {
	scanf("%d%d%d",&n,&m,&rt);
	for(int i=1;i<n;i++) {
		int u=0,v=0;
		scanf("%d%d",&u,&v);
		add(u,v);add(v,u);
	}
	dfs(rt,0);
	for(int i=1;i<=m;i++) {
		int u=0,v=0;
		scanf("%d%d",&u,&v);
		printf("%d\n",lca(u,v));
	}
	return 0;
}

0x66 Tarjan 算法与无向图连通性

无向图的割点与桥

时间戳:\(dfn_x\)

搜索树

追溯值:\(low_x\)

\(\operatorname{subtree}(x)\) 表示搜索树中以 \(x\) 为根的子树。\(low_x\) 为满足以下条件的节点的时间戳的最小值:

  1. \(\operatorname{subtree}(x)\) 中的节点;
  2. 通过 \(1\) 条不在搜索树上的边,能够到达 \(\operatorname{subtree}(x)\) 中的节点。

割边判定法则:无向边 \((x,y)\) 是桥,当且仅当搜索树上存在 \(x\) 的一个子节点 \(y\) 满足:\(dfn_x<low_y\). (即子节点不可以访问到比自己更早的节点)

割点

割点判定法则:

  1. \(x\) 不是搜索树的根节点,则 \(x\) 是割点当且仅当搜索树上存在 \(x\) 的一个子节点 \(y\),满足:\(dfn_x \leq low_y\)
  2. \(x\) 是搜索树的根节点,则 \(x\) 是割点当且仅当 \(x\) 存在至少两个子节点满足上一条件。
#include <cstdio>
#include <algorithm>

const int N=2e4+10;
const int M=1e5+10;

int h[N],ver[M<<1],nxt[M<<1],idx;

void add(int u,int v) {
	ver[++idx]=v;nxt[idx]=h[u];h[u]=idx;
}

int n,m,ans;
int dfn[N],low[N],t;
bool cut[N];

void tarjan(int u,int fa,int rt) {
	dfn[u]=low[u]=++t;
	int cnt=0;
	for(int i=h[u];i;i=nxt[i]) {
		int v=ver[i];
		if(v==fa) continue;
		if(!dfn[v]) {
			tarjan(v,u,rt);
			low[u]=std::min(low[u],low[v]);
			if(low[v]>=dfn[u]) {
				++cnt;
				if(u!=rt || cnt>=2) cut[u]=true;
			}
		} else low[u]=std::min(low[u],dfn[v]);
	}
}

int main() {
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++) {
		int u=0,v=0;
		scanf("%d%d",&u,&v);
		add(u,v);add(v,u);
	}
	for(int i=1;i<=n;i++) {
		if(dfn[i]) continue;
		tarjan(i,0,i);
	}
	for(int i=1;i<=n;i++) ans+=cut[i];
	printf("%d\n",ans);
	for(int i=1;i<=n;i++) {
		if(cut[i]) printf("%d ",i);
	}
	return 0;
}

无向图的双连通分量

边双(e-DCC)连通分量的求法

只需求出无向图中所有的桥,把桥都删除后,无向图会分成若干个连通块,每一个连通块就是一个边双连通分量。

#include <cstdio>
#include <vector>
#include <algorithm>

const int N=5e5+10;
const int M=2e6+10;

int h[N],ver[M<<1],nxt[M<<1],idx=1;

void add(int u,int v) {
	ver[++idx]=v;nxt[idx]=h[u];h[u]=idx;
}

int n,m;
int dfn[N],low[N],t;
bool cut[M<<1];

void tarjan(int u,int fa) {
	dfn[u]=low[u]=++t;
	for(int i=h[u];i;i=nxt[i]) {
		int v=ver[i];
		if(v==fa) continue;
		if(!dfn[v]) {
			tarjan(v,u);
			low[u]=std::min(low[u],low[v]);
			if(low[v]>dfn[u]) cut[i]=cut[i^1]=true;
		} else low[u]=std::min(low[u],dfn[v]);
	}
}

bool vis[N];
std::vector<int> ans[N];
int cnt;

void dfs(int u,int fa) {
	ans[cnt].push_back(u);vis[u]=true;
	for(int i=h[u];i;i=nxt[i]) {
		int v=ver[i];
		if(v==fa || cut[i]) continue;
		if(!vis[v]) dfs(v,u);
	}
}

int main() {
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++) {
		int u=0,v=0;
		scanf("%d%d",&u,&v);
		add(u,v);add(v,u);
	}
	for(int i=1;i<=n;i++) {
		if(!dfn[i]) tarjan(i,0);	
	}
	for(int i=1;i<=n;i++) {
		if(!vis[i]) ++cnt,dfs(i,0);
	}
	printf("%d\n",cnt);
	for(int i=1;i<=cnt;i++) {
		printf("%d ",ans[i].size());
		for(auto x:ans[i]) printf("%d ",x);
		puts("");
	}
	return 0;
}

点双连通分量(v-DCC)的求法

首先孤立点和自环一定是一个点双。

然后在 Tarjan 算法中维护一个栈,当一个节点第一次被访问时,入栈,当割点判定法则成立时,无论 \(x\) 是否为根,都要不断从栈顶弹出元素,直到节点 \(y\) 被弹出,最后与节点 \(x\) 一起构成一个点双。

#include <cstdio>
#include <vector>
#include <algorithm>

const int N=5e5+10;
const int M=2e6+10;

int h[N],ver[M<<1],nxt[M<<1],idx;

void add(int u,int v) {
	ver[++idx]=v;nxt[idx]=h[u];h[u]=idx;
}

int n,m;
int dfn[N],low[N],t;

int sta[N],top,cnt;
std::vector<int> ans[N];

void tarjan(int u) {
	dfn[u]=low[u]=++t;
	sta[++top]=u;
	for(int i=h[u];i;i=nxt[i]) {
		int v=ver[i];
		if(!dfn[v]) {
			tarjan(v);
			low[u]=std::min(low[u],low[v]);
			if(dfn[u]<=low[v]) {
				++cnt;int x=0;
				ans[cnt].push_back(u);
				do {
					x=sta[top--];
					ans[cnt].push_back(x);
				} while (x!=v);
			}
		} else low[u]=std::min(low[u],dfn[v]);
	}
	if(!h[u]) ans[++cnt].push_back(u);
}

int main() {
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++) {
		int u=0,v=0;
		scanf("%d%d",&u,&v);
		if(u==v) continue;
		add(u,v);add(v,u);
	}
	for(int i=1;i<=n;i++) {
		if(!dfn[i]) tarjan(i);
	}
	printf("%d\n",cnt);
	for(int i=1;i<=cnt;i++) {
		printf("%d ",ans[i].size());
		for(auto x:ans[i]) printf("%d ",x);
		puts("");
	}
	return 0;
}
posted @ 2025-08-07 16:49  ldh081122  阅读(25)  评论(0)    收藏  举报