编程语言
首页 > 编程语言> > 普及常见图论算法整理

普及常见图论算法整理

作者:互联网

目录


约定

我是怎么存图的呢? 普通的邻接表。

const int N = 1e5+15; // 点数 
const int M = 1e6+15; // 边数 
int ct,hd[N],nt[M<<1],vr[M<<1],vl[M<<1];
void ad(int a,int b,int c) { // 加一条 e = a->b, w(e)=c 的有向边 e
	vr[++ct]=b,vl[ct]=c;
	nt[ct]=hd[a],hd[a]=ct;
}

一般连通无向图的信息、操作

遍历

遍历嘛……

bool inq[N];
inline void prev(int x) { inq[x]=true;  }
inline void posv(int x) { inq[x]=false; }
void dfs(int x) {
	prev(x); //第一次访问时做点啥 
	for(int i=hd[x];i;i=nt[i]) {
		int y=vr[i];
		if(!inq[y]) dfs(y);
	}
	posv(x); //回溯时也总要做点啥 
	return;
} 

联通块

加点东西就行了, 使用的时候调用

for(int i=1;i<=n;++i)if(!col[i]) {
		++tot; dfs(i);
}

即可

int tot, col[N];
bool inq[N];
inline void prev(int x) { inq[x]=true; col[x]=tot;  }
inline void posv(int x) { inq[x]=false; }
void dfs(int x) {
	prev(x);
	for(int i=hd[x];i;i=nt[i]) {
		int y=vr[i];
		if(!inq[y]) dfs(y);
	}
	posv(x);
	return;
} 

二分图判定

没有奇环(环上节点数为奇数的环)就是二分图了, 具体可以对图二染色(对节点黑白染色,使得每个节点的相邻节点与其不同色), 看是否可以不矛盾地染完色。
使用时调用

col[1]=1, ans= dfs(1);

即可

int col[N];
bool dfs(int x) {
	for(int i=hd[x];i;i=nt[i]) {
		int y=vr[i];
		if(col[x] == col[y]) return false;
		if(!col[y]) {
			col[y]=3-col[x];
			if(!dfs(y)) return false;
		}
	}
	return true;
} 

割点割边

不想盗图。
在 \(dfs\) 过程中, 经过的边构成了一棵树,叫 dfs树。在dfs树上的边就叫 树边, 剩下的边叫 返祖边(其实叫 反向边 (back edge))。

割点的判定方法
树根 : 子节点数 \(\ge 2\) 时, 树根为割点。
非根 : 设此节点 \(u\) 在 dfs树 上存在一子节点 \(v\) 使得以 \(v\) 为根的子树中的所有节点 (包括 \(v\)) 都没有连向 \(u\) 的祖先的返祖边, 那么 \(u\) 就是割点, 反之不是。
(显然成立)

实现就很经典了, 用到 \(dfs\) 序和 \(low[]\) 数组。
使用时调用

dfs(1,0);

即可。

//注意区分根和非根节点
bool cut[N];
int rt, tot,dfn[N],low[N];
inline void prev(int x) {low[x]=dfn[x]= ++tot;  }
void dfs(int x, int fa) {
	prev(x);
	int ch= 0;
	for(int i=hd[x];i;i=nt[i]) {
		int y= vr[i];
		if(!dfn[y]) {
			++ch;
			dfs(y,x);
			if(fa && low[y]>=dfn[x]) cut[x]= true;
			low[x]= min(low[x],low[y]);
		} else if(dfn[y]<dfn[x] && y!=fa) low[x]=min(low[x],dfn[y]);
	}
	if(fa==0 && ch>=2) cut[x]= true;
}

割边的判定方法
同上, 在 \(dfs\) 树上判定割边。
设一个节点 \(u\) 在 dfs树 上存在一子节点 \(v\) 使得以 \(v\) 为根的子树中的所有节点 (包括 \(v\)) 都没有连向 \(u\) 及其祖先的返祖边, 那么 边\((u,v)\) 就是割边, 反之不是。
(显然成立)

实现时以上面的代码为基础加点东西就可以了, 但是要注意存无向边时是用两条有向边代替的, 当一条边 \((x,y)\) 是割边时, 边 \((y,x)\) 也是割边。


点双边双

概念明细

点--双联通边--双联通 都属于无向连通图的 \(性质\)。

称无向连通图的 极大 点--双联通子图(极大的,即往其中加了点就不满足性质的节点数最多的) 为其 点双连通分量

类似的,称无向连通图的 极大 边--双联通子图 为其 边双连通分量

怎么求?
点双不会。
边双 就简单了, 首先求出所有割边, 然后断开所有割边(\(dfs\) 时不走割边就行了), 剩下的每个联通块就是一个边双。

我懒, 不想写代码了

训练指南好啊。
边双基础题


有向图信息、操作

拓扑排序

拓扑排序 是作用于有向无环图的一种算法。
拓扑排序就是按点的入数大小为顺序删点, 删去一个点时可能会有没被删的点的入度减小。
(这个描述太 \(SD\) 我自己都忍俊不禁)

强连通分量、缩点

算法? Tarjan!!!
怎么简洁准确地描述它呢……
我没有足够的能力描述它。

至于 缩点, 就是将有向图的每个强连通分量看成一个点, 建立一个新图。
由于要存多个图, 所以如何写代码是我要思考的qwq。

强连通分量+缩点板子

#include<bits/stdc++.h>
using namespace std;
const int N = 1e4+15;
const int M = 1e5+15;

int n, m, val[N], val2[N];

int ct, hd[N], nt[M<<1], vr[M<<1];
void ad(int a,int b) {
	vr[++ct]=b, nt[ct]=hd[a], hd[a]=ct;
}

int deg[N];
int ct2, hd2[N], nt2[M<<1], vr2[M<<1];
void ad2(int a,int b) {
	vr2[++ct2]=b, nt2[ct2]=hd2[a], hd2[a]=ct2;
}

int sccno[N], scccnt;
int S[N], tp;
int dfn[N], low[N], clk;
void dfs(int x) {
	dfn[x]=low[x]= ++clk;
	S[++tp]=x;
	for(int i=hd[x];i;i=nt[i]) {
		int y=vr[i];
		if(!dfn[y]) {
			dfs(y);
			low[x]=min(low[x],low[y]);
		} else if(!sccno[y]) low[x]=min(low[x],dfn[y]);
	}
	if(low[x]==dfn[x]) {
		++scccnt;
		while(1) {
			int u=S[tp--];
			sccno[u] = scccnt;
			if(u==x) break;
		}
	}
}

int f[N];
int q[N], h=1, t=0;
void topo() {
	for(int i=1;i<=scccnt;++i) if(!deg[i]) f[q[++t]=i] = val2[i];
	while(h<=t) {
		int x=q[h++];
		for(int i=hd2[x];i;i=nt2[i]) {
			int y=vr2[i];
			f[y] = max(f[y], f[x]+val2[y]);
			if(--deg[y] == 0) q[++t] = y;
		}
	}
}

int main()
{
	scanf("%d%d", &n, &m);
	for(int i=1;i<=n;++i) scanf("%d", &val[i]);
	for(int i=0,x,y;i<m;++i) {
		scanf("%d%d",&x,&y); ad(x,y);
	}
	for(int i=1;i<=n;++i) if(!dfn[i]) dfs(i);
	for(int x=1;x<=n;++x) {
		val2[sccno[x]] += val[x];
		for(int j=hd[x];j;j=nt[j]) {
			int y=vr[j];
			if(sccno[x] != sccno[y]) ad2(sccno[x],sccno[y]), ++deg[sccno[y]];
		}
	}
	
	topo();
	int ans = 0;
	for(int i=1;i<=scccnt;++i) ans=max(ans, f[i]);
	cout << ans;
	return 0;
}

简单树论

直径

可以 树形DP 求, 也可以两次 dfs
dfs 方法好像得出方案更容易。
这里给出 dfs方法

dfs 求树直径的两端点

正确性?
如果 \(u\) 确实是某条直径上的端点, 那么 \(v\) 就一定是另一个端点。
为什么 \(u\) 一定是某条直径的一个端点呢? 明明 \(s\) 是随便选的啊!

可以这样想:假定直径的两个端点分别为 \(u、v\), 不管 \(s\) 在不在直径上, 只要存在任意一点 \(t\) 距离 \(s\) 最远(大于 \(s\) 到 \(u\) 的距离 和 \(s\) 到 \(v\) 的距离), 那么就可以推出
路径 \(u \rightarrow v\) 不是直径。

这题不错哟

重心

求法的话, 按照定义求就好了, 这里有一份参考代码。

int siz[N], cg=0, cgw=n; // cg:重心, cgw:重心的 w 函数值 
void dfs(int x) {
	siz[x] = 1;
	int w = 0;
	for(int i=hd[x];i;i=nt[i]) {
		int y=vr[i];
		if(siz[y]) continue;
		dfs(y);
		siz[x] += siz[y];
		w = max(w, siz[y]);
	}
	w = max(w, n-siz[x]);
	if(w < cgw) {
		cgw = w;
		cg = x;
	}
}

标签:图论,联通,int,连通,dfs,割边,算法,节点,普及
来源: https://www.cnblogs.com/tztqwq/p/13019726.html