1DFS(非递归,栈实现)
21. 前提条件
3
4我们前面写的是:
5
6void dfs(int u)
7{
8 visited[u]=1;
9
10 ...
11
12 dfs(v);
13}
14
15这里:dfs(v)
16
17其实就是:系统帮你压栈
18
19所以:递归 = 系统栈
20
21那么:不用递归
22
23自己写一个栈
24
25效果完全一样。
26
272. 为什么递归就是栈?
28
29例如:
30
31图:
32
330
34
35↓
36
371
38
39↓
40
412
42
43递归:
44
45dfs(0)
46
47↓
48
49dfs(1)
50
51↓
52
53dfs(2)
54
55实际上系统维护:
56
57栈
58
59↓
60
610
62
63↓
64
651
66
67↓
68
692
70
71当:dfs(2)
72
73结束以后:
74
75弹栈回到1
76
77结束:弹栈回到0
78
79所以:递归
80
81本质就是系统维护一个栈。
82
833. 自己维护栈
84
85数组实现:
86
87int stack[MAXN];
88
89int top = -1;
90
91压栈:
92
93stack[++top] = x;
94
95弹栈:
96
97int x = stack[top--];
98
994. DFS 非递归模板
100
101void dfs(int start)
102{
103 int stack[MAXN];
104 int top = -1;
105
106 stack[++top] = start;
107 visited[start] = 1;
108//先定义start的位置top是0
109 while(top != -1)
110 {
111 int u = stack[top--];
112
113 printf("%d ", u);
114 //这里是往外面弹栈
115 //弹空了之后重新入栈
116 struct EdgeNode* cur = adj[u];
117
118 while(cur != NULL)
119 {
120 int v = cur->to;
121
122 if(!visited[v])
123 {
124 visited[v] = 1;
125 stack[++top] = v;
126 }
127 //也就是后进先出
128 cur = cur->next;
129 }
130 }
131}
132
1335. 为什么压栈时就 visited?
134
135
136弹栈再visited
137
138容易:重复入栈
139
140例如:
141
142 0
143
144/ \
145
1461 2
147 \/
148
149 3
150
151如果:3没有提前visited
152
153那么:1会压一次3
154
1552又压一次3
156
157于是3进栈两次
158
159所以:
160
161入栈立刻visited
162保证:最多入栈一次。
163
1646. 举例
165
166图:
167
168 0
169/ \
170
1711 2
172
173|
174
1753
176
177开始:
178
179stack
180
1810
182
183弹:
184
1850
186
187访问:
188
1890
190
191压:
192
1931
194
1952
196
197栈:
198
1992
200
2011
202
203(注意栈先进后出)
204
205弹:2
206
207访问:
208
2090 2
210
211弹:1
212
213访问:
214
2150 2 1
216
217压:3
218
219弹:3
220
221访问:
222
2230 2 1 3
224
225结束。
226
2277. 为什么顺序可能不同?
228
229栈先进后出
230
231所以:如果想和递归一样。
232
233通常:逆序压栈
234
235只要求DFS。
236
2378. DFS总结
238
239递归DFS
240
241系统栈
242
243----------------
244
245非递归DFS
246
247自己写栈
248
249----------------
250
251本质一样
252
253都是:一路走到底再回来
254
255贪心算法(Greedy Algorithm)
256
2571. 前提条件
258
259贪心算法不是一种具体算法,而是一种:
260
261算法设计思想
262
263核心思想:
264
265每一步都选择当前最优
266
267希望最后得到全局最优
268
269例如:
270
271每次选最小
272
273每次选最大
274
275每次选最近
276
277每次选最便宜
278
279都是贪心。
280
2812. 贪心算法什么时候能用?
282
283不是所有题都能贪心。
284
285必须满足: 贪心选择性质
286
287局部最优一定能推出
288
289全局最优
290
291最优子结构:
292
293问题可以拆成很多子问题
294
295每个子问题最优
296整体仍然最优
297
2983. 贪心算法的一般步骤
299
300① 建立数据
301
302↓
303
304② 排序
305
306↓
307
308③ 从前往后扫描
309
310↓
311
312④ 当前最优就选择
313
314↓
315
316⑤ 更新状态
317
318↓
319
320⑥ 重复直到结束
321
322
3234. 贪心算法固定模板
324
325虽然没有统一模板,但大多数都长这样:
326
327//① 排序
328qsort(...);
329
330//② 扫描
331for(int i = 0; i < n; i++)
332{
333 if(当前元素满足条件)
334 {
335 选择当前元素;
336
337 更新答案;
338
339 更新状态;
340 }
341}
342
343其实就是:排序+不断做局部最优
344
3455. 例子一:Kruskal
346
347边:
348
3490-1 2
350
3510-2 5
352
3531-2 1
354
355排序:
356
3571
358
3592
360
3615
362
363代码:
364
365qsort(edge,
366 m,
367 sizeof(struct Edge),
368 cmp);
369
370for(int i = 0; i < m; i++)
371{
372 if(find(edge[i].u) != find(edge[i].v))
373 {
374 unite(edge[i].u,
375 edge[i].v);
376
377 total += edge[i].w;
378 }
379}
380
381这里:
382
383贪心就是:
384
385每次挑最短边
386
3876. 例子二:Prim
388
389Prim没有排序。
390
391因为:
392
393lowCost[]
394
395已经保存了
396
397当前最便宜
398
399所以:
400
401int u = -1;
402
403for(int i = 0; i < n; i++)
404{
405 if(!visited[i] &&
406 (u == -1 ||
407 lowCost[i] < lowCost[u]))
408 {
409 u = i;
410 }
411}
412
413实际上:
414
415就是:每次挑当前最便宜的点
416
417也是:贪心。
418
4197. 例子三:Dijkstra
420
421代码:
422
423int u = -1;
424
425for(int i = 0; i < n; i++)
426{
427 if(!visited[i] &&
428 (u == -1 ||
429 dist[i] < dist[u]))
430 {
431 u = i;
432 }
433}
434
435这里:贪心选择:
436
437dist最小
438
439直接确定。
440
441然后:
442
443dist[v] =
444min(dist[v],
445 dist[u]+w);
446
447更新。
448
4498. 一个贪心例子(活动安排)
450
451例如:
452
453会议:
454A
455
4568~10
457
458B
459
4609~11
461
462C
463
46410~12
465
466目标:
467
468最多安排多少场?
469
470步骤:
471
472① 排序
473
474按:结束时间
475
476排序。
477
478② 扫描:
479
480sort(endTime);
481
482int lastEnd = -1;
483
484for(int i = 0; i < n; i++)
485{
486 if(start[i] >= lastEnd)
487 {
488 ans++;
489
490 lastEnd = end[i];
491 }
492}
493
494为什么?
495
496因为:结束越早
497
498给后面的空间越大。
499
500这是:经典贪心。
501
5029. 一个失败例子(为什么不能乱贪)
503
504硬币:
505
5061
507
5083
509
5104
511
512找:
513
5146
515
516贪心:
517
5184
519
520↓
521
5221
523
524↓
525
5261
527
528三个硬币。
529
530但是:
531
5323
533
534↓
535
5363
537
538两个硬币。
539
540说明:贪心失败。
541
542所以:不是所有题
543
544都能贪心。
545
54610. 贪心 vs 动态规划
547
548例如:零钱兑换。
549
550DP:
551
552dp[i] =
553min(dp[i],
554 dp[i-coin]+1);
555
556比较:所有可能。
557
558而贪心:直接拿最大的。
559
560DP:会回头比较。
561
562贪心:不会回头。
563
56411. 贪心常见代码模式
565模式一:排序+扫描(最常见)
566qsort(...);
567
568for(...)
569{
570 if(满足条件)
571 {
572 更新答案;
573 }
574}
575
576例如:Kruskal
577
578活动安排
579
580区间覆盖
581
582模式二:不断找当前最优
583int best = -1;
584
585for(...)
586{
587 if(更优)
588 {
589 best = i;
590 }
591}
592
593例如:
594
595Prim
596
597Dijkstra
598
599模式三:优先队列
600
601以后会学:
602
603while(heap不空)
604{
605 取最优;
606
607 更新;
608}
609
610例如:
611
612Huffman树
613
614Dijkstra堆优化
615
616总结
617Greedy(贪心)
618
619核心思想:
620
621 每一步选择当前最优
622
623希望得到整体最优
624
625基本步骤:
626
627 建立数据
628
629↓
630
631 排序(很多题)
632
633↓
634
635 扫描
636
637↓
638
639 当前最优就选
640
641↓
642
643 更新状态
644
645↓
646
647 重复
648
649常见代码:
650
651① 排序+扫描
652
653 qsort()
654
655 for()
656
657② 不断找最优
658
659 best
660
661③ 优先队列
662
663 heap
664
665经典算法:
666
667 Prim
668 Kruskal
669 Dijkstra
670 Huffman
671 活动安排
672 区间覆盖
673
674与DP区别:
675
676Greedy:
677
678 不回头
679
680DP:
681
682 比较所有可能
DFS(非递归,栈实现)
- 前提条件
我们前面写的是:
void dfs(int u)
{
visited[u]=1;
...
dfs(v);
}
这里:dfs(v)
其实就是:系统帮你压栈
所以:递归 = 系统栈
那么:不用递归
自己写一个栈
效果完全一样。
- 为什么递归就是栈?
例如:
图:
0
↓
1
↓
2
递归:
dfs(0)
↓
dfs(1)
↓
dfs(2)
实际上系统维护:
栈
↓
0
↓
1
↓
2
当:dfs(2)
结束以后:
弹栈回到1
结束:弹栈回到0
所以:递归
本质就是系统维护一个栈。
- 自己维护栈
数组实现:
int stack[MAXN];
int top = -1;
压栈:
stack[++top] = x;
弹栈:
int x = stack[top--];
- DFS 非递归模板
void dfs(int start)
{
int stack[MAXN];
int top = -1;
stack[++top] = start;
visited[start] = 1;
//先定义start的位置top是0
while(top != -1)
{
int u = stack[top--];
printf("%d ", u);
//这里是往外面弹栈
//弹空了之后重新入栈
struct EdgeNode* cur = adj[u];
while(cur != NULL)
{
int v = cur->to;
if(!visited[v])
{
visited[v] = 1;
stack[++top] = v;
}
//也就是后进先出
cur = cur->next;
}
}
}
- 为什么压栈时就 visited?
弹栈再visited
容易:重复入栈
例如:
0
/ \
1 2
/
3
如果:3没有提前visited
那么:1会压一次3
2又压一次3
于是3进栈两次
所以:
入栈立刻visited
保证:最多入栈一次。
- 举例
图:
0
/ \
1 2
|
3
开始:
stack
0
弹:
0
访问:
0
压:
1
2
栈:
2
1
(注意栈先进后出)
弹:2
访问:
0 2
弹:1
访问:
0 2 1
压:3
弹:3
访问:
0 2 1 3
结束。
- 为什么顺序可能不同?
栈先进后出
所以:如果想和递归一样。
通常:逆序压栈
只要求DFS。
- DFS总结
递归DFS
系统栈
非递归DFS
自己写栈
本质一样
都是:一路走到底再回来
贪心算法(Greedy Algorithm)
- 前提条件
贪心算法不是一种具体算法,而是一种:
算法设计思想
核心思想:
每一步都选择当前最优
希望最后得到全局最优
例如:
每次选最小
每次选最大
每次选最近
每次选最便宜
都是贪心。
- 贪心算法什么时候能用?
不是所有题都能贪心。
必须满足: 贪心选择性质
局部最优一定能推出
全局最优
最优子结构:
问题可以拆成很多子问题
每个子问题最优
整体仍然最优
- 贪心算法的一般步骤
① 建立数据
↓
② 排序
↓
③ 从前往后扫描
↓
④ 当前最优就选择
↓
⑤ 更新状态
↓
⑥ 重复直到结束
- 贪心算法固定模板
虽然没有统一模板,但大多数都长这样:
//① 排序
qsort(...);
//② 扫描
for(int i = 0; i < n; i++)
{
if(当前元素满足条件)
{
选择当前元素;
更新答案;
更新状态;
}
}
其实就是:排序+不断做局部最优
- 例子一:Kruskal
边:
0-1 2
0-2 5
1-2 1
排序:
1
2
5
代码:
qsort(edge,
m,
sizeof(struct Edge),
cmp);
for(int i = 0; i < m; i++)
{
if(find(edge[i].u) != find(edge[i].v))
{
unite(edge[i].u,
edge[i].v);
total += edge[i].w;
}
}
这里:
贪心就是:
每次挑最短边
- 例子二:Prim
Prim没有排序。
因为:
lowCost[]
已经保存了
当前最便宜
所以:
int u = -1;
for(int i = 0; i < n; i++)
{
if(!visited[i] &&
(u == -1 ||
lowCost[i] < lowCost[u]))
{
u = i;
}
}
实际上:
就是:每次挑当前最便宜的点
也是:贪心。
- 例子三:Dijkstra
代码:
int u = -1;
for(int i = 0; i < n; i++)
{
if(!visited[i] &&
(u == -1 ||
dist[i] < dist[u]))
{
u = i;
}
}
这里:贪心选择:
dist最小
直接确定。
然后:
dist[v] =
min(dist[v],
dist[u]+w);
更新。
- 一个贪心例子(活动安排)
例如:
会议:
A
8~10
B
9~11
C
10~12
目标:
最多安排多少场?
步骤:
① 排序
按:结束时间
排序。
② 扫描:
sort(endTime);
int lastEnd = -1;
for(int i = 0; i < n; i++)
{
if(start[i] >= lastEnd)
{
ans++;
lastEnd = end[i];
}
}
为什么?
因为:结束越早
给后面的空间越大。
这是:经典贪心。
- 一个失败例子(为什么不能乱贪)
硬币:
1
3
4
找:
6
贪心:
4
↓
1
↓
1
三个硬币。
但是:
3
↓
3
两个硬币。
说明:贪心失败。
所以:不是所有题
都能贪心。
- 贪心 vs 动态规划
例如:零钱兑换。
DP:
dp[i] =
min(dp[i],
dp[i-coin]+1);
比较:所有可能。
而贪心:直接拿最大的。
DP:会回头比较。
贪心:不会回头。
- 贪心常见代码模式
模式一:排序+扫描(最常见)
qsort(...);
for(...)
{
if(满足条件)
{
更新答案;
}
}
例如:Kruskal
活动安排
区间覆盖
模式二:不断找当前最优
int best = -1;
for(...)
{
if(更优)
{
best = i;
}
}
例如:
Prim
Dijkstra
模式三:优先队列
以后会学:
while(heap不空)
{
取最优;
更新;
}
例如:
Huffman树
Dijkstra堆优化
总结
Greedy(贪心)
核心思想:
每一步选择当前最优
希望得到整体最优
基本步骤:
建立数据
↓
排序(很多题)
↓
扫描
↓
当前最优就选
↓
更新状态
↓
重复
常见代码:
① 排序+扫描
qsort()
for()
② 不断找最优
best
③ 优先队列
heap
经典算法:
Prim
Kruskal
Dijkstra
Huffman
活动安排
区间覆盖
与DP区别:
Greedy:
不回头
DP:
比较所有可能