普及常见图论算法整理

Posted tztqwq

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了普及常见图论算法整理相关的知识,希望对你有一定的参考价值。


约定

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

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)) 也是割边。


点双边双

概念明细

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

  • 对于一个无向连通图, 如果任意两点 (u、v) 之间都存在至少两条点不重复(不包括 (u、v)) 的路径, 则说这个图是 点--双联通 的。(等价于图中无割点,或任意两条边都在同一个简单环中)
  • 类似的, 如果一个无向连通图的任意两点 (u、v) 之间都存在至少两条边不重复的路径, 则则说这个图是 边--双联通 的。(等价于图中无割边,或每条边都在至少一个简单环中)

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

  • 每条边恰好属于一个点双连通分量, 两个点双连通分量的可能会有公共点,如果有,则公共点必为割点; 割点必同时在至少两个点双连通分量里

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

  • 每个点恰属于一个边双联通分量, 除割边外, 每条边恰属于一个边双联通分量; 一条割边连接两个边双连通分量

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

我懒, 不想写代码了

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


有向图信息、操作

拓扑排序

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

强连通分量、缩点

  • 若对于一个有向图的任意两点 (u、v) 都可以相互到达, 则称这个有向图是强联通的。(具体有没有这个称呼我不清楚)
    有向图的 极大 强连通子图 似乎理所应当就叫 强连通分量 了。

算法? 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 求树直径的两端点

  • 随便找一个点 (s), 随便选一个距离其最远的点 (u)
  • 随便选一个距离 (u) 最远的点 (v)
    那么路径 (u ightarrow v) 就是这棵数的直径

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

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

这题不错哟

重心

  • 一棵树 (T) 上某一个节点 (x) 的一个特征值 (w(x)) 定义为 : 删掉 (x) 后余下的各个联通块节点数的最大值。
  • (w(x)) 最小的那个 (x) 就是树 (T) 的重心。
  • 树的重心似乎是不唯一的

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

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;
	}
}
























以上是关于普及常见图论算法整理的主要内容,如果未能解决你的问题,请参考以下文章

图论算法模板整理及思路 不断更新 绝对精品

浙江金华 图论整理

[leetcode] 题型整理之图论

图论算法Dijkstra算法

最短路算法

图论动态规划算法——Floyd最短路径