图论——模版学习

对图论进行简单的复习

Dijkstra

Dijkstra的思想为(假设已完成初始化):

  1. 从未求得最短路径的顶点中找到一个离源点最近的点 t,确定其当前离源点的距离dist[t]为源点到该点的最短路径长
  2. 更新与 t 相邻的点到源点的最短距离

基础的Dijkstra采用邻接矩阵存图,假设共 n 个顶点,则上述操作需要进行 n 轮,每轮找点以及更新操作复杂度均为O(n),所以总时间复杂度为O(n2)

#include <bits/stdc++.h>

#define endl '\n'

using namespace std;
using i64 = long long;
using pii = pair<int, int>;

constexpr int INF = 0x3f3f3f3f;
constexpr int MOD = 1e9 + 7;
constexpr double eps = 1e-9;
constexpr int N = 1e5 + 7;

void solve()
{
    int n, m;
    cin >> n >> m; // n个点 m条边
    vector g(n + 1, vector<int>(n + 1, INF));
    for (int i = 0; i < m; i++)
    {
        int x, y, z;
        cin >> x >> y >> z;
        g[x][y] = min(g[x][y], z);
    }

    vector<int> dist(n + 1, INF);
    vector<bool> st(n + 1, false);
    auto Dijkstra = [&]() -> void
    {
        for (int k = 0; k < n; k++)
        {
            dist[1] = 0;
            int t = 0;
            for (int i = 1; i <= n; i++) // 找出一个 在所有还未确定最短路径的点中离原点最近的点
            {
                if (!st[i] && (!t || dist[i] < dist[t]))
                {
                    t = i;
                }
            }

            st[t] = true; // 确定原点到i点的最短路径已找到
            for (int i = 1; i <= n; i++)
            {
                if (!st[i] && dist[t] + g[t][i] < dist[i]) // 更新与i点相连的点到原点的最短路径长
                {
                    dist[i] = dist[t] + g[t][i];
                }
            }
        }
    };

    Dijkstra();
    if (dist[n] > INF / 2)
    {
        cout << -1 << endl;
    }
    else
        cout << dist[n] << endl;
}

int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int T = 1;
    // cin >> T;
    while (T--)
    {
        solve();
    }

    return 0;
}

堆优化的Dijkstra

堆优化的Dijkstra在王道的考研数据结构书中并没有详细介绍,但是提了一嘴有这个东西。这里简单写一下堆优化的版本,此处堆就不手写了,而是用STL中的priority_queue(省事好用)。下方Prim的堆优化版本与此类似。
既然每次只找一个离源点最近的点,那就不需要每次都遍历一轮n个点,可以每次更新距离后,将更新后的距离和被更新的点打包成一个二元组放入小根堆,这样每次找点就可以在小根堆中直接取到距离最小的点

堆优化后采用邻接表存图,每次找点为O(1),共n个点为O(n),更新距离为O(mlogn),其中log为小根堆所需,所以总时间复杂度为 O(n + mlogn)

int h[N], ne[N], ver[N], w[N];
int tot = 0;

void add(int x, int y, int z)
{
    ver[++tot] = y, w[tot] = z, ne[tot] = h[x], h[x] = tot;
}

    int n, m;
    cin >> n >> m; // n个点 m条边
    for (int i = 0; i < m; i++)
    {
        int x, y, z;
        cin >> x >> y >> z;
        add(x, y, z);
    }

    vector<int> dist(n + 1, INF);
    vector<bool> st(n + 1, false);
    auto Dijkstra = [&]() -> void
    {
        dist[1] = 0;
        priority_queue<pii, vector<pii>, greater<pii>> q; // 小根堆
        q.push({0, 1}); // 1为源点,初始1到1距离为零

        while (!q.empty())
        {
            auto t = q.top(); // 每次取距离最近的点
            q.pop();

            int vertex = t.second, distance = t.first;
            if (st[vertex]) continue; // 若该点最短路径已确定 则跳过
            st[vertex] = true;

            for (int i = h[vertex]; i; i = ne[i]) // 更新相邻点到源点的最短距离
            {
                int j = ver[i];
                if (dist[vertex] + w[i] < dist[j])
                {
                    dist[j] = dist[vertex] + w[i];
                    q.push({dist[j], j});
                }
            }
        }
    };

Floyd

Floyd的思想本质为动态规划

  • f[k][i][j]数组含义:从1k的节点作为中间经过的节点时,从ij的最短路径长度。
    状态转移方程 f[k][i][j] = min(f[k-1][i][j] , f[k-1][i][k] + f[k-1][k][j]),表示f[k][i][j]ij的最短路径不经过k节点;②经过k节点。
    根据动态规划,f[k]只可能与f[k-1]有关系,所以可以省略最外层的维度k,直接带入循环求解。

显而易见,复杂度为O(n3)

void floyd() {
    for(int k = 1; k <= n; k++)
        for(int i = 1; i <= n; i++)
            for(int j = 1; j <= n; j++)
                f[i][j] = min(f[i][j], f[i][k] + f[k][j]);
}

Prim

prim 算法采用的是一种贪心的策略。
每次将离连通部分最近的点和该点对应的边加入到连通部分,连通部分逐渐扩大,最后将整个图连通起来。
Prim类似于Dijkstra,复杂度为O(n2)

constexpr int INF = 0x3f3f3f3f;

int main()
{
    int n, m;
    cin >> n >> m; // 点数 边数

    vector g(n + 1, vector<int>(n + 1, INF));
    for (int i = 0; i < m; i++)
    {
        int u, v, w;
        cin >> u >> v >> w;
        g[u][v] = g[v][u] = min(g[u][v], w); 
    }

    vector<int> dist(n + 1, INF);
    vector<bool> st(n + 1, false); // 是否已经加入生成树
    auto prim = [&]() {
        int res = 0;
        dist[1] = 0;
        for (int i = 1; i <= n; i++)
        {
            int t = 0;
            for (int j = 1; j <= n; j++) // 找出离连通部分的距离最近的点
            {
                if (!st[j] && (!t || dist[j] < dist[t])) 
                {
                    t = j;
                }
            }

            if (dist[t] == INF) // 孤立点
            {
                cout << "No Minimum Spanning Tree" << endl;
                return INF;
            }

            st[t] = true;
            res += dist[t];
            for (int j = 1; j <= n; j++)
            {
                if (g[t][j] < dist[j]) // 区别于最短路的该点到源点的距离最近
                {                             
                    dist[j] = g[t][j]; // 最小生成树要求该点离连通部分的距离最近
                    // pre[j] = t; // 加一个数组用于记录传递关系 可以回溯树的生成路径
                }
            }
        }  
        return res; // 最小生成树的边权之和
    };


    // ......
}

类似于Dijkstra,Prim也有堆优化版本,具体就不写了,可以参考上方的Dijkstra的堆优化。
用优先队列代替堆,优化的Prim算法时间复杂度O(mlogn)。适用于稀疏图,但是稀疏图的时候求最小生成树,Kruskal 算法更加实用。

Kruskal

  • 将所有边按照权值的大小进行升序排序,然后从小到大分别判断。
  • 如果当前边与之前选择的所有边不会组成回路,就选择这条边;反之,舍去。
    使用并查集判断是否会产生回路。
  • 直到具有 n 个顶点的连通网筛选出来 n-1 条边为止。

共m条边,每次优先队列操作logm,并查集在路径压缩后查询操作的用时是O(1)级别的,总时间复杂度为 O(mlogm)

struct Edge {
    int u, v;
    int worth;

    friend bool operator<(Edge a, Edge b) { // 优先队列的比较需要双形参
        return a.worth > b.worth; // 对应优先队列就是小根堆
    }
    
};

void main()
{
    int n, m;
    cin >> n >> m;  // n个顶点 m条边

    priority_queue<Edge> q;
    for (int i = 0; i < m; i++)
    {
        int u, v, w;
        cin >> u >> v >> w;
        q.push({u, v, w});
    }

    vector<int> fa(n + 1); // 用于并查集
    auto find = [&](auto &self, int x) -> int { // 并查集查找祖宗
        if (fa[x] != x) return fa[x] = self(self, fa[x]); // 路径压缩
        return x;
    };

    auto kruskal = [&]() {
        int res = 0; // 最小生成树的边权之和
        int num = 0; // 加入的边数
        iota(fa.begin(), fa.end(), 0); // 初始 每个点的祖宗都是自己

        while (!q.empty())
        {
            auto t = q.top();
            q.pop();

            int fu = find(find, t.u), fv = find(find, t.v);
            if (fu == fv) continue; // 两点同属一个连通块 再连会形成回路
            fa[fv] = fu; // 将两个连通块连通
            res += t.worth;
            ++num;
        }

        if (num < n - 1) // 少于n-1条边 未能将n个点都连通
        {
            cout << "No Minimum Spanning Tree" << endl;
        }
        else cout << res << endl;
    };
    
    kruskal();

}

拓扑排序

  • 一个有向图,如果图中有入度为 0 的点,就把这个点及与这个点所连的边都删掉。
  • 一直进行上面的处理,如果所有点都能被删掉,则该图可以进行拓扑排序。
constexpr int N = 1e5 + 7;

int h[N], ne[N], ver[N];
int tot = 0;

void add(int u, int v)
{
    ver[++tot] = v, ne[tot] = h[u], h[u] = tot;
}

void solve()
{
    int n, m;
    cin >> n >> m;  // 顶点数 边数
    vector<int> in(n + 1); // 入度
    for (int i = 0; i < m; i++)
    {
        int x, y;
        cin >> x >> y;
        add(x, y);
        in[y]++; 
    }

    vector<int> q(n); // 滚动数组 存储拓扑序 同时模拟队列
    auto topSort = [&]() -> bool {
        int tail = -1, head = 0;
        for (int i = 0; i < n; i++)
        {
            if (!in[i + 1]) // 所有初始入度为0的点加入队列
            {
                q[++tail] = i + 1;
            }
        }

        while (head <= tail) // 队列非空
        {
            int t = q[head++]; // 队头取点
            for (int i = h[t]; i; i = ne[i])
            {
                if (--in[ver[i]] == 0) // 将t点及从t出发的边删除后 所有入度为0的点加入队列
                {
                    q[++tail] = ver[i];
                }
            }
        }

        return tail == n - 1; // 是否排序所有点
    };

    if (topSort())
    {
        for (int i = 0; i < n; i++)
        {
            cout << q[i] << ' ';
        }
    }
    else cout << -1 << endl;
}

——分割一下 以上为考研学习(王道书所讲算法)


bellman - ford算法

Bellman - ford 算法是求含负权图的单源最短路径的一种算法。其原理为连续进行松弛,每次松弛时把每条边都更新一下,若在 n-1 次松弛后还能更新,则说明图中有负环

bellman - ford算法的具体步骤:

for n次
    for 所有边 a,b,w (松弛操作) // a -> b 的边 权值为 w
        dist[b] = min(dist[b], last[a] + w)

注意:last[] 数组是上一轮迭代后 dist[] 数组的备份,由于是每个点同时向外出发,因此需要对 dist[] 数组进行备份,若不进行备份会因此发生串联效应,影响到下一个点

constexpr int N = 510, M = 10010;

struct Edge
{
    int u, v, w;
}edges[M];

int n, m;
int dist[N];
int last[N];

void bellman_ford()
{
    memset(dist, 0x3f, sizeof dist);

    dist[1] = 0;
    for (int i = 0; i < n; i++) // 这里的n可以换成其他数字,假设换成k,则最终求出的dist[]表示最多经过k条边的最短距离
    {
        memcpy(last, dist, sizeof dist); // last为dist数组的备份
        for (int j = 0; j < m; j ++ )
        {
            auto e = edges[j];
            dist[e.v] = min(dist[e.v], last[e.u] + e.w);
        }
    }
}

spfa(队列优化的Bellman-Ford算法)

int n;  
int h[N], w[N], e[N], ne[N], idx;   
int dist[N], cnt[N];        // dist[x]存储1号点到x的最短距离,cnt[x]存储1到x的最短路中经过的点数
bool st[N];     // 存储每个点是否在队列中

bool spfa()   // 如果存在负环,则返回true,否则返回false。
{
    // 不需要初始化dist数组
    // 原理:如果某条最短路径上有n个点(除了自己),那么加上自己之后一共有n+1个点,由抽屉原理一定有两个点相同,所以存在环。

    queue<int> q;
    for (int i = 1; i <= n; i ++ )
    {
        q.push(i);
        st[i] = true;
    }

    while (q.size())
    {
        auto t = q.front();
        q.pop();

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= n) return true;       // 如果从1号点到x的最短路中包含至少n个点(不包括自己),则说明存在环
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return false;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值