图论总结

图论

一、图的定义

  1. 图由顶点集 \(V(G)\) 和边集 \(E(G)\) 组成,记为 \(G=(V,E)\)。其中 \(E(G)\) 是边的有限集合,边是顶点的无序对(无向图)或有序对(有向图)。
  2. DAG,即有向无环图,之后的拓扑排序、网络流都会用到。
  3. 其他基本定义可以参见 oi-wiki
  4. 下文中,一般用 \(n\) 表示顶点数 \(|V|\),用 \(m\) 表示边数 \(|E|\)

二、图的存储

  1. 邻接矩阵:\(O(n^2)\) (代码过于简单,就不放了)
  2. 邻接表:\(O(n+m)\)
    代码:
int hd[N],cnt;
struct node{int to,nex,w;}e[M];
void add(int u,int v,int w)//加边
{e[++cnt] = {to,hd[u],w};hd[u] = cnt;}
//遍历
for(int i = hd[u];i = e[i].nex)
{
	int v = e[i].to,w = e[i].w;
	...
}

在一般写题的时候都是用的邻接表,邻接表也可以用vector代替,但这样常数会变大。

三、最短路

1.Dijkstra

概念:Dijkstra 算法是一种求解单源最短路的算法,可以在带权有向图中找到每一个点到起点的最短距离。

思路:首先把起点到所有点的距离存下来找个最短的,然后松弛一次再找出最短的,所谓的松弛操作就是,遍历一遍看通过刚刚找到的距离最短的点作为中转站会不会更近,如果更近了就更新距离,这样把所有的点找遍之后就存下了起点到其他所有点的最短距离。

注意事项:Dijkstra不能处理负权边。

时间复杂度:

  1. 朴素Dijkstra:\(O(n^2+m)\)
  2. 堆优化Dijkstra:\(O(m\log n)\)
代码
int dijkstra(int s,int t)
{
    memset(dis,0x3f,sizeof dis);
    q.push(make_pair(dis[s] = 0,s));
    while(!q.empty())
    {
        int u = q.top().second;q.pop();
        if(vis[u])continue;vis[u] = 1;
        for(int i = hd[u];i;i = e[i].nex)
        {
            int v = e[i].to,w = e[i].w;
            if(dis[v] > dis[u]+w)
            {
                dis[v] = dis[u]+w;
                if(!vis[v])q.push(make_pair(-dis[v],v));
            }
        }
    }
	return dis[t];
}

模板

2.SPFA

在这里插入图片描述
时间复杂度:SPFA 的时间复杂度非常玄学,平均是 \(O(km)\),其中 \(k\) 是一个较小的常数,但可能被特殊的图卡成 \(O(nm)\),所以在要用最短路时,能不用 SP FA就不要用。

适用范围:SPFA 的一个重要功能就是用来找负权环,也可以用来处理有负权边的图,比如在求最小费用最大流时会用到。

代码
bool spfa()
{
	memset(dis,inf,sizeof dis);
	memset(vis,0,sizeof vis);
	q.push(s);vis[s] = 1;dis[s] = 0;minf[s] = inf;
	while(!q.empty())
	{
		int u = q.front();q.pop();vis[u] = 0;
		for(int i = hd[u];i;i = e[i].nex)
		{
			int v = e[i].to;
			if(e[i].f<=0)continue;
			if(dis[v]>dis[u]+e[i].w)
			{
				dis[v] = dis[u]+e[i].w;pre[v] = i;
				minf[v] = min(minf[u],e[i].f);
				if(!vis[v])
				{vis[v] = 1;q.push(v);}
			}
		}
	}
	return dis[t] != inf;
}

总结:在这里插入图片描述

四、二分图

  1. 定义:二分图中的所有顶点能够分成两个相互独立的集合 \(S,T\),并且所有边都在集合之间而集合之内没有边。二分图的一个重要性质是二分图中无奇数环。
  2. 染色判断二分图:利用深度优先搜索,从任意一个顶点开始染色,共有两种颜色,保证每个顶点的颜色与它的父节点和子节点都不相同,时间复杂度:\(O(|V|+|E|)\)
  3. 匈牙利算法求最大匹配:匈牙利算法本质就是不断寻找增广路来扩大匹配数。但是其正确性证明比较复杂,在此略去。
    时间复杂度:\(O(n\times e+m)\),其中 \(n\) 是左部点个数,\(e\) 是图的边数,\(m\) 是右部点个数。
代码
bool dfs(int u)
{
    for(int i = 1;i <= m;i++)
       if(a[u][i]&&!vis[i])
       {
            vis[i] = 1;
            if(!f[i]||dfs(f[i]))return f[i] = u,1;
       }
    return 0;
}
void solve()
{
   for(int i = 1;i <= n;i++)
   {
       memset(vis,0,sizeof vis);
       ans += dfs(i);
   }
   printf("%d",ans);
}

P3386 【模板】二分图最大匹配

五、网络流

  1. 定义:带权的有向图 \(G=(V,E)\),满足以下条件,则称为网络流图(flow network):
    仅有一个入度为 \(0\) 的顶点 \(s\),称 \(s\) 为源点
    仅有一个出度为 \(0\) 的顶点 \(t\),称 \(t\) 为汇点
    每条边的权值都为非负数,称为该边的容量,记作 \(c(i,j)\)
    弧的流量:通过容量网络 \(G\) 中每条弧 \((u,v)\),上的实际流量(简称流量),记为 \(f(u,v);\)

  2. 可行流:对于任意一个时刻,设 \(f(u,v)\) 为实际流量,整个图 \(G\) 的流网络满足以下 \(3\) 个性质:

  • 容量限制:对任意 \(u,v\in V\)\(f(u,v)\le c(u,v)\)
  • 反对称性:对任意 \(u,v\in V\)\(f(u,v) = -f(v,u)\)。从 \(u\)\(v\) 的流量一定是从 \(v\)\(u\) 的流量的相反值。
  • 流守恒性:对任意 \(u\),若 \(u\) 不为 \(S\)\(T\),一定有 \(\sum f(u,v)=0\)\((u,v)\in E\)。即u到相邻节点的流量之和为 \(0\),因为流入 \(u\) 的流量和 \(u\) 点流出的流量相等, \(u\) 点本身不会”制造”和”消耗”流量。
  1. 最大流:在容量网络中,满足弧流量限制条件,且满足平衡条件并且具有最大流量的可行流,称为网络最大流,简称最大流。
  2. 弧的类型:
  • 饱和弧:即 \(f(u,v)=c(u,v)\);
  • 非饱和弧:即 \(f(u,v) < c(u,v)\)
  • 零流弧:即 \(f(u,v)=0\);
  • 非零流弧:即 \(f(u,v)>0\).

EK算法

求最大流的过程,就是不断找到一条源到汇的路径,若有,找出增广路径上每一段[容量-流量]的最小值delta,然后构建残余网络,再在残余网络上寻找新的路径,使总流量增加。然后形成新的残余网络,再寻找新路径,直到某个残余网络上找不到从源到汇的路径为止,最大流就算出来了。

时间复杂度:上限为 \(O(|V||E|^2)\),一般可以处理 \(10^3\)~\(10^4\) 的数据规模。

代码
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int INF=0x7ffffff;

queue <int> q;
int n,m,x,y,s,t,g[201][201],pre[201],flow[201],maxflow; 
//g邻接矩阵存图,pre增广路径中每个点的前驱,flow源点到这个点的流量 

inline int bfs(int s,int t)
{
	while (!q.empty()) q.pop();
    for (int i=1; i<=n; i++) pre[i]=-1;
    pre[s]=0;
    q.push(s);
    flow[s]=INF;
    while (!q.empty())
	{
        int x=q.front();
        q.pop();
		if (x==t) break;
		for (int i=1; i<=n; i++)
          //EK一次只找一个增广路 
		  if (g[x][i]>0 && pre[i]==-1)
		  {
			pre[i]=x;
			flow[i]=min(flow[x],g[x][i]);
			q.push(i);
          }
	}
	if (pre[t]==-1) return -1;
	else return flow[t];
}

//increase为增广的流量 
void EK(int s,int t)
{
	int increase=0;
	while ((increase=bfs(s,t))!=-1)//这里的括号加错了!Tle 
	{//迭代 
		int k=t;
		while (k!=s)
		{
			int last=pre[k];//从后往前找路径
			g[last][k]-=increase;
			g[k][last]+=increase;
			k=last;
		}
		maxflow+=increase;
	}
}

int main()
{
	scanf("%d%d",&m,&n);
	for (int i=1; i<=m; i++)
	{
		int z;
		scanf("%d%d%d",&x,&y,&z);
		g[x][y]+=z;//此处不可直接输入,要+= 
	}
	EK(1,n);
	printf("%d",maxflow);
	return 0;
}

dinic算法:

前面的网络流算法,每进行一次增广,都要做 一遍BFS,十分浪费。能否少做几次BFS?
这就是Dinic算法要解决的问题。

原理

dinic算法在EK算法的基础上增加了分层图的概念,根据从 \(s\) 到各个点的最短距离的不同,把整个图分层。寻找的增广路要求满足所有的点分别属于不同的层,且若增广路为 \(s,P_1,P_2…P_k,t\),点 \(v\) 在分层图中的所属的层记为 \(d_v\),那么应满足 \(d_{p_i}=d_{p_{i−1}}+1\)

算法流程

  • 先利用BFS对残余网络分层。一个节点的深度,就是源点到它最少要经过的边数。
  • 分完层后,从源点开始,用DFS从前一层向后一层反复寻找增广路(即要求DFS的每一步都必须要走到下一层的节点)。
  • DFS过程中,要是碰到了汇点,则说明找到了一条增广路径。此时要增加总流量的值,消减路径上各边的容量,并添加反向边,即所谓的进行增广。
  • DFS找到一条增广路径后,并不立即结束,而是回溯后继续DFS寻找下一个增广路径。
  • DFS结束后,对残余网络再次进行分层,然后再进行DFS。当残余网络的分层操作无法算出汇点的层次(即BFS到达不了汇点)时,算法结束,最大流求出。

时间复杂度:

  • 在普通情况下, DINIC算法时间复杂度为 \(O(|V|^2|E|)\);
  • 在二分图中, DINIC算法时间复杂度为 \(O(|E|\sqrt{|V|})\).
  • 一般情况下可处理 \(10^4\)~\(10^5\) 的数据规模。
代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
#define ll long long
using namespace std;
const int N = 205,M = 5005;
int d[N],rad[N],n,m,s,t;
ll ans;
int hd[N],cnt = 1;
struct node{int to,nex;ll w;}e[M << 1];
void add(int u,int v,ll w)
{e[++cnt] = {v,hd[u],w};hd[u] = cnt;}
queue<int> q;
bool bfs()
{
	memset(d,0,sizeof d);q.push(s);d[s] = 1;
	while(!q.empty())
	{
		int u = q.front();q.pop();
		rad[u] = hd[u];
		for(int i = hd[u];i;i = e[i].nex)
		{
			int v = e[i].to;
			if(!d[v]&&e[i].w)
			{d[v] = d[u]+1;q.push(v);}
		}
	}
	return d[t];
}
ll dfs(int u,ll cl)
{
	if(u==t)return cl;
	ll rem = cl;
	for(int i = rad[u];i;i = e[i].nex)
	{
		int v = e[i].to;rad[u] = i;
		if(d[v]!=d[u]+1||!e[i].w)continue;
		ll now = dfs(v,min(e[i].w,rem));
		e[i].w -= now;e[i^1].w += now;
		rem -= now;
	}
	return cl-rem;
}
inline int rd()
{
	char c;int f = 1;
	while((c = getchar())<'0'||c>'9')if(c=='-')f = -1;
	int x = c-'0';
	while('0' <= (c = getchar())&&c <= '9')x = x*10+(c^48);
	return x*f;
}
int main()
{
	//freopen(".in","r",stdin);
	//freopen(".out","w",stdout);
	n = rd();m = rd();s = rd();t = rd();
	for(int i = 1;i <= m;i++)
	{
		int u = rd(),v = rd();
		add(u,v,rd());add(v,u,0);
	}
	while(bfs())ans += dfs(s,1ll<<32);
	cout << ans;
	return 0;
}

P3376 【模板】网络最大流

割:

在这里插入图片描述

通俗的理解一下: 割集好比是一个恐怖分子,把你家和自来水厂之间的水管网络砍断了一些,
然后自来水厂无论怎么放水,水都只能从水管断口哗哗流走了,你家就停水了。
割的大小应该是恐怖分子应该关心的事,毕竟细管子好割一些,而最小割花的力气最小。

最小割最大流定理:网络流的最大流量等于最小割的容量。

费用流

现在我们想象假如我们有一个流量网络,现在每个边除了流量,现在还有一个单位费用,这条边的费用相当于它的单位费用乘上它的流量,我们要保持最大流的同时,还要保持边权最小,这就是最小费用最大流问题。因为在一个网络流图中,最大流量只有一个,但是“流法”有很多种,每种不同的流法所经过的边不同因此费用也就不同,所以需要用到最短路算法。总增广的费用就是最短路*总流量。

SPFA:就是把Dinic中的bfs改成spfa,再求最大流的过程中最小费用流也就求出来了。

代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int N = 5005,M = 5e4+5,inf = 0x7f7f7f7f;
int dis[N],minf[N],pre[N],n,m,s,t;
bool vis[N];
int hd[N],cnt = 1,maxf,ans;
struct node{int to,f,w,nex;}e[M << 1];
void add(int u,int v,int f,int w)
{e[++cnt] = {v,f,w,hd[u]};hd[u] = cnt;}
queue<int> q;
bool spfa()
{
	memset(dis,inf,sizeof dis);
	memset(vis,0,sizeof vis);
	q.push(s);vis[s] = 1;dis[s] = 0;minf[s] = inf;
	while(!q.empty())
	{
		int u = q.front();q.pop();vis[u] = 0;
		for(int i = hd[u];i;i = e[i].nex)
		{
			int v = e[i].to;
			if(e[i].f<=0)continue;
			if(dis[v]>dis[u]+e[i].w)
			{
				dis[v] = dis[u]+e[i].w;pre[v] = i;
				minf[v] = min(minf[u],e[i].f);
				if(!vis[v])
				{vis[v] = 1;q.push(v);}
			}
		}
	}
	return dis[t] != inf;
}
inline int rd()
{
	char c;int f = 1;
	while((c = getchar())<'0'||c>'9')if(c=='-')f = -1;
	int x = c-'0';
	while('0' <= (c = getchar())&&c <= '9')x = x*10+(c^48);
	return x*f;
}
int main()
{
	n = rd();m = rd();s = rd();t = rd();
	for(int i = 1;i <= m;i++)
	{
		int u = rd(),v = rd(),f = rd(),w = rd();
		add(u,v,f,w);add(v,u,0,-w);
	}
	while(spfa())
	{
		maxf += minf[t];ans += minf[t]*dis[t];
		int now = t;
		while(now!=s)
		{
			e[pre[now]].f -= minf[t];
			e[pre[now]^1].f += minf[t];
			now = e[pre[now]^1].to;
		}
	}
	printf("%d %d",maxf,ans);
	return 0;
}

P3381 【模板】最小费用最大流


参考文章:https://blog.csdn.net/A_Comme_Amour/article/details/79356220
https://blog.csdn.net/weixin_44548214/article/details/115571542

最小斯坦纳树

例题:P6192 【模板】最小斯坦纳树

给定一个 \(n\) 个点 \(m\) 条边的图,有 \(k\) 个点为关键点,你需要选出一些边,使得这 \(k\) 个点联通,且边权和最小。

\(n\le 100,m\le 500,k\le 10\)

首先这些边肯定构成一棵树,考虑状压 dp,设 \(f(i,S)\) 表示以 \(i\) 为根的一棵树,包含集合 \(S\) 中所有点的最小边权和。

那么有以下两种转移方式:

  • \(S\) 的某一个子集转移过来:\(f(i,S) \larr f(i,T)+f(i,S-T)\qquad T\subseteq S\)
  • 用一条边 \((j,i,w)\) 进行转移:\(f(i,S)\larr f(j,S)+w(j,i)\),这是一个三角形不等式,相当于对整张图进行一次松弛操作。

我们考虑从小到大枚举所有 \(S\),先用 \(S\) 的所有子集更新 \(S\),然后进行一次 SPFA,时间复杂度为 \(\mathcal{O}(n\times 3^k+nm\times 2^k)\),虽然 SPFA 可能会比较假,但实际上 SPFA 跑得比 dijkstra 快得多。

代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#define ll long long
using namespace std;
const int N = 1005,K = 15,inf = 0x3f3f3f3f;
int f[N][1<<K],hd[N],cnt,n,m,k,ans = inf;
bool vis[N];
struct node{int to,nex,w;}e[N << 1];
void add(int u,int v,int w)
{e[++cnt] = {v,hd[u],w};hd[u] = cnt;}
queue<int> q;
void spfa(int s)
{
    while(!q.empty())
    {
        int u = q.front();q.pop();
        vis[u] = 0;
        for(int i = hd[u];i;i = e[i].nex)
        {
            int v = e[i].to,w = e[i].w;
            if(f[v][s] > f[u][s]+w)
            {
                f[v][s] = f[u][s]+w;
                if(!vis[v])vis[v] = 1,q.push(v);
            }
        }
    }
}
inline int rd()
{
    char c;int f = 1;
    while(!isdigit(c = getchar()))if(c=='-')f = -1;
    int x = c-'0';
    while(isdigit(c = getchar()))x = x*10+(c^48);
    return x*f;
}
int main()
{
    // freopen(".in","r",stdin);
    // freopen(".out","w",stdout);
    n = rd();m = rd();k = rd();
    for(int i = 1;i <= m;i++)
    {
        int u = rd(),v = rd(),w = rd();
        add(u,v,w);add(v,u,w);
    }
    memset(f,inf,sizeof(f));
    for(int i = 1;i <= k;i++)f[rd()][1<<i-1] = 0;
    for(int s = 1;s < (1<<k);s++)
    {
        for(int i = 1;i <= n;i++)
        {
            for(int t = s;t;t = s&(t-1))
                f[i][s] = min(f[i][s],f[i][t]+f[i][s^t]);
            if(f[i][s] != inf)q.push(i),vis[i] = 1;
        }
        spfa(s);
    }
    for(int i = 1;i <= n;i++)ans = min(ans,f[i][(1<<k)-1]);
    cout << ans << endl;
    return 0;
}
posted @ 2024-09-18 09:38  max0810  阅读(19)  评论(1编辑  收藏  举报