Prim

858. Prim算法求最小生成树

给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环,边权可能为负数。

求最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible

给定一张边带权的无向图 G=(V,E),其中 V 表示图中点的集合,E 表示图中边的集合,n=|V|,m=|E|。

由 V 中的全部 n 个顶点和 E 中 n−1 条边构成的无向连通子图被称为 G 的一棵生成树,其中边的权值之和最小的生成树被称为无向图 G 的最小生成树。

输入格式

第一行包含两个整数 n 和 m。

接下来 m 行,每行包含三个整数 u,v,w,表示点 u 和点 v 之间存在一条权值为 w 的边。

输出格式

共一行,若存在最小生成树,则输出一个整数,表示最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible

数据范围

图中涉及边的边权的绝对值均不超过 10000。

输入样例:

1
2
3
4
5
6
4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4

输出样例:

1
6

AC

思路

S: 当前已经在联通块中的所有点的集合

  1. dist[i] = inf
  2. for n 次
    t<-S 外离 S 最近的点
    利用 t 更新 S 外点到 S 的距离
    st[t] = true
    n 次迭代之后所有点都已加入到 S 中

联系:Dijkstra 算法是更新到起始点的距离,Prim 是更新到集合 S 的距离

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 510, INF = 0x3f3f3f3f;

int n, m;
int g[N][N]; // 邻接矩阵,存储所有边
int dist[N]; // 存储其他点到当前最小生成树的距离
bool st[N]; // 存储每个点是否已经在生成树中

// 如果图不连通,则返回INF(值是0x3f3f3f3f), 否则返回最小生成树的树边权重之和
int prim() {
memset(dist, 0x3f, sizeof dist);

int res = 0;
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;
}
}

// 判断是否连通,有无最小生成树
if (i && dist[t] == INF) {
return INF;
}

if (i) {
res += dist[t];
}
st[t] = true;

for (int j = 1; j <= n; j++) {
dist[j] = min(dist[j], g[t][j]);
}
}

return res;
}

int main() {
cin >> n >> m;

memset(g, 0x3f, sizeof g);

while (m--) { // 无向图双边存储
int u, v, w;
cin >> u >> v >> w;
g[u][v] = g[v][u] = min(g[u][v], w);
}

int t = prim();

if (t == INF) {
cout << "impossible" << endl;
}
else {
cout << t << endl;
}

return 0;
}

Kruskal

859. Kruskal算法求最小生成树

给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环,边权可能为负数。

求最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible

给定一张边带权的无向图 G=(V,E),其中 V 表示图中点的集合,E 表示图中边的集合,n=|V|,m=|E|。

由 V 中的全部 n 个顶点和 E 中 n−1 条边构成的无向连通子图被称为 G 的一棵生成树,其中边的权值之和最小的生成树被称为无向图 G 的最小生成树。

输入格式

第一行包含两个整数 n 和 m。

接下来 m 行,每行包含三个整数 u,v,w,表示点 u 和点 v 之间存在一条权值为 w 的边。

输出格式

共一行,若存在最小生成树,则输出一个整数,表示最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible

数据范围

图中涉及边的边权的绝对值均不超过 10000。

输入样例:

1
2
3
4
5
6
4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4

输出样例:

1
6

AC

思路
  1. 将所有边按照权重从小到大排序 O(mlog(m))
  2. 枚举每条边(a, b, 权重c) O(m)
1
2
if a, b 两点不连通
将a, b边加入集合中

第二步与 连通块中点的数量 相似

使用并查集,查询两个结点是否属于一个集合, 合并两个结点

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <iostream>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010, M = 200010, INF = 0x3f3f3f3f;

int n, m;
int p[N];

struct Edge {
int x, y, w;

bool operator < (const Edge& W) const {
return w < W.w;
}
}edges[M];

int find(int x) { // 并查集核心操作
if (p[x] != x) {
p[x] = find(p[x]);
}

return p[x];
}

int kruskal() {
sort(edges, edges + m);

for (int i = 1; i <= n; i++) { // 初始化并查集
p[i] = i;
}

int res = 0, cnt = 0; // res记录最小生成树的树边权重之和,cnt记录的是全部加入到树的集合中边的数量(可能有多个集合)
for (int i = 0; i < m; i++) {
int x = edges[i].x, y = edges[i].y, w = edges[i].w;

/*
具体可以参考连通块中点的数量,如果a和b已经在一个集合当中了,说明这两个点已经被一种方式连接起来了,如果加入a-b这条边,会导致集合中有环的生成,而树中不允许有环生成,所以一个连通块中的点的数量假设为x,那么里面x个节点应该是被串联起来的,有x-1条边,所以只有当a,b所属的集合不同时,才能将a-b这条边加入到总集合当中去
*/
x = find(x), y = find(y);
if (x != y) { // 如果两个连通块不连通,则将这两个连通块合并
p[x] = y; // 两个集合连通
res += w; // 加入到集合中的边的权重之和
cnt++; // 连通后的集合边数+1
}
}

if (cnt < n - 1) { // 树中有n个节点便有n-1条边,如果cnt不等于n-1的话,说明无法生成有n个节点的树
return INF;
}

return res; // 可以生成最小生成树
}

int main() {
cin >> n >> m;

for (int i = 0; i < m; i++) {
int x, y, w;
cin >> x >> y >> w;
edges[i] = {x, y, w};
}

int t = kruskal();

if (t == INF) {
cout << "impossible" << endl;
}
else {
cout << t << endl;
}

return 0;
}

染色法判定二分图

860. 染色法判定二分图

给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环。

请你判断这个图是否是二分图。

输入格式

第一行包含两个整数 n 和 m。

接下来 m 行,每行包含两个整数 u 和 v,表示点 u 和点 v 之间存在一条边。

输出格式

如果给定图是二分图,则输出 Yes,否则输出 No

数据范围

输入样例:

1
2
3
4
5
4 4
1 3
1 4
2 3
2 4

输出样例:

1
Yes

AC

思路

染色法

  • 将所有点分成两个集合,使得所有边只出现在集合之间,就是二分图
  • 二分图:一定不含有奇数环,可能包含长度为偶数的环, 不一定是连通图

dfs版本

  • 代码思路:
    • 染色可以使用1和2区分不同颜色,用0表示未染色
    • 遍历所有点,每次将未染色的点进行dfs, 默认染成1或者2
    • 由于某个点染色成功不代表整个图就是二分图,
      • 因此只有某个点染色失败才能立刻break/return
      • 染色失败相当于存在相邻的2个点染了相同的颜色
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 1e5 + 10, M = 2e5 + 10;
int h[N], e[M], ne[M], idx; // 邻接表存储图
int color[N]; // 表示每个点的颜色,-1表示未染色,0表示白色,1表示黑色
int n, m;

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

bool dfs(int u, int c) {
color[u] = c;

for (int i = h[u]; i != -1; i = ne[i]) {
int j = e[i];
if (!color[j]) {
if (!dfs(j, 3 - c)) {
return false;
}
}
else if (color[j] == c) {
return false;
}
}

return true;
}

int main() {
cin >> n >> m;

memset(h, -1, sizeof h);
while (m--) {
int a, b;
cin >> a >> b;
add(a, b), add(b, a);
}

bool flag = true;
for (int i = 1; i <= n; i++) {
if (!color[i]) {
if (!dfs(i, 1)) {
flag = false;
break;
}
}
}

if (flag) {
cout << "Yes" << endl;
}
else {
cout << "No" << endl;
}

return 0;
}

匈牙利算法

861. 二分图的最大匹配

给定一个二分图,其中左半部包含 n1 个点(编号 1∼n1),右半部包含 n2个点(编号 1∼n2),二分图共包含 m 条边。

数据保证任意一条边的两个端点都不可能在同一部分中。

请你求出二分图的最大匹配数。

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

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

输入格式

第一行包含三个整数 n1、 n2 和 m。

接下来 m 行,每行包含两个整数 u 和 v,表示左半部点集中的点 u 和右半部点集中的点 v 之间存在一条边。

输出格式

输出一个整数,表示二分图的最大匹配数。

数据范围

输入样例:

1
2
3
4
5
2 2 4
1 1
1 2
2 1
2 2

输出样例:

1
2

AC

思路
  1. 什么是最大匹配?

    • 匹配:在图论中,一个「匹配」是一个边的集合,其中任意两条边都没有公共顶点。

    • 最大匹配:一个图所有匹配中,所含匹配边数最多的匹配,称为这个图的最大匹配。

下面是一些补充概念:

  • 完美匹配:如果一个图的某个匹配中,所有的顶点都是匹配点,那么它就是一个完美匹配。
  • 交替路:从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边…形成的路径叫交替路。
  • 增广路:从一个未匹配点出发,走交替路,如果途径另一个未匹配点(出发的点不算),则这条交替 路称为增广路(agumenting path)。
  1. 算法模板

    1. 存图模板

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      //邻接表写法,存稀疏图
      int h[N],ne[N],e[N],idx;
      //n1,n2分别是两个点集的点的个数
      int n1,n2,m;
      void add(int a , int b) {
      e[idx] = b, ne[idx] = h[a], h[a] = idx++;
      }
      void init() {
      memset(h,-1,sizeof h);
      }
      //存边只存一边就行了,虽然是无向图。
      for(int i = 0 ; i < n1 ; i ++) {
      int a,b;
      cin>>a>>b;
      add(a,b);
      }
    2. 匈牙利模板

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      //match[j]=a,表示女孩j的现有配对男友是a
      int match[N];
      //st[]数组我称为临时预定数组,st[j]=a表示一轮模拟匹配中,女孩j被男孩a预定了。
      int st[N];

      //这个函数的作用是用来判断,如果加入x来参与模拟配对,会不会使匹配数增多
      int find(int x) {
      //遍历自己喜欢的女孩
      for(int i = h[x] ; i != -1 ;i = ne[i]) {
      int j = e[i];
      if(!st[j]) { //如果在这一轮模拟匹配中,这个女孩尚未被预定
      st[j] = true;//那x就预定这个女孩了
      //如果女孩j没有男朋友,或者她原来的男朋友能够预定其它喜欢的女孩。配对成功,更新match
      if(!match[j]||find(match[j])) {
      match[j] = x;
      return true;
      }

      }
      }
      //自己中意的全部都被预定了。配对失败。
      return false;
      }

      //记录最大匹配
      int res = 0;
      for(int i = 1; i <= n1 ;i ++) {
      //因为每次模拟匹配的预定情况都是不一样的所以每轮模拟都要初始化
      memset(st,false,sizeof st);
      if(find(i))
      res++;
      }
    3. 递归过程

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>
#include <cstring>

using namespace std;

const int N = 510, M = 100010;

int n1, n2, m;
int h[N], e[M], ne[M], idx;
bool st[N];
int match[N];

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

int find(int u) {
for (int i = h[u]; i != -1; i = ne[i]) {
int j = e[i];
if (!st[j]) {
st[j] = true;
if (!match[j] || find(match[j])) {
match[j] = u;
return true;
}
}
}

return false;
}

int main() {
memset(h, -1, sizeof h);
cin >> n1 >> n2 >> m;

while (m--) {
int a, b;
cin >> a >> b;
add(a, b);
}

int res = 0;
for (int i = 1; i <= n1; i++) {
memset(st, false, sizeof st);

if (find(i)) {
res++;
}
}

cout << res << endl;
}