图论常用算法记录

本文章为参照这位博主图论总结进行学习的笔记 引用了部分该博主的内容

欧拉通路与欧拉回路问题

基本概念

欧拉通路:通过图中所有边一次且仅一次行遍所有顶点的通路
欧拉回路:通过图中所有边一次且仅一次行遍所有顶点的回路
欧拉图:具有欧拉回路的图
半欧拉图:具有欧拉通路而无欧拉回路的图

欧拉通路/回路的判定

无向图通路

除了起点和终点外都是偶度点

无向图回路

全都是偶度点

有向图通路

除了两个顶点(一个入度=出度+1,一个出度=入度+1)外其他点的入度等于出度

有向图回路

所有点的入度等于出度

算法实现

并查集判断是否有回路

首先记录连通分量,若连通分量大于1,则不存在回路。然后再统计是否存在奇度点,如果存在就不存在回路。

#include <iostream>
#include <cstring>
#define maxn 100
using namespace std;

int f[maxn], degree[maxn];
int Find(int x)
{
    if (f[x] == x) return x;
    return f[x] = Find(f[x]);
}

void merge(int x, int y)
{
    int f1 = Find(x), f2 = Find(y);
    if (f1 != f2) f[f1] = f2;
}

int main()
{
    for (int i = 0; i < maxn; i++)
        f[i] = i;
    memset(degree, 0, sizeof(degree));
    int n, x, y, group = 0, cnt = 0;
    cin >> n; //输入点的个数
    for (int i = 0; i < n; i++)
    {
        scanf("%d%d", &x, &y);
        degree[x]++;
        degree[y]++;
        merge(x, y);
    }
    for (int i = 0; i < n; i++)
    {
        if (f[i] == i) group++;   //集合数统计
        if (degree[i] & 1) cnt++; //奇度点统计
    }
    //如果集合数大于1,或者存在奇度点,则不存在欧拉回路
    if (group > 1 || cnt) cout << "no Eurler way" << endl;
    return 0;
}

Fleury 算法求无向图回路

在已经确定存在欧拉回路的情况下任选一个点开始遍历,经过一个点就将其入栈,并删去这条边,当某个点没有边的时候出栈,记录路径,然后从栈的下一个点开始遍历,不断重复以上过程直至所有的边都被删去。

//假设输入的数据保证是有回路的
#include <cstring>
#include <iostream>
#include <stack>
#define maxn 1000
using namespace std;

int g[maxn][maxn], path[maxn], n, m, cnt = 0;
stack<int> S;

void dfs(int x)
{
    S.push(x);
    for (int i = 1; i <= n; i++)
    {
        if (g[x][i])
        {
            g[x][i] = 0;
            g[i][x] = 0;
            dfs(i);
            break;
        }
    }
}
void fleury(int x)
{
    S.push(x);
    while (S.size())
    {
        int flag = 0, x = S.top();
        S.pop();
        for (int i = 1; i <= n; i++) //寻找与该点连接的边
        {
            if (g[x][i])
            {
                flag = 1;
                break;
            }
        }
        if (flag)
            dfs(x);
        else
            path[cnt++] = x;
    }
}
int main()
{
    freopen("in.txt", "r", stdin);
    memset(g, 0, sizeof(g));
    int x, y;
    cin >> n >> m;//输入点数和边数
    for (int i = 1; i <= m; i++)
    {
        scanf("%d%d", &x, &y);
        g[x][y] = g[y][x] = 1;
    }
    fleury(2);
    cout << "path: ";
    for (int i = 0; i < cnt; i++)
    {
        cout << path[i] << " ";
    }
    return 0;
}

通路的路径求法

从奇度点开始用Fleury算法遍历即可。

例题

一笔画问题
一笔画问题加强版(一条边可以通过多次)
例题

拓扑排序

概念和实现算法

将入度为0的点输出,每输出一个入度为0的点后就将该点所连接的所有点的入度减一,然后再输出剩下点中入度为0的点,不断重复。

AOV网

AOV网是一个有向无环图。点与点之间存在先后关系,必须完成所有前驱的点后才能进入下一个点,比如排课(修完了基础课程后才能学习这门课)。
例题

AOE网

在AOV网的基础上增加了边的权值,比如工程问题(权值代表工期)。
例题

图的连通性

概念

连通图:在无向图中从任意结点出发都能达到其他结点
连通分量:具有连通性的无向子图
强连通图:在有向图中从任意结点出发都能达到其他结点
连通分量:具有强连通性的有向子图

连通性判断

一次遍历能经过所有的点即可,dfs+栈

强连通性判断*

kosaraju算法

kosaraju算法是先正向dfs一次,该点的所有边都dfs完后入栈,然后根据栈的顺序反向dfs一遍(反向的意思是原来A→B的方向变为B→A)。

//kosaraju实现代码
#include <bits/stdc++.h>
#define maxn 105
using namespace std;

vector<int> g[maxn], rg[maxn]; //rg中保存的是反图
bool vis[maxn] = {0};
stack<int> S;
int f[maxn]; //所属的强连通分量

inline void addEdge(int u, int v)
{
    g[u].push_back(v);
    rg[v].push_back(u);
}
void dfs1(int x)//正向dfs
{
    vis[x] = 1;
    for (int i = 0; i < g[x].size(); i++)
        if (!vis[g[x][i]])
            dfs1(g[x][i]);
    S.push(x);
}
void dfs2(int x, int k)//反向dfs
{
    f[x] = k; //记录该顶点属于哪个强连通分量
    vis[x] = 0;
    for (int i = 0; i < rg[x].size(); i++)
        if (vis[rg[x][i]])
            dfs2(rg[x][i], k);
}

int main()
{
    //freopen("in.txt", "r", stdin);
    int n, m, u, v, cnt = 0;
    cin >> n >> m; //输入点数和边数
    while (m--)
    {
        cin >> u >> v;
        addEdge(u, v);
    }
    for (int i = 1; i <= n; i++)
        if (!vis[i])
            dfs1(i);
    while (!S.empty())
    {
        //正向dfs完后所有的vis都变为1,因此dfs反图时根据vis是否为1进行判断
        if (vis[S.top()])
            dfs2(S.top(), cnt++);
        S.pop();
    }
    cout << cnt << endl;
    return 0;
}

tarjan算法(通用模板见缩点中的tarjan函数)

tarjan算法也是用dfs,用dfn数组来记录一个点被访问到的顺序,用low数组来记录这个点属于哪个强连通分量里(也可以理解为这个强连通分量里最早被访问的点的dfn值)
在dfs的同时还要用一个栈来压入被访问的点,以及一个vis数组来记录某个点是否位于栈中(毕竟不可能将栈中的点一个个弹出来判断某个点是否在里面)一旦dfs中遇到了栈中的点,就更新当前点的low值。如果某个点的dfn和low相同,说明这个强连通分量中所有的点都被访问过了,那么就进行出栈。
B站视频讲解
例题:【NOIP2015】信息传递

//tarjun算法
//【NOIP2015】信息传递
#include <bits/stdc++.h>
#define maxn 200005
using namespace std;

int to[maxn]; //信息传递的下一个人
stack<int> stk;
int low[maxn]; //可以从哪个点到达(也可理解为属于哪个强连通分量里)
int dfn[maxn]; //被访问的顺序
int vis[maxn]; //记录是否在栈中
int tot = 0, ans = 0x3f3f3f3f;
void tarjan(int x)
{
    low[x] = dfn[x] = ++tot;
    vis[x] = 1;
    stk.push(x);
    int v = to[x];
    //更新low[x]有两种方式,根据v的情况进行选择(画一个图会很好理解)
    if (!dfn[v]) //v还没有入栈
    {
        tarjan(v);
        low[x] = min(low[x], low[v]); //如果v能够到达在栈中的点,那么low[v]会更新,low[x]也将随之更新
    }
    else if (vis[v]) //v已经入过栈
        low[x] = min(low[x], dfn[v]);
    //回溯到强连通分量的起点处,开始出栈
    if (low[x] == dfn[x])
    {
        int cnt = 1;
        while (stk.top() != x)
        {
            vis[stk.top()] = 0;
            stk.pop();
            cnt++;
        }
        stk.pop();
        //游戏进行几轮取决于最小的强连通分量中点的个数(前提是点数大于1)
        if (cnt > 1) ans = min(ans, cnt);
    }
}
int main()
{
    int n, v;
    cin >> n;
    for (int i = 1; i <= n; i++)
    {
        scanf("%d", &v);
        to[i] = v;
    }
    for (int i = 1; i <= n; i++)
        if (!dfn[i]) tarjan(i);
    cout << ans << endl;
    return 0;
}

缩点

对于一个有向图,求出最少加几条边可以将这个图变成一个强连通图。可以将每个强连通分量看作一个点,然后枚举每一个点,如果这个点能到达另一个连通集的点,那么该点的连通集出度为0,另一点的连通集入度为0,假设入度不为0的连通集个数为a,出度不为0 的连通集个数为b,最后的结果就是max(a,b)

#include <bits/stdc++.h>
#define maxn 100
using namespace std;

int dfn[maxn] = {0}, low[maxn] = {0}, group[maxn] = {0}, tot = 0, cnt = 0, m, n;
bool vis[maxn] = {0}, in[maxn], out[maxn];
vector<int> g[maxn];
stack<int> S;

void tarjan(int x)
{
    dfn[x] = low[x] = ++tot;
    S.push(x);
    vis[x] = 1;
    for (int i = 0; i < g[x].size(); i++)
    {
        int v = g[x][i];
        if (!vis[v]) //这个点还没有被访问过
        {
            tarjan(v);
            low[x] = min(low[x], low[v]);
        }
        else if (!group[v]) //这个点不属于任何一个连通集(说明还在栈中)
        {
            low[x] = min(low[x], dfn[v]);
        }
    }
    if (low[x] == dfn[x]) //该顶点为连通集的第一个被访问元素
    {
        cnt++; //连通集个数加一
        while (S.top() != x)
        {
            group[S.top()] = cnt; //记录所属的连通集
            S.pop();
        }
        group[x] = cnt;
        S.pop();
    }
}
int shrink()
{
    if (cnt == 1) return 0;
    for (int i = 1; i <= cnt; i++)
        in[i] = out[i] = 1;
    for (int i = 0; i < n; i++) //枚举所有的点
    {
        for (int j = 0; j < g[i].size(); j++)
        {
            int v = g[i][j];
            if (group[i] != group[v])
            {
                out[group[i]] = 0;
                in[group[v]] = 0;
            }
        }
    }
    int a = 0, b = 0;
    for (int i = 1; i <= cnt; i++)
    {
        a += in[i];
        b += out[i];
    }
    return a > b ? a : b;
}

int main()
{
    int u, v;
    cin >> n >> m;
    for (int i = 0; i < m; i++)
    {
        scanf("%d%d", &u, &v);
        g[u].push_back(v);
    }
    for (int i = 0; i < n; i++)
        if (!vis[i]) tarjan(i);
    int res = shrink();
    cout << "有" << cnt << "个连通集" << endl;
    cout << "最少需要添加" << res << "条边" << endl;
    return 0;
}

割点

在一个无向图中,如果去掉一个点和它所连出去的的所有边,使得剩下的点不连通(即分成一个以上的强连通分量)时,这个点被称为割点。
求割点: 将图看成树的样子,同时沿用tarjan算法中的low和dfn数组对点进行标记。任选一个点作为根进行dfs,如果根与两个连通分量相连,那么这个点就是割点(两个连通分量之间的点不通过该点不能互相到达,即至少进行两次dfs才能将该点所连的边都遍历到)。如果子结点能再连接一个连通分量,那么也是割点。具体实现方式见代码注释。

//割点模板
#include <bits/stdc++.h>
#define maxn 100
using namespace std;

int low[maxn], dfn[maxn] = {0}, tot = 0;
vector<int> g[maxn];
bool sign[maxn] = {0}; //记录是否为割点

void tarjan(int x, int root)
{
    int cnt = 0; //记录根节点所连的连通集个数
    dfn[x] = low[x] = ++tot;
    for (int i = 0; i < g[x].size(); i++)
    {
        int v = g[x][i];

        if (!dfn[v]) //该点还没访问过
        {
            tarjan(v, root);
            low[x] = min(low[x], low[v]);
            if (x != root && low[v] >= dfn[x]) //子结点所能到达的最早的点也在x之后到达,说明和之前的点没有边相连,是一个独立的连通分量
                sign[x] = 1;
            else if (x == root) //如果是根结点,那么连通集个数加一
                cnt++;
        }
        else //遇到访问过的点,更新low
            low[x] = min(low[x], dfn[v]);
    }
    if (cnt >= 2)
        sign[root] = 1;
}

int main()
{
    int n, m, u, v;
    vector<int> ans;
    cin >> n >> m;
    for (int i = 0; i < m; i++)
    {
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }
    for (int i = 0; i < n; i++)
        if (!dfn[i]) tarjan(i, i);
    for (int i = 0; i < n; i++)
        if (sign[i]) ans.push_back(i);
    cout << "割点个数为:" << ans.size() << endl;
    for (int i = 0; i < ans.size(); i++)
        cout << ans[i] << " ";
    return 0;
}
/*
sample input:
7 8
1 2
1 0
2 0
3 4
0 3
4 5
5 6
6 3
sample output:
割点个数为:2
0 3
*/

桥(割边)

在一个无向图中,如果去掉一条边,使得剩下的点不连通,则这条边就是桥。
割边求法:
解释1: 需要加一个pre数组来记录当前的点的前一个点,防止回退。然后枚举所有的边,如果一个边的low[x]!=low[y]那么就是割边。
解释2: 从当前点x的边出发访问未访问的点y,如果low[y]>dfn[x],那么xy就是一条割边。

//割边模板
#include <bits/stdc++.h>
#define maxn 100
using namespace std;
struct edge
{
    int x, y;
};
int low[maxn], dfn[maxn] = {0}, pre[maxn], tot = 0;
vector<int> g[maxn];
vector<edge> ans;
void tarjan(int x)
{
    dfn[x] = low[x] = ++tot;
    for (int i = 0; i < g[x].size(); i++)
    {
        int v = g[x][i];
        if (pre[x] == v) continue; //防止回退访问重复的点
        if (!dfn[v])               //该点还没访问过
        {
            pre[v] = x;
            tarjan(v);
            low[x] = min(low[x], low[v]);
            if (low[v] > dfn[x]) ans.push_back(edge{x, v});
        }
        else //遇到访问过的点,更新low
            low[x] = min(low[x], dfn[v]);
    }
}

int main()
{
    int n, m, u, v;
    cin >> n >> m;
    for (int i = 0; i < m; i++)
    {
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }
    for (int i = 0; i < n; i++)
        if (!dfn[i])
        {
            pre[i] = i;
            tarjan(i);
        }
    cout << "割边数为:" << ans.size() << endl;
    for (int i = 0; i < ans.size(); i++)
        printf("(%d,%d) ", ans[i].x, ans[i].y);
    return 0;
}
/*
sample input:
7 8
1 2
1 0
2 0
3 4
0 3
4 5
5 6
6 3
sample output:
割边数为:1
(0,3)
*/

2-SAT算法*

学习该算法需要先学习强连通分量的判断
2-SAT问题学习笔记+POJ3648例题题解+3道例题

最短路径算法

Dijkstra算法普通版本

// dijkstra最短路径算法,适用于单源最短路径,指定一个起点,计算所有点到该点的最短路径长度
// 有两个集合,分别记录已经确定最短路径的点和未确定的点
// 将起点加入已经确定的点的集合中,然后从未确定的集合中找距离起点最近的点,将其移动到已经确定的集合中
// 将已经确定的集合中的点同未确定的集合中的所有点进行比较,将未确定的集合中的点中最短路径的点移动到已经确定的集合中
// 重复以上过程,直到一个集合为空

void dijkstra(int start)//若不传入终点,将计算起点到所有点的最短距离
{
    int mlen, id, jj;
    len[start] = 0;
    visted.push_back(start);//加入已经确定的点集中 
    rest.erase(rest.begin() + start);//从未确定的点集中删除起点
    while (rest.size())
    {
        mlen = inf;
        jj = -1;
        for (int i = 0; i < visted.size(); i++)//从已经确定的点集中取点
        {
            int x = visted[i], y;
            for (int j = 0; j < rest.size(); j++)//从未确定的点集中取点
            {
                y = rest[j];
                if (len[x] + g[x][y] < mlen)//找到了距离更短的点
                {
                    p[y] = x;
                    mlen = len[x] + g[x][y];//更新最小距离
                    id = y;//记录距离最小的点
                    jj = j; //jj记录后面要从rest中删除的下标
                }
            }
        }
    }
    if (jj == -1)//不存在新的点,或者已经搜索到终点
        return; 
    len[id] = mlen;//最后将确定的点更新信息即可,其余的点不用管
    rest.erase(rest.begin() + jj); //将路径最短的点删去
    visted.push_back(id);          //加入新的点
}

Dijkstra算法堆优化版本

由于普通版本的Dijkstra算法复杂度为O(n*n),在大多数的算法竞赛题中都会超时,因此堆优化版本的Dijkstra使用率更高。
堆优化借助一个优先队列(一个小根堆)和一个状态表示结构体(记录点的下标和距离),每次可以用O(logn)的时间找到剩余点中距离最小的点,对于稀疏图的时间复杂度为O(nlogn),但是在稠密图中可能会退化到O(n*n),此时和普通版本的Dijkstra差别不大。

#include <bits/stdc++.h>
#define mem(a, v) memset(a, v, sizeof(a))
const int inf = 2e9 + 5;
const int N = 1e5 + 5;
using namespace std;

struct edge //链式前向星方式存边
{
    int to;       //与之有边相连的点
    int next;     //下一条边的下标
    int distance; //边权
} e[N * 2];

struct node //堆优化的Dijkstra算法状态记录结构体
{
    int index;
    int dis;
    node() {}
    node(int i, int d) { index = i, dis = d; }
};
bool operator<(node a, node b) { return a.dis > b.dis; } //运算符重载,用于实现优先队列的排序

int head[N], cnt = 0;
int dis[N];
bool vis[N];

void addEdge(int u, int v, int dis)
{
    e[++cnt].to = v;
    e[cnt].distance = dis;
    e[cnt].next = head[u];
    head[u] = cnt;
}

void Dijkstra(int s, int n)
{
    for (int i = 1; i <= n; i++)
        dis[i] = inf;
    mem(vis, 0);
    priority_queue<node> q;
    q.push(node(s, 0));
    dis[s] = 0;
    while (q.size())
    {
        int x = q.top().index; //取堆顶的点作为下一个确定的点
        int d = q.top().dis;
        q.pop();
        if (vis[x]) continue; // x是确定的点,跳过
        vis[x] = 1;
        for (int i = head[x]; i; i = e[i].next)
        {
            int to = e[i].to;
            if (vis[to]) continue; //已经确定的点就跳过
            if (d + e[i].distance < dis[to])
            {
                dis[to] = d + e[i].distance;
                q.push(node(to, dis[to])); //将更新的点状态入堆
            }
        }
    }
}

int main()
{
    mem(head, 0);
    int n, m, s, u, v;
    int w;
    scanf("%d%d%d", &n, &m, &s);
    while (m--)
    {
        scanf("%d%d%d", &u, &v, &w);
        addEdge(u, v, w); //添加有向边
    }
    Dijkstra(s, n);
    for (int i = 1; i <= n; i++)
        printf("%d ", dis[i]);
    return 0;
}

Floyd算法

弗洛伊德最短路径算法,适用于多源最短无向图路径,可以求出任意两点间的最短路径
一般采用临接矩阵来存储路径长度,复杂度为O(n^3),因此点数不易过多
算法实现原理:类似动态规划
选定一个中转点k,比较x->y和x->k->y的路径哪个更短,然后更新路径
用三重循环,第一重是中转点k,第二重是起点x,第三重是终点y,三重循环过后即可算出任意两点间的最短路径长度

//核心算法,三重循环
for (int k = 1; k <= n; k++) //k表示中转点
	for (int i = 1; i <= n; i++) //i表示起点
		for (int j = 1; j <= n; j++) //k表示终点
			if (dis[i][j] > dis[i][k] + dis[k][j]) //走中转点的路径更短,那么就更新路径
				dis[i][j] = dis[i][k] + dis[k][j];

Bellman-Ford 算法与 SPFA

Bellman-Ford 算法可以看作是Dijkstra算法的改进,最大的特点是可以处理负边权的情况(Dijkstra算法在遇到负边权时就会失效)。Ford 算法是每次更新所有的边,从而确定一个点的最短距离。在Bellman-Ford算法中,边是有方向的(即不能往回走,否则负边权两点间可以重复来回使得距离不断减小)
优化一: 判断负权回路,如果在进行了n-1次更新后还可以继续更新,那么必定存在负权回路。
优化二: SPFA是加了负回路判断的队列版本(可以继续用优先队列进行优化)。其利用队列以进行 Ford 算法的过程,初始时将起点加入队列,每次从队列中取出一个元素,并对所有与它相邻的点进行修改,若相邻的点修改成功,则将其入队,直到队列为空时算法结束。

参考文章

//bellman-ford算法原始版
#include <bits/stdc++.h>
#define maxn 105
#define inf 0x3f3f3f3f
using namespace std;
struct edge
{
    int x, y, w;
};
vector<edge> vedge;
int dis[maxn]; //距离起点的距离

void init(int n) //初始化
{
    vedge.clear();
    for (int i = 0; i <= n; i++)
        dis[i] = inf;
}

void bellman_ford(int start, int n) //算法主体
{
    dis[start] = 0;
    for (int k = 0; k < n; k++)
    {
        for (int i = 0; i < vedge.size(); i++)
        {
            int u = vedge[i].x;
            int v = vedge[i].y;
            int w = vedge[i].w;
            dis[v] = min(dis[v], dis[u] + w);
        }
    }
}

int main()
{
    freopen("bellman_in.txt", "r", stdin);
    int n, m, u, v, w, start;
    cin >> n >> m; //输入点和边数
    cin >> start;
    init(n);
    for (int i = 0; i < m; i++)
    {
        scanf("%d%d%d", &u, &v, &w);
        vedge.push_back(edge{u, v, w});
    }
    bellman_ford(start, n);
    for (int i = 0; i < n; i++)
        if (i != start) printf("dis[%d]=%d , ", i, dis[i]);
    return 0;
}

优化一:(判断负权回路),只需要在n-1次松弛后再加一轮循环即可

int flag = 0;
for (int i = 0; i < vedge.size(); i++)
{
    int u = vedge[i].x;
    int v = vedge[i].y;
    int w = vedge[i].w;
    if (dis[v] > dis[u] + w)
    {
        flag = 1;
        break;
    }
}
if (flag) printf("存在负权回路!");

优化二:SPFA(队列版本) 实际操作中往往不需要n-1次循环,并且一些点更新一次即可,无需每次都枚举所有的边,如果用优先队列优化每次先更新小的更容易先更新完,但是入队出队的时间增加到了log级别,需要权衡

//bellman-ford算法SPFA带负环判断
#include <bits/stdc++.h>
#define maxn 105
#define inf 0x3f3f3f3f
using namespace std;
struct edge
{
    int v, w;
};
vector<edge> g[maxn]; //记录该点所出的边
bool vis[maxn]; //记录一个点是否在队列中
int dis[maxn]; //距离起点的距离
int cnt[maxn]; //记录一个点松弛的次数(用于判断负环)

void init(int n) //初始化
{
    memset(vis, 0, sizeof(vis));
    for (int i = 0; i <= n; i++)
    {
        dis[i] = inf;
        cnt[i] = 0;
    }
}

bool bellman_ford(int start, int n) //算法主体
{
    dis[start] = 0;
    queue<int> q;
    q.push(start);
    vis[start] = 1;
    while (!q.empty())
    {
        int u = q.front();
        q.pop();
        vis[u] = 0;
        for (int i = 0; i < g[u].size(); i++)
        {
            int v = g[u][i].v;
            int w = g[u][i].w;
            if (dis[v] > dis[u] + w)
            {
                dis[v] = dis[u] + w;
                if (!vis[v])
                {
                    q.push(v);
                    vis[v] = 1;
                }
                if (++cnt[v] >= n) return false; //如果一个边松弛次数超过了n-1次,则存在负环
            }
        }
    }
    return true;
}

int main()
{
    freopen("bellman_in.txt", "r", stdin);
    int n, m, u, v, w, start;
    cin >> n >> m; //输入点和边数
    cin >> start;
    init(n);
    for (int i = 0; i < m; i++)
    {
        scanf("%d%d%d", &u, &v, &w);
        g[u].push_back(edge{v, w});
    }
    if (bellman_ford(start, n))
    {
        for (int i = 0; i < n; i++)
            if (i != start)
                printf("dis[%d]=%d , ", i, dis[i]);
    }
    else
        printf("存在负环");
    return 0;
}

最小生成树

prime算法

与Dijkstra算法类似,先选定一个起点,遍历与这个点相连的所有边,选择最短的一条将其加入,然后继续从已经加入的点中遍历相连的边,选择最短的加入。n个点,则需要加入n-1条边,适用于稠密图,对于稀疏图的效率不是很高。

kruskal算法

先将所有的边按从小到大排序,选择其中最小的一条边,将其加入,然后从剩余的边中继续选择最小的边加入,在加入边时,边的两端点不能位于同一棵树中,即不能构成回路,为了判断是否位于同一棵树,需要用到并查集。

最小生成树例题(选自PTA)

PTA公路村村通
AC代码
prime版本:

//采用prime算法,用时47ms,对于稀疏图效率不是很高,算法与dijskar算法类似
#include <bits/stdc++.h>
using namespace std;
#define mem(a, n) memset(a, n, sizeof(a))
#define inf 0x3f3f3f3f
typedef unsigned long long ull;
int len[1001][1001];
int n, m, x, y, l, cnt = 0, vis[1001] = {0}, sum = 0;
vector<int> g[1001];
vector<int> visted; //保存已经加入最小树的点
int main()
{ //ios::sync_with_stdio(false);cin.tie(0);
    vis[1] = 1;
    cin >> n >> m;
    visted.push_back(1);
    while (m--)
    {
        cin >> x >> y >> l;
        g[x].push_back(y);
        g[y].push_back(x);
        len[x][y] = l;
        len[y][x] = l;
    }
    //prime算法主体
    while (cnt < n - 1)
    {
        int x, y, id = -1, mlen = inf, xx;
        for (int i = 0; i < visted.size(); i++) //从已经确定的点中找与其连接的最短路径
        {
            x = visted[i];
            for (int j = 0; j < g[x].size(); j++) //遍历与该点链接的所有边
            {
                y = g[x][j];
                if (!vis[y] && len[x][y] < mlen) //如果没有加入最小树并且距离比当前的最小距离更小就更新
                {
                    id = y;
                    xx = x;
                    mlen = len[x][y];
                }
            }
        }
        if (id == -1) //不能生成最小树
        {
            cnt = -1;
            break;
        }
        else
        {
            cnt++;
            sum += len[xx][id];
            visted.push_back(id);
            vis[id] = 1;
        }
    }
    if (cnt == -1)
        cout << -1;
    else
        cout << sum;
    return 0;
}

kruskal版本

//采用kruskal算法,用时7ms,对于稀疏图的效率较高,需要用到并查集(堆可以不用)
#include <bits/stdc++.h>
using namespace std;
#define mem(a, n) memset(a, n, sizeof(a))
#define inf 100000000

int fa[1001];
struct side
{
    int x;
    int y;
    int len;
} s[3001];
bool cmp(side a, side b)
{
    return a.len < b.len;
}

int Find(int x)
{
    if (fa[x] == x)
        return x;
    return fa[x] = Find(fa[x]);
}

void merge(int x, int y)
{
    int f1 = Find(x), f2 = Find(y);
    if (f1 != f2)
    {
        fa[f1] = fa[f2];
    }
}

int main()
{ //ios::sync_with_stdio(false);cin.tie(0);
    int n, m, x, y, l, cnt = 0, sum = 0, left = 0;
    cin >> n >> m;
    for(int i=1;i<=n;i++) fa[i]=i;
    for (int i = 0; i < m; i++)
    {
        cin >> x >> y >> l;
        s[i].x = x;
        s[i].y = y;
        s[i].len = l;
    }
    sort(s, s + m, cmp); //将所有边按权重进行升序排列
    while (cnt < n - 1 && left < m)//找最短边,如果该边的两点在同一个集合中,就跳过
    {
        side t = s[left];
        if (Find(t.x) != Find(t.y))
        {
            merge(t.x, t.y);
            sum += t.len;
            cnt++;
        }
        left++;
    }
    if(cnt==n-1) cout<<sum;
    else cout<<-1;
    return 0;
}

次小生成树

讲解文章链接

最小瓶颈生成树

所谓瓶颈生成树,即对于图 G 中的生成树树上最大的边权值在所有生成树中最小。最小瓶颈生成树不一定是最小生成树。无向图中的最小生成树一定是最小瓶颈生成树。在有向图中采用kruskal算法即可。

二分图

参考文章
二分图的定义:一个图中的所有结点可以划分为两个集合,划分后,所有边的两端都分别位于两个集合中。即不存在一条边的两个端点在同一个集合内。

匹配

匹配是一个边的集合,集合中没有两条边存在公共点,下面是两个匹配的示意图,其中右边既是最大匹配也是完美匹配。如果所有的顶点都是匹配点,则称为完美匹配。完美匹配一定是最大匹配,但是完美匹配不一定存在。
在这里插入图片描述

匈牙利算法求最大匹配

原理: 将两个集合划分为X和Y,每次选择X中的一个点x1,去Y中寻找一个点y1与X进行匹配。
y1会存在以下两种情况:

  • y1还没有被匹配过,则将y1与x1进行匹配,结束。
  • y1已经被匹配了,那么从y1的匹配点x2开始,尝试找一个新的匹配点y2与x2进行匹配,从而令y1能够与x1进行匹配。对于x2,执行同样的做法。

POJ1469为例介绍匈牙利算法的两种版本
题目大意:一共有P门课程和N个学生,每个学生可以选候选列表里的一门课或不选,能否保证每门课都有人选,且选课的学生中没有人选相同的课。很明显是一道求最大匹配的问题。

DFS版本

#include <cstdio>
#include <cstring>
#include <vector>
#define maxn 1005
using namespace std;
int p, n;
int vis[maxn];       //记录在dfs中是否被访问过
int cp[maxn];        //记录匹配点
vector<int> g[maxn]; //记录每门课的候选学生

bool dfs(int x) //找x的匹配点,一旦找到了就返回true
{
    for (int i = 0; i < g[x].size(); i++) //遍历所有能和x匹配的点
    {
        int y = g[x][i];
        if (vis[y]) continue; //已经访问过了,就不用重复走这条路了
        vis[y] = true;
        // 1、y还没有被匹配,
        // 2、y的匹配点z可以去匹配其他的点使得y与x能够匹配
        if (cp[y] == -1 || dfs(cp[y]))
        {
            cp[y] = x; //更改y的匹配点
            return true;
        }
    }
    return false;
}
bool check() //检查每门课是否都能找到匹配
{
    for (int i = 1; i <= p; i++)
    {
        memset(vis, 0, sizeof(vis));
        if (!dfs(i))
            return 0;
    }
    return 1;
}
int main()
{
    int t;
    scanf("%d", &t);
    while (t--)
    {
        for (int i = 0; i < maxn; i++)
        {
            g[i].clear();
            cp[i] = -1;
        }
        scanf("%d%d", &p, &n);
        for (int i = 1; i <= p; i++)
        {
            int temp, stu;
            scanf("%d", &temp);
            while (temp--)
            {
                scanf("%d", &stu);
                g[i].push_back(stu);
            }
        }
        if (p > n) //课程数大于学生数,肯定无解
            printf("NO\n");
        else
        {
            if (check())
                printf("YES\n");
            else
                printf("NO\n");
        }
    }
    return 0;
}

BFS版本

BFS版本实际上就是去模拟DFS的递归过程,相比DFS可以减少一些递归上的时间开销

#include <cstdio>
#include <cstring>
#include <queue>
#include <vector>
#define maxn 1005
using namespace std;

int p, n;                 //记录左右侧点的数量
int lcp[maxn], rcp[maxn]; //记录左侧元素的匹配点以及右侧元素的匹配点
int pre[maxn];            //记录前驱结点,在回溯更新匹配点时用到
int vis[maxn];            //记录该点被哪个点访问过,避免重复访问
vector<int> g[maxn];      //临接表存图

int Hungarian()
{
    memset(vis, 0, sizeof(vis));
    for (int i = 0; i < maxn; i++)
        lcp[i] = rcp[i] = -1;
    for (int i = 1; i <= p; i++) //给每个点找匹配
    {
        if (lcp[i] != -1) continue; //如果该点已经匹配,进行下一轮循环
        queue<int> q;
        q.push(i);
        pre[i] = -1;
        bool flag = false; //是否找到匹配
        while (q.size() && !flag)
        {
            int x = q.front();
            q.pop();
            for (int j = 0; j < g[x].size(); j++)
            {
                int y = g[x][j];
                if (vis[y] == i) continue; //如果之前从i点到过y点,说明这条路无解,不用继续走了
                vis[y] = i;
                //如果右侧的点已经匹配,将对应的左侧匹配点入队
                if (rcp[y] != -1)
                {
                    q.push(rcp[y]);
                    pre[rcp[y]] = x;
                }
                else //如果右侧有未匹配的点,则匹配成功
                {
                    flag = true;
                    //进行回溯更新匹配边
                    while (x != -1)
                    {
                        int temp = lcp[x]; //记录原来的匹配点
                        lcp[x] = y;
                        rcp[y] = x;
                        x = pre[x];
                        y = temp;
                    }
                    break;
                }
            }
        }
    }
    //统计有多少点找到了匹配点
    int cnt = 0;
    for (int i = 1; i <= p; i++)
        if (lcp[i] != -1) cnt++;
    return cnt;
}

int main()
{
    int t;
    scanf("%d", &t);
    while (t--)
    {
        for (int i = 0; i < maxn; i++)
            g[i].clear();
        scanf("%d%d", &p, &n);
        for (int i = 1; i <= p; i++)
        {
            int temp, stu;
            scanf("%d", &temp);
            while (temp--)
            {
                scanf("%d", &stu);
                g[i].push_back(stu);
            }
        }
        if (Hungarian() == p)
            printf("YES\n");
        else
            printf("NO\n");
    }
    return 0;
}

网络流

FF算法(最大流基础)

B站介绍视频FF算法
用一道例题来辅助理解
洛谷例题

//FF算法(效率较低,容易被卡),此题有两个测试点会TLE
#include <bits/stdc++.h>
#define maxm 20005
#define maxn 505
#define mem(a) memset(a, 0, sizeof(a))
#define inf 0x3f3f3f3f
typedef long long ll;
using namespace std;
struct edge
{
    int to, next;
    ll cap;
} e[maxm];
int head[maxn] = {0}, cnt = 1, S, T;
bool vis[maxn] = {0};
//链式前向星方式存边
void add_edge(int u, int v, ll cap)
{
    e[++cnt] = edge{v, head[u], cap};
    head[u] = cnt;
}
ll dfs(int u, ll flow)
{
    if (u == T) return flow;
    vis[u] = 1; //防止dfs往回走
    for (int i = head[u]; i != 0; i = e[i].next)
    {
        int v = e[i].to;
        if (!e[i].cap || vis[v]) continue;
        ll delta = dfs(v, flow < e[i].cap ? flow : e[i].cap);
        if (!delta) continue;  //delta=0说明不是增广路
        e[i].cap -= delta;     //正向边减去流量
        e[i ^ 1].cap += delta; //反向边加流量
        //关于i^1的解释:每次加边的时候都是正向边和反向边一起加,因此正向边和反向边的下标相邻
        //e的有效下标从2开始,因此如果正向边是2*n,那么反向边就是2*n+1,也就是(2*n)^1的结果
        //反之如果正向边是2*n+1,那么反向边就是2*n,而(2*n+1)^1=2*n
        return delta;
    }
    return 0; //如果这个点的所有边都不是增广路 返回0
}
ll getmaxflow()
{
    ll maxflow = 0, delta;
    while (1)
    {
        mem(vis);
        delta = dfs(S, inf);
        if (!delta) break; //没有增广路了 退出
        maxflow += delta;
    }
    return maxflow;
}
int main()
{
    int n, m, u, v;
    ll w;
    cin >> n >> m >> S >> T;
    for (int i = 0; i < m; i++)
    {
        scanf("%d%d%lld", &u, &v, &w);
        add_edge(u, v, w);
        add_edge(v, u, 0);
    }
    printf("%lld", getmaxflow());
    return 0;
}

Dinic算法(对FF算法的改进)

Dinic算法作了三个改进:
1.多路增广,即一次BFS后可以找到多条增广路
2.将点进行分层(根据BFS的遍历顺序进行编号)
3.弧优化(可以应对一个点连很多边的情况)

//Dinic算法 可以通过全部测试点
#include <bits/stdc++.h>
#define maxm 20005
#define maxn 505
#define mem(a) memset(a, 0, sizeof(a))
#define inf 0x3f3f3f3f
typedef long long ll;
using namespace std;
struct edge
{
    int to, next;
    ll cap;
} e[maxm];
int head[maxn] = {0}, deep[maxn], cur[maxn], cnt = 1, n, m, S, T;
//链式前向星方式存边
void add_edge(int u, int v, ll cap)
{
    e[++cnt] = edge{v, head[u], cap};
    head[u] = cnt;
}
bool bfs()
{
    queue<int> q;
    q.push(S);
    int u, v;
    mem(deep);
    deep[S] = 1;
    while (q.size())
    {
        u = q.front();
        q.pop();
        for (int i = head[u]; i; i = e[i].next)
        {
            v = e[i].to;
            if (deep[v] || e[i].cap == 0) continue; //已经访问过或者没有残余流量,也算弧优化的一种
            q.push(v);
            deep[v] = deep[u] + 1; //将点进行分层
        }
    }
    //先将cur数组初始化
    for (int i = 0; i <= n; i++)
        cur[i] = head[i];
    return deep[T] != 0; //如果T点无法到达说明没有增广路了
}
ll dfs(int u, ll flow)
{
    if (u == T) return flow;
    for (int i = cur[u]; i != 0; i = e[i].next)
    {
        cur[u] = i;
        //弧优化,记录当前这个点所dfs到的点,可以避免重复访问前面的点
        //如果是从head[u]开始的化要把与u连接的所有点都遍历到,大大增加了时间
        //对于是否会漏点的问题,由于dfs在调用的过程中存在一个隐式的栈,
        //这一层的dfs访问不到的点会在上一层中被访问
        int v = e[i].to;
        if (deep[u] + 1 != deep[v] || !e[i].cap) continue;
        ll delta = dfs(v, flow < e[i].cap ? flow : e[i].cap);
        if (!delta) continue;  //delta=0说明不是增广路
        e[i].cap -= delta;     //正向边减去流量
        e[i ^ 1].cap += delta; //反向边加流量
        return delta;
    }
    return 0; //如果这个点的所有边都不是增广路 返回0
}
ll getmaxflow()
{
    ll maxflow = 0, delta;
    while (bfs())
        while (delta = dfs(S, inf))
            maxflow += delta;
    return maxflow;
}
int main()
{
    int u, v;
    ll w;
    cin >> n >> m >> S >> T;
    for (int i = 0; i < m; i++)
    {
        scanf("%d%d%lld", &u, &v, &w);
        add_edge(u, v, w);
        add_edge(v, u, 0);
    }
    printf("%lld", getmaxflow());
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Python算法是一种基于Python语言的计算机算法设计和实现的手段。它包括数学领域、计算机科学、机器学习等领域中的常见算法,如排序算法、搜索算法、动态规划、贪心算法图论等等。Python算法的实现需要掌握基本的Python编程技巧以及算法设计理论和分析技能。下面我们以“快速排序”算法为例介绍一下Python算法的完整案例。 快速排序是一种常用的排序算法,其基本思想是通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分关键字小,然后对这两部分继续进行排序,以达到整个序列有序的目的。 Python算法实现步骤: 1. 实现递归函数 定义一个递归函数来实现快速排序算法。该函数的参数包括待排序的列表以及列表起始位置和结束位置。在函数中实现列表分区、递归调用和排序合并等子功能。 2. 实现列表分区 在递归函数中实现列表的分区功能。分区过程中需要设置一个基准值,将待排序列表分为两部分。对于小于基准值的元素放在基准值左边,对于大于等于基准值的元素放在基准值右边。 3. 递归调用和排序合并 在递归函数中实现对分区后的子列表进行递归调用和排序合并操作。对于子列表的快速排序操作采用递归方式实现,当子列表大小小于等于1时停止递归。最后,将各个子列表的排序结果进行合并。 4. 实现排序测试 编写一个排序测试函数,用于测试快速排序算法的效率和准确性。在测试函数中调用快速排序算法,并生成不同大小的随机数据列表,测试排序结果的准确性和时间开销。 Python算法工具: Python算法的实现可以借助各种工具,并且这些工具大多都是开源的。 一些常用的Python算法工具有:NumPy、SciPy、Matplotlib、Pandas等等。这些工具包含了许多数学和科学计算的基本功能,能够有效地提高Python算法的开发效率。其中,NumPy和SciPy是科学计算的基础包,Matplotlib和Pandas则用于数据可视化和处理方面。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值