forked from soulmachine/leetcode
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathchapBFS.tex
1199 lines (993 loc) · 40.4 KB
/
chapBFS.tex
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
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
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
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
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
\chapter{广度优先搜索}
当题目看不出任何规律,既不能用分治,贪心,也不能用动规时,这时候万能方法——搜索,
就派上用场了。搜索分为广搜和深搜,广搜里面又有普通广搜,双向广搜,A*搜索等。
深搜里面又有普通深搜,回溯法等。
广搜和深搜非常类似(除了在扩展节点这部分不一样),二者有相同的框架,如何表示状态?
如何扩展状态?如何判重?尤其是判重,解决了这个问题,基本上整个问题就解决了。
\section{Word Ladder} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\label{sec:word-ladder}
\subsubsection{描述}
Given two words (start and end), and a dictionary, find the length of shortest transformation sequence from start to end, such that:
\begindot
\item Only one letter can be changed at a time
\item Each intermediate word must exist in the dictionary
\myenddot
For example, Given:
\begin{Code}
start = "hit"
end = "cog"
dict = ["hot","dot","dog","lot","log"]
\end{Code}
As one shortest transformation is \code{"hit" -> "hot" -> "dot" -> "dog" -> "cog"}, return its length $5$.
Note:
\begindot
\item Return 0 if there is no such transformation sequence.
\item All words have the same length.
\item All words contain only lowercase alphabetic characters.
\myenddot
\subsubsection{分析}
求最短路径,用广搜。
\subsubsection{单队列}
\begin{Code}
//LeetCode, Word Ladder
// 时间复杂度O(n),空间复杂度O(n)
struct state_t {
string word;
int level;
state_t() { word = ""; level = 0; }
state_t(const string& word, int level) {
this->word = word;
this->level = level;
}
bool operator==(const state_t &other) const {
return this->word == other.word;
}
};
namespace std {
template<> struct hash<state_t> {
public:
size_t operator()(const state_t& s) const {
return str_hash(s.word);
}
private:
std::hash<std::string> str_hash;
};
}
class Solution {
public:
int ladderLength(const string& start, const string &end,
const unordered_set<string> &dict) {
queue<state_t> q;
unordered_set<state_t> visited; // 判重
auto state_is_valid = [&](const state_t& s) {
return dict.find(s.word) != dict.end() || s.word == end;
};
auto state_is_target = [&](const state_t &s) {return s.word == end; };
auto state_extend = [&](const state_t &s) {
unordered_set<state_t> result;
for (size_t i = 0; i < s.word.size(); ++i) {
state_t new_state(s.word, s.level + 1);
for (char c = 'a'; c <= 'z'; c++) {
// 防止同字母替换
if (c == new_state.word[i]) continue;
swap(c, new_state.word[i]);
if (state_is_valid(new_state) &&
visited.find(new_state) == visited.end()) {
result.insert(new_state);
}
swap(c, new_state.word[i]); // 恢复该单词
}
}
return result;
};
state_t start_state(start, 0);
q.push(start_state);
visited.insert(start_state);
while (!q.empty()) {
// 千万不能用 const auto&,pop() 会删除元素,
// 引用就变成了悬空引用
const auto state = q.front();
q.pop();
if (state_is_target(state)) {
return state.level + 1;
}
const auto& new_states = state_extend(state);
for (const auto& new_state : new_states) {
q.push(new_state);
visited.insert(new_state);
}
}
return 0;
}
};
\end{Code}
\subsubsection{双队列}
\begin{Code}
//LeetCode, Word Ladder
// 时间复杂度O(n),空间复杂度O(n)
class Solution {
public:
int ladderLength(const string& start, const string &end,
const unordered_set<string> &dict) {
queue<string> current, next; // 当前层,下一层
unordered_set<string> visited; // 判重
int level = -1; // 层次
auto state_is_valid = [&](const string& s) {
return dict.find(s) != dict.end() || s == end;
};
auto state_is_target = [&](const string &s) {return s == end;};
auto state_extend = [&](const string &s) {
unordered_set<string> result;
for (size_t i = 0; i < s.size(); ++i) {
string new_word(s);
for (char c = 'a'; c <= 'z'; c++) {
// 防止同字母替换
if (c == new_word[i]) continue;
swap(c, new_word[i]);
if (state_is_valid(new_word) &&
visited.find(new_word) == visited.end()) {
result.insert(new_word);
}
swap(c, new_word[i]); // 恢复该单词
}
}
return result;
};
current.push(start);
visited.insert(start);
while (!current.empty()) {
++level;
while (!current.empty()) {
// 千万不能用 const auto&,pop() 会删除元素,
// 引用就变成了悬空引用
const auto state = current.front();
current.pop();
if (state_is_target(state)) {
return level + 1;
}
const auto& new_states = state_extend(state);
for (const auto& new_state : new_states) {
next.push(new_state);
visited.insert(new_state);
}
}
swap(next, current);
}
return 0;
}
};
\end{Code}
\subsubsection{相关题目}
\begindot
\item Word Ladder II,见 \S \ref{sec:word-ladder-ii}
\myenddot
\section{Word Ladder II} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\label{sec:word-ladder-ii}
\subsubsection{描述}
Given two words (start and end), and a dictionary, find all shortest transformation sequence(s) from start to end, such that:
\begindot
\item Only one letter can be changed at a time
\item Each intermediate word must exist in the dictionary
\myenddot
For example, Given:
\begin{Code}
start = "hit"
end = "cog"
dict = ["hot","dot","dog","lot","log"]
\end{Code}
Return
\begin{Code}
[
["hit","hot","dot","dog","cog"],
["hit","hot","lot","log","cog"]
]
\end{Code}
Note:
\begindot
\item All words have the same length.
\item All words contain only lowercase alphabetic characters.
\myenddot
\subsubsection{分析}
跟 Word Ladder比,这题是求路径本身,不是路径长度,也是BFS,略微麻烦点。
求一条路径和求所有路径有很大的不同,求一条路径,每个状态节点只需要记录一个前驱即可;求所有路径时,有的状态节点可能有多个父节点,即要记录多个前驱。
如果当前路径长度已经超过当前最短路径长度,可以中止对该路径的处理,因为我们要找的是最短路径。
\subsubsection{单队列}
\begin{Code}
//LeetCode, Word Ladder II
// 时间复杂度O(n),空间复杂度O(n)
struct state_t {
string word;
int level;
state_t() { word = ""; level = 0; }
state_t(const string& word, int level) {
this->word = word;
this->level = level;
}
bool operator==(const state_t &other) const {
return this->word == other.word;
}
};
namespace std {
template<> struct hash<state_t> {
public:
size_t operator()(const state_t& s) const {
return str_hash(s.word);
}
private:
std::hash<std::string> str_hash;
};
}
class Solution {
public:
vector<vector<string> > findLadders(const string& start,
const string& end, const unordered_set<string> &dict) {
queue<state_t> q;
unordered_set<state_t> visited; // 判重
unordered_map<state_t, vector<state_t> > father; // DAG
auto state_is_valid = [&](const state_t& s) {
return dict.find(s.word) != dict.end() || s.word == end;
};
auto state_is_target = [&](const state_t &s) {return s.word == end; };
auto state_extend = [&](const state_t &s) {
unordered_set<state_t> result;
for (size_t i = 0; i < s.word.size(); ++i) {
state_t new_state(s.word, s.level + 1);
for (char c = 'a'; c <= 'z'; c++) {
// 防止同字母替换
if (c == new_state.word[i]) continue;
swap(c, new_state.word[i]);
if (state_is_valid(new_state)) {
auto visited_iter = visited.find(new_state);
if (visited_iter != visited.end()) {
if (visited_iter->level < new_state.level) {
// do nothing
} else if (visited_iter->level == new_state.level) {
result.insert(new_state);
} else { // not possible
throw std::logic_error("not possible to get here");
}
} else {
result.insert(new_state);
}
}
swap(c, new_state.word[i]); // 恢复该单词
}
}
return result;
};
vector<vector<string>> result;
state_t start_state(start, 0);
q.push(start_state);
visited.insert(start_state);
while (!q.empty()) {
// 千万不能用 const auto&,pop() 会删除元素,
// 引用就变成了悬空引用
const auto state = q.front();
q.pop();
// 如果当前路径长度已经超过当前最短路径长度,
// 可以中止对该路径的处理,因为我们要找的是最短路径
if (!result.empty() && state.level + 1 > result[0].size()) break;
if (state_is_target(state)) {
vector<string> path;
gen_path(father, start_state, state, path, result);
continue;
}
// 必须挪到下面,比如同一层A和B两个节点均指向了目标节点,
// 那么目标节点就会在q中出现两次,输出路径就会翻倍
// visited.insert(state);
// 扩展节点
const auto& new_states = state_extend(state);
for (const auto& new_state : new_states) {
if (visited.find(new_state) == visited.end()) {
q.push(new_state);
}
visited.insert(new_state);
father[new_state].push_back(state);
}
}
return result;
}
private:
void gen_path(unordered_map<state_t, vector<state_t> > &father,
const state_t &start, const state_t &state, vector<string> &path,
vector<vector<string> > &result) {
path.push_back(state.word);
if (state == start) {
if (!result.empty()) {
if (path.size() < result[0].size()) {
result.clear();
result.push_back(path);
reverse(result.back().begin(), result.back().end());
} else if (path.size() == result[0].size()) {
result.push_back(path);
reverse(result.back().begin(), result.back().end());
} else { // not possible
throw std::logic_error("not possible to get here ");
}
} else {
result.push_back(path);
reverse(result.back().begin(), result.back().end());
}
} else {
for (const auto& f : father[state]) {
gen_path(father, start, f, path, result);
}
}
path.pop_back();
}
};
\end{Code}
\subsubsection{双队列}
\begin{Code}
//LeetCode, Word Ladder II
// 时间复杂度O(n),空间复杂度O(n)
class Solution {
public:
vector<vector<string> > findLadders(const string& start,
const string& end, const unordered_set<string> &dict) {
// 当前层,下一层,用unordered_set是为了去重,例如两个父节点指向
// 同一个子节点,如果用vector, 子节点就会在next里出现两次,其实此
// 时 father 已经记录了两个父节点,next里重复出现两次是没必要的
unordered_set<string> current, next;
unordered_set<string> visited; // 判重
unordered_map<string, vector<string> > father; // DAG
int level = -1; // 层次
auto state_is_valid = [&](const string& s) {
return dict.find(s) != dict.end() || s == end;
};
auto state_is_target = [&](const string &s) {return s == end;};
auto state_extend = [&](const string &s) {
unordered_set<string> result;
for (size_t i = 0; i < s.size(); ++i) {
string new_word(s);
for (char c = 'a'; c <= 'z'; c++) {
// 防止同字母替换
if (c == new_word[i]) continue;
swap(c, new_word[i]);
if (state_is_valid(new_word) &&
visited.find(new_word) == visited.end()) {
result.insert(new_word);
}
swap(c, new_word[i]); // 恢复该单词
}
}
return result;
};
vector<vector<string> > result;
current.insert(start);
while (!current.empty()) {
++ level;
// 如果当前路径长度已经超过当前最短路径长度,可以中止对该路径的
// 处理,因为我们要找的是最短路径
if (!result.empty() && level+1 > result[0].size()) break;
// 1. 延迟加入visited, 这样才能允许两个父节点指向同一个子节点
// 2. 一股脑current 全部加入visited, 是防止本层前一个节点扩展
// 节点时,指向了本层后面尚未处理的节点,这条路径必然不是最短的
for (const auto& state : current)
visited.insert(state);
for (const auto& state : current) {
if (state_is_target(state)) {
vector<string> path;
gen_path(father, path, start, state, result);
continue;
}
const auto new_states = state_extend(state);
for (const auto& new_state : new_states) {
next.insert(new_state);
father[new_state].push_back(state);
}
}
current.clear();
swap(current, next);
}
return result;
}
private:
void gen_path(unordered_map<string, vector<string> > &father,
vector<string> &path, const string &start, const string &word,
vector<vector<string> > &result) {
path.push_back(word);
if (word == start) {
if (!result.empty()) {
if (path.size() < result[0].size()) {
result.clear();
result.push_back(path);
} else if(path.size() == result[0].size()) {
result.push_back(path);
} else {
// not possible
throw std::logic_error("not possible to get here");
}
} else {
result.push_back(path);
}
reverse(result.back().begin(), result.back().end());
} else {
for (const auto& f : father[word]) {
gen_path(father, path, start, f, result);
}
}
path.pop_back();
}
};
\end{Code}
\subsubsection{图的广搜}
本题还可以看做是图上的广搜。给定了字典 \fn{dict},可以基于它画出一个无向图,表示单词之间可以互相转换。本题的本质就是已知起点和终点,在图上找出所有最短路径。
\begin{Code}
//LeetCode, Word Ladder II
// 时间复杂度O(n),空间复杂度O(n)
class Solution {
public:
vector<vector<string> > findLadders(const string& start,
const string &end, const unordered_set<string> &dict) {
const auto& g = build_graph(dict);
vector<state_t*> pool;
queue<state_t*> q; // 未处理的节点
// value 是所在层次
unordered_map<string, int> visited;
auto state_is_target = [&](const state_t *s) {return s->word == end; };
vector<vector<string>> result;
q.push(create_state(nullptr, start, 0, pool));
while (!q.empty()) {
state_t* state = q.front();
q.pop();
// 如果当前路径长度已经超过当前最短路径长度,
// 可以中止对该路径的处理,因为我们要找的是最短路径
if (!result.empty() && state->level+1 > result[0].size()) break;
if (state_is_target(state)) {
const auto& path = gen_path(state);
if (result.empty()) {
result.push_back(path);
} else {
if (path.size() < result[0].size()) {
result.clear();
result.push_back(path);
} else if (path.size() == result[0].size()) {
result.push_back(path);
} else {
// not possible
throw std::logic_error("not possible to get here");
}
}
continue;
}
visited[state->word] = state->level;
// 扩展节点
auto iter = g.find(state->word);
if (iter == g.end()) continue;
for (const auto& neighbor : iter->second) {
auto visited_iter = visited.find(neighbor);
if (visited_iter != visited.end() &&
visited_iter->second < state->level + 1) {
continue;
}
q.push(create_state(state, neighbor, state->level + 1, pool));
}
}
// release all states
for (auto state : pool) {
delete state;
}
return result;
}
private:
struct state_t {
state_t* father;
string word;
int level; // 所在层次,从0开始编号
state_t(state_t* father_, const string& word_, int level_) :
father(father_), word(word_), level(level_) {}
};
state_t* create_state(state_t* parent, const string& value,
int length, vector<state_t*>& pool) {
state_t* node = new state_t(parent, value, length);
pool.push_back(node);
return node;
}
vector<string> gen_path(const state_t* node) {
vector<string> path;
while(node != nullptr) {
path.push_back(node->word);
node = node->father;
}
reverse(path.begin(), path.end());
return path;
}
unordered_map<string, unordered_set<string> > build_graph(
const unordered_set<string>& dict) {
unordered_map<string, unordered_set<string> > adjacency_list;
for (const auto& word : dict) {
for (size_t i = 0; i < word.size(); ++i) {
string new_word(word);
for (char c = 'a'; c <= 'z'; c++) {
// 防止同字母替换
if (c == new_word[i]) continue;
swap(c, new_word[i]);
if ((dict.find(new_word) != dict.end())) {
auto iter = adjacency_list.find(word);
if (iter != adjacency_list.end()) {
iter->second.insert(new_word);
} else {
adjacency_list.insert(pair<string,
unordered_set<string>>(word, unordered_set<string>()));
adjacency_list[word].insert(new_word);
}
}
swap(c, new_word[i]); // 恢复该单词
}
}
}
return adjacency_list;
}
};
\end{Code}
\subsubsection{相关题目}
\begindot
\item Word Ladder,见 \S \ref{sec:word-ladder}
\myenddot
\section{Surrounded Regions} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\label{sec:surrounded-regions}
\subsubsection{描述}
Given a 2D board containing \fn{'X'} and \fn{'O'}, capture all regions surrounded by \fn{'X'}.
A region is captured by flipping all \fn{'O'}s into \fn{'X'}s in that surrounded region .
For example,
\begin{Code}
X X X X
X O O X
X X O X
X O X X
\end{Code}
After running your function, the board should be:
\begin{Code}
X X X X
X X X X
X X X X
X O X X
\end{Code}
\subsubsection{分析}
广搜。从上下左右四个边界往里走,凡是能碰到的\fn{'O'},都是跟边界接壤的,应该保留。
\subsubsection{代码}
\begin{Code}
// LeetCode, Surrounded Regions
// BFS,时间复杂度O(n),空间复杂度O(n)
class Solution {
public:
void solve(vector<vector<char>> &board) {
if (board.empty()) return;
const int m = board.size();
const int n = board[0].size();
for (int i = 0; i < n; i++) {
bfs(board, 0, i);
bfs(board, m - 1, i);
}
for (int j = 1; j < m - 1; j++) {
bfs(board, j, 0);
bfs(board, j, n - 1);
}
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
if (board[i][j] == 'O')
board[i][j] = 'X';
else if (board[i][j] == '+')
board[i][j] = 'O';
}
private:
void bfs(vector<vector<char>> &board, int i, int j) {
typedef pair<int, int> state_t;
queue<state_t> q;
const int m = board.size();
const int n = board[0].size();
auto state_is_valid = [&](const state_t &s) {
const int x = s.first;
const int y = s.second;
if (x < 0 || x >= m || y < 0 || y >= n || board[x][y] != 'O')
return false;
return true;
};
auto state_extend = [&](const state_t &s) {
vector<state_t> result;
const int x = s.first;
const int y = s.second;
// 上下左右
const state_t new_states[4] = {{x-1,y}, {x+1,y},
{x,y-1}, {x,y+1}};
for (int k = 0; k < 4; ++k) {
if (state_is_valid(new_states[k])) {
// 既有标记功能又有去重功能
board[new_states[k].first][new_states[k].second] = '+';
result.push_back(new_states[k]);
}
}
return result;
};
state_t start = { i, j };
if (state_is_valid(start)) {
board[i][j] = '+';
q.push(start);
}
while (!q.empty()) {
auto cur = q.front();
q.pop();
auto new_states = state_extend(cur);
for (auto s : new_states) q.push(s);
}
}
};
\end{Code}
\subsubsection{相关题目}
\begindot
\item 无
\myenddot
\section{小结} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\label{sec:bfs-template}
\subsection{适用场景}
\textbf{输入数据}:没什么特征,不像深搜,需要有“递归”的性质。如果是树或者图,概率更大。
\textbf{状态转换图}:树或者DAG图。
\textbf{求解目标}:求最短。
\subsection{思考的步骤}
\begin{enumerate}
\item 是求路径长度,还是路径本身(或动作序列)?
\begin{enumerate}
\item 如果是求路径长度,则状态里面要存路径长度(或双队列+一个全局变量)
\item 如果是求路径本身或动作序列
\begin{enumerate}
\item 要用一棵树存储宽搜过程中的路径
\item 是否可以预估状态个数的上限?能够预估状态总数,则开一个大数组,用树的双亲表示法;如果不能预估状态总数,则要使用一棵通用的树。这一步也是第4步的必要不充分条件。
\end{enumerate}
\end{enumerate}
\item 如何表示状态?即一个状态需要存储哪些些必要的数据,才能够完整提供如何扩展到下一步状态的所有信息。一般记录当前位置或整体局面。
\item 如何扩展状态?这一步跟第2步相关。状态里记录的数据不同,扩展方法就不同。对于固定不变的数据结构(一般题目直接给出,作为输入数据),如二叉树,图等,扩展方法很简单,直接往下一层走,对于隐式图,要先在第1步里想清楚状态所带的数据,想清楚了这点,那如何扩展就很简单了。
\item 如何判断重复?如果状态转换图是一颗树,则永远不会出现回路,不需要判重;如果状态转换图是一个图(这时候是一个图上的BFS),则需要判重。
\begin{enumerate}
\item 如果是求最短路径长度或一条路径,则只需要让“点”(即状态)不重复出现,即可保证不出现回路
\item 如果是求所有路径,注意此时,状态转换图是DAG,即允许两个父节点指向同一个子节点。具体实现时,每个节点要\textbf{“延迟”}加入到已访问集合\fn{visited},要等一层全部访问完后,再加入到\fn{visited}集合。
\item 具体实现
\begin{enumerate}
\item 状态是否存在完美哈希方案?即将状态一一映射到整数,互相之间不会冲突。
\item 如果不存在,则需要使用通用的哈希表(自己实现或用标准库,例如\fn{unordered_set})来判重;自己实现哈希表的话,如果能够预估状态个数的上限,则可以开两个数组,head和next,表示哈希表,参考第 \S \ref{subsec:eightDigits}节方案2。
\item 如果存在,则可以开一个大布尔数组,来判重,且此时可以精确计算出状态总数,而不仅仅是预估上限。
\end{enumerate}
\end{enumerate}
\item 目标状态是否已知?如果题目已经给出了目标状态,可以带来很大便利,这时候可以从起始状态出发,正向广搜;也可以从目标状态出发,逆向广搜;也可以同时出发,双向广搜。
\end{enumerate}
\subsection{代码模板}
广搜需要一个队列,用于一层一层扩展,一个hashset,用于判重,一棵树(只求长度时不需要),用于存储整棵树。
对于队列,可以用\fn{queue},也可以把\fn{vector}当做队列使用。当求长度时,有两种做法:
\begin{enumerate}
\item 只用一个队列,但在状态结构体\fn{state_t}里增加一个整数字段\fn{level},表示当前所在的层次,当碰到目标状态,直接输出\fn{level}即可。这个方案,可以很容易的变成A*算法,把\fn{queue}替换为\fn{priority_queue}即可。
\item 用两个队列,\fn{current, next},分别表示当前层次和下一层,另设一个全局整数\fn{level},表示层数(也即路径长度),当碰到目标状态,输出\fn{level}即可。这个方案,状态里可以不存路径长度,只需全局设置一个整数\fn{level},比较节省内存;
\end{enumerate}
对于hashset,如果有完美哈希方案,用布尔数组(\fn{bool visited[STATE_MAX]}或\fn{vector<bool> visited(STATE_MAX, false)})来表示;如果没有,可以用STL里的\fn{set}或\fn{unordered_set}。
对于树,如果用STL,可以用\fn{unordered_map<state_t, state_t > father}表示一颗树,代码非常简洁。如果能够预估状态总数的上限(设为STATE_MAX),可以用数组(\fn{state_t nodes[STATE_MAX]}),即树的双亲表示法来表示树,效率更高,当然,需要写更多代码。
\subsubsection{如何表示状态}
\begin{Codex}[label=bfs_common.h]
/** 状态 */
struct state_t {
int data1; /** 状态的数据,可以有多个字段. */
int data2; /** 状态的数据,可以有多个字段. */
// dataN; /** 其他字段 */
int action; /** 由父状态移动到本状态的动作,求动作序列时需要. */
int level; /** 所在的层次(从0开始),也即路径长度-1,求路径长度时需要;
不过,采用双队列时不需要本字段,只需全局设一个整数 */
bool operator==(const state_t &other) const {
return true; // 根据具体问题实现
}
};
// 定义hash函数
// 方法1:模板特化,当hash函数只需要状态本身,不需要其他数据时,用这个方法比较简洁
namespace std {
template<> struct hash<state_t> {
size_t operator()(const state_t & x) const {
return 0; // 根据具体问题实现
}
};
}
// 方法2:函数对象,如果hash函数需要运行时数据,则用这种方法
class Hasher {
public:
Hasher(int _m) : m(_m) {};
size_t operator()(const state_t &s) const {
return 0; // 根据具体问题实现
}
private:
int m; // 存放外面传入的数据
};
/**
* @brief 反向生成路径,求一条路径.
* @param[in] father 树
* @param[in] target 目标节点
* @return 从起点到target的路径
*/
vector<state_t> gen_path(const unordered_map<state_t, state_t> &father,
const state_t &target) {
vector<state_t> path;
path.push_back(target);
for (state_t cur = target; father.find(cur) != father.end();
cur = father.at(cur))
path.push_back(cur);
reverse(path.begin(), path.end());
return path;
}
/**
* 反向生成路径,求所有路径.
* @param[in] father 存放了所有路径的树
* @param[in] start 起点
* @param[in] state 终点
* @return 从起点到终点的所有路径
*/
void gen_path(unordered_map<state_t, vector<state_t> > &father,
const string &start, const state_t& state, vector<state_t> &path,
vector<vector<state_t> > &result) {
path.push_back(state);
if (state == start) {
if (!result.empty()) {
if (path.size() < result[0].size()) {
result.clear();
result.push_back(path);
} else if(path.size() == result[0].size()) {
result.push_back(path);
} else {
// not possible
throw std::logic_error("not possible to get here");
}
} else {
result.push_back(path);
}
reverse(result.back().begin(), result.back().end());
} else {
for (const auto& f : father[state]) {
gen_path(father, start, f, path, result);
}
}
path.pop_back();
}
\end{Codex}
\subsubsection{求最短路径长度或一条路径}
\textbf{单队列的写法}
\begin{Codex}[label=bfs_template.cpp]
#include "bfs_common.h"
/**
* @brief 广搜,只用一个队列.
* @param[in] start 起点
* @param[in] data 输入数据
* @return 从起点到目标状态的一条最短路径
*/
vector<state_t> bfs(state_t &start, const vector<vector<int>> &grid) {
queue<state_t> q; // 队列
unordered_set<state_t> visited; // 判重
unordered_map<state_t, state_t> father; // 树,求路径本身时才需要
// 判断状态是否合法
auto state_is_valid = [&](const state_t &s) { /*...*/ };
// 判断当前状态是否为所求目标
auto state_is_target = [&](const state_t &s) { /*...*/ };
// 扩展当前状态
auto state_extend = [&](const state_t &s) {
unordered_set<state_t> result;
for (/*...*/) {
const state_t new_state = /*...*/;
if (state_is_valid(new_state) &&
visited.find(new_state) != visited.end()) {
result.insert(new_state);
}
}
return result;
};
assert (start.level == 0);
q.push(start);
while (!q.empty()) {
// 千万不能用 const auto&,pop() 会删除元素,
// 引用就变成了悬空引用
const state_t state = q.front();
q.pop();
visited.insert(state);
// 访问节点
if (state_is_target(state)) {
return return gen_path(father, target); // 求一条路径
// return state.level + 1; // 求路径长度
}
// 扩展节点
vector<state_t> new_states = state_extend(state);
for (const auto& new_state : new_states) {
q.push(new_state);
father[new_state] = state; // 求一条路径
// visited.insert(state); // 优化:可以提前加入 visited 集合,
// 从而缩小状态扩展。这时 q 的含义略有变化,里面存放的是处理了一半
// 的节点:已经加入了visited,但还没有扩展。别忘记 while循环开始
// 前,要加一行代码, visited.insert(start)
}
}
return vector<state_t>();
//return 0;
}
\end{Codex}
\textbf{双队列的写法}
\begin{Codex}[label=bfs_template1.cpp]
#include "bfs_common.h"
/**
* @brief 广搜,使用两个队列.
* @param[in] start 起点
* @param[in] data 输入数据
* @return 从起点到目标状态的一条最短路径
*/
vector<state_t> bfs(const state_t &start, const type& data) {
queue<state_t> next, current; // 当前层,下一层
unordered_set<state_t> visited; // 判重
unordered_map<state_t, state_t> father; // 树,求路径本身时才需要
int level = -1; // 层次
// 判断状态是否合法
auto state_is_valid = [&](const state_t &s) { /*...*/ };
// 判断当前状态是否为所求目标
auto state_is_target = [&](const state_t &s) { /*...*/ };
// 扩展当前状态
auto state_extend = [&](const state_t &s) {
unordered_set<state_t> result;
for (/*...*/) {