返回 LeetCode 刷题

Markdown File

DFS栈非递归和贪婪算法

DFS栈非递归和贪婪算法.md

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 比较所有可能
Rendered Preview

DFS(非递归,栈实现)

  1. 前提条件

我们前面写的是:

void dfs(int u)
{
visited[u]=1;

...

dfs(v);

}

这里:dfs(v)

其实就是:系统帮你压栈

所以:递归 = 系统栈

那么:不用递归

自己写一个栈

效果完全一样。

  1. 为什么递归就是栈?

例如:

图:

0

1

2

递归:

dfs(0)

dfs(1)

dfs(2)

实际上系统维护:

0

1

2

当:dfs(2)

结束以后:

弹栈回到1

结束:弹栈回到0

所以:递归

本质就是系统维护一个栈。

  1. 自己维护栈

数组实现:

int stack[MAXN];

int top = -1;

压栈:

stack[++top] = x;

弹栈:

int x = stack[top--];

  1. 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;
    }
}

}

  1. 为什么压栈时就 visited?

弹栈再visited

容易:重复入栈

例如:

0

/ \

1 2
/

3

如果:3没有提前visited

那么:1会压一次3

2又压一次3

于是3进栈两次

所以:

入栈立刻visited
保证:最多入栈一次。

  1. 举例

图:

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

结束。

  1. 为什么顺序可能不同?

栈先进后出

所以:如果想和递归一样。

通常:逆序压栈

只要求DFS。

  1. DFS总结

递归DFS

系统栈


非递归DFS

自己写栈


本质一样

都是:一路走到底再回来

贪心算法(Greedy Algorithm)

  1. 前提条件

贪心算法不是一种具体算法,而是一种:

算法设计思想

核心思想:

每一步都选择当前最优

希望最后得到全局最优

例如:

每次选最小

每次选最大

每次选最近

每次选最便宜

都是贪心。

  1. 贪心算法什么时候能用?

不是所有题都能贪心。

必须满足: 贪心选择性质

局部最优一定能推出

全局最优

最优子结构:

问题可以拆成很多子问题

每个子问题最优
整体仍然最优

  1. 贪心算法的一般步骤

① 建立数据

② 排序

③ 从前往后扫描

④ 当前最优就选择

⑤ 更新状态

⑥ 重复直到结束

  1. 贪心算法固定模板

虽然没有统一模板,但大多数都长这样:

//① 排序
qsort(...);

//② 扫描
for(int i = 0; i < n; i++)
{
if(当前元素满足条件)
{
选择当前元素;

    更新答案;

    更新状态;
}

}

其实就是:排序+不断做局部最优

  1. 例子一: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;
}

}

这里:

贪心就是:

每次挑最短边

  1. 例子二:Prim

Prim没有排序。

因为:

lowCost[]

已经保存了

当前最便宜

所以:

int u = -1;

for(int i = 0; i < n; i++)
{
if(!visited[i] &&
(u == -1 ||
lowCost[i] < lowCost[u]))
{
u = i;
}
}

实际上:

就是:每次挑当前最便宜的点

也是:贪心。

  1. 例子三: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);

更新。

  1. 一个贪心例子(活动安排)

例如:

会议:
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. 一个失败例子(为什么不能乱贪)

硬币:

1

3

4

找:

6

贪心:

4

1

1

三个硬币。

但是:

3

3

两个硬币。

说明:贪心失败。

所以:不是所有题

都能贪心。

  1. 贪心 vs 动态规划

例如:零钱兑换。

DP:

dp[i] =
min(dp[i],
dp[i-coin]+1);

比较:所有可能。

而贪心:直接拿最大的。

DP:会回头比较。

贪心:不会回头。

  1. 贪心常见代码模式
    模式一:排序+扫描(最常见)
    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:

比较所有可能