图论算法&模板整理--供自查--持续更新

学了忘,忘了学,学了还得忘

文章目录

  • 欧拉回路
  • 二分图匹配
  • 最短路
    • **Dijkstra + 优先队列**
    • Bellman - Ford
    • SPFA
  • K短路
  • 最小环
    • 全局求解
    • 部分求解
  • 差分约束
  • 强连通分量
    • Kosaraju算法
  • 最小有向生成树

欧拉回路

//欧拉路径:一条通过每条边一次且仅一次的路径
//欧拉回路:一条通过每条边一次且仅一次的回路
//无向图欧拉回路:所有顶点度数为偶数
//有向图欧拉回路:所有顶点入度等于出度
//无向图欧拉路径:除了起点与终点度为奇数,其它都是偶树
//有向图欧拉路径:起点出度比入度大一,终点入度比出度大一,其它入读等于出度

//递归(深度优先搜索)欧拉回路
void Euler(int u)
{
	int v;
	for(v=0;v<n;v++)
	{
		if(graph[u][v]==1)
		{
			graph[u][v]=graph[v][u]=0;
			Euler(v);
			path[pc++]=v;
		}
	}
}

//调用代码
int pc=0;Euler(s);path[pc++]=s;
for(int i=pc-1;i>=0;--i)
	printf("%d ",path[i]+1);

二分图匹配

在二分图里面找一个最大匹配
匹配:任意两个边没有公共点
最大匹配:边数最多

如果要在一般图中找最大匹配,需要开花算法(带花树)

//二分图:可以分为两部分的图
//性质:没有奇数顶点的环
//二分图最大匹配:匹配边数最多,每个匹配边连接来自不同部分的点,每个点只和一条匹配边连接

//匈牙利算法dfs版:
int linker[maxn];//存放右部点的匹配点,初始化为-1;
bool vis[maxn];
bool graph[maxn][maxn];//邻接矩阵存放图,顶点从0编号
bool dfs(int u)//u总是左部点
{
	vis[u]=true;
	for(int i=0;i<n;++i)
	{
		if(graph[u][v]&&!vis[v])
		{
			if(linker[v]==-1||dfs(linker[v]))
			{
				linker[v]=u;//最后只对右部匹配点存放了匹配信息
				return true;
			}
		}
	}
	return false;
}

int hungary()//返回最大匹配边数
{
	int cnt=0;
	memset(linker,-1,sizeof(linker));
	for(int i=0;i<n;++i)//对于每个左部点
	{
		memset(vis,false,sizeof(vis));
		if(dfs(i))++cnt;//可以增广
	}
	return cnt;
}

最短路

以下求最短路的算法都采用前向星

const int maxn = 1e5 + 10;
struct node
{
    int u, v, w, next;
    node(int _u = 0, int _v = 0, int _w = 0, int _next = 0):u(_u), v(_v), w(_w), next(_next){};
}edges[maxn];
int cnt, head[maxn];
void addedge(int u, int v, int w)
{
    edges[++cnt] = node(u, v, w, head[u]);
    head[u] = cnt;
}

Dijkstra + 优先队列

要求边权全部为正 O ( v l o g v + e ) O(vlogv + e) O(vlogv+e)

int d[maxn], vis[maxn];//vis数组可以优化掉
struct mynode
{
    int first, second;
    bool operator < (const mynode &a)const
    {
        return first > a.first;//!!!
    }
};
void Dijkstra(int s, int n)//s是起点,n是顶点数
{
    memset(vis, 0, sizeof(vis));
    const int INF = INT_MAX;
    for(int i = 1; i <= n; ++i)
    {
        d[i] = INF;
    }
    d[s] = 0;
    priority_queue<mynode > q;//自己定义优先级,从小到大排
    q.push({0, s});
    while(!q.empty())
    {
        int dis = q.top().first, u = q.top().second;
        q.pop();
        if(vis[u])continue;//一个节点可能多次入队
        d[u] = dis, vis[u] = 1;
        for(int i = head[u]; i; i = edges[i].next)
        {
            int v = edges[i].v, w = edges[i].w;
            if(d[v] > dis + w)
            {
                q.push({dis + w, v});
            }
        }
    }
}

Bellman - Ford

O ( e v ) O(ev) O(ev),边权可以为负,可以检查出负环。

每次对全图进行一次松弛,因为最短路上最多只有n - 1条边,所以松弛n - 1次就能得到最短路。

松弛n - 1次后检查一下是否能继续松弛,如果可以的话,说明存在负权环。

int dis[maxn];
bool bellman_ford(int s)//不存在负权环返回true,存在返回false
{
    const int INF = INT_MAX;
    for(int i = 1; i <= n; ++i)dis[i] = INF;
    dis[s] = 0;
    for(int pc = 1;pc <= n - 1; ++pc)//n - 1次松弛操作
    {
        bool updated = false;//如果没有更新,就可以提前退出循环
        for(int i = 1; i <= cnt; ++i)
        {
            if(dis[edges[i].u] != INF&&dis[edges[i].v] > dis[edges[i].u] + edges[i].w)
                dis[edges[i].v] = dis[edges[i].u] + edges[i].w, updated = true;
        }
        if(!updated)return true;
    }
    for(int i = 1; i <= cnt; ++i)
    {
        if(dis[edges[i].v] > dis[edges[i].u] + edges[i].w)
            return false;
    }
    return true;
}

SPFA

SPFA是基于Bellman-Ford的思想,采用先进先出(FIFO)队列进行优化的一个计算单源最短路的快速算法。利用一个先进先出的队列用来保存待松弛的结点,每次取出队首结点u,并且枚举从u出发的所有边(u, v),如果d[u] + w(u, v) < d[v],则更新d[v] = d[u] + w(u, v),然后判断v点在不在队列中,如果不在就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。

只要最短路径存在,SPFA算法必定能求出最小值

如果存在负权圈,并且起点可以通过一些顶点到达负权圈,那么利用SPFA算法会进入一个死循环,因为d值会越来越小,并且没有下限,使得最短路不存在。那么我们假设不存在负权圈,则任何最短路上的点必定小于等于n个(没有圈),换言之,用一个数组c[i]来记录i这个点入队的次数,所有的c[i]必定都小于等于n,所以一旦有一个c[i] > n,则表明这个图中存在负权圈。

假设图中所有边的边权都为1,那么SPFA其实就是一个BFS(Breadth First Search,广度优先搜索)

使用双向队列,可以加速

int dis[maxn], inq[maxn], viscount[maxn];
bool SPFA(int s)
{
    const int INF = INT_MAX;
    for(int i = 1; i <= n; ++i)dis[i] = INF;
    dis[s] = 0;
    memset(inq, 0, sizeof(inq));memset(viscount, 0, sizeof(viscount));
    deque<int> q;
    q.push_front(s);
    inq[s] = 1;
    while(!q.empty())
    {
        int u = q.front(); q.pop_front();
        inq[u] = 0;
        if(++viscount[u] > n)return false;//有负环
        for(int i = head[u]; i; i = edges[i].next)
        {
            int v = edges[i].v, w = edges[i].w;
            if(dis[u] + w < dis[v])
            {
                dis[v] = dis[u] + w;
                if(!inq[v])
                {
                    inq[v] = 1;
                    if(q.empty()||dis[v] < dis[q.front()])q.push_front(v);
                    else q.push_back(v);
                }
            }
        }
    }
    return true;
}

K短路

**问题描述:**给定一个又n个节点,m条有向边的图,问从 s s s t t t 的第 k k k短路的长度(无向图k短路没多大意义,可以拆边用这个方法)

**算法描述:**借鉴A*算法,设估价函数 f ( x ) = g ( x ) + h ( x ) f(x)=g(x)+h(x) f(x)=g(x)+h(x),其中 g ( x ) g(x) g(x) 是从初始状态(起点)到当前状态(当前节点)的实际代价,另 h ( x ) h(x) h(x) 为当前状态到结束状态的真实最小代价(当前节点到终点的最短路径),那么优先队列第 i i i 个出来状态 t t t 就是第 i i i 短路

**算法步骤:**用反向图求出所有点到终点的最短距离,A*搜索(当一个节点出队超过k次时,可以跳过这个节点的更新)

//代码来源网站:https://oi-wiki.org/graph/kth-path/
const int maxn = 5010;
const int maxm = 400010;
const int inf = 2e9;
int n, m, s, t, k, u, v, ww, H[maxn], cnt[maxn];
int cur, h[maxn], nxt[maxm], p[maxm], w[maxm];
int cur1, h1[maxn], nxt1[maxm], p1[maxm], w1[maxm];
bool tf[maxn];
void add_edge(int x, int y, double z) {
  cur++;
  nxt[cur] = h[x];
  h[x] = cur;
  p[cur] = y;
  w[cur] = z;
}
void add_edge1(int x, int y, double z) {
  cur1++;
  nxt1[cur1] = h1[x];
  h1[x] = cur1;
  p1[cur1] = y;
  w1[cur1] = z;
}
struct node {
  int x, v;
  bool operator<(node a) const { return v + H[x] > a.v + H[a.x]; }
};
priority_queue<node> q;
struct node2 {
  int x, v;
  bool operator<(node2 a) const { return v > a.v; }
} x;
priority_queue<node2> Q;
int main() {
  scanf("%d%d%d%d%d", &n, &m, &s, &t, &k);
  while (m--) {
    scanf("%d%d%d", &u, &v, &ww);
    add_edge(u, v, ww);
    add_edge1(v, u, ww);
  }
  //反向图求最短路
  for (int i = 1; i <= n; i++) H[i] = inf;
  Q.push({t, 0});
  while (!Q.empty()) {
    x = Q.top();
    Q.pop();
    if (tf[x.x]) continue;
    tf[x.x] = true;
    H[x.x] = x.v;
    for (int j = h1[x.x]; j; j = nxt1[j]) Q.push({p1[j], x.v + w1[j]});
  }
  q.push({s, 0});
  while (!q.empty()) {
    node x = q.top();
    q.pop();
    cnt[x.x]++;
    if (x.x == t && cnt[x.x] == k) {
      printf("%d\n", x.v);
      return 0;
    }
    if (cnt[x.x] > k) continue;
    for (int j = h[x.x]; j; j = nxt[j]) q.push({p[j], x.v + w[j]});
  }
  printf("-1\n");
  return 0;
}

最小环

给出一个图,问其中的有 个节点构成的边权和最小的环( n > = 3 n>=3 n>=3)是多大。

图的最小环也称围长。

全局求解

基于 F l o y d Floyd Floyd,复杂度 O ( n 3 ) O(n^3) O(n3)

注意到 F l o y d Floyd Floyd 算法有一个性质:在最外层循环到点 k k k 时(实际未计算到 k k k),最短路 d i s dis dis 数组中, d i s u , v dis_{u,v} disu,v 表示 u u u v v v且只经过$ [1,k) $点的最短路。

对每个环,假设我们要更新这个最小环的答案到贡献中。假定这个环中编号最大的顶点是 w w w ,环上与 w w w 相邻的两个点是 i , j i,j i,j ,那么在外层循环到 w w w 时,可以计算出该环边权和 = d i s [ i ] [ j ] + v a l [ i ] [ w ] + v a l [ j ] [ w ] =dis[i][j] + val[i][w] + val[j][w] =dis[i][j]+val[i][w]+val[j][w]

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

方法二:(适用于边权为1的图) 对图上每个点 v v v,可以从点 v v v开始 b f s bfs bfs, 记录下访问的深度,如果重复访问,可以计算环的大小。该方法不能单独求v所在环边数,可以用来bfs图上每个点求全局最小环。

int ans = INT_MAX;
int d[maxn], vis[maxn];
void bfs(int u, int f)
{
	vis[u] = 1, d[u] = 1;
	queue<pair<int, int> > q;//传入节点和父亲(避免用父亲更新到ans = min(ans, d[it] + d[v] -1))
	q.push({u, 0});
	while (!q.empty())
	{
		int v = q.front().first, f = q.front().second; q.pop();
 
		for (auto it : graph[v])
		{
			if (it == f)continue;
			if (!vis[it])
			{
				d[it] = d[v] + 1;
				vis[it] = 1;
				q.push({it, v});
			}
            //如果u不在it和v的环上,d[it] + d[v] - 1不是真实环大小,比真实环大
            //所以该方法可以用于全局求解,而不能单独求过点u的最小环
			else ans = min(ans, d[it] + d[v] - 1);
		}
	}
    if(ans == INT_MAX)ans = -1;
}

部分求解

删除一条边 < u , v > <u,v> ,计算 u , v u,v u,v 间的最短路径,如果可达,那么 < u , v > <u,v> 边所在环的边权和等于 d i s u , v + w u , v dis_{u,v}+w_{u,v} disu,v+wu,v注意重边的影响

差分约束

n个未知量( x 0 , x 1 x . . . x n − 1 x_0,x_1x_...x_{n-1} x0,x1x...xn1),m个约束关系,即m个形如 x i − x j < = c k x_i-x_j<=c_k xixj<=ck 的方程组,问 x t − x 0 x_t-x_0 xtx0 的最大值是多少?

建立有向图,如果 x i − x j < = c k x_i-x_j<=c_k xixj<=ck 则建立一条 j j j i i i 权为 c k c_k ck 的边

以0为起点跑单源最短路,同时判断是否存在负环,负环说明找不到满足约束关系的一组解, x t − x 0 x_t-x_0 xtx0的最大值是 d i s [ t ] dis[t] dis[t]

强连通分量

Kosaraju算法

Kosaraju 算法依靠两次简单的 DFS 实现。

第一次 DFS,选取任意顶点作为起点,遍历所有未访问过的顶点,并在回溯之前给顶点编号,也就是后序遍历。

第二次 DFS,对于反向后的图,以标号最大的顶点作为起点开始 DFS。这样遍历到的顶点集合就是一个强连通分量。对于所有未访问过的结点,选取标号最大的,重复上述过程。

两次 DFS 结束后,强连通分量就找出来了,Kosaraju 算法的时间复杂度为 O ( n + m ) O(n+m) O(n+m)

//未测试
const int maxn = 10000 + 10;
int n;
//g原图,gv反向图
vector<int> g[maxn], gv[maxn];
//所处连通分量编号
int color[maxn],dpc = 0, vis[maxn];
stack<int> q;
void dfs(int u)
{
	for (auto v : g[u])
		if (!vis[v])dfs(v);
	q.push(u);//入队的点,dfs后序从小到大排列
}
void dfs2(int u, int c)
{
	color[u] = c;
	for (auto v : gv[u])
		if (!color[v])dfs2(v, c);
}

void Kosaraju()
{
	for (int i = 1; i <= n; ++i)if (!vis[i])dfs(i);
	int cpc = 0;
	while (!q.empty())
	{
		if (!color[q.top()])dfs2(q.top(), ++cpc);
		q.pop();
	}
}

最小有向生成树

给定一个带权有向图G和其中一个节点u,找出一个以u为根节点的,边权和最小的有向生成树。有向生成树(directed spanning tree)也叫做树形图(arborescence),是指一个类似树的有向图,满足以下条件。

  1. 恰好有一个点入度为0,即树根
  2. 其余每个点的入度都为1
  3. 根节点到其它任意节点存在一条路径

求解最小有向生成树,可以使用朱-刘算法。主要分为预处理和主循环两步

预处理:去除自环边,并判断根节点是否对其它任意节点可达,如不可达,无解,终止算法

主循环:

  1. 对根节点外每个节点找出权值最小的入边共n-1条
  2. 如果选出的边不形成环,可以证明,这n-1条边构成最小有向生成树。否则,将环缩成点,重复1,2步。

那树形图的边权和怎么计算呢?

首先加入选中的每条入边的贡献,包括环上的边
显然,一个环上实际有一条边不能选,那么如果缩点后选中的入边对应的实际边是u到v,那么环上v的入边的贡献就应该减去。
在实际算法实现中,应该当舍弃的边的负贡献更新到缩点后的边权上,然后继续循环选最小的入边,此句话可能难以理解,详见代码。

struct node//边的权和顶点
{
	int u, v;
	type w;
}edge[MAXN * MAXN];
int pre[MAXN], id[MAXN], vis[MAXN], n, m, pos;
type in[MAXN];//存最小入边权,pre[v]为该边的起点
type Directed_MST(int root, int V, int E)
{
	type ret = 0;//存最小树形图总权值
	while (true)
	{
		int i;
		//1.找每个节点的最小入边
		for (i = 0; i < V; i++)
			in[i] = INF;//初始化为无穷大
		for (i = 0; i < E; i++)//遍历每条边
		{
			int u = edge[i].u;
			int v = edge[i].v;
			if (edge[i].w < in[v] && u != v)//说明顶点v有条权值较小的入边  记录之
			{
				pre[v] = u;//节点u指向v
				in[v] = edge[i].w;//最小入边
				if (u == root)//这个点就是实际的起点
					pos = i;
			}
		}
		for (i = 0; i < V; i++)//判断是否存在最小树形图
		{
			if (i == root)
				continue;
			if (in[i] == INF)
				return -1;//除了根以外有点没有入边,则根无法到达它  说明它是独立的点 一定不能构成树形图
		}
		//2.找环
		int cnt = 0;//记录环数
		memset(id, -1, sizeof(id));
		memset(vis, -1, sizeof(vis));
		in[root] = 0;
		for (i = 0; i < V; i++) //标记每个环
		{
			ret += in[i];//权值加入答案
			int v = i;
			while (vis[v] != i && id[v] == -1 && v != root)//如果能遍历到root则v不在环上,否则当遍历到此轮遍历过的点时停止,点在环上
			{
				vis[v] = i;
				v = pre[v];
			}
			if (v != root && id[v] == -1)//没有遍历到root,这是一个环
			{
				for (int u = pre[v]; u != v; u = pre[u])
					id[u] = cnt;//标记节点u为第几个环
				id[v] = cnt++;
			}
		}
		if (cnt == 0)
			break; //无环   找到答案break
		for (i = 0; i < V; i++)
			if (id[i] == -1)
				id[i] = cnt++;
		//3.建立新图   缩点,重新标记
		for (i = 0; i < E; i++)
		{
			int u = edge[i].u;
			int v = edge[i].v;
			edge[i].u = id[u];
			edge[i].v = id[v];
			if (id[u] != id[v])//如果后边这条边被选中,则in[v]代表的边要被删除,将其贡献从答案中减去
				edge[i].w -= in[v];
		}
		V = cnt;
		root = id[root];
	}
	return ret;
}

你可能感兴趣的:(算法-从入门到放弃)