返回 LeetCode 刷题

Markdown File

图论基础

图论基础.md

1一、图 Graph 基础
2
31. 前提条件
4
5之前学的树:
6
7一个根
8父子关系
9没有环
10
11图比树更一般:
12
13点和点之间可以任意连接
14可以有方向
15可以有权值
16可以有环
17
18例如:
19
20A —— B
21| |
22C —— D
23
24这里:A、B、C、D 是顶点
25线是边
26
272. 核心概念
28
29图由两部分组成:G = (V, E)
30
31其中:
32
33V:顶点集合
34E:边集合
35
36比如:
37
38V = {A, B, C, D}
39
40E = {(A,B), (A,C), (B,D), (C,D)}
41
42二、有向图和无向图
43
441. 无向图
45
46边没有方向。
47
48A —— B
49
50表示:
51
52A 可以到 B
53B 也可以到 A
54
552. 有向图
56
57边有方向。
58
59A -> B
60
61表示:
62
63A 可以到 B
64但 B 不一定能到 A
65
66比如关注关系、任务依赖关系。
67
68三、带权图
69
70如果边上有数值,就叫带权图。
71
72A --5-- B
73
74这里:5 是权值
75
76可以表示:
77
78距离
79时间
80费用
81代价
82
83最短路径算法就是处理这种图。
84
85四、度、入度、出度
86
871. 无向图的度
88
89一个点连了几条边,度就是几。
90
91A —— B
92|
93C
94
95A 连着 B 和 C。
96
97所以:A 的度 = 2
98
992. 有向图的入度
100
101指向这个点的边数。
102
103A -> C
104B -> C
105
106C 的入度:2
107
1083. 有向图的出度
109
110从这个点出去的边数。
111
112A -> B
113A -> C
114
115A 的出度:2
116
117五、图的存储方式
118
119最常用两种:邻接矩阵和邻接表
120
121六、邻接矩阵
122
1231. 前提条件
124
125邻接矩阵用二维数组存图。
126
127如果有 n 个点,就开:
128
129int graph[n][n];
130
131含义:graph[i][j] 表示 i 到 j 是否有边
132
1332. 无权图
134
135如果没有权值:
136
137有边:1
138无边:0
139
140例如:
141
1420 —— 1
1430 —— 2
1441 —— 2
145
146矩阵:
147
148 0 1 2
1490 0 1 1
1501 1 0 1
1512 1 1 0
152
153因为是无向图,所以矩阵关于主对角线对称。
154
1553. 有向图
156
157例如:
158
1590 -> 1
1600 -> 2
1612 -> 1
162
163矩阵:
164
165 0 1 2
1660 0 1 1
1671 0 0 0
1682 0 1 0
169
170注意:
171
172graph[0][1] = 1
173
174不代表
175
176graph[1][0] = 1
177
1784. 带权图
179
180如果有权值:
181
182有边:权值
183无边:INF
184
185例如:
186
1870 --5-- 1
1880 --2-- 2
189
190可以写:
191
192graph[0][1] = 5
193graph[1][0] = 5
194
195graph[0][2] = 2
196graph[2][0] = 2
197
198无边用:
199
200#define INF 1000000000
201
2025. 邻接矩阵代码
203#define MAXN 100
204#define INF 1000000000
205
206int graph[MAXN][MAXN];
207
208void initGraph(int n)
209{
210 for(int i = 0; i < n; i++)
211 {
212 for(int j = 0; j < n; j++)
213 {
214 if(i == j)
215 {
216 graph[i][j] = 0;
217 }
218 else
219 {
220 graph[i][j] = INF;
221 }
222 }
223 }
224}
225
226无向带权边:
227
228void addUndirectedEdge(int u, int v, int w)
229{
230 graph[u][v] = w;
231 graph[v][u] = w;
232}
233
234有向带权边:
235
236void addDirectedEdge(int u, int v, int w)
237{
238 graph[u][v] = w;
239}
240
241七、邻接表
2421. 前提条件
243
244邻接表是:数组 + 链表
245
246每个点开一条链表,存它能到哪些点。
247
248例如:
249
2500 -> 1
2510 -> 2
2521 -> 2
253
254邻接表:
255
2560: 1 -> 2
2571: 2
2582:
259
2602. 为什么邻接表常用?
261
262如果点很多,边很少,邻接矩阵很浪费。
263
264比如:
265
2661000 个点
267只有 2000 条边
268
269邻接矩阵要:1000 × 1000 = 1000000 个位置
270
271邻接表只存真实存在的边。
272
273所以:
274
275边少用邻接表
276边多用邻接矩阵
277
278八、邻接表 C 代码
279
2801. 结构体
281#include <stdlib.h>
282
283#define MAXN 100
284
285struct EdgeNode {
286 int to;
287 int weight;
288 struct EdgeNode* next;
289};
290
291struct EdgeNode* adj[MAXN];
292
293含义:
294
295adj[i] 是 i 号点的边链表头指针
296
297to 表示这条边到哪个点
298weight 表示边权
299next 指向下一条边
300
3012. 初始化
302void initAdjList(int n)
303{
304 for(int i = 0; i < n; i++)
305 {
306 adj[i] = NULL;
307 }
308}
309
3103. 加有向边
311void addDirectedEdge(int u, int v, int w)
312{
313 struct EdgeNode* node =
314 malloc(sizeof(struct EdgeNode));
315
316 node->to = v;
317 node->weight = w;
318
319 node->next = adj[u];
320 adj[u] = node;
321}
322
323意思:从 u 出发,能到 v,权值是 w
324
325用的是链表中的头插法。
326
3274. 加无向边
328
329无向边:u —— v
330
331等价于两条有向边:
332
333u -> v
334v -> u
335
336代码:
337
338void addUndirectedEdge(int u, int v, int w)
339{
340 addDirectedEdge(u, v, w);
341 addDirectedEdge(v, u, w);
342}
343
344九、遍历邻接表
345
346如果要看某个点 u 能到哪些点:
347
348void printNeighbors(int u)
349{
350 struct EdgeNode* cur = adj[u];
351
352 while(cur != NULL)
353 {
354 printf("%d %d\n", cur->to, cur->weight);
355
356 cur = cur->next;
357 }
358}
359
360十、区别
361
362邻接矩阵:
363优点:
364 判断 u 到 v 有没有边很快
365 O(1)
366
367缺点:
368 空间大
369 O(n²)
370
371适合:
372 点少、边多的稠密图
373
374--------------------------------
375
376邻接表:
377
378优点:
379 省空间
380 O(n + m)
381
382缺点:
383 判断 u 到 v 有没有边要遍历链表
384
385适合:
386 点多、边少的稀疏图
387
388其中:
389
390n = 点数
391m = 边数
392
393十一、总结
394
395图 Graph
396
397组成:G = (V, E)
398
399V:顶点
400E:边
401
402--------------------------------
403
404图的类型:
405
406无向图:
407 边没有方向
408
409有向图:
410 边有方向
411
412带权图:
413 边有权值
414
415--------------------------------
416
417度:
418
419无向图:
420 度 = 连着几条边
421
422有向图:
423 入度 = 指向它的边数
424 出度 = 从它出去的边数
425
426--------------------------------
427
428存储方式:
429
4301. 邻接矩阵
431
432graph[i][j]
433
434表示 i 到 j 是否有边或边权
435
436空间:
437 O(n²)
438
439适合:
440 稠密图
441
442--------------------------------
443
4442. 邻接表
445
446adj[i]
447
448表示 i 能到的所有点
449
450空间:
451 O(n + m)
452
453适合:
454 稀疏图
455
456--------------------------------
457
458无向边:
459
460u - v
461
462存两次:
463
464u -> v
465v -> u
466
467--------------------------------
468
469有向边:
470
471u -> v
472
473只存一次
Rendered Preview

一、图 Graph 基础

  1. 前提条件

之前学的树:

一个根
父子关系
没有环

图比树更一般:

点和点之间可以任意连接
可以有方向
可以有权值
可以有环

例如:

A —— B
| |
C —— D

这里:A、B、C、D 是顶点
线是边

  1. 核心概念

图由两部分组成:G = (V, E)

其中:

V:顶点集合
E:边集合

比如:

V = {A, B, C, D}

E = {(A,B), (A,C), (B,D), (C,D)}

二、有向图和无向图

  1. 无向图

边没有方向。

A —— B

表示:

A 可以到 B
B 也可以到 A

  1. 有向图

边有方向。

A -> B

表示:

A 可以到 B
但 B 不一定能到 A

比如关注关系、任务依赖关系。

三、带权图

如果边上有数值,就叫带权图。

A --5-- B

这里:5 是权值

可以表示:

距离
时间
费用
代价

最短路径算法就是处理这种图。

四、度、入度、出度

  1. 无向图的度

一个点连了几条边,度就是几。

A —— B
|
C

A 连着 B 和 C。

所以:A 的度 = 2

  1. 有向图的入度

指向这个点的边数。

A -> C
B -> C

C 的入度:2

  1. 有向图的出度

从这个点出去的边数。

A -> B
A -> C

A 的出度:2

五、图的存储方式

最常用两种:邻接矩阵和邻接表

六、邻接矩阵

  1. 前提条件

邻接矩阵用二维数组存图。

如果有 n 个点,就开:

int graph[n][n];

含义:graph[i][j] 表示 i 到 j 是否有边

  1. 无权图

如果没有权值:

有边:1
无边:0

例如:

0 —— 1
0 —— 2
1 —— 2

矩阵:

0 1 2

0 0 1 1
1 1 0 1
2 1 1 0

因为是无向图,所以矩阵关于主对角线对称。

  1. 有向图

例如:

0 -> 1
0 -> 2
2 -> 1

矩阵:

0 1 2

0 0 1 1
1 0 0 0
2 0 1 0

注意:

graph[0][1] = 1

不代表

graph[1][0] = 1

  1. 带权图

如果有权值:

有边:权值
无边:INF

例如:

0 --5-- 1
0 --2-- 2

可以写:

graph[0][1] = 5
graph[1][0] = 5

graph[0][2] = 2
graph[2][0] = 2

无边用:

#define INF 1000000000

  1. 邻接矩阵代码
    #define MAXN 100
    #define INF 1000000000

int graph[MAXN][MAXN];

void initGraph(int n)
{
for(int i = 0; i < n; i++)
{
for(int j = 0; j < n; j++)
{
if(i == j)
{
graph[i][j] = 0;
}
else
{
graph[i][j] = INF;
}
}
}
}

无向带权边:

void addUndirectedEdge(int u, int v, int w)
{
graph[u][v] = w;
graph[v][u] = w;
}

有向带权边:

void addDirectedEdge(int u, int v, int w)
{
graph[u][v] = w;
}

七、邻接表

  1. 前提条件

邻接表是:数组 + 链表

每个点开一条链表,存它能到哪些点。

例如:

0 -> 1
0 -> 2
1 -> 2

邻接表:

0: 1 -> 2
1: 2
2:

  1. 为什么邻接表常用?

如果点很多,边很少,邻接矩阵很浪费。

比如:

1000 个点
只有 2000 条边

邻接矩阵要:1000 × 1000 = 1000000 个位置

邻接表只存真实存在的边。

所以:

边少用邻接表
边多用邻接矩阵

八、邻接表 C 代码

  1. 结构体
    #include <stdlib.h>

#define MAXN 100

struct EdgeNode {
int to;
int weight;
struct EdgeNode* next;
};

struct EdgeNode* adj[MAXN];

含义:

adj[i] 是 i 号点的边链表头指针

to 表示这条边到哪个点
weight 表示边权
next 指向下一条边

  1. 初始化
    void initAdjList(int n)
    {
    for(int i = 0; i < n; i++)
    {
    adj[i] = NULL;
    }
    }

  2. 加有向边
    void addDirectedEdge(int u, int v, int w)
    {
    struct EdgeNode* node =
    malloc(sizeof(struct EdgeNode));

    node->to = v;
    node->weight = w;

    node->next = adj[u];
    adj[u] = node;
    }

意思:从 u 出发,能到 v,权值是 w

用的是链表中的头插法。

  1. 加无向边

无向边:u —— v

等价于两条有向边:

u -> v
v -> u

代码:

void addUndirectedEdge(int u, int v, int w)
{
addDirectedEdge(u, v, w);
addDirectedEdge(v, u, w);
}

九、遍历邻接表

如果要看某个点 u 能到哪些点:

void printNeighbors(int u)
{
struct EdgeNode* cur = adj[u];

while(cur != NULL)
{
    printf("%d %d\n", cur->to, cur->weight);

    cur = cur->next;
}

}

十、区别

邻接矩阵:
优点:
判断 u 到 v 有没有边很快
O(1)

缺点:
空间大
O(n²)

适合:
点少、边多的稠密图


邻接表:

优点:
省空间
O(n + m)

缺点:
判断 u 到 v 有没有边要遍历链表

适合:
点多、边少的稀疏图

其中:

n = 点数
m = 边数

十一、总结

图 Graph

组成:G = (V, E)

V:顶点
E:边


图的类型:

无向图:
边没有方向

有向图:
边有方向

带权图:
边有权值


度:

无向图:
度 = 连着几条边

有向图:
入度 = 指向它的边数
出度 = 从它出去的边数


存储方式:

  1. 邻接矩阵

graph[i][j]

表示 i 到 j 是否有边或边权

空间:
O(n²)

适合:
稠密图


  1. 邻接表

adj[i]

表示 i 能到的所有点

空间:
O(n + m)

适合:
稀疏图


无向边:

u - v

存两次:

u -> v
v -> u


有向边:

u -> v

只存一次