适用场景
输入数据:没什么特征,不像深搜,需要有“递归”的性质。如果是树或者图,概率更大。
状态转换图:树或者图。
求解目标:求最短。
思考的步骤
1. 是求路径长度,还是路径本身(或动作序列)?
(a) 如果是求路径长度,则状态里面要存路径长度(或双队列+ 一个全局变量)
(b) 如果是求路径本身或动作序列
i. 要用一棵树存储宽搜过程中的路径
ii. 是否可以预估状态个数的上限?能够预估状态总数,则开一个大数组,用树的双亲
表示法;如果不能预估状态总数,则要使用一棵通用的树。这一步也是第4 步的必
要不充分条件。
2. 如何表示状态?即一个状态需要存储哪些些必要的数据,才能够完整提供如何扩展到下一步
状态的所有信息。一般记录当前位置或整体局面。
3. 如何扩展状态?这一步跟第2 步相关。状态里记录的数据不同,扩展方法就不同。对于固定
不变的数据结构(一般题目直接给出,作为输入数据),如二叉树,图等,扩展方法很简单,直接往下一层走,对于隐式图,要先在第1 步里想清楚状态所带的数据,想清楚了这点,那如何扩展就很简单了。
4. 关于判重,状态是否存在完美哈希方案?即将状态一一映射到整数,互相之间不会冲突。
(a) 如果不存在,则需要使用通用的哈希表(自己实现或用标准库,例如unordered_set)
来判重;自己实现哈希表的话,如果能够预估状态个数的上限,则可以开两个数组,head
和next,表示哈希表,参考第§??节方案2。
(b) 如果存在,则可以开一个大布尔数组,作为哈希表来判重,且此时可以精确计算出状态
总数,而不仅仅是预估上限。
5. 目标状态是否已知?如果题目已经给出了目标状态,可以带来很大便利,这时候可以从起始
状态出发,正向广搜;也可以从目标状态出发,逆向广搜;也可以同时出发,双向广搜。
代码模板
广搜需要一个队列,用于一层一层扩展,一个hashset,用于判重,一棵树(只求长度时不需
要),用于存储整棵树。
对于队列,可以用queue,也可以把vector 当做队列使用。当求长度时,有两种做法:
1. 只用一个队列,但在状态结构体state_t 里增加一个整数字段step,表示走到当前状态用
了多少步,当碰到目标状态,直接输出step 即可。这个方案,可以很方便的变成A* 算法,
把队列换成优先队列即可。
2. 用两个队列,current, next,分别表示当前层次和下一层,另设一个全局整数level,表
示层数(也即路径长度),当碰到目标状态,输出level 即可。这个方案,状态可以少一个字
段,节省内存。
对于hashset,如果有完美哈希方案,用布尔数组(bool visited[STATE_MAX] 或vector<bool> visited(STATE_MAX, false)) 来表示;如果没有,可以用STL 里的set 或unordered_set。
对于树,如果用STL,可以用unordered_map<state_t, state_t > father 表示一颗树,代码非
常简洁。如果能够预估状态总数的上限(设为STATE_MAX),可以用数组(state_t nodes[STATE_-MAX]),即树的双亲表示法来表示树,效率更高,当然,需要写更多代码。
双队列的写法
001 | /** 状态 */ |
002 | struct state_t { |
003 | int data1; /** 状态的数据,可以有多个字段. */ |
004 | int data2; /** 状态的数据,可以有多个字段. */ |
005 | // dataN; /** 其他字段 */ |
006 | int action; /** 由父状态移动到本状态的动作,求动作序列时需要. */ |
007 | int count; /** 所花费的步骤数(也即路径长度-1),求路径长度时需要; |
008 | 不过,采用双队列时不需要本字段,只需全局设一个整数 */ |
009 | bool operator==( const state_t &other) const { |
010 | return true ; // 根据具体问题实现 |
011 | } |
012 | }; |
013 |
014 | // 定义hash函数 |
015 |
016 | // 方法1:模板特化,当hash函数只需要状态本身,不需要其他数据时,用这个方法比较简洁 |
017 | namespace std { |
018 | template <> struct hash<state_t> { |
019 | size_t operator()( const state_t & x) const { |
020 | return 0; // 根据具体问题实现 |
021 | } |
022 | }; |
023 | } |
024 |
025 | // 方法2:函数对象,如果hash函数需要运行时数据,则用这种方法 |
026 | class Hasher { |
027 | public : |
028 | Hasher( int _m) : m(_m) {}; |
029 | size_t operator()( const state_t &s) const { |
030 | return 0; // 根据具体问题实现 |
031 | } |
032 | private : |
033 | int m; // 存放外面传入的数据 |
034 | }; |
035 |
036 | /** |
037 | * @brief 反向生成路径. |
038 | * @param[in] father 树 |
039 | * @param[in] target 目标节点 |
040 | * @return 从起点到target的路径 |
041 | */ |
042 | template < typename state_t> |
043 | vector<state_t> gen_path( const unordered_map<state_t, state_t> &father, |
044 | const state_t &target) { |
045 | vector<state_t> path; |
046 | path.push_back(target); |
047 |
048 | for (state_t cur = target; father.find(cur) != father.end(); |
049 | cur = father.at(cur)) |
050 | path.push_back(cur); |
051 |
052 | reverse(path.begin(), path.end()); |
053 |
054 | return path; |
055 | } |
056 |
057 | /** |
058 | * @brief 广搜. |
059 | * @param[in] state_t 状态,如整数,字符串,一维数组等 |
060 | * @param[in] start 起点 |
061 | * @param[in] grid 输入数据 |
062 | * @return 从起点到目标状态的一条最短路径 |
063 | */ |
064 | template < typename state_t> |
065 | vector<state_t> bfs( const state_t &start, const vector<vector< int >> &grid) { |
066 | queue<state_t> next, current; // 当前层,下一层 |
067 | unordered_set<state_t> visited; // 判重 |
068 | unordered_map<state_t, state_t> father; // 树 |
069 |
070 | int level = 0; // 层次 |
071 | bool found = false ; // 是否找到目标 |
072 | state_t target; // 符合条件的目标状态 |
073 |
074 | // 判断当前状态是否为所求目标 |
075 | auto state_is_target = [&]( const state_t &s) { return true ; }; |
076 | // 扩展当前状态 |
077 | auto state_extend = [&]( const state_t &s) { |
078 | vector<state_t> result; |
079 | // ... |
080 | return result; |
081 | }; |
082 |
083 | current.push(start); |
084 | visited.insert(start); |
085 | while (!current.empty() && !found) { |
086 | ++level; |
087 | while (!current.empty() && !found) { |
088 | const state_t state = current.front(); |
089 | current.pop(); |
090 | vector<state_t> new_states = state_extend(state); |
091 | for (auto iter = new_states.cbegin(); |
092 | iter != new_states.cend() && ! found; ++iter) { |
093 | const state_t new_state(*iter); |
094 |
095 | if (state_is_target(new_state)) { |
096 | found = true ; //找到了 |
097 | target = new_state; |
098 | father[new_state] = state; |
099 | break ; |
100 | } |
101 |
102 | next.push(new_state); |
103 | // visited.insert(new_state); 必须放到 state_extend()里 |
104 | father[new_state] = state; |
105 | } |
106 | } |
107 | swap(next, current); //!!! 交换两个队列 |
108 | } |
109 |
110 | if (found) { |
111 | return gen_path(father, target); |
112 | //return level + 1; |
113 | } else { |
114 | return vector<state_t>(); |
115 | //return 0; |
116 | } |
117 | } |
只用一个队列的写法
双队列的写法,当求路径长度时,不需要在状态里设置一个count 字段记录路径长度,只需全
局设置一个整数level,比较节省内存;只用一个队列的写法,当求路径长度时,需要在状态里设
置一个count 字段,不过,这种写法有一个好处——可以很容易的变为A* 算法,把queue 替换为
priority_queue 即可。
01 | // 与模板1相同的部分,不再重复 |
02 | // ... |
03 |
04 | /** |
05 | * @brief 广搜. |
06 | * @param[in] state_t 状态,如整数,字符串,一维数组等 |
07 | * @param[in] start 起点 |
08 | * @param[in] grid 输入数据 |
09 | * @return 从起点到目标状态的一条最短路径 |
10 | */ |
11 | template < typename state_t> |
12 | vector<state_t> bfs(state_t &start, const vector<vector< int >> &grid) { |
13 | queue<state_t> q; // 队列 |
14 | unordered_set<state_t> visited; // 判重 |
15 | unordered_map<state_t, state_t> father; // 树 |
16 |
17 | int level = 0; // 层次 |
18 | bool found = false ; // 是否找到目标 |
19 | state_t target; // 符合条件的目标状态 |
20 |
21 | // 判断当前状态是否为所求目标 |
22 | auto state_is_target = [&]( const state_t &s) { return true ; }; |
23 | // 扩展当前状态 |
24 | auto state_extend = [&]( const state_t &s) { |
25 | vector<state_t> result; |
26 | // ... |
27 | return result; |
28 | }; |
29 |
30 | start.count = 0; |
31 | q.push(start); |
32 | visited.insert(start); |
33 | while (!q.empty() && !found) { |
34 | const state_t state = q.front(); |
35 | q.pop(); |
36 | vector<state_t> new_states = state_extend(state); |
37 | for (auto iter = new_states.cbegin(); |
38 | iter != new_states.cend() && ! found; ++iter) { |
39 | const state_t new_state(*iter); |
40 |
41 | if (state_is_target(new_state)) { |
42 | found = true ; //找到了 |
43 | target = new_state; |
44 | father[new_state] = state; |
45 | break ; |
46 | } |
47 |
48 | q.push(new_state); |
49 | // visited.insert(new_state); 必须放到 state_extend()里 |
50 | father[new_state] = state; |
51 | } |
52 | } |
53 |
54 | if (found) { |
55 | return gen_path(father, target); |
56 | //return level + 1; |
57 | } else { |
58 | return vector<state_t>(); |
59 | //return 0; |
60 | } |
61 | } |