图论算法模板

图论

树: 无环连通图

所以呢, 理解图即可理解树

1.图的存储

对于图的存储有两种

  1. 运用邻接矩阵 主要用于稠密图

    稠密图 m 约等于 n^2, m 表示边数, n 表示结点个数

// 假设边数最大为N 条
typename q[N][N] 
// q[i][j] 表示从i 到 j 有一条边 权重为q[i][j], (权重:比如距离, 有还是没有)
// if q[i][j] 表示距离需要初始化
for (int i = 0; i < n; i++) {
    for (int j = 0; j < n; j ++) {
        if (i == j) q[i][j] = 0;
        else q[i][j] = 0x3f3f3f3f;// 这里定义0x3f3f3f3f为最大, 其实不是2^31 - 1 > 0x3f3f3f3f, 但是好memset(q, 0x3f, sizeof q);
    }
}

//or 

memset(q, 0x3f, sizeof q);
for (int i = 0; i < n; i++) {
    q[i][i] = 0;
}
  1. 运用邻接表,说白了就是N个单链表, 也可以理解在哈希冲突中的链地址法

    主要运用于稀疏图

     稀疏图:m 约等于 n, m, n 如上
    
// 假设最多N个节点, M条边
int h[N], e[M], ne[M], idx;
// 与单链表非常像, 就是head变成了head[N]

// 插入操作
void add(int a, int b) {
    e[idx] = b;
    ne[idx] = a;
    h[a] = idx++;
}
// 这样就形成了从a到b的边

// 注意一定要init
void init() {
    memset(h, -1, sizeof h);0
}
// 建议用一个函数包起来, 而不是直接在main() 中写, 因为会经常忘记

IMG_0607!](…/…/IMG_0607.PNG)

假设你需要创建一个这样的图

参考代码

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

const int N = 1e5 + 10;

int h[N], e[N], ne[N], idx;

//init
void init() {
    memset(h, -1, sizeof h);
}

// insert
void add(int a, int b) {
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx++;
}

int main() {
    // 记得初始化
    init();
    
    add(1, 2);
    add(2, 4);
    add(1, 3);
    add(3, 4);
    add(1, 4);
    return 0;
}

这时候你可能会问, 如果a到b之间有距离, 那么中间的距离怎么办呢?

  1. 可以将e[N]变成结构体
struct E{
    // 表示e[N]中的e, w代表a到b的距离
    int e, w;
}e[N];
  1. 可以再加入一个数组
// w数组保存的是距离
int h[N], e[N], ne[N], w[N], idx;

void add(int a, int b, int len) {
    e[idx] = b;
    w[idx] = len;
    ne[idx] = h[a];
    h[a] = idx++;
}

2.图的遍历

图的遍历-> 青岛大学王卓老师

// 就是dfs的简单模式
// 参考代码

// book数组标记哪些结点被标记过
bool book[N];
// int u 代表现在遍历到了结点u
void dfs(int u) {
    
    // 这个点已经遍历过了
    book[u] = true;
    printf("%d ", u);
    
    // 探索u理解的点
    for (int i = h[u]; i != -1; i = ne[i]) {
        
        // i 表示的是单链表的next
        
        // j 表示的是当前结点, 就是单链表里面的data
        int j = e[i];
    	// 如果j 没有被遍历就遍历j
        if (!book[j]) {
            dfs(j);
        }
    
    }
}

ex:

[题目链接](846. 树的重心 - AcWing题库)

Snipaste_2023-05-18_16-24-11

无向边解释:

​ 无向边就是没有方向的边, 就像路一样(假设a,b 之间有一条无向边, 那么a可以到b, b 也可以到a), 现实中类似:人行道

有向边解释:

​ 假设a到b有一条有向边, 那么a可以到b, 但是b不能到a;(类似显示中的:单向路)

树重心解释

IMG_20230518_163800

那么这个问题该怎么解决呢?

  • 首先, 我们可以发现对于这点的上面那个连通块是无法求结点个数(因为dfs是从上面下来的book[上面那个结点] = true), 但是你会发现上面那个连通快 == n - sum(子树节点和) - 1, n -> 代表结点总个数, 1 表示结点本身, 那么这个问题就转化为求sum(子树结点和).
  • 那么怎么求子树节点和, 其实我们可以在分, 只需要遍历一下这个结点, 如何求每个子树的结点就可以了.
  • 最终问题转化为: 如何求子树结点个数

这里可以用dfs, 也可以用bfs, 但是这里讲的是dfs那就用dfs吧

测试数据:
9
1 2
1 7
1 4
2 8
2 5
4 3
3 9
4 6
样例输出:
4

参考代码:

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

// N 表示结点个数, M 表示边的个数, 因为是无向边, 所以是2*(N - 1);
const int N = 1e5 + 10, M = 2 * N;

int n;
// 邻接表
int h[N], e[M], ne[M], idx;

// 定义答案为无穷大, 因为要取个个结点的最小值
int ans = 0x3f3f3f3f;

// 用来表示结点是否访问过
bool book[N];

// 初始化
void init() {
    memset(h, -1, sizeof h);
}

// insert
// 一般使用的是头插
// 你用其他也可以(比如中间插, 尾插)
void add(int a, int b) {
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx++;
}

// dfs返回的是以step为root所含结点个数 
int dfs(int step) {
    // step 结点已经访问过了
    book[step] = true;

    // sum 来记录以step为root的结点个数
    // res 表示子树中结点数量的最大值

    int sum = 1, res = 0;

    // 遍历step
    for (int i = h[step]; i != -1; i = ne[i]) {
        // j 为结点编号
        int j = e[i];
        // 如果这个结点没有访问过
        if (!book[j]) {
            // t 为以j 为root的结点个数
            int t = dfs(j);
            // 结点的最大值
            res = max(res, t);
            // 求和
            sum += t;
        }
    }
    // 将子树最大与树外面的求最大
    res = max(res, n - sum);
    // 这就是删除step结点后连通块结点的最大值 res

    // 再将最大值与答案求最小即为重心
    ans = min(res, ans);

    
    return sum;
}

int main() {
    init();
    cin >> n;
    for (int i = 1; i < n; i++ ) {
        int a, b;
        scanf("%d%d", &a, &b);
        // 无向边所以做一条a->b, 在做一条b->a;
        add(a, b);
        add(b, a);
    }

    dfs(1);
    cout << ans << endl;

    return 0;
}

bfs() 遍历图

[链接](847. 图中点的层次 - AcWing题库)

Snipaste_2023-05-18_19-04-40

输入样例:
4 5
1 2
2 3
3 4
1 3
1 4
输出样例:
1

重边解释:

​ 就算说之前出现过a到b这条边, 后面又有a到b这条边

自环解释:

​ 就是有a到a这条边

学完后面后可以尝试用单源最短路来实现

3.拓扑排序

  1. 什么叫入度与出度以及什么叫拓扑序列?

    IMG_20230518_191632

解法:

  • 对于拓扑序列以及入度的描述, 当我们需要输出某个值时, 一定会使他上面的输出,就相当于没有输出的里面这个点的入度一定为0

  • 所以我们可以用一个数组d来维护入度, 在add() 时可以让d[b] ++, 当a输出时d[b] –

参考代码:

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

const int N = 1e5 + 10;

// n, m相等, 用邻接矩阵
int h[N], e[N], ne[N], idx;

// 维护入度
int d[N];
int n, m;

// 用手搓队列
// 下面有解释
int que[N], tt, hh;

void add(int a, int b) {
    e[idx] = b;
    ne[idx] = h[a];
    d[b] ++;
    h[a] = idx++;
}


// 判断是否合法
bool topsort() {
    // 1. 将入度为0的点加入队列
    
    for (int i = 1; i <= n; i++) {
        if (!d[i]) {
            que[tt++] = i;
        }
    }
    // 遍历队列, 因为队列里面存放的是入度为0的结点, 需要将他相邻的点的入度减一
    while(hh < tt) {
        // 取出队头元素
        int t = que[hh++];
    
        // 遍历 让其相邻的点的入度减1
        for (int i = h[t]; i != -1; i = ne[i]) {
            int j = e[i];
            d[j] -- ;
            // 如果说d[j] == 0, 就让他入队
            if (!d[j]) {
                que[tt ++] = j;
            }
        }
    }
    // tt 对于n 说明队里面有n个元素合法
    // 并且你会发现que里面存的就是拓扑序列, 这就是为什么要用手搓队, 而不用queue<int> que;
    return tt == n;
}

void init() {
    memset(h, -1, sizeof h);
}

int main() {
    init();
    cin >> n >> m;
    while(m -- ) {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b);
    }

    if (topsort()) {
        for (int i = 0; i < n; i++) {
            printf("%d ", que[i]);
        }
    }else {
        printf("-1\n");
    }

    system("pause");
    return 0;
}

4. dijkstra(狄克斯特拉)算法

Snipaste_2023-05-18_19-53-42

对于单源路最推荐spfa他是正数与负数均可以做

个人感觉非常不错的dijkstra

IMG_20230518_200351

对应代码

// O(n ^ 2)
// 朴素版的用于稠密图,我们开邻接矩阵
// n 结点数
int dist[N], n;
bool st[N];
int dijkstra() {
    // 初始化为inf
    memset(dist, 0x3f, sizeof dist);
	dist[1] = 0;
    // 循环遍历n - 1 遍
    // 因为一次只能加一个点进来, 本来要n 次, 但是最后一次就他自己一个人, 所以无需遍历
    for (int i = 0; i < n - 1; i ++ ) {
		int t = -1; 
        for (int j = 1; j <= n; j ++) {
            // !st[j] 表示j 不在st里面
            // t == -1 or dist[t] > dist[j] 
            // t == -1其实就是说, 想让t 初始化为不在st中的第一个, 但是你不知道是哪个
            // dist[t] > dist[j] 找最小
            if (!st[j] && (t == -1 || dist[t] > dist[j])) {
                t = j;
            }
        }
        
        st[t] = true;
        
        // 遍历所有邻边
        for (int j = 1; j <= n; j ++) {
            dist[j] = min(dist[j], dist[t] + t到j的距离);
        }
    }
    return dist[n];
}

Snipaste_2023-05-18_20-19-55

重边:

​ 我们取重边小的作为q[a][b], 对于邻接矩阵,不管

输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3
#include<bits/stdc++.h>
using namespace std;

const int N = 510;
int q[N][N];
int dist[N];
bool st[N];
int n, m;

int dijkstra() {
    // 初始化为inf
    memset(dist, 0x3f, sizeof dist);
	dist[1] = 0;
    // 循环遍历n - 1 遍
    // 因为一次只能加一个点进来, 本来要n 次, 但是最后一次就他自己一个人, 所以无需遍历
    for (int i = 0; i < n - 1; i ++ ) {
		int t = -1; 
        for (int j = 1; j <= n; j ++) {
            // !st[j] 表示j 不在st里面
            // t == -1 or dist[t] > dist[j] 
            // t == -1其实就是说, 想让t 初始化为不在st中的第一个, 但是你不知道是哪个
            // dist[t] > dist[j] 找最小
            if (!st[j] && (t == -1 || dist[t] > dist[j])) {
                t = j;
            }
        }
        
        st[t] = true;
        
        // 遍历所有邻边
        for (int j = 1; j <= n; j ++) {
            dist[j] = min(dist[j], dist[t] + q[t][j]);
        }
    }
    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

int main() {
    // 初始化为inf, 因为有重边
    memset(q, 0x3f, sizeof q);
    cin >> n >> m;
    while ( m -- ) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        // 对应上面为什么要初始化
        q[a][b] = min(q[a][b], c);
    }
    cout << dijkstra() << endl;
    return 0;
}

5.dijkstra()算法优化

如何优化呢?

  • 对于上述第二步找最小值, 我们可以用小根堆来实现
// 优化版的对应于稀疏图
// O(mlogn) 如果是稠密图, m == n ^ 2 --> n^2 logn > n^2

// 稀疏图用邻接表来存储
typedef pair<int, int> pii;
int dijkstra() {
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    // pair.frist 存距离, pair.second 存编号
    // 因为greater<> 是按照字典序排列, 所以不能反, 不知道的可以百度
    
    priority_queue<pii, vector<pii>, greater<pii>> que;
    // 将第一个点加进去
    que.push({0, 1});
    
    while(que.size()) {
        // 拿出最小值
        auto t = que.top();
        que.pop();
        
        // 先将其表示出来, 不表示也没有关系, 只是写起来有点麻烦

        int distance = t.first,  location = t.second;
        // 因为用priority_queue会出现冗余, 
        // 冗余下面会解释
        // 就要判断一下是否是冗余
        
        // 之前就在st里面了, 就不用更新了, 因为没有必要, 节约时间
        if (st[location]) continue;
        st[location] = true;
        
        // 更新临近的点
        for (int i = h[location]; i != -1; i = ne[i]) {
            int j = e[i];
            // w[] 存储的是距离, 上面邻接表已经讲过了
            if (dist[j] > distance + w[i]) {
                dist[j] = distance + w[i];
                // dist改变后我们就然他加入que
			  // 这就是冗余的来源, 前面有个数据更新了dist[a], a 进来que
                // 后来进来了本身是要将原理的a中的que更改, 但是priority_queue没有这个功能
                // 没有办法就只好把他在加进去, 这样就形成了冗余
                // 使得时间复杂度变成了mlogm但是m 最多也就n^2, 所以mlogm <= 2mlogn = mlogn
                que.push({dist[j], j});
            }
        }   
    }
    return dist[n];
    
}

[题目链接](850. Dijkstra求最短路 II - AcWing题库)

Snipaste_2023-05-18_20-56-48

输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3

参考代码:

#include<bits/stdc++.h>
using namespace std;
typedef pair<int, int > pii;

const int N = 2e5 + 10;
int h[N], e[N], ne[N], w[N], idx;
int dist[N];
bool st[N];
int n, m;
// 优化版的对应于稀疏图
// O(mlogn) 如果是稠密图, m == n ^ 2 --> n^2 logn > n^2

// 稀疏图用邻接表来存储

int dijkstra() {
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    // pair.frist 存距离, pair.second 存编号
    // 因为greater<> 是按照字典序排列, 所以不能反, 不知道的可以百度
    
    priority_queue<pii, vector<pii>, greater<pii>> que;
    // 将第一个点加进去
    que.push({0, 1});
    
    while(que.size()) {
        // 拿出最小值
        auto t = que.top();
        que.pop();
        
        // 先将其表示出来, 不表示也没有关系, 只是写起来有点麻烦

        int distance = t.first,  location = t.second;
        // 因为用priority_queue会出现冗余, 
        // 冗余下面会解释
        // 就要判断一下是否是冗余
        
        // 之前就在st里面了, 就不用更新了, 因为没有必要, 节约时间
        if (st[location]) continue;
        st[location] = true;
        
        // 更新临近的点
        for (int i = h[location]; i != -1; i = ne[i]) {
            int j = e[i];
            // w[] 存储的是距离, 上面邻接表已经讲过了
            if (dist[j] > distance + w[i]) {
                dist[j] = distance + w[i];
                // dist改变后我们就然他加入que
			  // 这就是冗余的来源, 前面有个数据更新了dist[a], a 进来que
                // 后来进来了本身是要将原理的a中的que更改, 但是priority_queue没有这个功能
                // 没有办法就只好把他在加进去, 这样就形成了冗余
                // 使得时间复杂度变成了mlogm但是m 最多也就n^2, 所以mlogm <= 2mlogn = mlogn
                que.push({dist[j], j});
            }
        }   
    }
    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}
   

void add(int a, int b, int c) {
    e[idx] = b;
    ne[idx] = h[a];
    w[idx] = c;
    h[a] = idx++;
}

int main() {
    memset(h, -1, sizeof h);
    cin >> n >> m;
    while(m --) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }
    
    cout << dijkstra() << endl;
    return 0;
    
}

手搓堆版请自行实现(比较麻烦)

[用这个堆](839. 模拟堆 - AcWing题库)

6.bellman_ford算法

IMG_20230518_213754

// 因为bellman_ford算法需要遍历所有的边, 所有我们可以建立一个结构体来存储, 可以不用其他的(邻接表,邻接矩阵)
struct edge{
    int a, b, w;
}Edge[M];

int bellman_ford() {
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    // 这层遍历的意思是:不超过n条边到达n结点
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j ++) {
            // 你觉得Edge很难写的话可以这样
            // int a = Edge[j].a, b = Edge[j].b, w = Edge[j].w;
            // dist[b] = min(dist[b], dist[a] + w);
            dist[Edge[j].b] = min(dist[Edge[j].a] + Edge[j].w, dist[Edge[j].b]);
        }
    }
}

其实这段代码是有问题的

IMG_0608

修改后:

IMG_0609

那该如何解决呢?

  • 其实你会发现, dist如果是上一次的结果就好了, 这样就不会影响结果
// 因为bellman_ford算法需要遍历所有的边, 所有我们可以建立一个结构体来存储, 可以不用其他的(邻接表,邻接矩阵)
struct edge{
    int a, b, w;
}Edge[M];

// 用来保存dist的上一次结果
int back[N];

int bellman_ford() {
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    // 这层遍历的意思是:不超过n条边到达n结点
    for (int i = 0; i < n; i++) {
        // 将dist的上一次结果复制给back
        memcpy(back, dist, sizeof dist);
        
        for (int j = 0; j < m; j ++) {
            // 这里将back数组代替为原来的dist
            dist[Edge[j].b] = min(back[Edge[j].a] + Edge[j].w, dist[Edge[j].b]);
        }
    }
    return dist[n];
}

ex:

[题目链接](853. 有边数限制的最短路 - AcWing题库)

Snipaste_2023-05-19_08-32-12

数据范围
1≤n,k≤500
1≤m≤10000
1≤x,y≤n
任意边长的绝对值不超过 10000

输入样例:
3 3 1
1 2 1
2 3 1
1 3 3
输出样例:
3

参考代码:

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

const int N = 1e5 + 10, M = 510;
struct ss {
    int a, b, w;
}s[N];
int n, m, k;
int dist[M], back[M];

// 因为bellman_ford算法需要遍历所有的边, 所有我们可以建立一个结构体来存储, 可以不用其他的(邻接表,邻接矩阵)


int bellman_ford() {
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    // 这层遍历的意思是:不超过n条边到达n结点
    for (int i = 0; i < k; i++) {
        // 将dist的上一次结果复制给back
        memcpy(back, dist, sizeof dist);
        
        for (int j = 0; j < m; j ++) {
            // 这里将back数组代替为原来的dist
            dist[s[j].b] = min(back[s[j].a] + s[j].w, dist[s[j].b]);
        }
    }
return dist[n];
}


int main() {
    cin >> n >> m >> k;
    for (int i = 0; i < m; i++) {
        int a, b, w;
        scanf("%d%d%d", &a, &b, &w);
        s[i] = {a, b, w};
    }
    int t = bellman_ford();
    if (t > 0x3f3f3f3f / 2) printf("impossible\n");
    else printf("%d\n", t);
    return 0;
}
if (t > 0x3f3f3f3f / 2) printf("impossible\n");
// 解释下这句话, 就是说因为有负权边, 所以在dist[n] 虽然从1到不了但是可以从后面到(因为我们是遍历所有的边)
// 如果说后面那几个到n是负边就会使得dist[n]变小, 所以我们用/2 (你可以根据数据范围算一算会不会到/2)
// 比如这题边长为10000(最长边长)*500(这是k) == 5000000 < 0x3f3f3f3f/2 所以可以用 
// 或者你写大于5000000也对

7.spfa()算法

那我们来看看如何优化上述代码

  • 如果说dist[b]没有变小的话其实这重循环是没有用的
  • 所以我们只需要找哪些变小的dist然后遍历这个点的邻居
// st来保存队列中元素是否出现
bool st[N];
int spfa() {
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    // 定义一个queue来存储变小的结点
    queue<int> que;
    // 将第一个结点加入队列
    que.push(1);
    st[1] = true;
	
    while(que.size()) {
    	int i = que.fornt();
        que.pop();
        // 结点i已经出队了
        st[i] = false;
    	遍历所有邻边
            if dist[邻边] > dist[i] + i->邻边距离 {
                dist[邻边] = dist[i] + i->邻边距离;
                // 接下来就是将邻边加入队列, 
                // 如果邻边不在队列的话就加入, 否则的话就不加入,
                // 因为你重复做两次一样的事情是没有必要的, 节约时间, 你不用st数组也可以但是不节约时间
                if (!st[邻边]) {
					que.push(邻边)
                }
            }
    }    
    
}

ex:

Snipaste_2023-05-19_09-54-31

输入样例:
3 3
1 2 5
2 3 -3
1 3 4
输出样例:
2

参考代码:

// 应该都会
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int h[N], e[N], ne[N], w[N], idx;
int dist[N];
bool st[N];
int n, m;

int spfa() {
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    st[1] = true;
    queue<int> q;
    q.push(1);

    while(q.size()) {
        int 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];
                if (!st[j]) {
                    q.push(j);
                    st[j] = true;
                }
            }
        }

    }
    return dist[n];
}

void add(int a, int b, int c) {
    e[idx] = b;
    ne[idx] = h[a];
    w[idx] = c;
    h[a] = idx++;
}

int main() {
    cin >> n >> m;
    memset(h, -1 , sizeof h);
    while ( m --) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }
    int t = spfa();
    if (t > 0x3f3f3f3f / 2) printf("impossible\n");
    else printf("%d\n", t);
    system("pause");
    return 0;
}

Snipaste_2023-05-19_09-58-16

负权回路:

​ a -> b -> c -> d … -> a 这些边权相加<0

如果存在负权回路那么spfa算法会一直进入这个回路导致数据超过2^31- 1 或者time limit error 那么我们肯定要在某个时候停止,那我们就想正权回路和负权回路的区别, 易知:正权回路中边的个数不会超过n - 1(n是结点个数)

// st来保存队列中元素是否出现
bool st[N];
// cnt 数组表示结点边的个数
int cnt[N];

int spfa() {
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    // 定义一个queue来存储变小的结点
    queue<int> que;
    // 将第一个结点加入队列
    que.push(1);
    st[1] = true;
	
    while(que.size()) {
    	int i = que.fornt();
        que.pop();
        // 结点i已经出队了
        st[i] = false;
    	遍历所有邻边
            if dist[邻边] > dist[i] + i->邻边距离 {
                dist[邻边] = dist[i] + i->邻边距离;
                // 这里就说明i到邻边会有一条边
                // 邻边数就等于 i 的边数加1
                cnt[邻边] = cnt[i] + 1;
                // 邻边数量大于n - 1 了就是有负权回路
                if (cnt[邻边] >= n) return true;
                
                // 接下来就是将邻边加入队列, 
                // 如果邻边不在队列的话就加入, 否则的话就不加入,
                // 因为你重复做两次一样的事情是没有必要的, 节约时间, 你不用st数组也可以但是不节约时间
                if (!st[邻边]) {
					que.push(邻边)
                }
            }
    }    
    return false;
}

参考代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int h[N], e[N], ne[N], w[N], idx;
int dist[N], cnt[N];
bool st[N];
int n, m;

bool spfa() {
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    st[1] = true;
    queue<int> q;
    for (int i = 1; i <= n; i++) {
        q.push(i);
    }

    while(q.size()) {
        int 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;
                if (!st[j]) {
                    q.push(j);
                    st[j] = true;
                }
            }
        }

    }
    return false;
}

void add(int a, int b, int c) {
    e[idx] = b;
    ne[idx] = h[a];
    w[idx] = c;
    h[a] = idx++;
}

int main() {
    cin >> n >> m;
    memset(h, -1 , sizeof h);
    while ( m --) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }
    if (spfa()) puts("Yes");
    else puts("No");
    system("pause");
    return 0;
}


8. floyd(弗洛伊德)算法

  • floyd()算法就是a->b的距离如果想要更加段哪必须在经过其他点a->c->b
  • 就好比你想从A城市去B城市,最简单的就是直达, 哪有没有更加短的呢,其实就是借助中间城市c, a ->c -> b 就有可能比a->b最短;
// 它可以求多源最短路,q[a][b] a->b的最短路
// floyd() 算法只有一种存储邻接矩阵
for (int i = 0; i < n; i++) {
	for (int j = 0;j < n; j ++) {
		for (int k = 0; k < n; k ++) {
            q[j][k] = min(q[j][k], q[j][i] + q[i][k]);
        }
    }    
}

Snipaste_2023-05-19_12-48-22

输入样例:
3 3 2
1 2 1
2 3 2
1 3 1
2 1
1 3
输出样例:
impossible
1

参考代码:

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

const int N = 210, inf = 0x3f3f3f3f;
int q[N][N];
int n, m, k;

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

int main() {
    cin >> n >> m >> k;
    
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            if (i == j) q[i][j] = 0;
            else q[i][j] = inf;
        }
    }
    
    while (m --) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        q[a][b] = min(q[a][b], c);
    }
    
    floyd();
    while (k --) {
        int a, b;
        scanf("%d%d", &a, &b);
        if (q[a][b] > inf / 2) printf("impossible\n");
        else printf("%d\n", q[a][b]);
    }
    
    return 0;
}

9.prim() 算法

  • prim算法与dijkstra() 算法非常的像
// O(n ^ 2)
// 朴素版的用于稠密图,我们开邻接矩阵
// n 结点数
int dist[N], n;
bool st[N];
int dijkstra() {
    // 初始化为inf
    memset(dist, 0x3f, sizeof dist);
	dist[1] = 0;
    int ans  = 0;
    // 循环遍历n - 1 遍
    // 因为一次只能加一个点进来, 本来要n 次, 但是最后一次就他自己一个人, 所以无需遍历
    for (int i = 0; i < n - 1; i ++ ) {
		int t = -1; 
        for (int j = 1; j <= n; j ++) {
            // !st[j] 表示j 不在st里面
            // t == -1 or dist[t] > dist[j] 
            // t == -1其实就是说, 想让t 初始化为不在st中的第一个, 但是你不知道是哪个
            // dist[t] > dist[j] 找最小
            if (!st[j] && (t == -1 || dist[t] > dist[j])) {
                t = j;
            }
        }
        
        st[t] = true;
//        ---------------------
// 		这里加一句
        ans += dist[t];
        // 遍历所有邻边
        for (int j = 1; j <= n; j ++) {
            // 这一句不一样
// --------------------------------------------------------------------
            dist[j] = min(dist[j], dist[t] + t到j的距离);
//-----------------------------------------------------------------------       
        // prim算法是
            dist[j] = min(dist[j], t到j的距离);
        }
    }
//    ----------------
    return dist[n];
//-----------------
    return ans;
}
  • 什么叫最小生成树呢?

IMG_0610

ex:

Snipaste_2023-05-19_14-15-30

输入样例:
4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
输出样例:
6

参考代码:

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

const int N = 520, inf = 0x3f3f3f3f;
int q[N][N];
int n, m;
int dist[N];
bool st[N];


int frim() {
    
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    // ans
    int res = 0;
    // 这里要循环n次而不是n - 1
    // dijkstra是优化为n - 1
    for (int i = 0; i < n ; i++) {
        // 找最小值
        int t = -1;
        for (int j = 1; j <= n; j++) {
            if (!st[j] && (t == -1 || dist[t] > dist[j])) {
                t = j;
            }
        }
        
        st[t] = true;
        res += dist[t];
        // 这是优化, 可以不写
        if (dist[t] == inf) return inf;
        
        
        for (int j = 1; j <= n; j++) {
            dist[j] = min(dist[j], q[t][j]);
        }
    }
    return res;
}



int main() {
    cin >> n >> m;
    memset(q, 0x3f, sizeof q);
    while( m -- ) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c); 
        // 无向图
        q[a][b] = q[b][a] = min(q[a][b], c);
    }
    int t = frim();
    if (t == inf) {
        printf("impossible\n");
    }else {
        printf("%d\n", t);
    }
    return 0;
}

10.kruskal()算法

也是求最小生成树

Snipaste_2023-05-19_14-14-42

// 因为要枚举所有的边, 不妨用结构体来存储图
struct ss {
    int a, b, w;
	// 按照边权距离从小到大排序
    bool operator< (const ss &x)const {
        return x.w > w;
    }
}s[N];
int ans = 0;
// 并查集, 需要用
int p[N];
int find(int x) {
    if (x != p[x]) p[x] = find(p[x]);
    return p[x];
}

bool kruskal() {
    // 排序
    sort(s, s + n);
    // cnt -> 集合边个数
    int cnt = 0;
    // 初始化并查集
    for (int i = 1; i <= n; i++) {
        p[i] = i;
    }
    
    for (int i = 0; i < m; i++) {
        // a, b不连通相当于 并查集中a,b不连通, 就需要用并查集的知识
        int a = s[i].a, int b = s[i].b, int w = s[i].w;
        a = find(a), b = find(b);
        // 不在同一个集合
        if (a != b) {
            // 加入集合
            p[a] = b;
            // 和prim中ans一样
            ans += w;
 			// 边数加一
            cnt++;
        }
    }
    return cnt == n - 1;
}

int main() {
    if (kruskal()) {
        printf("%d\n", ans);
    }else {
        没有最小生成树;
    }
    return 0;
}

Snipaste_2023-05-19_14-30-29

输入样例:
4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
输出样例:
6

参考代码:

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

const int N = 2e5 + 10;
int p[N];
struct ss {
    int a, b, w;
    bool operator< (const ss &x)const {
        return x.w > w;
    }
}s[N];
int n, m;

int find(int x) {
    if (x != p[x]) p[x] = find(p[x]);
    return p[x];
}

int ans;

bool kruskal() {
    
    for (int i = 1; i <= n; i++) p[i] = i;
    
    sort(s, s + m);
    
    int cnt = 0;
    for (int i = 0; i < m; i++) {
        int a = s[i].a, b = s[i].b , w = s[i].w;
        a = find(a), b = find(b);
        if (a != b) {
            p[a] = b;
            ans += w;
            cnt++;
        }
    }   
   return cnt == n - 1;
}

int main() {
    cin >> n >> m;
    for (int i = 0; i < m; i++) {
        int a, b, w;
        scanf("%d%d%d", &a, &b, &w);
        s[i] = {a, b, w};
    }
	if (kruskal()) {
        printf("%d\n", ans);
    }else {
        printf("impossible\n");
    }
    
    system("pause");
    return 0;
}

集合
p[a] = b;
// 和prim中ans一样
ans += w;
// 边数加一
cnt++;
}
}
return cnt == n - 1;
}

int main() {
if (kruskal()) {
printf(“%d\n”, ans);
}else {
没有最小生成树;
}
return 0;
}


[外链图片转存中...(img-PhX4EozW-1684478115147)]

输入样例:
4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
输出样例:
6


参考代码:

```c
#include <bits/stdc++.h>
using namespace std;

const int N = 2e5 + 10;
int p[N];
struct ss {
    int a, b, w;
    bool operator< (const ss &x)const {
        return x.w > w;
    }
}s[N];
int n, m;

int find(int x) {
    if (x != p[x]) p[x] = find(p[x]);
    return p[x];
}

int ans;

bool kruskal() {
    
    for (int i = 1; i <= n; i++) p[i] = i;
    
    sort(s, s + m);
    
    int cnt = 0;
    for (int i = 0; i < m; i++) {
        int a = s[i].a, b = s[i].b , w = s[i].w;
        a = find(a), b = find(b);
        if (a != b) {
            p[a] = b;
            ans += w;
            cnt++;
        }
    }   
   return cnt == n - 1;
}

int main() {
    cin >> n >> m;
    for (int i = 0; i < m; i++) {
        int a, b, w;
        scanf("%d%d%d", &a, &b, &w);
        s[i] = {a, b, w};
    }
	if (kruskal()) {
        printf("%d\n", ans);
    }else {
        printf("impossible\n");
    }
    
    system("pause");
    return 0;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值