图论模板笔记

图论

链式前向星

struct node{
	int v;
	ll w;
	int next;
}e[maxm];
int head[maxn];
int n, m, fr, to, cnt;
ll w, d[maxn];
void add_edge(int t1, int t2, ll w){
	e[cnt].v=t2, e[cnt].w=w;
	e[cnt].next=head[t1];
	head[t1]=cnt++;
	return;
}
for(int i=head[u]; ~i; i=e[i].next)

LAC
vector<int> v[MXN];
vector<int> w[MXN];
// 题目点从0到n-1
// 代码点从1到n

int fa[MXN][31], cost[MXN][31], dep[MXN];
int n, m;
int a, b, c;
void dfs(int root, int fno) {
  fa[root][0] = fno;
  dep[root] = dep[fa[root][0]] + 1;
  for (int i = 1; i < 31; ++i) {
    fa[root][i] = fa[fa[root][i - 1]][i - 1];
    cost[root][i] = cost[fa[root][i - 1]][i - 1] + cost[root][i - 1];
  }
  int sz = v[root].size();
  for (int i = 0; i < sz; ++i) {
    if (v[root][i] == fno) continue;
    cost[v[root][i]][0] = w[root][i];
    dfs(v[root][i], root);
  }
}
int lca(int x, int y) {
  if (dep[x] > dep[y]) swap(x, y);
  int tmp = dep[y] - dep[x], ans = 0;
  for (int j = 0; tmp; ++j, tmp >>= 1)
    if (tmp & 1) ans += cost[y][j], y = fa[y][j];
  if (y == x) return ans;
  for (int j = 30; j >= 0 && y != x; --j) {
    if (fa[x][j] != fa[y][j]) {
      ans += cost[x][j] + cost[y][j];
      x = fa[x][j];
      y = fa[y][j];
    }
  }
  ans += cost[x][0] + cost[y][0];
  return ans;
}
int main() {
  memset(fa, 0, sizeof(fa));
  memset(cost, 0, sizeof(cost));
  memset(dep, 0, sizeof(dep));
  scanf("%d", &n);
  for (int i = 1; i < n; ++i) {
    scanf("%d %d %d", &a, &b, &c);
    ++a, ++b;
    v[a].push_back(b);
    v[b].push_back(a);
    w[a].push_back(c);
    w[b].push_back(c);
  }
  dfs(1, 0);
  scanf("%d", &m);
  for (int i = 0; i < m; ++i) {
    scanf("%d %d", &a, &b);
    ++a, ++b;
    printf("%d\n", lca(a, b));
  }
  return 0;
}

//zzx的模板

const int LOG=20, maxv=100000;

vector<int> G[maxv];
int root=1;

int fa[maxv][LOG];
int dep[maxv];

void dfs(int v, int p, int d) {
	fa[v][0]=p;
	dep[v]=d;
	for(int i=0; i<(int)G[v].size(); i++) {
		if(G[v][i]!=p) dfs(G[v][i], v, d+1);
	}
}

void init(int n) {
	dfs(root, -1, 0);
	for(int k=0; k+1<LOG; k++) {
		for(int v=1; v<=n; v++) {
			if(fa[v][k]<0) fa[v][k+1]=-1;
			else fa[v][k+1]=fa[fa[v][k]][k];
		}		
	}
}

int LCA(int u, int v) {
	if(dep[u]>dep[v]) swap(u, v);
	for(int k=LOG-1; k>=0; k--)
	if(fa[v][k]!=-1 && dep[fa[v][k]]>=dep[u]) v=fa[v][k];
	if(u==v) return u;
	
	for(int k=LOG-1; k>=0; k--)
	if(fa[u][k] != fa[v][k]) {
		u=fa[u][k];
		v=fa[v][k];
	}
	return fa[u][0];
}
int dis(int x, int y) {
	return dep[x]+dep[y]-2*dep[LCA(x, y)];
}
int main() {
	int n, a, b;
	cin>>n;
	for(int i=1; i<n; i++) {
		cin>>a>>b;
		G[a].push_back(b); G[b].push_back(a);
	}
	init(n) ;
	for(int i=1; i<=n; i++) cout<<dep[i]<<' '<<fa[i][0]<<endl; 
	while(cin>>a>>b) {
		cout<<LCA(a, b)<<endl;
	}
	return 0;
}
树的重心
int mins, id; //记录最小max(max(dp[j]), n-dp[i]), j为i的子节点
void dfs(int u, int fa) {
	dp[u] = 1;        //u节点自身先算上
	int maxs = 0;
	for(int i = head[u]; ~i; i = edge[i].next) {
		int v = edge[i].v;
		if(v != fa) {
			dfs(v, u);
			dp[u] += dp[v];
			maxs = max(maxs, dp[v]);   //求max(dp[j])
		}
	}
	maxs = max(maxs, n - dp[u]); //求max(dp[j], n-dp[i])
	if(maxs < mins) {
		mins = maxs;
		id = u;
	}
}
mins=inf;
dfs(1, 0); //dfs(根节点, 不存在编号)
树的直径
两点直径

d1+d2

直径经过某点\(u\),直径一定为\(u\)子树的最长边+次长边。

vector<int> G[maxn];
int n, d;
int dfs(int u, int p) {
	int d1=0, d2=0;
	for (auto v: G[u]) {
		if(v==p) continue;
		int d=dfs(v, u)+1;
		if(d>d1) d2=d1, d1=d;
		else if(d>d2) d2 = d;
	}
	d=max(d, d1+d2);
	return d1;
}
int main() {
	cin>>n;
	for(int i=0; i<n-1; ++i) {
		int a, b;
		cin>>a>>b;
		G[a].push_back(b);
		G[b].push_back(a);
	}
	d=0;//全局变量
	dfs(1, 0);
	cout<<d<<endl;
}

2dfs

随便选一点\(u\),离\(u\)最远的点\(x\),一定可以是直径的一个端点。只需对\(x\)再进行一次深搜。

vector<int> G[maxn];
int d[maxn], md, mu;
int n, pr[maxn];
bool vis[maxn];//记录直径vis=1
void dfs(int u, int p, int dep) {
	d[u]=dep, pr[u]=p;
	if(d[u]>md) md=d[u], mu=u;
	for(auto v: G[u])
		if(v!=p) dfs(v, u, dep+1);
}
int main() {
	cin>>n;
	for(int i=0; i<n-1; ++i) {
		int a, b;
		cin>>a>>b;
		G[a].push_back(b);
		G[b].push_back(a);
	}
	int x, y, d;//直径端点和直径长
	md=-1;
	dfs(1, 0, 0);
	x=mu, md=-1;
	dfs(x, 0, 0);
	y=mu, d=md, vis[x]=1;
	int ty=y;
	while(pr[ty]){
		vis[ty]=1, ty=pr[ty];
	} 
	return 0;
}
三点直径

直径为在\(n\ge2\)树中取两个不同点使简单路覆盖最大,现在\(n\ge3\)树中取3个不同点使3对简单路径覆盖最大,可以证明,其中两个点一定是直径\((x,y)\)。注意,对直径深搜后得到的\(z\)若等与\(x\)\(y\),使\(z=pr[y]\)

网络流

最大流

//白书,过简单数据
struct edge{
	int to, cap, rev;
};
vector<edge> G[maxn*2];
bool used[maxn*2];
void add(int from ,int to, int cap){
	G[from].push_back((edge){to, cap, (int)G[to].size()});
	G[to].push_back((edge){from, 0, (int)G[from].size()-1});
}
int dfs(int v, int t, int f){
	if(v==t) return f;
	used[v]=true;
	for(int i=0; i<(int)G[v].size(); i++){
		edge &e=G[v][i];
		if(!used[e.to] && e.cap>0){
			int d=dfs(e.to, t ,min(f, e.cap));
			if(d>0){
				e.cap-=d;
				G[e.to][e.rev].cap+=d;
				return d;
			}
		}
	}
	return 0;
}
int max_f(int s, int t){
	int flow=0;
	for(;;){
		memset(used, 0, sizeof(used));
		int f=dfs(s, t, inf);
		if(f==0) return flow;
		flow+=f; 
	}
}

trie树

输入和查询间隔一个空行
#include<bits/stdc++.h>
using namespace std;
#define ll long long
struct node{
	int data;
	int child[26];
	int ans;
}t;
vector<node> v;
string s;
int num=0;
void insert(string s){
	int si=(int)s.size();
	int cur=0;
	for(int i=0; i<si; i++){
		int data=s[i]-'a';
		if(v[cur].child[data]==0){
			v[cur].child[data]=(int)v.size();
			t.data=data;
			t.ans=0;
			v.push_back(t);
		}
		cur=v[cur].child[data];
		v[cur].ans++;
	}
}
int qurry(string s){
	int si=(int)s.size();
	int cur=0;
	for(int i=0; i<si; i++){
		int data=s[i]-'a';
		if(v[cur].child[data]==0 && i!=si-1) return 0;
		cur=v[cur].child[data];
	}
	return v[cur].ans;
}
int main(){
	t.data=-1, t.ans=0;
	v.push_back(t);
	while(getline(cin, s)){
		if((int)s.size()==0) break;
		insert(s);
	}
	while(cin>>s) cout<<qurry(s)<<endl;
	return 0;
}

矩阵树

求图的生成树个数

该内容内所有图可有向无向,允许重边,不允许自环

无向图

\(D(G)\)\(D_{ii}(G)=deg(i),D_{ij}=0,i\neq j\)

\(A(G)\)\(A_{ij}(G)=A_{ji}(G)=\#e(i,j),i\neq j\)

\(Laplace\)矩阵:\(L(G)=D(G)-A(G)\)

\(t(G)=detL(G)(_{1,2,\cdots,i-1,i+1,\cdots,n}^{1,2,\cdots,i-1,i+1,\cdots,n})\),即缺少第\(i\)行第\(i\)

\(t(G)=\frac{1}{n}\lambda_1\lambda_2\cdots\lambda_{n-1}\),其中\(\lambda_1,\lambda_2,\cdots,\lambda_{n-1}\)\(L(G)\)\(n-1\)个非零特征值

#define maxn 113
#define maxm 413

ll L[maxn][maxn];
ll gauss(int n){
	ll res=1;
	for(int i=1; i<=n; i++){
		if(!L[i][i]){
			bool flag=0;
			for(int j=i+1; j<=n; j++){
				if(L[j][i]){
					flag=1;
					for(int k=i; k<n; k++){
						swap(L[i][k], L[j][k]);
					}
					res=-res;
					break;
				}
			}
			if(!flag) return 0;
		}
		for(int j=i+1; j<=n; j++){
			while(L[j][i]){
				ll t=L[i][i]/L[j][i];
				for(int k=i; k<=n; k++){
					L[i][k]-=L[j][k]*t;
					swap(L[i][k], L[j][k]);
				}
				res=-res;
			}
		}
		res*=L[i][i];
	}
	return res;
}
int a, b, n, m;
void init(){
	for(int i=1; i<=n; i++){
		for(int j=1; j<=i; j++)
			L[i][j]=L[j][i]=0;
	}
	return;	
}
int main(){
	int T;
	cin>>T;
	while(T--){
		scanf("%d%d", &n, &m);
		init();
		for(int i=0; i<m; i++){
			scanf("%d%d", &a, &b);
			L[a][a]++; L[a][b]--, L[b][a]--; L[b][b]++;
		}
		for(int i=1; i<=n; i++){
			for(int j=1; j<=n; j++){
				cout<<L[i][j]<<" ";
			}
			cout<<"\n";
		}
		n=n-1;
		ll ans=gauss(n);
		printf("%lld\n", (ll)ans);
	}
	return 0;
}

有向图

\(D^{out}(G)\)\(D_{ii}^{out}(G)=deg^{out}(i),D_{ij}^{out}=0,i\neq j\)

\(A(G)\)\(A_{ij}(G)=\#e(i,j),i\neq j\)

\(L^{out}(G)=D^{out}(G)-A(G)\)

同样定义\(L^{in}(G)\)

记图\(G\)的以\(r\)为根的所有根向树形图个数为\(t^{root}(G,r)\)。所谓根向树形图,是说这张图的基图是一棵树,所有的边全部指向父亲。

记图\(G\)的以\(r\)为根的所有叶向树形图个数为\(t^{leaf}(G,r)\)。所谓叶向树形图,是说这张图的基图是一棵树,所有的边全部指向儿子。

\(t^{root}(G,k)=detL^{out}(G)(_{1,2,\cdots,k-1,k+1,\cdots,n}^{1,2,\cdots,k-1,k+1,\cdots,n})\)

\(t^{leaf}(G,k)=detL^{in}(G)(_{1,2,\cdots,k-1,k+1,\cdots,n}^{1,2,\cdots,k-1,k+1,\cdots,n})\)

二分图

判定是否为二分图:dfs或bfs,若不存在奇环则为二分图。

最大匹配

寻找二分图边数最大的匹配。

二分图最小点覆盖

二分图中,求最少点集,使得每一条边都至少有一个有端点在这个点集中。

对比最小覆盖的DLX,增加了条件每列一定有且只有2个1,每行仍然可以0,1,2到任意多个1。把边当成列,点当成行,一列两行有值求解最小行覆盖所有列。为重复覆盖,而非精确覆盖。

二分图最小点覆盖=二分图中最大匹配。

二分图最小边覆盖

二分图中,求最小边集,使得每个点都在这个边集至少一条边的端点上。

对比最小覆盖的DLX,增加了条件每行一定有且只有2个1,每列仍然可以0,1,2到任意多个1。把边当成行,点当成列,一行两列有值求解最小行覆盖所有列。为重复覆盖,而非精确覆盖。

二分图最小边覆盖=顶点数-最小点覆盖。

匈牙利算法\(O(NM)\)

#include <bits/stdc++.h>
using namespace std;
#define maxn 2006
int n, m, e;
vector<int> G[maxn];  //使用邻接表来储存边
int match[maxn], vis[maxn];
bool dfs(int u){
	int len=G[u].size();
	for (int i=0; i<len; i++){  //遍历每一条边
		int v=G[u][i];
		if(vis[v]) continue;
		vis[v]=1;
		if (!match[v] || dfs(match[v])){  //如果v没有匹配,或者v的匹配找到了新的匹配
			match[v]=u;
			match[u]=v;  //更新匹配信息
			return 1;
		}
	}
	return 0;
}
void add(int a, int b){
	G[a].push_back(n+b);
	G[n+b].push_back(a);
}
int main(){
	scanf("%d %d %d", &n, &m, &e);
	for(int i= 1; i<= e; i++){
		int a, b;
		scanf("%d %d", &a, &b);
		if(a>n || b>m) continue;
		add(a, b);
	}
	int ans=0;
	for(int i=1; i<=n; i++){  //对每一个点尝试匹配
		for (int j=1; j<=n+m; j++) vis[j]=0;
		if(dfs(i)) ans++;
	}
	printf("%d", ans);
	return 0;
}
/*
for(int i=1; i<=n+m; i++){
	memset(vis, 0, sizeof(vis));
	if(dfs(i)) ans++;
}
*/

eg:给出集合\(a[i]\),集合中数两两不同,求删去一些数,使最终不存在存在\(i,j\)使\(a[j]\%a[i]==0\ \&\&\ a[j]/a[i]!=prime\)。求最终集合的最大大小。观察可知计数每个数的全部素因子的个数\(\Omega(n)\),按奇偶分类,因为同奇偶性的数一定不矛盾,可放在二分图的左右边。连接矛盾的,答案为原集合大小减去最大二分匹配。

最大权匹配

最小环

边权和最小,\(n\geq3\)。图的最小环也称为围长。

暴力:枚举所有边,每次删除一条\((u,v)\)\(u\)\(v\)跑单源最短路,最小环是\(dis(u,v)+w\)

\(floyd\)

int val[maxn + 1][maxn + 1];  // 原图的邻接矩阵,无向图
inline int floyd(const int &n) {
	static int dis[maxn + 1][maxn + 1];  // 最短路矩阵
	for (int i = 1; i <= n; ++i)
		for (int j = 1; j <= n; ++j) dis[i][j] = val[i][j];  // 初始化最短路矩阵
	int ans = inf;
	for (int k = 1; k <= n; ++k) {
		for (int i = 1; i < k; ++i)
			for (int j = 1; j < i; ++j)
				ans = std::min(ans, dis[i][j] + val[i][k] + val[k][j]);  // 更新答案
		for (int i = 1; i <= n; ++i)
			for (int j = 1; j <= n; ++j)
				dis[i][j] = std::min(
					dis[i][j], dis[i][k] + dis[k][j]);  // 正常的 floyd 更新最短路矩阵
	}
	return ans;
}

负环

bfs负环:跑spfa时判断每一个点的入栈数++cnt[i]>n则有负环

bool vis[maxn];
ll d[maxn];
void spfa(int u){
    vis[u]=1;
    for(int i=head[u]; ~i; i=e[i].next){
    	int v=e[i].to;
    	ll w=e[i].w;
        if(d[v]>d[u]+w){
            d[v]=d[u]+w;
            if(vis[v] || flag){
            	flag=1;
            	return;
			}
            spfa(v);
        }
    }
    vis[u]=0;
    return;
}
for(int i=1; i<=n; i++){//防止图不连通
	spfa(i);
	if(flag) break;
}

拆点

点权到边权:一个点拆为两个,\(1\Rightarrow 1_1\rightarrow1_2\)

负边

新建节点0,跑\(spfa\)记为\(h_i\),则\(w\Rightarrow w+h_u-h_v\)

最短路

SPFA
vector<int> G[maxn];
struct edges{
	int to;
	ll w;
}e;
vector<edges> E;
int n, m, fr, to;
ll w, d[maxn];
void spfa(int s){
	queue<int> q;
	while(!q.empty()) q.pop();
	bool inq[maxn]={};
	for(int i=0; i<maxn; i++) d[i]=inf;
	d[s]=0, q.push(s), inq[s]=1;
	while(!q.empty()){
		int u=q.front();
		q.pop(), inq[u]=0;
		for(int i=0; i<(int)G[u].size(); i++){
			e=E[G[u][i]];
			int v=e.to;
           	w=e.w;
			if(d[v]>d[u]+w){
				d[v]=d[u]+w;
				if(!inq[v]){
					q.push(v);
					inq[v]=1;
				}
			}
		}
	}
	return;
}
void add_edge(int t1, int t2, ll w){
	e.to=t2, e.w=w;
	G[t1].push_back((int)E.size());
	E.push_back(e);
	e.to=t1;
	G[t2].push_back((int)E.size());
	E.push_back(e);
	return;
}
int main(){
	cin>>m>>n;
	for(int i=0; i<m; i++){
		scanf("%d %d %lld", &fr, &to, &w);
		add_edge(fr, to, w);
	}
	spfa(1);
	cout<<d[n];
	return 0;
}
stack实现的SPFA(+负环)

邻接表(较慢,eg: poj3159会tle)

stack<int> st;
void spfa(int s){
	while(!st.empty()) st.pop();
	bool inq[maxn]={};
	for(int i=0; i<maxn; i++) d[i]=inf;
	d[s]=0, st.push(s), inq[s]=1;
	while(!st.empty()){
		int u=st.top();
		st.pop();
		inq[u]=0;
		for(int i=0; i<(int)G[u].size(); i++){
			e=E[G[u][i]];
			int v=e.to;
			w=e.w;
			if(d[v]>d[u]+w){
				d[v]=d[u]+w;
				if(!inq[v]){
					st.push(v);
					inq[v]=1;
				}
			}
		}
	}
	return;
}
链式前向星(较快)
struct node{
	int v;
	ll w;
	int next;
}e[maxm];
int head[maxn];
int n, m, fr, to, cnt;
ll w, d[maxn];
stack<int> st;
void spfa(int s){
	while(!st.empty()) st.pop();
	bool inq[maxn]={};
	for(int i=0; i<maxn; i++) d[i]=inf;
	d[s]=0, st.push(s), inq[s]=1;
	while(!st.empty()){
		int u=st.top();
		st.pop();
		inq[u]=0;
		for(int i=head[u]; ~i; i=e[i].next){
			int v=e[i].v;
			w=e[i].w;
			if(d[v]>d[u]+w){
				d[v]=d[u]+w;
				if(!inq[v]){
					st.push(v);
					inq[v]=1;
				}
			}
		}
	}
	return;
}
void add_edge(int t1, int t2, ll w){
	e[cnt].v=t2, e[cnt].w=w;
	e[cnt].next=head[t1];
	head[t1]=cnt++;
	return;
}
dijkstra
struct edge{
	int to;
	ll w;
	edge(){}
	edge(int _to, ll _w){
		to=_to, w=_w;
	}
}e;
vector<edge> G[maxn];
int n;
void add(int u, int v, ll w){
	G[u].push_back(edge(v, w));
}
ll d[maxn];
priority_queue<pair<ll, int>, vector<pair<ll, int> >, 
	greater<pair<ll, int> > > q;
void dij(int s){
	for(int i=1; i<=n; i++) d[i]=inf;
	d[s]=0;
	q.push(make_pair(0, s));
	while(!q.empty()){
		ll dis=q.top().first;
		int u=q.top().second;
		q.pop();
		if(d[u]<dis) continue;
		int si=(int)G[u].size();
		for(int i=0; i<si; i++){
			int v=G[u][i].to;
			ll w=G[u][i].w;
			if(d[v]>d[u]+w){
				d[v]=d[u]+w;
				q.push(make_pair(d[v], v));
			}
		}
	}
}

生成树

次小生成树

充分不必要:在MST上仅替换一条边形成。

1.kruskal求最小生成树\(o(eloge)\), 枚举删除MST每条边\(o(n)\)或枚举添加非MST中每条边(边多时,后者较慢),kruskal求最小生成树\(o(n)\)\(o(eloge+n^2)\)

2.prim求最小生成树\(o(n^2)\)(也可\(o(elog_n)\),但没必要),枚举每条不在MST中的边,添加并减去环中最长边。\(o(n^2)\)

3.kruskal+lca??(不知道什么东西)

//法1
struct node{
	int a, b, id;
	ll w;
	bool operator <(const node &n)const{
		return w<n.w;
	}
}no;
int fa[maxn];
int n, m, a, b, cnt;
ll w, ans, po[maxn];
int x[maxn], y[maxn];
vector<node> q;
bool vi[maxn];
void init(){
	for(int i=0; i<maxn; i++)
		fa[i]=i;
	cnt=0;
	ans=0;
}
int find(int a){
	if(fa[a]==a) return a;
	return fa[a]=find(fa[a]);
}
bool uni(int a, int b){
	int ro1=find(a), ro2=find(b);
	if(ro1==ro2) return 1;
	fa[ro1]=ro2;
	return 0;
}
bool kru(int id){
	int i=0;
	while(cnt<n-1){
		no=q[i++];
		if(id==no.id) continue;
		a=no.a, b=no.b, w=no.w;
		if(uni(a, b)) continue;
		cnt++, ans+=w;
	}
	return 0;
}
int main(){
	int T;
	cin>>T;
	while(T--){
		scanf("%d %d", &n, &m);
		init();
		q.clear();
		for(int i=1; i<=m; i++){
			scanf("%d %d %lld", &no.a, &no.b, &no.w);
			no.id=i;
			q.push_back(no);
		}
		sort(q.begin(), q.end());
		memset(vi, 0, sizeof(vi));
		int num=0, si=(int)q.size();
		while(cnt<n-1){
			no=q[num++];
			a=no.a, b=no.b, w=no.w;
			if(uni(a, b)) continue;
			vi[no.id]=1;
			cnt++, ans+=w;
		}
		ll mi=ans, res=inf;
		for(int i=0; i<si; i++){
			no=q[i];
			if(!vi[no.id]){
				init();
				uni(no.a, no.b);
				ans+=no.w, cnt++;
				kru(no.id);
				res=min(res, ans);
			}
		}
		printf("%lld %lld\n", mi, res);
	}
	return 0;
}
最小生成树

1.prim可用\(o(n^2)\)计算\(d[i][j]\)\(d\)可表示所有最短路可以跑的东西。

并查集

2.kruskal。关于并查集,最好使用启发式算法,并查集应按秩合并,对大小不一样的集合,将小的集合合并到大的集合中。

struct edge{
	int u, v;
	ll d;
	edge(){}
	edge(int _u, int _v, ll _d){
		u=_u, v=_v, d=_d;
	}
	bool operator<(const edge &n)const{
		return d>n.d;
	}
};
vector<edge> res;
int fa[maxn], n;
void init(){
	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]);
}
bool uni(int x, int y){
	int rx=find(x), ry=find(y);
	if(rx==ry) return 0;
	fa[ry]=rx;
	return 1;
}
priority_queue<edge> q;
ll ans;
void add(int u, int v, ll d){
	q.push(edge(u, v, d));
}
ll kru(){
	ll ans=0;
	res.clear();
	while(!q.empty()){
		edge e=q.top();
		q.pop();
		if(uni(e.u, e.v)){
			ans+=e.d;
			res.push_back(e);
		}
	}
	return ans;
}

带权并查集

int siz[maxn], fa[maxn];
int find(int a){
	if(fa[a]==a) return a;
	return fa[a]=find(fa[a]);
}
void merge(int x, int y){
	int rx=find(x), ry=find(y);
	if(siz[rx]<siz[ry]) swap(rx, ry);
	fa[ry]=rx;
	siz[rx]+=siz[ry];
}
带权并查集
void init(){
	for(int i=0; i<maxn; i++)
		fa[i]=i, r[i]=0;
}
int find(int a){
	if(fa[a]==a) return a;
	int ro=find(fa[a]);
	r[a]^=r[fa[a]];//注意是跟最近的fa[x],而不是ro
	return fa[a]=ro;
}
void uni(int a, int b, bool w){
	int ro1=find(a), ro2=find(b);
	if(ro1!=ro2){
        //if(ro1<ro2) swap(ro1, ro2);把小数字当根
		fa[ro1]=ro2;
		r[ro1]=w^r[b]^r[a];	
	}
}
最小树形图

有向图上的最小生成树(Minimum Directed Spanning Tree)。

常用:朱刘算法(也称 Edmonds 算法)。\(O(nm)\)

1)对于每个点,选择它入度最小的那条边

2)如果没有环,算法终止;否则进行缩环并更新其他点到环的距离。

注:定义中为确定根。若求不定根,不应mdst(1-n)(可能会t),而是添加虚根。

double mdst(int ro){
	double ans=0;
	memset(vis, 0, sizeof(vis));
	while(1){
    	//找最小入边
		for(int i=1; i<=n; i++)
			if(i!=ro && !flag[i]){
				w[i][i]=inf, pre[i]=i;
				for(int j=1; j<=n; j++)
					if(!flag[j] && w[j][i]!=inf &&
						w[j][i]<w[pre[i]][i])
						pre[i]=j;
				if(pre[i]==i) return -1;
			}
		int i=1;
        //找环
		for(i=1; i<=n; i++){
			if(i!=ro && !flag[i]){
				int j=i, cnt=0;
				while(j!=ro && pre[j]!=i && cnt<=n)
					j=pre[j], cnt++;
				if(j==ro || cnt>n) continue; 
                //>n存在环,但i不在环内 
				break;
			}
		}
        //无环,结束
		if(i>n){
			for(int i=1; i<=n; i++)
				if(i!=ro && !flag[i])
					ans+=w[pre[i]][i];
				return ans;
		}
		int j=i;
		memset(vis, 0, sizeof(vis));
        //有环,缩点
		do{
			ans+=w[pre[j]][j], j=pre[j], vis[j]=flag[j]=1;
		}while(j!=i);
		flag[i]=0;
		for(int k=1; k<=n; k++) if(vis[k]){
			for(int j=1; j<=n; j++)	if(!vis[j]){
				if(w[k][j]!=inf && w[i][j]>w[k][j])
					w[i][j]=w[k][j];
                //内向外不变
				if(w[j][k]<inf && w[j][k]-w[pre[k]][k]<w[j][i])
					w[j][i]=w[j][k]-w[pre[k]][k];
                //外向内-pre
			}
		}
	}
	return ans;
}

优化

double mdst(int ro){
	double ans=0;
	while(1){
		for(int i=0; i<n; i++) in[i]=inf;
		in[ro]=0;
		for(int i=0; i<m; i++){
			u=E[i].fr, v=E[i].to, w=E[i].w;
			if(in[v]>w && u!=v && v!=ro){
				in[v]=w;
				pre[v]=u;
			}
		}
		for(int i=0; i<n; i++)
			if(in[i]==inf) return -1;
		int cnt=0;
		memset(id, -1, sizeof(id));//是否属于环
		memset(used, -1, sizeof(used));
		//可重复使用的vis,无实际意义 
		for(int i=0; i<n; i++){
			ans+=in[i];
			v=i;
			while(used[v]!=i && id[v]==-1 && v!=ro){
				//使用i与cnt>n目的相同,区分存在环且不属于环 
				used[v]=i;
				v=pre[v];
			}
			if(v!=ro && id[v]==-1){//找到环,i可不在环内 
				u=pre[v];
				while(u!=v) id[u]=cnt, u=pre[u];
				id[v]=cnt++;
			}
		}
		if(cnt==0) break;//没有自环
		 for(int i=0; i<n; i++) if(id[i]==-1) id[i]=cnt++;
		 for(int i=0; i<m; i++){
		 	u=E[i].fr, v=E[i].to;
		 	E[i].fr=id[u], E[i].to=id[v];
		 	if(id[u]!=id[v]) E[i].w-=in[v];
		 }
		 n=cnt;
		 ro=id[ro];
	}
	return ans;
}

连通分量

tarjan

强连通分量

基础:求图强连通的最小输入和最小添加。

最少几个点出发以到达所有点 \(\Rightarrow\) 几个入度为0的强连通分量。

最少添加几条边可从任意一点出发 \(\Rightarrow\) 最少添加几条边构成强连通分量 \(\Rightarrow\) 强连通分量缩点(构成\(GAD\))后\(max(入度为0的点,出度为0的点)\)

注:若原图为强连通分量,入度为0的点=出度为0的点=1,但添加0条边。

struct node{
	int v;
	ll w;
	int next;
}E[maxm];
int head[maxn], n, cnt;
void add(int u, int v, ll w){
	E[cnt].v=v, E[cnt].w=w;
	E[cnt].next=head[u];
	head[u]=cnt++;
}
int dfn[maxn], low[maxn], num;
stack<int> st;
int scc[maxn], sc; //所在SSC编号 
int sz[maxn]; //强连通分量大小
void tarjan(int u){
	low[u]=dfn[u]=++num;
	st.push(u);
	for(int i=head[u]; ~i; i=E[i].next){
		int v=E[i].v;
		if(!dfn[v]){
			tarjan(v);
			low[u]=min(low[u], low[v]);
		}
		else if(!scc[v]){//v<u => instack(v)
			low[u]=min(low[u], dfn[v]);
		}
	} 
	if(dfn[u]==low[u]){
		++sc;
		while(st.top()!=u){
			scc[st.top()]=sc, sz[sc]++, st.pop();
		}
		scc[u]=sc, sz[sc]++, st.pop();
	}
}
int ind[maxn], oud[maxn];
void init(){
	cnt=num=sc=0;
	memset(head, -1, sizeof(head));
	while(!st.empty()) st.pop();
	memset(dfn, 0, sizeof(dfn));
	memset(scc, 0, sizeof(scc));
	memset(sz, 0, sizeof(sz));
	memset(ind, 0, sizeof(ind));
	memset(oud, 0, sizeof(oud));
}
void sol(int n){
	for(int i=1; i<=n; i++){
		if(dfn[i]==0){
			tarjan(i);
		}
	}
	if(sc==1){
		printf("1\n0\n");
		return;
	}
	for(int u=1; u<=n; u++){
		for(int j=head[u]; ~j; j=E[j].next){
			int v=E[j].v;
			if(scc[u]==scc[v]) continue;
			oud[scc[u]]++, ind[scc[v]]++;
		}
	}
	int a=0, b=0;
	for(int i=1; i<=sc; i++){
		if(ind[i]==0) a++;
		if(oud[i]==0) b++;
	}
	printf("%d\n%d\n", a, max(a, b));
	return;
}

强连通分量缩点正常模板

过的https://codeforces.com/contest/1248/problem/F

vector<int> G[maxn];
void add(int u, int v) {
	G[u].push_back(v);
}
int dfn[maxn], low[maxn], num;
stack<int> st;
int scc[maxn], sc;//i所在scc编号,共sc个强连通分量
int sz[maxn];//强连通分量i大小
void tarjan(int u) {
	low[u]=dfn[u]=++num;
	st.push(u);
	for(auto v: G[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(dfn[u]==low[u]) {
		++sc;
		while(st.top()!=u) {
			scc[st.top()]=sc, sz[sc]++, st.pop();
		}
		scc[u]=sc, sz[sc]++, st.pop();
	}
}
int n, m;
void init() {
	num=sc=0;
	for(int i=0; i<=n; i++) {
		G[i].clear();
		dfn[i]=scc[i]=sz[i]=0;
	}
	while(!st.empty()) st.pop();
}
//调用
for(int i=1; i<=n; i++) {
	if(!dfn[i]) tarjan(i);
}
割点

对于一个无向图,如果把一个点删除后这个图的极大连通分量数增加了,那么

这个点就是这个图的割点(又称割顶)。

充要条件:

1)u为树根且有多于一个子树。(因为根在2)中全为割点,但仅有1个儿子时除外)。

2)u不为根,至少存在一个儿子满足\(dfn(u)<=low(subtree(u))\)。即:\(v\)不能不经过\(u\)回到不包括\(u\)的祖先节点.

注意:

1)\(maxm\)\(m\times2\)

2)割点重边时不影响,但影响割边。因\(u\)不可返回\(pre\),不可判断重边。当有重边时,是按一条边无向边计算的,当题目有重边时因注意。

区分求强连通分量不用判断\(instack\):因为无向图没有横叉边和前向边,仅有树边和返祖边,因此可以保证所有非树边连接的两个点在生成书上都满足一个是另一个祖先,故当前访问的\(u\)可到达已访问的\(v\)必定\(instack\)

struct node{
	int v;
	ll w;
	int next;
}E[maxm];
int head[maxn], n, cnt;
void add(int u, int v, ll w){
	E[cnt].v=v, E[cnt].w=w;
	E[cnt].next=head[u];
	head[u]=cnt++;
}
int dfn[maxn], low[maxn], num;
bool cut[maxn];
void tarjan(int u, int pre){
	low[u]=dfn[u]=++num;
	int son=0;
	for(int i=head[u]; ~i; i=E[i].next){
		int v=E[i].v;
		if(v==pre) continue;
		if(!dfn[v]){
			son++;
			tarjan(v, u);
			low[u]=min(low[u], low[v]);
			if(pre!=-1 && low[v]>=dfn[u])
				cut[u]=1;
		}
		else{
			low[u]=min(low[u], dfn[v]);
		}
	}
	if(pre==-1 && son>1) cut[u]=1;
	return;
}
void init(){
	cnt=num=0;
	memset(head, -1, sizeof(head));
	memset(dfn, 0, sizeof(dfn));
	memset(cut, 0, sizeof(cut));
}
int main(){
	int n, m, u, v;
	cin>>n>>m;
	init();
	for(int i=0; i<m; i++){
		scanf("%d%d", &u, &v);
		add(u, v, 1);
		add(v, u, 1);
	}
	int ans=0;
	for(int i=1; i<=n; i++){
		if(dfn[i]==0){
			tarjan(i, -1);
		}
	}
	for(int i=1; i<=n; i++){
		if(cut[i]) ans++;
	}
	printf("%d\n", ans);
	for(int i=1; i<=n; i++){
		if(cut[i]) printf("%d ", i);
	}
	return 0;
}
割边(桥)

对于一个无向图,如果删掉一条边后图中的连通分量数增加了,则称这条边为桥或者割边。

严谨地:

假设有连通图\(G={V,E}\)\(e\)是其中一条边(即\(e\in E\)),若\(E-e\)不连通,则\(e\)是图\(G\)的一条割边(桥)。

对比割点:

1)不用考虑根节点。

2)去掉等号。即由\(v\)不能不经过\(u\)回到不包括\(u\)的祖先节点\(\Rightarrow\)\(v\)不能不经过\(u\)回到包括\(u\)的祖先节点

模板

子旋给的

#include<bits/stdc++.h>
using namespace std;
const int maxn=300010;
const int MOD=998244353;
typedef long long ll;

int dfn[N], low[N], dfncnt, s[N], tp;
int scc[N], sc;  // 结点 i 所在 scc 的编号
int sz[N];       // 强连通 i 的大小(点的个数)

void tarjan(int u) {  // 有向图找强连通
  low[u] = dfn[u] = ++dfncnt, s[++tp] = u;
  for (int i = 0; i < (int)G[u].size(); i++) {
    const int &v = G[u][i];
    if (!dfn[v])
      tarjan(v), low[u] = min(low[u], low[v]);
    else if (!scc[v])
      low[u] = min(low[u], dfn[v]);
  }
  if (dfn[u] == low[u]) {
    ++sc;
    while (s[tp] != u) scc[s[tp]] = sc, sz[sc]++, --tp;
    scc[s[tp]] = sc, sz[sc]++, --tp;
  }
}

//无向图仙人掌的判定和环计数:
//首先判断一个图是否是连通的,dfs一次判断点的个数即可,之后每找到一个环,
//就将环上除了根(最先遍历到的点)之外的点度数都+1,
//如果一个点度数为2,说明这个点的前向边被两个环所共有,即该图不是仙人掌图。

//无向图找环用dfs树就行 dfs树的性质:dep相同的一定没有直接相连的边 

void tarjan(int u){ // 无向图仙人掌判定or找环
    // siz++ siz==n 则连通
    dfn[u] = ++dcnt;
    for(int i = 0; i < (int)G[u].size(); i++){
        int v = G[u][i];
        if(v==fa[u])continue;
        if(!dfn[v]){
            dep[v]=dep[u]+1;
            fa[v]=u;
            tarjan(v);
        }
    }
    for(int i = 0; i < (int)G[u].size(); i++){
        int v = G[u][i];
        if(fa[v] != u && dfn[u] < dfn[v]) {
            int cnt = dep[v] - dep[u] + 1; // 环的边数
//            ans = (ans * (ksm(2, cnt) - 1 + MOD) ) % MOD;
//            cc -= cnt;
            cal(v,u)
        }
    }
}

bool cal(int v,int u){ //计算入读
    for(v; v != u; v = fa[v]){//这里用点来代替dfs树边
        if(++cnt[v]==2)return 0;//如果一个边被两个环共用,那么不是仙人掌图
    }
    return 1;
}





void tarjan(int u) {  // 有向图仙人掌 注意判断连通否
  low[u] = dfn[u] = ++dfncnt, s[++tp] = u;
  for (int i = 0; i < (int)G[u].size(); i++) {
    const int &v = G[u][i];
    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[v] != dfn[v]) flag = 1;//一条边属于两个或两个以上的环
    }

  }
  if (dfn[u] == low[u]) {
    ++sc;
    while (s[tp] != u) scc[s[tp]] = sc, sz[sc]++, --tp;
    scc[s[tp]] = sc, sz[sc]++, --tp;
  }
}

void tarjan(int u,int fa){ //无向图求桥AC
    scc++;
    low[u]=dfn[u]=++dfncnt;
    for(int i=0; i<(int)G[u].size(); i++){
        int v=G[u][i].to;
        if(!dfn[v]){
            tarjan(v, u);
            low[u]=min(low[u],low[v]);
        }
        else if(v != fa) {
            low[u]=min(low[u],dfn[v]);
        }
        if(edge[u][v] < 2 && low[v] > dfn[u]) { //edge判断重边 也可用存边中的亦或做
            ans = min (ans, G[u][i].w);// u到v这条边就是桥
        }
    }
}

void tarjan(ll u,ll pre){//无向图求桥网友代码
    low[u]=dfn[u]=++id;
    for(int i=head[u];i!=-1;i=a[i].next){
        ll v=a[i].v;
        if(i==(pre^1)) continue;
        if(!dfn[v]){
            tarjan(v,i),low[u]=min(low[u],low[v]);
            if(dfn[u]<low[v]) bridge=min(bridge,a[i].w);
        }
        else low[u]=min(low[u],dfn[v]);
    }
}

void tarjan(int u,int rt){ //无向图求割点AC
    low[u]=dfn[u]=++dfsid;
    int child=0;
    for(int i=0; i<(int)G[u].size(); i++){
        int v = G[u][i];
        if(!dfn[v]){
            child++;
            tarjan(v, rt);
            low[u]=min(low[u],low[v]);
            if(u!=rt && low[v]>=dfn[u]){
                is_cut[u] = 1;
            }
        }
        low[u]=min(low[u],dfn[v]);
    }
    if(u==rt && child>1) is_cut[u] = 1;
}

posted @ 2021-03-02 14:52  bqlp  阅读(105)  评论(0)    收藏  举报