[算法笔记]基础图论总结一(最短路、最小生成树、拓扑排序)

零、基础:

1、图的存储方式:

1)、邻接表:

vector<int> h(n, -1);  // 邻接表表头指针
vector<int> ne(n);  // next指针
vector<int> e(n);  // value
vector<int> w(n);  // 边的权值,有时不需要
int idx = 0;  // 结点的全局标识符

// 1、增加一条边
void add(int a, int b, int c) {
    ne[idx] = h[a], e[idx] = b, w[idx] = c, h[a] = idx++;
}

// 2、遍历与t点相连的所有边
for (int i = h[t]; i != -1; i = ne[i]) {
    int j = e[i];
    // t, j就是相连的两个点
}

2)、邻接矩阵:

vector<vector<int>> g;

// 1、初始化
for (int i = 0; i < n; i++) {
    for (int j = 0; j < n; j++) {
        if (i == j) g[i][j] = 0;
        else g[i][j] = 1e8;
    }
}

// 2、增加一条边
g[i][j] = w;

// 3、遍历t点所有边
// 邻接矩阵无法记住相邻关系,所有只有全部遍历
for (int i = 0; i < n; i++) {
    cout << g[t][i];
}

3)、结构体:

有些算法(例如Bellman-Ford算法)不一定需要将图完整表示出来,我们只关注边的信息
struct Edge {
    int from, to, w;
    bool operator< (const Edge& e) const {
        return w < e.w;
    }
}

一、最短路算法:

1、Dijkstra算法:

int dijkstra(...) {
	vector<int> dist(n, 1e8);  // 距离起点的距离
    vector<bool> vis(n, false);  // 是否确定了最短路径
    priority_queue<PII, vector<PII>, greater<>> q;  // 优先队列,确定离起点最近的非树结点
    dist[0] = 0;
    q.push({0, 0});  // {dist[i], i},dist置于pair第一维,用于排序
    while (!q.empty()) {
        int t = q.top().second;
        q.pop();
        if (!vis[t]) { 
            // 遍历结点,根据图的不同存储方式,有两种形式,参考上文,这里使用邻接表形式
            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];
                    q.push({dist[j], j});
                }
            }
            vis[t] = true;
        }
    }
    return dist[n - 1];
}

2、朴素Bellman-Ford算法:

  • 朴素Bellman-Ford算法效率较低,但是有一个特别的应用场景:有边数限制的最短路算法,假设边数限制为k。
  • Bellman-Ford只关注边,因此选用上述结构体方式存图。
vector<Edge> edges;  // 图已建好

int bellman_ford(...) {
    vector<int> dist(n, 1e8);
    dist[0] = 0;
	for (int i = 0; i < k; i++) {
        auto last = dist;  // 必须使用上一次的dist,因为这一轮的dist可能被更新
        for (auto& edge : edges) {
            auto [a, b, c] = edge;
            dist[b] = min(dist[b], last[a] + c);
        }
    }
    if (dist[n - 1] >= 1e8 / 2) return -1;  // 不直接判断相等是因为在权值为负情况下,有可能会被更新
    return dist[n - 1];
}

3、SPFA算法(队列优化Bellman-Ford算法):

  • 优化思路:假设源点s到点b之间有点a,当dist[a]没有被更新时,dist[b]更新没有意义。
  • 由于使用队列,需要表示点,因此这里选择邻接表。还需要一个数组inQue表示是否在队列中。
  • SPFA与Dijkstra一样使用数组标识一个结点的状态,不同的是:前者表示是否在队列中,因此每次往队列中加入元素j时,inQue[j] = true; 后者表示该点最小路径是否确定,因此必须是t结点被pop出时确定。
int spfa(...) {
    vector<int> dist(n, 1e8);
    vector<bool> inQue(n, false);
    queue<int> q;
	dist[0] = 0;
    q.push(0);
    inQue[0] = true;
    while (!q.empty()) {
        int t = q.front();
        q.pop();
        inQue[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];
                if (!inQue(j)) {
                    q.push(j);
                    inQue[j] = true;
                }
            }
        }
    }
    if (dist[n - 1] >= 1e8 / 2) return -1;  // 不直接判断相等是因为在权值为负情况下,有可能会被更新
    return dist[n - 1];
}
  • Bellman-Ford算法还有一个应用,一般用SPFA来写,即判断负权环是否存在。只要在上述代码上做些修改即可。
bool spfa(...) {
    vector<int> dist(n, 1e8);
	dist[0] = 0;
    vector<int> cnt(n);  // 从源点到点x的边数
    // 为了防止负权环并不在 点0 ~ 点n-1的路径上,把所有点都加入队列中
    for (int i = 0; i < n; i++) {
        q.push(i);
        inQue[i] = true;
    }
    
    while (!q.empty()) {
        int t = q.front();
       // ...
        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;  // 说明这条路径上存在负权环
                if (!inQue(j)) {
					// ...
                }
            }
        }
    }
    return false
}

4、Floyd算法:

  • Floyd算法一般用来求多源最短路径,即可以求图中任意两点的最短路径。

  • 使用邻接矩阵,类似于动态规划

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

二、最小生成树算法:

1、Prim算法:

  • 类似于Dijkstra算法,实际上Dijkstra也再次发现了Prim算法。
  • Prim算法每次将非最小生成树中的dist最小点加入最小生成树,Dijkstra算法将非最短路径树的最小点加入最短路径树。
  • 基于邻接表实现的时间复杂度在边非常多时容易TLE,这里给出邻接矩阵实现方式。
int prim() {
	vector<int> dist(n, 1e8);  // 距离起点的距离
    vector<bool> vis(n, false);  // 是否确定了最短路径
    int res = 0;
    dist[0] = 0;
    for (int i = 0; i < n; i++) {
        int t = -1;
        // 遍历所有节点,找到距离生成树集合最近的节点
        for (int j = 0; j < n; j++) {
            if (!vis[j] && (t == -1 || dist[j] < dist[t])) {
                t = j;
            }
        }
        // 不存在可以加入最小生成树的点,直接返回false
        if (dist[t] == 1e8) return -1;
        res += dist[t];
        vis[t] = true;
        // 松弛操作
        for (int j = 0; j <= n; j++) {
            dist[j] = min(dist[j], g[t][j]);
        }
    }
    return res;
}

2、Kruskal算法:

  • 基于并查集实现。
vector<int> p(n);

int find(int x) {
    return x == p[x] ? x : (p[x] = find(p[x]));
}

// 对m条边进行排序,每次取最小边加入最小生成树
Edge edges[m];

int kruskal() {
    //  res:最小生成树代价,cnt:最小生成树中节点个数,如果最后小于n,则返回-1
    int res = 0, cnt = 0;
    sort(edges, edges + m);
    for (int i = 0; i < n; i++) p[i] = i;
    for (int i = 0; i < m; i++) {
        int a = edges[i].from, b = edges[i].to, c = edges[i].w;
        a = find(a), b = find(b);
        // 如果两个点不属于一个连通分量中,则加入最小生成树
        if (a != b) {
            p[a] = b;
            res += c;
            cnt++;
        }
    }
    if (cnt < n - 1) return -1;
    return res;
}

三、拓扑排序:

  • 基本要点很简单,首先存图,接着将所有点放入优先队列中,取出队头入度为0的节点,所有与该节点相连的节点的入度--。不断重复该操作。
  • 但是需要注意的是,在STL自带优先队列中动态更改值比较麻烦。因此我们需要每次手动遍历寻找ind为0的节点,因此使用普通的queue,甚至数组都是可以的,这里我们使用模拟队列。
// 邻接表存图
// 每个点的入度
int h[N], ne[M], e[M], ind[N], idx;

void add(int a, int b) {
    // b节点入度++
    ne[idx] = h[a], e[idx] = b, ind[b]++, h[a] = idx++;
} 

bool top_sort() {
    // 模拟队列
    int q[N], hh = 0, tt = -1;
    for (int i = 0; i < n; i++) {
        if (!ind[i]) {
            q[++tt] = i;
        }
    }
    while (hh <= tt) {
        int t = q[hh++];
        for (int i = h[t]; i != -1; i = ne[i]) {
            int j = e[i];
            ind[j]--;
            if (!ind[j]) {
                q[++tt] = j;
            }
        }
    }
    // 队列中应包含所有节点
    return tt == n - 1;
}
posted @ 2020-11-26 18:51  macguz  阅读(133)  评论(0编辑  收藏  举报