【算法笔记】图论基础(二):最短路、判环、二分图

news2025/3/25 21:01:05

目录

  • 最短路
    • 松弛操作
    • Dijkstra
      • 朴素Dijkstra
        • 时间复杂度
        • 算法过程
        • 例题
      • 堆优化Dijkstra
        • 时间按复杂度
        • 算法过程
        • 例题
    • bellman-ford
      • 时间复杂度
      • 为什么dijkstra不能处理负权边?
        • dijkstra的三个步骤:
        • 反例
        • 失效的原因
      • 算法过程
      • 例题
    • spfa
      • 时间复杂度
      • 算法过程
      • 例题
        • spfa求最短路
        • spfa判环
    • Floyd
      • 时间复杂度
      • 算法原理
      • 例题
    • 什么时候用哪个最短路算法?
    • 一些特殊题型
      • 次短路
        • 例题
      • 最短路计数
        • 例题
      • 分层图
        • 分层图的思想
        • 例题
  • 二分图
    • 什么是二分图
    • 结论
    • 染色法判定二分图
      • 算法原理
      • 例题
    • 二分图最大匹配:匈牙利算法
      • 什么是二分图匹配
      • 算法原理
      • 例题


最短路

松弛操作

很多最短路算法和最小生成树的算法都涉及到一个操作叫松弛操作,那什么叫松弛操作呢?

  • 考虑节点u以及它的邻居v,从起点跑到v有好多跑法,有的跑法经过u,有的不经过。
  • 经过u的跑法的距离就是 dist[u] + u到v的距离。
  • 所谓松弛操作,就是看一看 dist[v] 和 dist[u] + u到v的距离 哪个大一点。
  • 如果前者大一点,就说明当前的不是最短路,就要赋值为后者,这就叫做松弛。

举一个最经典的例子:如果dist[i]表示某个点到点i的最短距离,而对于此时的一条边u -> v边权为w:

if(dist[v] > dist[u] + w)
	dist[v] = dist[u] + w;

或者直接写成

dist[v] = min(dist[v], dist[u] + w);

这就是对u -> v 的一次松弛操作

Dijkstra

Dijkstra算法和求最小生成树的Prim算法思路相同,也是将所有的点划分成两个区间,然后n次迭代,不断地向连通部分中加点,不同的是Dijkstra的“连通部分”表示的是已经确定最短路径的点(dist已经更新为到源点的最短距离的点)

注意: Prim中的dist[i]表示的是i距离连通部分的最短距离,Dijkstra中的dist[i]表示的是i距离源点的最短距离,别弄混了,

朴素Dijkstra

时间复杂度

O ( n 2 ) O(n^2) O(n2),适合n较小的稠密图,并且只能处理正权图

算法过程
  1. 将所有点的dist初始化成正无穷(因为后面要取min)
  2. 将源点的dist赋值成0(源点到源点的最短距离为0)
  3. 接下来每次迭代,遍历所有没确定最短距离的点,找到离源点最近(dist最小)的点,此时的dist就是最短距离,标记一下已确定其最短距离。
  4. 用该点对其他所有点进行松弛,更新其他点的dist
  5. 继续下一次迭代,直到所有点都确定好最短路(由于一次加一个点,所以n个点只需迭代n 次)
  6. dist[x]即你要求的点x到源点的最短路
例题

Dijkstra求最短路 I
在这里插入图片描述

因为是稠密图,所以适合用邻接矩阵存图

#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
#include <queue>
#include <functional>
#define endl '\n'
using namespace std;
typedef pair<int, int> PII;
const int N = 505;

int n, m;
int g[N][N]; // 邻接矩阵存图
int dist[N]; // dist[i]表示当前i离源点的最短距离
bool st[N]; // st标记当前点是否已经确定最短路,st[i] = true说明已经确定了,st[i] = false说明还没确定

int dijkstra(){
    memset(dist, 0x3f, sizeof dist); // 一开始,初始化所有点到源点最短距离为正无穷
    memset(st, false, sizeof st); // 一开始,所有点都没确定最短路
    dist[1] = 0; // 源点到源点的最短距离为0
    for(int i = 0; i < n; i++){ // 迭代n次
        int t = -1; // t来记录未确定的点中距离源点最近的点
        for(int j = 1; j <= n; j++){ // 遍历所有未确定最短路的点, 找到离源点最近的点
            if(!st[j] && (t == -1 || dist[t] > dist[j])) t = j; // t == -1:防止越界; dist[t] > dist[j]:点j比点t离源点近
        }
        st[t] = true; // t已经确定了距距离源点的最短距离
        for(int j = 1; j <= n; j++){ // 遍历所有点,对其他没确定的点进行松弛
            dist[j] = min(dist[j], dist[t] + g[t][j]);
        }
    }
    if(dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

int main(){
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    cin >> n >> m;
    memset(g, 0x3f, sizeof g); // 由于要取min,所以初始化成正无穷
    while(m--){
        int a, b, c;
        cin >> a >> b >> c;
        g[a][b] = min(g[a][b], c); // 求最短路,如果有重边就只要最短边
    }
    cout << dijkstra() << endl;
    return 0;
}

堆优化Dijkstra

时间按复杂度

O ( m l o g n ) O(mlogn) O(mlogn) ,适合稀疏图,并且只能处理正权图。

算法过程

和朴素版Dijkstra相同,就是迭代的过程用优先队列替代了双重for循环,每次就可以用log级别的复杂度找到当前没确定最短路的距离源点最近的点(堆顶)

例题

Dijkstra求最短路 II
在这里插入图片描述
因为是稀疏图,所以适合用邻接表存图

#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
#include <queue>
#include <functional>
#define endl '\n'
using namespace std;
typedef pair<int, int> PII;
const int N = 2e5 + 10;

int n, m;
vector<PII> g[N]; // 邻接表存图
int dist[N]; // dist[i]表示当前i离源点的最短距离
bool st[N]; // st标记当前点是否已经加入连通部分,st[i] = true说明已经加入了,st[i] = false说明还没加入

int dijkstra(){
    memset(dist, 0x3f, sizeof dist); // 一开始,连通部分中没有点,所有点到源点最短距离为正无穷
    memset(st, false, sizeof st); // 一开始,连通部分中没有点
    priority_queue<PII, vector<PII>, greater<PII>> q; // 优先队列-小跟堆
    dist[1] = 0; // 源点到源点的最短距离为0
    q.push({0, 1}); // 将源点入队,用源点松弛其他的点
    while(!q.empty()){
        auto t = q.top(); // 小跟堆的堆顶就是dist最小的点,就是距离源点最近的点
        q.pop(); 
        int ver = t.second; // 距离源点最近的点
        if(st[ver]) continue; // 如果已经确定过最短距离,说明已经用这个点松弛过其他的点了,再松驰一遍就没意义了,直接continue
        st[ver] = true; // ver已经确定了距距离源点的最短距离
        for(auto i : g[ver]){ // 遍历ver的所有出边,对ver的所有临点进行松弛
            if(dist[i.first] > dist[ver] + i.second){ // 松弛操作
                dist[i.first] = dist[ver] + i.second;
                q.push({dist[i.first], i.first}); // 松弛完了这个点的dist就变了,将这个点入队,用新的dist去松弛其他点
            }
        }
    }
    if(dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

int main(){
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    cin >> n >> m;
    while(m--){
        int a, b, c;
        cin >> a >> b >> c;
        g[a].push_back({b, c});
    }
    cout << dijkstra() << endl;
    return 0;
}

注意:Dijkstra算法优先队列如果用pair<int, int> 的话,一定要把dist放在第一维,因为pair排序是优先按第一维排,第一维相等再按第二维排的。

bellman-ford

时间复杂度

O ( n m ) O(nm) O(nm),效率很低,基本不会用,但可以处理负权边,并且可以求有边数限制的最短路

为什么dijkstra不能处理负权边?

dijkstra的三个步骤:
  • 找到当前未标识的且离源点最近的点t
  • 对t进行标识
  • 用t更新其他点的距离
反例

在这里插入图片描述

Dijkstra算法在图中走出来的最短路径是1 -> 2 -> 4 -> 5,算出 1 号点到 5 号点的最短距离是2 + 2 + 1 = 5
然而还存在一条路径是1 -> 3 -> 4 -> 5,该路径的长度是5 + (-2) + 1 = 4,因此 Dijkstra 算法失效

失效的原因

Dijkstra算法是基于贪心的思想去不断迭代来找最短路,每一步看的都是当前的最优解,比如现在dist[2] = 2, dist[3] = 5,如果都是正权边的话,后面经过3到达的点距离1的距离一定是dist[3]加上一个正数,只会更大不会更小,即dist[3] + w > dist[3] > dist[2],所以选择走2这个局部最优解也一定可以的得到全局最优解。

而如果有负权边的话,无法确定dist[3] + w 和dist[2]到底哪个小,就不能每次贪心地走局部最优的点,贪心失效,Dijkstra算法也就失效了。

算法过程

Bellman - ford 的原理为连续进行松弛,在每次松弛时把每条边都更新一下,若在 n-1 次松弛后还能更新,则说明图中有负环,因此无法得出结果。

(通俗的来讲就是:假设 1 号点到 n 号点是可达的,每一个点同时向指向的方向出发,更新相邻的点的最短距离,通过循环 n-1 次操作,若图中不存在负环,则 1 号点一定会到达 n 号点,若图中存在负环,则在 n-1 次松弛后一定还会更新(相当于存在一条经过大于n 个点的最短路,但这张图只有n个点))

例题

853. 有边数限制的最短路
在这里插入图片描述
由于要每次迭代都要遍历一遍所有的边,所以适合用结构体存边

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 10010;
int n, m, k;
int dist[N]; // dist数组
int last[N]; // 用来临时存上次迭代后的dist数组

struct Edge{
    int a, b, w; // 结构体存边
} edges[N];

void bellman_ford(){
    memset(dist, 0x3f, sizeof dist); // 一开始,还没开始迭代,所有点都没确定到源点的最短距离,dist都初始化成正无穷
    dist[1] = 0; // 源点到源点的最短距离为0
    for(int i = 0; i < k; i++){ // 迭代k次后的dist[i]就是源点到i的最多经过k条边的最短路
        memcpy(last, dist, sizeof dist); // 将dist复制一遍,防止前面被松弛的dist后面再去松弛其他点
        for(int j = 0; j < m; j++){ // 每次遍历所有m条边,用上次迭代的结果进行松弛
            auto e = edges[j];
            dist[e.b] = min(dist[e.b], last[e.a] + e.w); // 松弛操作
        }
    }
}

int main(){
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    cin >> n >> m >> k;
    for(int i = 0; i < m; i++){
        int a, b, c;
        cin >> a >> b >> c;
        edges[i] = {a, b, c}; // 存边
    }
    bellman_ford();
    if(dist[n] > 0x3f3f3f3f / 2) cout << "impossible";
    else cout << dist[n];
    return 0;
}

注意:

  1. 在每次迭代只前要把当前的dist数组复制一份,由于是每个点同时向外出发,后遍历的点的dist有可能被先遍历的点松弛更新,如果这个时候再用改变后的dist去更新其他点,就会多走一条边,对于限制边数的最短路的问题就会出错。
  2. 最后判断点n不可达的条件一定要写dist[n] > 0x3f3f3f3f / 2,而不是dist[n] == 0x3f3f3f3f,因为0x3f3f3f3f是一个确定的值,而非真正的无穷大,这个值在迭代的过程中也可能会受其他点影响而减小,所以判断只需dist[n]大于某个与0x3f3f3f3f相同数量级的数即可

spfa

spfa其实就是bellman_ford算法的升级版本

时间复杂度

最好情况 O ( m ) O(m) O(m),最坏情况 O ( n m ) O(nm) O(nm),一般情况下和堆优化Dijkstra相当,但如果出题人想卡spfa,就会退化成 O ( n m ) O(nm) O(nm)的bellman_ford算法

算法过程

Bellman-ford算法中,循环n次,每次遍历m条边,每次遍历的时候,把每条边终点的距离更新成最小。
然而,这样就循环遍历了很多用不到的边。比如第一次遍历,只有第一个点的临边是有效的。
事实上,我们只用遍历那些到源点距离变小的点所连接的边即可。

只有当一个点的前驱结点更新了,该节点才会得到更新; 因此考虑到这一点,我们将创建一个队列,每一次加入距离被更新的结点,每次用队列中的节点松弛其临点,再将被松弛的点放到队列里,最后队列为空的时候说明已经没有点可被松弛,也就确定好了所有点到源点的最短路。

同bellman_ford算法一样,spfa也可以判环,用一个cnt数组记录一下当前点被松弛了多少次,每被松弛一次就会经过一个点,如果cnt[i] >= 图的点数-1,那就说明有负环。

如过题中问有没有正环,就改成求spfa求最长路即可

例题

spfa求最短路

spfa求最短路
在这里插入图片描述

#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
#include <queue>
#include <functional>
#define endl '\n'
using namespace std;
typedef pair<int, int> PII;
const int N = 1e5 + 10;

int n, m;
vector<PII> g[N]; // 邻接表存图
int dist[N]; // dist[i]存当前i到源点的最短路
bool st[N]; // 判断当前点在不在队列中,st[i] = true说明在队列中,st[i] = false说明不在队列中

int spfa(){
    memset(dist, 0x3f, sizeof dist); // 一开始,还没开始迭代,所有点都没确定到源点的最短距离,dist都初始化成正无穷
    dist[1] = 0; // 源点到源点的最短距离为0
    queue<int> q;
    q.push(1); // 将源点如对,去松弛它的临点
    st[1] = true; // 1在队列中
    while(!q.empty()){
        int t = q.front();
        q.pop();
        st[t] = false; // t出队了
        for(auto i : g[t]){ // 遍历t的所有出边
        	int ver = i.first, w = i.second; // ver是t的临点,w是边权
            if(dist[ver] > dist[t] + w){ // 松弛
                dist[ver] = dist[t] + w;
                if(!st[ver]){ // 如果被松弛的点不在队列里,就给它入队
                    q.push(ver);
                    st[ver] = true; // 在队列里了
                }
            }
        }
    }
    return dist[n];
}

int main(){
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    cin >> n >> m;
    while(m--){
        int a, b, c;
        cin >> a >> b >> c;
        g[a].push_back({b, c}); // a->b 权值为c的边
    }
    int t = spfa();
    if(t == 0x3f3f3f3f) cout << "impossible" << endl;
    else cout << t << endl;
    return 0;
}
spfa判环

spfa判断负环
在这里插入图片描述

#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
#include <queue>
#include <functional>
#define endl '\n'
using namespace std;
typedef pair<int, int> PII;
const int N = 1e5 + 10;

int n, m;
vector<PII> g[N]; // 邻接表存图
int dist[N]; // dist[i]存当前i到源点的最短路
int cnt[N]; // cnt[i]表示当前i点被松弛了多少次(dist[i]是经过多少条边的最短路)
bool st[N]; // 判断当前点在不在队列中,st[i] = true说明在队列中,st[i] = false说明不在队列中

bool spfa(){
    memset(dist, 0x3f, sizeof dist); // 一开始,还没开始迭代,所有点都没确定到源点的最短距离,dist都初始化成正无穷
    dist[1] = 0; // 源点到源点的最短距离为0
    queue<int> q;
    for(int i = 1; i <= n; i++){ // 因为负环不一定经过哪几个点,所以一开始要把所有点都入队
    	q.push(i);
    	st[i] = true; // i在队列中
    }
    while(!q.empty()){
        int t = q.front();
        q.pop();
        st[t] = false; // t出队了
        for(auto i : g[t]){ // 遍历t的所有出边
        	int ver = i.first, w = i.second; // ver是t的临点,w是边权
            if(dist[ver] > dist[t] + w){ // 松弛
                dist[ver] = dist[t] + w; // 如果被松弛的点不在队列里,就给它入队
                cnt[ver] = cnt[t] + 1; // 被松弛的次数+1
                if(cnt[ver] >= n) return true; // 如果他被松弛了超过n-1次,就一定有负环
                if(!st[ver]){
                    q.push(ver); 
                    st[ver] = true; // ver入队
                }
            }
        }
    }
    return false;
}

int main(){
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    cin >> n >> m;
    while(m--){
        int a, b, c;
        cin >> a >> b >> c;
        g[a].push_back({b, c}); // a->b权值为c的边
    }
    if(spfa()) cout << "Yes" << endl;
    else cout << "No" << endl;
    return 0;
}

注意:spfa求负环一开始一定要将所有点都入队,因为负环不一定经过源点,如果你只给源点入队,这图还恰巧不连通,负环还在另一个联通块,就判断错了

Floyd

时间复杂度

O ( n 3 ) O(n^3) O(n3),效率低,基本点数到500就很极限了,但其他的算法大多只能处理单源最短路,floyd算法可以处理全源最短路(求任意两点之间的最短距离)。

算法原理

floyd算法是基于动态规划的思想, d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k]表示从i走到j的路径上除i和j点外只经过1到k的点的所有路径的最短距离

状态转移方程: d p [ i , j , k ] = m i n ( d p [ i , j , k − 1 ] , d p [ i , k , k − 1 ] + d p [ k , j , k − 1 ] dp[i, j, k] = min(dp[i, j, k - 1], dp[i, k, k - 1] + dp[k, j, k - 1] dp[i,j,k]=min(dp[i,j,k1],dp[i,k,k1]+dp[k,j,k1]

优化掉第三维之后,就变成了floyd算法: d [ i ] [ j ] = m i n ( d [ i ] [ j ] , d [ i ] [ k ] + d [ k ] [ j ] ) d[i][j] = min(d[i][j], d[i][k] + d[k][j]) d[i][j]=min(d[i][j],d[i][k]+d[k][j])

简单地说:d[i][j]就是从i到j不经过k的最短路,d[i][k] + d[k][j]就是从i到j经过k的最短路,遍历所有i,j,再遍历所有中间节点k,不断松弛,最后所有的d[i][j]就都是从i到j的最短路了。

例题

Floyd求最短路
在这里插入图片描述

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 205;
int n, m, k;
int d[N][N];

void floyd(){
    for(int k = 1; k <= n; k++){
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= n; j++){
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]); // 状态转移(松弛操作)
            }
        }
    }
}

int main(){
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    cin >> n >> m >> k;
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= n; j++){
            if(i == j)
                d[i][j] = 0; // 自己到自己的距离为0
            else
                d[i][j] = 0x3f3f3f3f; // 其他初始化成正无穷
        }
    }
    while(m--){
        int a, b, c;
        cin >> a >> b >> c;
        d[a][b] = min(d[a][b], c); // 要求最短路,有重边就只要最短的一条
    }
    floyd();
    while(k--){
        int a, b;
        cin >> a >> b;
        int t = d[a][b]; // floyd跑完后,d[a][b]就变成了从a到b的最短路
        if(t > 0x3f3f3f3f / 2)
            cout << "impossible" << endl;
        else
            cout << t << endl;
    }
    return 0;
}

注意:循环位置不能改,一定要先遍历k,具体为什么,如果会动态规划(dp)的话,再好好想想。

什么时候用哪个最短路算法?

借用一下闫学灿的图
在这里插入图片描述
一句话就是:求多源最短路就用floyd,求单源最短路,如果没有负权边就用dijkstra,有负权边就用spfa

如果你记不住那么多最短路算法,堆优化Dijkstra、spfa、floyd,这仨一定要会。

一些特殊题型

次短路

如果一道题要求次短路的话,其实只需要在原来用dist数组记录最短路的基础上,再加一个dist1数组记录到每个点的次短路,在求最短路算法的基础上,在更新时像下面这样写就可以了。

if(dist[j] > dist[t] + w){ // 找到了更短的最短路
	dist1[j] = dist[j]; // 原来的最短路就变成次短路
	dist[j] = dist[t] + w; // 更新最短路
}
else if(dist1[j] > dist1[t] + w){ // 如果不比最短路小但是比次短路小
	dist1[j] = dist1[t] + w; // 就更新次短路
}

简单点解释,就是:如果当前的距离比最短路小,就更新最短路,此时原来的最短路就变成了次短路。如果不比最短路小,就再看当前距离是不是比次短路小,如果比次短路小就更新次短路。

比如用堆优化的dijkstra算法在求最短路的同时求次短路,就可以这样写

int dijkstra(){
    memset(dist, 0x3f, sizeof dist);
    memset(dist1, 0x3f, sizeof dist1);
    dist[1] = 0;
    priority_queue<PII, vector<PII>, greater<PII>> q;
    q.push({0, 1});
    while(!q.empty()){
        auto t = q.top();
        q.pop();
        int ver = t.second, dis = t.first;
        if(dis > dist1[ver]) continue;
        for(auto i : g[ver]){
            int new_dist = dis + i.second;
            if(dist[i.first] > new_dist){
                dist1[i.first] = dist[i.first];
                dist[i.first] = new_dist;
                q.push({dist[i.first], i.first});
            }
            else if(dist[i.first] < new_dist && dist1[i.first] > new_dist){
                dist1[i.first] = new_dist;
                q.push({dist1[i.first], i.first});
            }
        }
    }
    return dist1[n];
}
例题

P2865 [USACO06NOV] Roadblocks G
Two Paths

最短路计数

和求次短路思路和代码大致相同

int cnt[N]; // cnt 数组用于记录从起点到达某个顶点的不同最短路径的条数。

if(dist[j] > dist[t] + w){
	cnt[j] = cnt[t];
	dist[j] = dist[t] + w;
}
else if(dist[j] == dist[t] + w){ 
	cnt[j] += cnt[t]; 
}
例题

P1144 最短路计数

分层图

有些求最短路的题目在每个端点都有边权的基础上,还有几次“白嫖”的能力,比如一共有n个点,m条路,走每一条路都要交一定的过路费,同时你还有k次不交过路费的能力,问你从1~n最少要花多少钱,这个时候普通的建边方式就难以解决,就要用到分层图。

分层图的思想

每层有n个点,点a + i * n就是第i层中的a点(从第0层到第k层)。

在建边的时候,层内的点正常连边,不同层之间的点,想实现“不交钱”,你就可以对于每条边,在该层的起点和下一层的终点之间连一条长度为0的边。

// 有向图
cin >> a >> b;
add(a, b, c); 
for(int i = 0; i < k; i++){
     add(a + i * n, b + (i + 1) * n, 0); // 连相邻两层之间的边
     add(a + (i + 1) * n, b + (i + 1) * n, c); // 连同层内的边
 }
// 无向图
cin >> a >> b;
add(a, b, c), add(b, a, c); 
for(int i = 0; i < k; i++){
     add(a + i * n, b + (i + 1) * n, 0), add(b + i * n, a + (i + 1) * n, 0); // 连相邻两层之间的边
     add(a + (i + 1) * n, b + (i + 1) * n, c), add(b + (i + 1) * n, a + (i + 1) * n, c); // 连同层内的边
 }

在这里插入图片描述
这样每向下跑一层,就相当于免了一次过路费,最多免k次过路费,就只需要建k层图即可

最后,再给每相邻两层的终点向下连上长度为0的边(因为免过路费的次数可能为0~k次,所以到任意一层的终点都可以,所以要循环一遍,取每个终点的dist的最小值,但为了方便统计,直接用这种方式,就只需要求起点到最后一层的终点的最短路就可以了)

例题

P4568 [JLOI2011] 飞行路线
P2939 [USACO09FEB] Revamping Trails G
P4822 [BJWC2012] 冻结

二分图

什么是二分图

如果一个图中的所有点可以划分成两个点集,使得同一点集中任意两点之间没有边相连,这个图就叫二分图

说人话就是:图中点通过移动能分成左右两部分,左侧的点只和右侧的点相连,右侧的点只和左侧的点相连。

就像下面这样
在这里插入图片描述

结论

一个图是二分图 ⇔ \Leftrightarrow 这个图中没有奇数环

证明也非常简单,对于下面这个环,尝试将所有点都分到左右两边,如果他是奇数环,通过推一圈下来,第一个点一定是冲突的,所以一定不是二分图
在这里插入图片描述

染色法判定二分图

算法原理

想知道一个图是不是二分图,只需给每条边的两个端点染上不同的两种颜色,如果出现了染色冲突的情况,就说明此图不是二分图。

  1. 一开始对任意一未染色的顶点染色。
  2. 判断其相邻的顶点是否已经染色,若未染色则将其染上和相邻顶点不同的颜色。
  3. 若已经染色且颜色和相邻顶点的颜色相同则说明不是二分图,若颜色不同则继续判断。

例题

染色法判定二分图

#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
#define endl '\n'
using namespace std;
const int N = 1e5 + 10;

int n, m;
int color[N]; // 存当前顶点的颜色,0表示还没染色,1表示已经染了第一种颜色,2表示已经染了第二种颜色
vector<int> g[N]; // 邻接表存图

bool dfs(int x, int c){ // 给点i染第c种颜色
    color[x] = c; // 染色
    for(int i : g[x]){ // 遍历x的临点
        if(!color[i]){ // 如果x的临点没染色,就尝试染成另一种颜色
            if(!dfs(i, 3 - c)) return false; // 染不了就不是二分图
        }
        else if(color[i] == c) return false; // 如果临点已经和x染了同一种颜色,染色冲突,不是二分图
    }
    return true; // 没有冲突就是二分图
}

int main(){
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    cin >> n >> m;
    while(m--){
        int a, b;
        cin >> a >> b;
        g[a].push_back(b), g[b].push_back(a); // 无向边
    }
    for(int i = 1; i <= n; i++){ // 遍历所有点,给没染色的点染色
        if(!color[i]){
            if(!dfs(i, 1)){
                cout << "No" << endl;
                return 0;
            }
        }
    }
    cout << "Yes" << endl;
    return 0;
}

二分图最大匹配:匈牙利算法

此算法如果买acwing的算法基础课了强烈建议听闫总讲一遍,非常有趣~
传送门

什么是二分图匹配

二分图的匹配:给定一个二分图 G G G,在 G G G 的一个子图 M M M 中, M M M 的边集 { E } \{E\} {E} 中的任意两条边都不依附于同一个顶点,则称 M M M 是一个匹配。

二分图的最大匹配:所有匹配中包含边数最多的一组匹配被称为二分图的最大匹配,其边数即为最大匹配数。

举个例子:每个男生都有几个喜欢的女生,此时由每个男女生作为节点,由喜欢的关系作为边,组成的图就是一个二分图,这个时候,现在你是月老,来给他们牵线,你最多能凑成多少对,这个对数就是二分图最大匹配数。

一个拓展结论:二分图最大匹配数 = 最小点覆盖 = 总点数 - 最大独立集 = 总点数 - 最小路径覆盖(因为是图论入门,具体概念不在此赘述)

算法原理

借用闫学灿的解释方法(因为比较通俗易懂并且比较有趣)

void 男找心仪的女() 
{
    if (女单身) 2人在一起
    else if (男友有备胎) 就绿了男友
    else 男继续单身
}

如果你想找的妹子已经有了男朋友,
你就去问问她男朋友,
你有没有备胎,
把这个让给我好吧

例题

861. 二分图的最大匹配

#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
#define endl '\n'
using namespace std;
const int N = 505;

int n1, n2, m; //已知二分图,左侧点数为n1,右侧点数为n2
bool st[N]; // 存某个女生被没被考虑过
int match[N]; //记录已有配对情况(match[i]表示当前和i这个女生配对的男生) 
vector<int> g[N]; // 邻接表存边
int res; 

bool dfs(int x){ // 找x
    for(int i : g[x]){ // 遍历x所有喜欢的女生
        if(!st[i]){ // 如果这个女生没考虑过
            st[i] = true; // 考虑一下
            if(!match[i] || dfs(match[i])){ // 她没男朋友,或她现在的男朋友可以换一个女朋友
                match[i] = x; // 他就和这个女生配对
                return true; // 这个男生能找到女朋友
            }
        }
    }
    return false; // 找不到
}

int main(){
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    cin >> n1 >> n2 >> m;
    while(m--){
        int a, b;
        cin >> a >> b;
        g[a].push_back(b);
    }
    for(int i = 1; i <= n1; i++){
        memset(st, false, sizeof st);
        if(dfs(i)){ // 如果一个男生能找到女朋友
            res++; // 对数(匹配数) + 1
        }
    }
    cout << res << endl;
    return 0;
}

为什么后来者居上,因为前者他不争不抢啊~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2321535.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

EMS小车技术特点与优势:高效灵活的自动化输送解决方案

北成新控伺服技术丨EMS小车调试视频 EMS小车是一种基于单轨运行的电动输送系统&#xff0c;通过电力驱动实现物料的高效搬运和输送&#xff0c;具有高效灵活、节能环保、多功能集成、行业适配性强等特性&#xff0c;广泛应用于汽车制造、工程机械、家电生产、仓储物流等行业自动…

uniapp运行到支付宝开发者工具

使用uniapp编写专有钉钉和浙政钉出现的样式问题 在支付宝开发者工具中启用2.0构建的时候&#xff0c;在开发工具中页面样式正常 但是在真机调试和线上的时候不正常 页面没问题&#xff0c;所有组件样式丢失 解决 在manifest.json mp-alipay中加入 "styleIsolation&qu…

C++ 性能优化隐藏陷阱:从系统调用到并发开销的深度反思

作为一名C++技术专家,我深知性能优化不仅是代码层面的艺术,更是理解硬件与语言交互的科学。在现代计算中,C++的抽象为开发者提供了便利,却也隐藏了硬件的复杂性。如何揭开这些“谎言”,让代码与硬件协同工作?本文将以小案例为载体,通过优化前后的对比,深入剖析每个章节…

Unity 使用 Protobuf(Pb2)二进制数据全流程工具详解

前言 在Unity游戏开发中&#xff0c;高效、快速、安全地读取配置数据是一项重要需求。本文介绍一种完整的解决方案——使用Protobuf二进制格式&#xff08;Pb2&#xff09;存储和读取游戏数据&#xff0c;并详细分享实现全流程的Unity工具。 一、技术流程概览 实现Unity读取…

基于QT(C++)实现绘图程序

绘图程序 1 核心算法 1.1 图元生成 1.1.1 直线 画直线的算法采用了课上讲到的 Bresenhan 算法&#xff0c;采用整数增量运算&#xff0c;精确而有效的光栅设备生成算法。 基本思想是&#xff1a;当直线斜率的绝对值小于 1 时&#xff0c;从左端点开始作为起点&#…

深入剖析ReLU激活函数:特性、优势与梯度消失问题的解决之道,以及Leaky ReLU 和 Parametric ReLU

深入剖析ReLU激活函数&#xff1a;特性、优势与梯度消失问题的解决之道 在深度学习领域&#xff0c;激活函数的选择直接影响神经网络的训练效果和性能。整流线性单元&#xff08;Rectified Linear Unit&#xff0c;简称ReLU&#xff09;因其简单性、高效性以及对梯度消失问题的…

服务注册/服务发现-Eureka

目录 1.引言&#xff1a;如果一个父项目中有多个子项目&#xff0c;但是这些子项目如何如何相互调用彼此的业务呢&#xff1f; 2.什么是注册中心 3.CAP理论 4.EureKa 5.服务注册 6.服务发现 7.负载均衡 1.引言&#xff1a;如果一个父项目中有多个子项目&#xff0c;但是…

计算机网络——数据链路层的功能

目录 物理链路 逻辑链路 封装成帧&#xff08;组帧&#xff09; 帧定界 透明传输 SDU 差错控制 可靠传输 流量控制 介质访问控制 主机需要实现第一层到第五层的功能&#xff0c;而路由器这种节点只需要实现第一层到第三层的这些功能 假设左边用户需要给右边用户发送…

第60天:Web攻防-XSS跨站文件类型功能逻辑SVGPDFSWFPMessageLocalStorage

#知识点 1、Web攻防-XSS跨站-文件类型-html&pdf&swf&svg 2、Web攻防-XSS跨站-功能逻辑-postMessage&localStorage 术语&#xff1a;上传xss->其实就是将有恶意js代码的各类文件&#xff08;swf,pdf,svg,html.xml等&#xff09;上传->访问该文件->让浏…

C/C++都有哪些开源的Web框架?

CppCMS CppCMS是一个采用C语言开发的高性能Web框架&#xff0c;通过模版元编程方式实现了在编译期检查RESTful路由系统&#xff0c;支持传统的MVC模式和多种语言混合开发模式。 CppCMS最厉害的功能是WebSocket&#xff0c;10万连接在内存中长期保存占用的大小不超过600MB&…

RISC-V AIA学习2---IMSIC

我在学习文档这章时&#xff0c;对技术术语不太理解&#xff0c;所以用比较恰当的比喻来让自己更好的理解。 比较通俗的理解&#xff1a; 将 RISC-V 系统比作一个工厂&#xff1a; hart → 工厂的一条独立生产线IMSIC → 每条生产线配备的「订单接收员」MSI 中断 → 客户通过…

2024年MathorCup数学建模B题甲骨文智能识别中原始拓片单字自动分割与识别研究解题全过程文档加程序

2024年第十四届MathorCup高校数学建模挑战赛 B题 甲骨文智能识别中原始拓片单字自动分割与识别研究 原题再现&#xff1a; 甲骨文是我国目前已知的最早成熟的文字系统&#xff0c;它是一种刻在龟甲或兽骨上的古老文字。甲骨文具有极其重要的研究价值&#xff0c;不仅对中国文…

Python----计算机视觉处理(Opencv:霍夫变换)

一、霍夫变换 霍夫变换是图像处理中的一种技术&#xff0c;主要用于检测图像中的直线、圆或其他形状。其基本思想就是将图像空间中的点映射到参数空间中&#xff0c;通过在参数空间中寻找累计最大值来实现对特定形状的检测。 二、 霍夫直线变换 那么对于一个二值化后的图形来说…

多语言生成语言模型的少样本学习

摘要 大规模生成语言模型&#xff0c;如GPT-3&#xff0c;是极具竞争力的少样本学习模型。尽管这些模型能够共同表示多种语言&#xff0c;但其训练数据以英语为主&#xff0c;这可能限制了它们的跨语言泛化能力。在本研究中&#xff0c;我们在一个涵盖多种语言的语料库上训练了…

QT开发(4)--各种方式实现HelloWorld

目录 1. 编辑框实现 2. 按钮实现 前面已经写过通过标签实现的了&#xff0c;所以这里就不写了&#xff0c;通过这两个例子&#xff0c;其他的也是同理 1. 编辑框实现 编辑框分为单行编辑框&#xff08;QLineEdit&#xff09;双行编辑框&#xff08;QTextEdit&#xff09;&am…

Flutter 输入组件 Radio 详解

1. 引言 在 Flutter 中&#xff0c;Radio 是用于单选的按钮组件&#xff0c;适用于需要用户在多个选项中选择一个的场景&#xff0c;如表单、设置选项等。Radio 通过 value 和 groupValue 进行状态管理&#xff0c;并结合 onChanged 监听选中状态的变化。本文将介绍 Radio 的基…

3.23学习总结

完成了组合Ⅲ&#xff0c;和电话号码的字母组合两道算法题&#xff0c;都是和回溯有关的&#xff0c;很类似。 学习了static的关键字和继承有关知识

力扣刷题-热题100题-第23题(c++、python)

206. 反转链表 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/reverse-linked-list/solutions/551596/fan-zhuan-lian-biao-by-leetcode-solution-d1k2/?envTypestudy-plan-v2&envIdtop-100-liked 常规法 记录前一个指针&#xff0c;当前指针&am…

vue3 项目的最新eslint9 + prettier 配置

注意&#xff1a;eslint目前升级到9版本了 在 ESLint v9 中&#xff0c;配置文件已经从 .eslintrc 迁移到了 eslint.config.js 配置的方式和之前的方式不太一样了&#xff01;&#xff01;&#xff01;&#xff01; 详见自己的语雀文档&#xff1a;5、新版eslint9prettier 配…

SAP GUI Script for C# SAP脚本开发快速指南与默认主题问题

SAP GUI Script for C# 快速指南 SAP 脚本的快速使用与设置. 解决使用SAP脚本执行后,默认打开的SAP是经典主题的问题 1. 解决默认主题问题 如果您使用的是SAP GUI 740&#xff0c;并遇到无法打开对话框的问题&#xff0c;请先将主题设置为经典主题&#xff08;Classic Theme…