0%

宽度优先搜索(BFS) | 性价比很高

本文主要关于BFS算法的理解和相关经典习题的练习。

  1. 什么是队列(Queue)?

    队列(queue)是一种采用先进先出(FIFO,first in first out)策略的抽象数据结构。比如生活中排队,总是按照先来的先服务,后来的后服务。队列在数据结构中举足轻重,其在算法中应用广泛,最常用的就是在宽度优先搜索(BFS)中,记录待扩展的节点

  2. Queue内部存储数据的两种结构

    • Array:数组对随机访问有较好性能。queue.pop()的时间复杂度是O(n)。
    • LinkedList:插入删除元素有较好性能。
  3. C++中,使用中的queue模板类,模板需两个参数,元素类型和容器类型,元素类型必要,而容器类型可选,默认deque,可改用list(链表)类型。

  4. ==如何自己用数组实现一个队列???==

    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
    template <typename T>
    bool Queue<T>::enqueue(T data)
    {
    if(isFull()) {
    return false;
    }

    queue_[rear_] = data;
    rear_ = (rear_ + 1) % capacity_;

    return true;
    }

    template <typename T>
    T Queue<T>::dequeue()
    {
    T data;

    if(isEmpty()) {
    throw std::out_of_range("Queue::dequeue() - Empty queue");
    }

    data = queue_[front_];
    front_ = (front_ + 1) % capacity_;

    return data;
    }
  5. 什么时候使用宽搜?二叉树、图、矩阵、拓扑排序

    • 树/图的遍历 Traversal in Graph: 比如给出无向连通图(Undirected Connected Graph)中的一个点,找到这个图里的所有点。

      1. 层级遍历 Level Order Traversal
      2. 由点及面 Connected Component
      3. 拓扑排序 Topological Sorting
    • 最短路径 Shortest Path in Simple Graph

      1. 最短路径算法有很多种,==BFS 是其中一种,但是必须是在Simple Graph (每条边长度都是1)中求最短路径==。
      2. 大部分Simple Graph中使用 BFS 算法时,都是无向图,也有可能是有向图,但是在面试中极少会出现。==A* search在面试中问过,推荐掌握。==
      3. ==如果问最长路径,则要用Dynamic Programming。分层:走到第i层一定会经过第i - 1层。不可以分层:DFS。==
    • 非递归的方式找所有方案 ???

  6. 二叉树中的BFS 和图的 BFS 最大的区别:

    ==二叉树中无需使用 HashSet(C++: unordered_map, Python: dict) 来存储访问过的节点(丢进过 queue 里的节点)(这样做是为了避免存在环的情况,那么同一个节点就可能重复进入队列)==。

  7. 图的表示形式

    • 邻接矩阵 Adjacency Matrix
    1
    2
    3
    4
    5
    6
    7
    8
    9
    [
    [1,0,0,1],
    [0,1,1,0],
    [0,1,1,0],
    [1,0,0,1]
    ]
    /* A[i][j] = 1 means ith node connects to/with the the jth node
    上式中,0号点和3号点有连边。1号点和2号点有连边。当然,每个点和自己也是默认有连边的。图中的 `0` 表示不连通,`1` 表示连通。邻接矩阵我们可以直接用一个二维数组表示,如`int[][] matrix;`。这种数据结构因为耗费 O(n^2) 的空间,所以在稀疏图上浪费很大,因此并不常用。
    */
    • 邻接表 Adjacency List - 相当于Adjacency Matrix的稀疏表示
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    [
    [1],
    [0,2,3],
    [1],
    [1]
    ]
    /* A[i] is a list/vector of nodes who connect with ith node
    上图表示 0 和 1 之间有连边,1 和 2 之间有连边,1 和 3 之间有连边。即每个点上存储自己有哪些邻居(有哪些连通的点)。
    而邻接矩阵因为耗费空间过大,我们通常在工程中都是使用邻接表作为图的存储结构。
    */
    • 用HashMap 和 HashSet 搭配的方式来实现Adjacency Matrix
    1
    2
    3
    4
    5
    unordered_map<T, unordered_set<T>> graph;

    /*
    通常这种hashmap的实现可以换成vector<vector<T>>来表示,但是如果在过程中需要check是否已有某个node,用map和set会比较方便,如果不需要的话就只是用固定数量的vector,就比较省空间时间。"vector可以方便实现就用vector,不方便就还是用hashmap或者hashset"
    */

    而邻接矩阵因为耗费空间过大(尤其是在稀疏图上浪费很大),我们通常在工程中都是使用邻接表作为图的存储结构。另外,还可以用自定义的类来实现邻接表,但一般是在实际的工程中应用。

  8. 拓扑排序 Topological Sorting

    • Topological Order的定义

      在图论中,由一个有向无环图的顶点组成的序列,如果满足以下条件:

      1. 每个顶点出现且只出现一次;

      2. 若A在序列中排在B的前面,则在图中不存在从B到A的路径。

        拓扑排序是对有向无环图的顶点的一种排序,它使得如果存在一条从顶点A到顶点B的路径,那么在排序中B出现在A的后面。

    • 实际应用:

      • 检测编译时的循环依赖
      • 制定有依赖关系的任务的执行顺序
    • ==Topological Sorting 并不是一种严格意义上的 Sorting Algorithm==。

    • ==一张图的拓扑序列可以有很多个,也可能没有。== 拓扑排序只需要找到其中一个序列,无需找到所有序列。

    • Topological Sorting Algorithm Description 在有向图中,如果存在一条有向边 A-->B,那么我们认为这条边为 A 增加了一个==出度(out-degree)==,为 B 增加了一个==入度(in-degree)==。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      Topological Sorting based on BFS

      1. 统计所有点的入度,并初始化拓扑序列为空。
      2. 将所有入度为 0 的点,放到宽度优先搜索的队列中
      3. 将队列中的点一个一个的释放出来,放到拓扑序列中,每次释放出某个点 A 的时候,就访问 A 的相邻点(所有A指向的点),并把这些点的入度减去 1
      4. 如果发现某个点的入度被减去 1 之后变成了 0,则放入队列中。
      5. 直到队列为空时,算法结束。

      Tological Sorting based on DFS
    • 拓扑排序的4种问法

      • return any topological order
      • whether exist topological sorder?
      • return all topological orders - DFS
      • whether unique topological order? - Queue最多同时只有1个节点
  9. BFS traversal模板

    • 复杂度:O(N + M) N为点数,M为边数。说是O(M)问题也不大,因为M一般都比N大。
    • 需要分层遍历的bfs
      • 如果问题需要你区分开不同层级的结果信息,如binary tree level order traversal
      • 简单图最短路径问题(因为要以traverse的level数量作为最短路径长度)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    queue<T> que;
    unordered_set<T, Hasher> seen; //记录是否已被访问过

    seen.insert(start);
    que.push(start);
    while (!que.empty()) {
    int size = que.size();
    //必须单独拎出来。que.size()直接放在for loop的条件中会成为一个动态变化的值,因为这个每次loop都可能改变que的size。
    //所以必须先把当前层一共有多少个节点存在局部变量 size 中,才不会把下一层的节点也在当前层进行扩展。
    for (int i = 0; i < size; ++i) {
    const T &head = que.front();
    que.pop();
    for (const T &neighbor : head.neighbors) {
    if (!seen.count(neighbor)) {
    seen.insert(neighbor);
    que.push(neighbor);
    }
    }
    }
    }
    • 无需分层遍历的bfs
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    queue<T> que;
    unordered_set<T, Hasher> seen; //记录是否已被访问过(这在图中需要,但在二叉树中则不需要)

    seen.insert(start);
    que.push(start);
    while (!que.empty()) {
    const T &head = que.front();
    que.pop();
    for (const T &neighbor : head.neighbors) {
    if (!seen.count(neighbor)) { //check whether already visited
    seen.insert(neighbor); //insert the just-visited node into visited set
    que.push(neighbor); //push into Queue-to-explore(存储等待被拓展到下一层的节点)
    }
    }
    }
    //set/seen 与 queue 是一对好基友,无时无刻都一起出现,往 queue 里新增一个节点,就要同时丢到 set 里。
  10. 对于矩阵R * C的情况,复杂度是O(RC + 2RC) = O(RC)

1
2
3
//坐标变化数组(矩阵中允许四个方向的走法)
int[] deltaX = {1,0,0,-1};
int[] deltaY = {0,1,-1,0};

  1. 其他常见实现方法:

    • 两个队列来回迭代,来代表相邻的层级

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      queue<T> q1, q2;
      q1.push(startNode);
      int level = 0;

      while (!q1.empty()) {
      int size = q1.size();
      for (int i = 0; i < size; i++) {
      T head = q1.front();
      q1.pop();
      for (all neighbors of head) {
      q2.push(neighbor);
      }
      }
      q1 = q2; //直接通过将q1替换为q2,实现到下一层的转换
      level++;
      }
    • 使用 dummy node 来间隔不同的层级

      • Dummy Node 一般本身不存储任何实际有意义的值,通常用作"占位",或者链表的“虚拟头”。如很多的链表问题中,我们会在原来的头head的前面新增一个节点,这个节点没有任何值,但是 next 指向 head。这样就会方便对 head 进行删除或者在前面插入等操作。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      queue<T> q;
      q.push(startNode);
      q.push(NULL);
      int level = 0;

      // 因为有 dummy node 的存在,不能再用 isEmpty 了来判断是否还有没有拓展的节点了
      while (q.size() > 1) {
      T head = q.front();
      q.pop();
      if (head == NULL) {
      level++;
      q.push(NULL);
      continue;
      }
      for (all neighbors of head) {
      q.push(neighbor);
      }
      }
  2. Summary

    • 能用BFS的时候一定不要用DFS(除非面试官有特别要求)
    • 找所有连通图(块)的问题,相当于是==找所有点,用BFS==。
    • 找所有排列的问题,相当于是==找所有路径,用DFS==。
    • BFS的三中使用情况:
      1. 图的遍历(由点及面,层级遍历)
      2. 简单图最短路径
      3. ==拓扑排序必须掌握!!!== (依据indegree和outdegree的概念)
    • 是否需要层级遍历
      1. size = queue.size()
    • 坐标变化数组:
      1. deltaX, deltaY
      2. inBound

图的遍历

Binary Tree Level Order Traversal(层级遍历)

Given a binary tree, return the level order traversal of its nodes' values. (ie, from left to right, level by level).

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
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode * root) {
vector<vector<int>> results;
if (!root) return results;

queue<TreeNode *> q;
q.push(root);
while(!q.empty()) {
vector<int> level;
int n = q.size();
for (int i = 0; i < n; i++){
TreeNode * node = q.front();
q.pop();

level.push_back(node->val);

if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
results.push_back(level);
}

return results;
}
};

Number of Islands(由点及面)

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
class Solution {
private:
vector<int> delta_x = {-1, 0, 1, 0};
vector<int> delta_y = {0, -1, 0, 1};
public:
int numIslands(vector<vector<bool>> &grid) {
int m = grid.size();
int n = m > 0 ? grid[0].size() : 0;
if (m == 0) return 0;

int count = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 0) continue;
bfs(grid, i, j); //由点及面
count++;
}
}

return count;
}

//bfs traversal(由点及面)
void bfs(vector<vector<bool>> & grid, int r, int c) {
int m = grid.size(), n = grid[0].size(); //调用此函数时m肯定是不为0的

queue<pair<int, int>> q;
q.push({r, c});
grid[r][c] = 0; //note as visited by modifying into 0

while(!q.empty()) {
int size = q.size();
for (int i = 0; i < size; i++) {
pair<int, int> curr = q.front();
q.pop();

//explore neighbors
for (int j = 0; j < 4; j++) {
pair<int, int> next(curr.first + delta_x[j], curr.second + delta_y[j]);
if (next.first < 0 || next.first >= m || next.second < 0 || next.second >= n) {
continue;
}
if (grid[next.first][next.second] == 1) {
q.push(next);
grid[next.first][next.second] = 0; //note as visited by modifying into 0
}
}
}
}
}
};

Serialize and Deserialize Binary Tree (层级遍历)

Design an algorithm and write code to serialize and deserialize a binary tree. Writing the tree to a file is called serialization and reading back from the file to reconstruct the exact same binary tree is deserialization.

  • serialize: This method will be invoked first, you should design your own algorithm to serialize a binary tree which denote by a root node to a string which can be easily deserialized by your own "deserialize" method later.
  • deserialize: This method will be invoked second, the argument data is what exactly you serialized at method "serialize", that means the data is not given by system, it's given by your own serialize method. So the format of data is designed by yourself, and deserialize it here as you serialize it in "serialize" method.
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
class Solution {
public:
string serialize(TreeNode * root) {
if (root == NULL) return "";

queue<TreeNode*> q;
q.push(root);
string ans = "";

//在bfs的同时直接转成expected string,而不经过中间阶段(vector of nodes)
while (!q.empty()) {
TreeNode* node = q.front();
q.pop();
if (node == NULL) {
ans += "#,";
continue;
}
ans += (to_string(node->val) + ",");
q.push(node->left);
q.push(node->right);
}
ans.erase(ans.end() - 1); //删掉最后的','
return ans;
}

TreeNode * deserialize(string &data) {
if (data == "") return NULL;

//vector<string> vals = split(data, ",");
vector<string> vals;
istringstream ss(data);
string token;
while(getline(ss, token, ',')) {
vals.push_back(token);
}

TreeNode * root = new TreeNode(atoi(vals[0].c_str())); //先要确定root
queue<TreeNode *> q; //用queue只是为了实现level order traversal
q.push(root);

//最关键的部分!!!
bool isLeftChild = true; //初始情况下是先走left child
for (int i = 1; i < vals.size(); i++) {
if (vals[i] != "#") {
TreeNode * node = new TreeNode(atoi(vals[i].c_str()));
if (isLeftChild) q.front()->left = node;
else q.front()->right = node;
q.push(node);
}
if (!isLeftChild) q.pop(); //关键步骤:当一个node的leftChild和rightChild都已经连接之后,就pop掉queue里这个parent。
isLeftChild = !isLeftChild;
}

return root;
}

//split according to delim可以用stl的getline(ss, token, delim)来代替。
vector<string> split(string &data, string delim) {
int lastInd = 0, ind;
vector<string> results;
while((ind = data.find(delim, lastInd)) != string::npos) {
string tmp = data.substr(lastInd, ind - lastInd);
results.push_back(tmp);
lastInd = ind + delim.size(); //update start searching index
}
//单独push最后一个substring
if (lastInd < data.size()) {
string tmp = data.substr(lastInd, data.size() - lastInd);
results.push_back(tmp);
}
return results;
}
};

Clone Graph

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
//bfs
class Solution1 {
private:
unordered_map<UndirectedGraphNode*, UndirectedGraphNode*> map;
//用map的目的一是为了后面去check是否visited,二是为了建立一一对应的关系而实现deepcopy.
public:
UndirectedGraphNode* cloneGraph(UndirectedGraphNode* node) {
if (!node) return NULL;

UndirectedGraphNode* newNode = new UndirectedGraphNode(node->label);
map[node] = newNode;

queue<UndirectedGraphNode*> q;
q.push(node);
while(!q.empty()) {
UndirectedGraphNode* curr = q.front();
q.pop();
for (UndirectedGraphNode* neighbor : curr->neighbors) {
if (map.find(neighbor) == map.end()) {
map[neighbor] = new UndirectedGraphNode(neighbor->label);
q.push(neighbor);
}
map[curr]->neighbors.push_back(map[neighbor]); //!!!
}
}
return newNode;
}
};

//dfs
class Solution2 {
private:
unordered_map<UndirectedGraphNode*, UndirectedGraphNode*> map;
public:
UndirectedGraphNode* cloneGraph(UndirectedGraphNode* node) {
if (!node) return NULL;

if (map.find(node) == map.end()) {
map[node] = new UndirectedGraphNode(node->label);
for (UndirectedGraphNode* neighbor : node->neighbors) {
map[node]->neighbors.push_back(cloneGraph(neighbor));//clongGraph return的是和输入对应的copied node
}
}
return map[node];
}
};

简单图最短路径

Shortest Path in Undirected Graph

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
/**
* Definition for Undirected graph.
* struct UndirectedGraphNode {
* int label;
* vector<UndirectedGraphNode *> neighbors;
* UndirectedGraphNode(int x) : label(x) {};
* };
*/
class Solution {
public:
int shortestPath(vector<UndirectedGraphNode*> graph, UndirectedGraphNode* A, UndirectedGraphNode* B) {
queue<UndirectedGraphNode*> q;
unordered_set<UndirectedGraphNode*> visited; //记录visited,避免重复
q.push(A);
visited.insert(A);

int level = 0;
while(!q.empty()) {
int size = q.size();
for (int i = 0; i < size; i++) {
UndirectedGraphNode* curr = q.front();
q.pop();
for (UndirectedGraphNode* neighbor : curr->neighbors) {
if (visited.find(neighbor) != visited.end()) {
continue;
}
if (neighbor == B) {
return level + 1;
}
q.push(neighbor);
visited.insert(neighbor);
}
}
level++;
}
}
};

双向宽度优先搜索算法(Bidirectional BFS

  • 无向图

  • 所有边的长度都为 1 或者长度都一样

  • 同时给出了起点和终点

    以上 3 个条件都满足的时候,可以使用双向宽度优先搜索来求起点和终点的最短距离

    如果在面试中被问到了如何优化 BFS 的问题,Bidirectional BFS 几乎就是标准答案了。

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
//算法描述:双向宽度优先搜索本质上还是BFS,只不过变成了起点向终点和终点向起点同时进行扩展,直至两个方向上出现同一个子节点,搜索结束。我们还是可以利用队列来实现:一个队列保存从起点开始搜索的状态,另一个保存从终点开始的状态,两边如果相交了,那么搜索结束。起点到终点的最短距离即为起点到相交节点的距离与终点到相交节点的距离之和。

//假设单向BFS需要搜索 N 层才能到达终点,每层的判断量为 X,那么总的运算量为 X ^ N. 如果换成是双向BFS,前后各自需要搜索 N / 2 层,总运算量为 2 * X ^ {N / 2} 。如果 N 比较大且X 不为 1,则运算量相较于单向BFS可以大大减少,差不多可以减少到原来规模的根号的量级。

int doubleBFS(Node * start, Node * end) {
if (start == end) return 1;
queue<Node> startQ, endQ;
startQ.push(start);
endQ.push(end);
unordered_set<Node> startVisited, endVisited;
startVisited.insert(start);
endVisited.insert(end);

int step = 0;
while (!startQ.empty() || !endQ.empty()) {
int startSize = startQ.size();
step++;
for (int i = 0; i < startSize; i++) {
Node * curr = startQ.front();
startQ.pop();
for (Node * neighbor : curr->neighbors) {
if (startVisited.count(neighbor)) { //重复节点
continue;
}
if (endVisited.count(neighbor)) { //相交
return step;
}
startQ.push(neighbor);
startVisited.insert(neighbor);
}
}

int endSize = endQ.size();
step++;
for (int i = 0; i < endSize; i++) {
Node * curr = endQ.front();
endQ.pop();
for (Node * neighbor : curr->neighbors) {
if (endVisited.count(neighbor)) { //重复节点
continue;
}
if (startVisited.count(neighbor)) { //相交
return step;
}
endQ.push(neighbor);
endVisited.insert(neighbor);
}
}
}

return -1; //不连通
}

Word Ladder(隐式简单图)

Given two words (start and end), and a dictionary, find the shortest transformation sequence from start to end, output the length of the sequence. Transformation rule such that:

  1. Only one letter can be changed at a time
  2. Each intermediate word must exist in the dictionary. (Start and end words do not need to appear in the dictionary )
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
# 自己的solution:BFS traverse nodes level by level; return level + 1 when the next node matches end.
# 一个难点在于:Implicit BFS - 要自己去构建graph,这里的每个node是string
class Solution {
public:
int ladderLength(string &start, string &end, unordered_set<string> &dict) {
if (start == end) return 1; //corner case

int count = 1;
queue<string> q;
q.push(start);
if (dict.find(start) != dict.end()) dict.erase(start); //通过erase已经访问过的string来记录为visited

while(!q.empty()) { //开始bfs
int n = q.size();
for (int i = 0; i < n; i++) {
string curr = q.front();
q.pop();

//explore neighbors of curr
for (int i = 0; i < curr.size(); i++) { //枚举替换位置
for (int j = 0; j < 26; j++) { //枚举替换字母
string tmp = curr;
if (tmp[i] == 'a' + j) continue;
tmp[i] = 'a' + j;
if (tmp == end) return count + 1;

if (dict.find(tmp) != dict.end()) {
q.push(tmp);
dict.erase(tmp); //通过erase已经访问过的string来记录为visited
}
}
}
}
count++; //count levels traversed during bfs
}
return 0;
}
};

Knight Shortest Path(grid简单图)

Given a knight in a chessboard (a binary matrix with 0 as empty and 1 as barrier) with a source position, find the shortest path to a destination position, return the length of the route. Return -1 if destination cannot be reached.

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
/**
* Definition for a point.
* struct Point {
* int x;
* int y;
* Point() : x(0), y(0) {}
* Point(int a, int b) : x(a), y(b) {}
* };
*/

class Solution {
private:
vector<int> delta_x = {1, 1, -1, -1, 2, 2, -2, -2};
vector<int> delta_y = {2, -2, 2, -2, 1, -1, 1, -1};

public:
int shortestPath(vector<vector<bool>> &grid, Point &source, Point &destination) {
if (grid.size() == 0) return -1;
if (grid[0].size() == 0) return -1;
if (grid[destination.x][destination.y] == 1) return -1;
if (source.x == destination.x && source.y == destination.y) return 0;

queue<Point> q;
q.push(source);
int level = 0; //level数就是可能走的步数
while(!q.empty()) {
int size = q.size();
for (int i = 0; i < size; i++) {
Point curr = q.front();
q.pop();

for (Point &next : get_next(curr, grid)) {
if (next.x == destination.x && next.y == destination.y) {
return level + 1;
}
q.push(next);

}
}
level++;
}

return -1;
}

//helper
vector<Point> get_next(Point &p, vector<vector<bool>> &grid) {
vector<Point> results;
int m = grid.size(), n = grid[0].size();

for (int i = 0; i < 8; i++) {
int next_x = p.x + delta_x[i];
int next_y = p.y + delta_y[i];
if (next_x < 0 || next_x >= m || next_y < 0 || next_y >= n) continue;//out of bounds
if (grid[next_x][next_y] == 1) continue; //避开障碍点
Point next_p(next_x, next_y);
results.push_back(next_p);
}

return results;
}
};

Knight Shortest Path II

起点和终点分别是左上角和右下角。移动过程中限制"只能往右移动"。

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
class Solution1 {
private:
vector<int> delta_x = {1, -1, 2, -2};
vector<int> delta_y = {2, 2, 1, 1}; //only from left to right

public:
int shortestPath2(vector<vector<bool>> &grid) {
int n = grid.size();
if (n == 0) return -1;
int m = grid[0].size();
if (m == 0) return -1;

queue<pair<int, int>> q;
q.push({0, 0});
vector<vector<bool>> visited(n, vector<bool>(m, false));
visited[0][0] = true;

int level = 0;
while (!q.empty()) {
int size = q.size();
for (int i = 0; i < size; i++) {
pair<int, int> curr = q.front();
q.pop();

for (int j = 0; j < 4; j++) {
int next_x = curr.first + delta_x[j];
int next_y = curr.second + delta_y[j];

if (next_x < 0 || next_x >= n || next_y >= m ) continue;
if (grid[next_x][next_y]) continue;
if (visited[next_x][next_y]) continue;

pair<int, int> next = {next_x, next_y};
if (next.first == n - 1 && next.second == m - 1) {
return level + 1;
}
q.push(next);
//cout << "x: " << next.first << " y: " << next.second << endl;
visited[next.first][next.second] = true;
}
}
level++;
}

return -1;
}
};

//DP解法:每个点都有至多4个前缀点,直接求最小值转移即可。
class Solution {
public:
int shortestPath2(vector<vector<bool>> &grid) {
int n = grid.size();
if (n == 0) return -1;
int m = grid[0].size();
if (m == 0) return -1;

vector<vector<int>> f(n, vector<int>(m, INT_MAX));
f[0][0] = 0;
for (int j = 1; j < m; j++) {
for (int i = 0; i < n; i++) {
if (grid[i][j]) continue;
//以下四种情况分别对应四个前缀点
if (i - 1 >= 0 && j - 2 >= 0 && f[i - 1][j - 2] != INT_MAX) {
f[i][j] = min(f[i][j], f[i - 1][j - 2] + 1);
}
if (i + 1 < n && j - 2 >= 0 && f[i + 1][j - 2] != INT_MAX) {
f[i][j] = min(f[i][j], f[i + 1][j - 2] + 1);
}
if (i - 2 >= 0 && j - 1 >= 0 && f[i - 2][j - 1] != INT_MAX) {
f[i][j] = min(f[i][j], f[i - 2][j - 1] + 1);
}
if (i + 2 < n && j - 1 >= 0 && f[i + 2][j - 1] != INT_MAX) {
f[i][j] = min(f[i][j], f[i + 2][j - 1] + 1);
}
}
}

if (f[n - 1][m - 1] == INT_MAX) return -1;
return f[n - 1][m - 1];
}
};

#拓扑排序问题

Topological Sorting(原型)

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
/**
* Definition for Directed graph.
* struct DirectedGraphNode {
* int label;
* vector<DirectedGraphNode *> neighbors;
* DirectedGraphNode(int x) : label(x) {};
* };
*/

class Solution {
public:
vector<DirectedGraphNode*> topSort(vector<DirectedGraphNode*>& graph) {
if (graph.size() == 0) return vector<DirectedGraphNode*>();

//Calculating indegree
unordered_map<DirectedGraphNode*, int> node_to_degree;
for (int i = 0; i < graph.size(); i++) {
for (DirectedGraphNode* neighbor : graph[i]->neighbors) {
node_to_degree[neighbor]++;
}
}
/*因为有些nodes在前面并没有被统计到,比如{0,1,2,3,4#1,3,4#2,1,4#3,4#4}这个case就没有先统计到0。
所以需要这一步来检查没统计到map里的nodes,将in-degree主动设为0(这也是前面没被统计到的原因)。*/
for (DirectedGraphNode* node : graph) {
if (node_to_degree.count(node) == 0) {
node_to_degree[node] = 0;
}
}

vector<DirectedGraphNode*> ans;
queue<DirectedGraphNode*> q;
//先是所有indegree为0的nodes
for (auto& m : node_to_degree) {
if (m.second == 0) q.push(m.first);
}
//按照bfs的套路来,每遇到新的indegree=0的node(要先--),就push到queue中。
while(!q.empty()) {
int size = q.size();
for (int i = 0; i < size; i++) {
DirectedGraphNode* curr = q.front();
q.pop();
ans.push_back(curr);

for (DirectedGraphNode* neighbor : curr->neighbors) {
node_to_degree[neighbor]--;
if (node_to_degree[neighbor] == 0) {
q.push(neighbor);
}
}
}
}

return ans;
}
};

Course Schedule (是否可行)

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
//裸拓扑排序(不考虑分层)
class Solution {
public:
bool canFinish(int numCourses, vector<pair<int, int>>& prerequisites) {
vector<unordered_multiset<int>> g(numCourses);
vector<int> degree(numCourses, 0); //未在prerequisites中被统计到的话,就自动归为0了。
for (pair<int, int>& p: prerequisites) {
g[p.second].insert(p.first);
degree[p.first]++;
}

queue<int> q;
//push in-degree-0 nodes
for (int i = 0; i < numCourses; i++) {
if (degree[i] == 0) q.push(i);
}

int count = 0;
while(!q.empty()) {
int curr = q.front();
q.pop();
count++;

for (int neighbor : g[curr]) {
degree[neighbor]--;
if (degree[neighbor] == 0) q.push(neighbor);
}
}
return count == numCourses;
//与count对应的node的sequence如果正好包括全部的node,那就说明形成了topologic order。
}
};

// 通过bfs去check每一个course,是否会形成一个循环(如果有会形成循环的,则不能finish;如果没有,则能finish)。
// - runtime error(一些冗长的case)
class Solution2 {
public:
bool canFinish(int numCourses, vector<pair<int, int>>& prerequisites) {
//create graph: 以每门课的prerequistes作为neighbors
unordered_map<int, unordered_set<int>> g;
for (pair<int, int> pre : prerequisites) {
g[pre.second].insert(pre.first);
}

//check
for (int i = 0; i < numCourses; i++) {
if (g[i].empty()) continue;
if (isCyle(i, g)) return false;
}
return true;
}

//check whether course forms a cycle starting from course and ending with course
bool isCyle(int course, unordered_map<int, unordered_set<int>>& graph) {
queue<int> q;
q.push(course);
unordered_set<int> visited;
visited.insert(course);

while(!q.empty()) {
int curr = q.front();
q.pop();
for (int neighbor : graph[curr]) {
if (neighbor == course) {
return true;
}
if (visited.find(neighbor) == visited.end()) {
q.push(neighbor);
}
}
}
return false;
}
};

Course Schedule II (一种拓扑order)

  • 这里值得注意的是:对于prerequisites中可能会出现重复的情况,一种就是用multiset相同的数也都insert进去,后面再去掉;另外一种就是用unordered_set, 出现相同的情况直接跳过(不然的话,degree增加了,但实际的neighbors没有发生变化,导致出错)。
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
class Solution {
public:
vector<int> findOrder(int numCourses, vector<pair<int, int>> &prerequisites) {
vector<unordered_set<int>> g(numCourses);
vector<int> degree(numCourses);
for(pair<int, int> &p : prerequisites) {
if (g[p.second].count(p.first) == 0) { //只是为了避免出现相同prerequisite的情况
g[p.second].insert(p.first);
degree[p.first]++;
}
}

queue<int> q;
vector<int> order;
for (int i = 0; i < numCourses; i++) {
if (degree[i] == 0) q.push(i);
}
while(!q.empty()) {
int curr = q.front();
q.pop();
order.push_back(curr);

for (int next : g[curr]) {
degree[next]--;
if (degree[next] == 0) q.push(next);
}
}

if (order.size() == numCourses) return order;
return {};
}
};

Sequence Reconstruction (Unique Order)

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
// seqs里的seq是sequence的subsequence,比如[1, 2],这意味着1指向2。
// 判断是否有Unique Topological Order:每次iteration的时候queue里只有一个node,其他的做法和general的类似。
class Solution {
public:
/**
* @param org: a permutation of the integers from 1 to n
* @param seqs: a list of sequences
* @return: true if it can be reconstructed only one or false
*/
bool sequenceReconstruction(vector<int> &org, vector<vector<int>> &seqs) {
int n = org.size();

int count = 0; //count the total length of all seqs
unordered_map<int, unordered_set<int>> graph;
vector<int> indegree(n);
for (vector<int> seq : seqs) {
count += seq.size();
if (seq.size() >= 1 && (seq[0] < 1 || seq[0] > n)) return false;
for (int i = 1; i < seq.size(); i++) {
if (seq[i] < 1 || seq[i] > n) return false;
//避免重复计算indegree,比如这个case: [[5,2,6,3],[4,1,5,2]] 5->2有两次
if (graph[seq[i - 1]].count(seq[i]) == 0) {
graph[seq[i - 1]].insert(seq[i]); //相邻node之间有前后指向关系
indegree[seq[i] - 1]++; //node n 对应到 indegree的第n-1位
}
}
}

/*case: [1]
[]
*/
if (count < n) return false;

queue<int> q;
for (int i = 0; i < indegree.size(); i++) {
if (indegree[i] == 0) q.push(i + 1);
}
int k = 0;
while(q.size() == 1) { //queue should always have only one node
int curr = q.front();
q.pop();
for (int next : graph[curr]) {
indegree[next - 1]--;
if (indegree[next - 1] == 0) q.push(next);
}
if (curr != org[k]) return false; //而且唯一的那一个要和unique order里的对应node一样
k++;
}

return k == n;
}
};
Buy me a Coffee.