引言
对图的遍历:深搜、广搜
与图连通性相关的算法:并查集
深搜dfs
深搜是认准一个方向去搜,直到碰壁之后再换方向;换方向是撤销原路径,改为节点连接的下一个路径
比如从节点1到节点6,那么可以走1->5->4->3->6
然后回溯,走1->5->4->6
dfs的框架和回溯差不多:
void dfs(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本节点所连接的其他节点) {
处理节点;
dfs(图,选择的节点); // 递归
回溯,撤销处理结果
}
}
广搜bfs
广搜是一圈一圈的搜索,如下图,从start开始,一圈一圈,向外搜索
广搜适合解决两点之间的最短路径问题
广搜中一圈一圈的搜索使用的容器可以是queue,也可以是stack
用队列的话,就是保证每一圈都是一个方向去转,例如统一顺时针或者逆时针
如果用栈的话,就是第一圈顺时针遍历,第二圈逆时针遍历,第三圈又顺时针遍历
因为queue比较方便,所以后续写题直接用queue
广搜模板:
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 表示四个方向
// grid 是地图,也就是一个二维数组
// visited标记访问过的节点,不要重复访问
// x,y 表示开始搜索节点的下标
void bfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int x, int y) {
queue<pair<int, int>> que; // 定义队列
que.push({x, y}); // 起始节点加入队列
visited[x][y] = true; // 只要加入队列,立刻标记为访问过的节点
while(!que.empty()) { // 开始遍历队列里的元素
pair<int, int> cur = que.front();//当前节点
que.pop();
for (int i = 0; i < 4; i++) { // 开始想当前节点的四个方向左右上下去遍历
int nextx = cur.first + dir[i][0];
int nexty = cur.second + dir[i][1]; // 获取周边四个方向的坐标
if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 坐标越界了,直接跳过
if (!visited[nextx][nexty]) { // 如果节点没被访问过
que.push({nextx, nexty}); // 队列添加该节点为下一轮要遍历的节点
visited[nextx][nexty] = true; // 只要加入队列立刻标记,避免重复访问
}
}
}
}
并查集
并查集可以解决连通性问题(一般是解决无向图的连通性问题,如果是有向图的话,直接dfs或bfs更简单),或查看两个节点是否在同一个集合
并查集有两个功能:
①将两个元素添加到一个集合中
②判断两个元素在不在同一个集合
并查集的简单思想是:如果将A,B,C放到同一个集合,即将其连通,那么可以通过father[A] = B, father[B] = C
表示连通,比放到二维vector方便很多
模板:
int n = 1005; // n根据题目中节点数量而定,若题目说明节点从0开始记,那n=题目数组的size;若题目说从1开始记,那n=题目数组的size+1
vector<int> father = vector<int> (n, 0); // C++里的一种数组结构
// 并查集初始化
void init() {
for (int i = 0; i < n; ++i) {
father[i] = i;
}
}
// 并查集里寻根的过程
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]); // 路径压缩
//路径压缩就是将非根节点直接指向根节点,这样访问的时候更快,如下图
}
// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
// 将v->u 这条边加入并查集
void join(int u, int v) {
u = find(u); // 寻找u的根
v = find(v); // 寻找v的根
if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
father[v] = u;
}
并查集的空间复杂度是 o(n)
时间复杂度在 o(logn) 和 o(1) 之间, 且随着查询或合并操作的增加,时间复杂度会越来越趋于 o(1)
岛屿数量
题目
深搜解法
class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
vector<vector<bool>> visited(grid.size(), vector<bool>(grid[0].size(), false));
int res = 0;
for (int i = 0; i < grid.size(); i++) {
for (int j = 0; j < grid[0].size(); j++) {
if (!visited[i][j] && grid[i][j] == '1') {
dfs(grid, visited, i, j);
res++;
}
}
}
return res;
}
private:
int dir[4][2] = {1, 0, 0, -1, -1, 0, 0, 1};
void dfs(vector<vector<char>>& grid, vector<vector<bool>>& visited ,int startX, int startY) {
visited[startX][startY] = true;
for (int i = 0; i < 4; i++) {
int nextX = startX + dir[i][0];
int nextY = startY + dir[i][1];
if (nextX < 0 || nextY < 0 || nextX >= grid.size() || nextY >= grid[0].size()) continue;
if (!visited[nextX][nextY] && grid[nextX][nextY] == '1') {
visited[nextX][nextY] = true;
dfs(grid, visited, nextX, nextY);
}
}
}
};
广搜解法
class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
vector<vector<bool>> visited(grid.size(), vector<bool>(grid[0].size(), false));
int res = 0;
for (int i = 0; i < grid.size(); i++) {
for (int j = 0; j < grid[0].size(); j++) {
if (!visited[i][j] && grid[i][j] == '1') {
bfs(grid, visited, i, j);
res++;
}
}
}
return res;
}
private:
int dir[4][2] = {0, 1, 1, 0, 0, -1, -1, 0};
void bfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int startX, int startY) {
queue<pair<int, int>> que;
que.push({startX, startY});
visited[startX][startY] = true;
while (!que.empty()) {
pair<int, int> cur = que.front();
que.pop();
for (int i = 0; i < 4; i++) {
int nextX = cur.first + dir[i][0];
int nextY = cur.second + dir[i][1];
if (nextX < 0 || nextX >= grid.size() || nextY < 0 || nextY >= grid[0].size()) continue;
if (!visited[nextX][nextY] && grid[nextX][nextY] == '1') {
que.push({nextX, nextY});
visited[nextX][nextY] = true;
}
}
}
}
};
被围绕的区域
题目
思路
先遍历边界,把边界上的‘O’全部换成‘A’;再遍历整个表,把内部的‘O’全部换成‘X’,把‘A’全部换成‘O’
广搜解法
以广搜举例:
class Solution {
public:
void solve(vector<vector<char>>& board) {
//处理边界,将与边界相连的一整块的O改为A
for (int i = 0; i < board.size(); i++) {
if (board[i][0] == 'O') bfs(board, i, 0);
if (board[i][board[0].size() - 1] == 'O') bfs(board, i, board[0].size() - 1);
}
for (int j = 0; j < board[0].size(); j++) {
if (board[0][j] == 'O') bfs(board, 0, j);
if (board[board.size() - 1][j] == 'O') bfs(board, board.size() - 1, j);
}
//处理节点,将O全部换成X,将A全部换成O
for (int i = 0; i < board.size(); i++) {
for (int j = 0; j < board[0].size(); j++) {
if (board[i][j] == 'O') board[i][j] = 'X';
if (board[i][j] == 'A') board[i][j] = 'O';
}
}
}
private:
int dir[4][2] = {0, 1, 1, 0, 0, -1, -1, 0};
void bfs(vector<vector<char>>& board, int startX, int startY) {
board[startX][startY] = 'A';
queue<pair<int, int>> que;
que.push({startX, startY});
while (!que.empty()) {
pair<int, int> cur = que.front();
que.pop();
for (int i = 0; i < 4; i++) {
int nextX = cur.first + dir[i][0];
int nextY = cur.second + dir[i][1];
if (nextX < 0 || nextX >= board.size() || nextY < 0 || nextY >= board[0].size()) continue;
if (board[nextX][nextY] == 'O') {
que.push({nextX, nextY});
board[nextX][nextY] = 'A';
}
}
}
}
};
太平洋大西洋水流问题
题目
暴力解法
若按题意去查,遍历每个节点,看这个点能不能同时到太平洋和大西洋,那么代码如下(以dfs举例):
class Solution {
public:
vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {
vector<vector<int>> result;
for (int i = 0; i < heights.size(); i++) {
for (int j = 0; j < heights[0].size(); j++) {
OceanStream = {false, false};
vector<vector<bool>> visited(heights.size(), vector<bool>(heights[0].size(), false));
visited[i][j] = true;
dfs(heights, visited, i, j);
if (OceanStream.first && OceanStream.second) result.push_back({i, j});
}
}
return result;
}
private:
pair<bool, bool> OceanStream;//first:可流入太平洋, second:可流入大西洋
int dir[4][2] = {0, 1, 1, 0, 0, -1, -1, 0};
void dfs(vector<vector<int>>& heights, vector<vector<bool>>& visited, int startX, int startY) {
if (startX == 0 || startY == 0) OceanStream.first = true;
if (startX == heights.size() - 1 || startY == heights[0].size() - 1) OceanStream.second = true;
for (int i = 0; i < 4; i++) {
int nextX = startX + dir[i][0];
int nextY = startY + dir[i][1];
if (nextX < 0 || nextX >= heights.size() || nextY < 0 || nextY >= heights[0].size()) continue;
if (heights[startX][startY] >= heights[nextX][nextY] && !visited[nextX][nextY]) {
visited[nextX][nextY] = true;
dfs(heights, visited, nextX, nextY);
}
}
}
};
由于深搜的时间复杂度是o(m * n),并且需要遍历每一个节点,因此整个题目的时间复杂度是o(m^2 * n^2),提交则超时。究其原因是很多节点是重复遍历了
优化方法
可以 反过来想,从太平洋边上的节点 逆流而上,将遍历过的节点都标记上。 从大西洋的边上节点 逆流而长,将遍历过的节点也标记上。 然后两方都标记过的节点就是既可以流太平洋也可以流大西洋的节点。
class Solution {
public:
//从太平洋的边界点从低往高深搜,经历的节点就是能流入太平洋的节点;同理,找到能流入大西洋的节点
vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {
vector<vector<bool>> isPacific(heights.size(), vector<bool>(heights[0].size(), false));//可流入太平洋的
vector<vector<bool>> isAtlantic(heights.size(), vector<bool>(heights[0].size(), false));//可流入大西洋的
for (int i = 0; i < heights.size(); i++) {
dfs(heights, isPacific, i, 0);
dfs(heights, isAtlantic, i, heights[0].size() - 1);
}
for (int j = 0; j < heights[0].size(); j++) {
dfs(heights, isPacific, 0, j);
dfs(heights, isAtlantic, heights.size() - 1, j);
}
vector<vector<int>> result;
for (int i = 0; i < heights.size(); i++) {
for (int j = 0; j < heights[0].size(); j++) {
if (isPacific[i][j] && isAtlantic[i][j]) result.push_back({i, j});
}
}
return result;
}
private:
int dir[4][2] = {0, 1, 1, 0, 0, -1, -1, 0};
void dfs(vector<vector<int>>& heights, vector<vector<bool>>& visited, int startX, int startY) {
if (visited[startX][startY]) return;
visited[startX][startY] = true;
for (int i = 0; i < 4; i++) {
int nextX = startX + dir[i][0];
int nextY = startY + dir[i][1];
if (nextX < 0 || nextX >= heights.size() || nextY < 0 || nextY >= heights[0].size()) continue;
if (heights[startX][startY] <= heights[nextX][nextY]) dfs(heights, visited, nextX, nextY);//注意一定有"="
}
}
};
由于此方法中visited记录了 走过的节点,因此 pacificAtlantic 函数中,第一个for循环和第二个for循环分别用 isPacific 和 isAtlantic 遍历了两次图,所以是 o(2 * m * n),最后一个for循环也是o(m * n),所以时间复杂度是 o(m * n)
最大人工岛
题目
暴力解法
遍历地图尝试 将每一个 0 改成1,然后去搜索地图中的最大的岛屿面积
class Solution {
public:
int largestIsland(vector<vector<int>>& grid) {
int res = 0;
for (int i = 0; i < grid.size(); i++) {
for (int j = 0; j < grid[0].size(); j++) {
if (grid[i][j] == 0) {
aera = 0;
vector<vector<bool>> visited(grid.size(), vector<bool>(grid[0].size(), false));
grid[i][j] = 1;
dfs(grid, visited, i, j);
res = max(aera, res);
grid[i][j] = 0;
}
}
}
if (res == 0) return grid.size() * grid[0].size();//res == 0 说明grid中没有0,只有1
return res;
}
private:
int dir[4][2] = {0, 1, 1, 0, 0, -1, -1, 0};
int aera = 0;
void dfs(vector<vector<int>>& grid, vector<vector<bool>>& visited, int startX, int startY) {
visited[startX][startY] = true;
aera++;
for (int i = 0; i < 4; i++) {
int nextX = startX + dir[i][0];
int nextY = startY + dir[i][1];
if (nextX < 0 || nextX >= grid.size() || nextY < 0 || nextY >= grid[0].size()) continue;
if (!visited[nextX][nextY] && grid[nextX][nextY] == 1) dfs(grid, visited, nextX, nextY);
}
}
};
时间复杂度:遍历地图+深搜,总共是 o(n^4)
优化方法
由于暴力方法做了很多重复的工作,接下来进行优化:
①遍历地图,把岛屿的面积记录下来,用map记录,key是岛屿编号,value是岛屿面积
如
变为:
②遍历地图,将0变成1,统计该1周围的岛屿面积,相加即可;遍历整个图,求最大的相加面积
代码如下:
class Solution {
public:
int largestIsland(vector<vector<int>>& grid) {
unordered_map<int, int> umap;
int mask = 2;
bool isLand = true;//定义是否全为陆地的flag,防止整个陆地都是1而计算错误
for (int i = 0; i < grid.size(); i++) {
for (int j = 0; j < grid[0].size(); j++) {
if (grid[i][j] == 1) {
aera = 0;
dfs(grid, mask, i, j);
umap[mask] = aera;
mask++;
}
}
}
int res = 0;
unordered_map<int, int> visitedGrid;//同一块面积不能计算多次的flag,比如改成1的节点周围都是3,面积是10,那么算面积的时候只能+10算一次,不能算多次
for (int i = 0; i < grid.size(); i++) {
for (int j = 0; j < grid[0].size(); j++) {
if (grid[i][j] == 0) {
isLand = false;
int count = 1;//原来的0也算一块面积
visitedGrid.clear();
for (int k = 0; k < 4; k++) {
int nextX = i + dir[k][0];
int nextY = j + dir[k][1];
if (nextX < 0 || nextX >= grid.size() || nextY < 0 || nextY >= grid[0].size()) continue;
if (grid[nextX][nextY] != 0 && grid[nextX][nextY] != 1 && visitedGrid[grid[nextX][nextY]] != 1) {
count += umap[grid[nextX][nextY]];
visitedGrid[grid[nextX][nextY]] = 1;
}
}
res = max(res, count);
}
}
}
if (isLand) return grid.size() * grid[0].size();
return res;
}
private:
int dir[4][2] = {0, 1, 1, 0, 0, -1, -1, 0};
int aera = 0;
void dfs(vector<vector<int>>& grid, int mask, int startX, int startY) {
grid[startX][startY] = mask;
aera++;
for (int i = 0; i < 4; i++) {
int nextX = startX + dir[i][0];
int nextY = startY + dir[i][1];
if (nextX < 0 || nextX >= grid.size() || nextY < 0 || nextY >= grid[0].size()) continue;
if (grid[nextX][nextY] == 1) dfs(grid, mask, nextX, nextY);
}
}
};
单词接龙
题目
思路
以实例1为例,建图
本质是查找从 “hit” 到 “cog” 的 最短路径,所以用 广搜 更合适
直接遍历wordList
class Solution {
public:
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
queue<string> que;
unordered_map<string, int> umap;//记录到某个节点的最短路径
que.push(beginWord);
umap[beginWord] = 1;
while (!que.empty()) {
string cur = que.front();
int path = umap[cur];
que.pop();
for (int i = 0; i < wordList.size(); i++) {
if (umap[wordList[i]] == 1) continue;
int difNum = 0;//记录wordList中的单词和cur的不同的字母个数
for (int j = 0; j < cur.size(); j++) {
if (cur[j] != wordList[i][j]) difNum++;
if (difNum > 1) break;
}
if (difNum == 1 && umap[wordList[i]] == 0) {//如果不同的字母数是1并且还没有访问
que.push(wordList[i]);
umap[wordList[i]] = path + 1;
}
}
if (cur == endWord) {
return path;
}
}
return 0;
}
};
这个超时了,具体原因不太清楚
考虑在26个英文字母上改变字符
class Solution {
public:
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
unordered_set<string> wordSet(wordList.begin(), wordList.end());//为了遍历快点,用uset
if (wordSet.find(endWord) == wordSet.end()) return 0;
queue<string> que;
unordered_map<string, int> umap;//记录访问过的节点和访问此节点的最短路径长度
que.push(beginWord);
umap.insert({beginWord, 1});
while (!que.empty()) {
string cur = que.front();
que.pop();
int path = umap[cur];
for (int i = 0; i < cur.size(); i++) {
string newWord = cur;
for (int j = 0; j < 26; j++) {
newWord[i] = 'a' + j;
if (newWord == endWord) return path + 1;
if (wordSet.find(newWord) != wordSet.end() && umap.find(newWord) == umap.end()) {//newWord在uset中,并且不在umap中(没有访问过)
que.push(newWord);
umap.insert({newWord, path + 1});
}
}
}
}
return 0;
}
};
钥匙和房间
题目
深搜
class Solution {
public:
bool canVisitAllRooms(vector<vector<int>>& rooms) {
visited.insert(0);
doorNum++;
dfs(rooms, 0);
return doorNum == rooms.size();
}
private:
int doorNum = 0;
unordered_set<int> visited;
void dfs(vector<vector<int>>& rooms, int start) {
for (int i = 0; i < rooms[start].size(); i++) {
if (visited.find(rooms[start][i]) == visited.end()) {
visited.insert(rooms[start][i]);
doorNum++;
dfs(rooms, rooms[start][i]);
}
}
}
};
岛屿的周长
题目
题解
可以深搜,也可以广搜,也可以直接遍历整个地图,记录周长
class Solution {
public:
int islandPerimeter(vector<vector<int>>& grid) {
int res = 0;
for (int i = 0; i < grid.size(); i++) {
for (int j = 0; j < grid[0].size(); j++) {
if (grid[i][j] == 1) {
for (int k = 0; k < 4; k++) {
int nextX = i + dir[k][0];
int nextY = j + dir[k][1];
if (nextX < 0 || nextX >= grid.size() || nextY < 0 || nextY >= grid[0].size()
|| grid[nextX][nextY] == 0) res++;
}
}
}
}
return res;
}
private:
int dir[4][2] = {0, 1, 1, 0, 0, -1, -1, 0};
};
寻找图中是否存在路径
题目
题解
由于此题是 无向图, 因此本质就是查看 图是否连通,直接使用 并查集
class Solution {
public:
bool validPath(int n, vector<vector<int>>& edges, int source, int destination) {
//判断是否连通
//创建数组
vector<int> father = vector<int>(n);
//初始化
for (int i = 0; i < n; i++) {
father[i] = i;
}
//加入并查集
for (int i = 0; i < edges.size(); i++) {
join(edges[i][0], edges[i][1], father);
}
return isSame(source, destination, father);
}
private:
//寻根
int find(int x, vector<int>& father) {
return x == father[x] ? x : father[x] = find(father[x], father);
}
//判断是否同根
bool isSame(int u, int v, vector<int>& father) {
u = find(u, father);
v = find(v, father);
return u == v;
}
//加入并查集
void join(int u, int v, vector<int>& father) {
u = find(u, father);
v = find(v, father);
if (u == v) return;
father[v] = u;
}
};
冗余连接
题目
并查集解法
class Solution {
public:
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
vector<int> father(edges.size() + 1);//并查集数组,此题节点编号是从1开始的,因此father数组的长度需要+1
init(father);
for (auto edge : edges) {
if (isSame(edge[0], edge[1], father)) return edge;//如果同根,说明形成了环,直接返回
else join(edge[0], edge[1], father);
}
return {};
}
private:
void init(vector<int>& father) {
for (int i = 1; i < father.size(); i++) {
father[i] = i;
}
}
int find(int x, vector<int>& father) {
return x == father[x] ? x : father[x] = find(father[x], father);
}
bool isSame(int u, int v, vector<int>& father) {
return find(u, father) == find(v, father);
}
void join(int u, int v, vector<int>& father) {
u = find(u, father);
v = find(v, father);
if (u == v) return;
father[v] = u;
}
};
冗余连接Ⅱ
题目
题解
三种情况:
1.某节点入读为2并且形成有向环,这时候得看看处理哪个入度;
2.某节点入读为2但是没有有向环,这时候直接处理最后出现的即可;
3.没有入读为2的节点,但是出现了有向环,这时候只需处理有向环中最后出现的边即可
class Solution {
public:
vector<int> findRedundantDirectedConnection(vector<vector<int>>& edges) {
//并查集数组建立
vector<int> father(edges.size() + 1);
vector<int> degrees(edges.size() + 1);//存放每个节点的入度
for (auto edge : edges) degrees[edge[1]]++;
vector<vector<int>> degreeEqual2;//存放入度为2的节点的两个入度的边
for (auto edge : edges) {
if (degrees[edge[1]] == 2) degreeEqual2.push_back(edge);
}
if (!degreeEqual2.empty()) {//非空意味着有入度为2的点。直接从后往前遍历这两个入度边,判断删除是否图变树即可。但是一定要从后往前遍历,因为优先输出后面的变
if (deleteEdgeIsTree(edges, degreeEqual2[1], father)) return degreeEqual2[1];
else return degreeEqual2[0];
}
//到这一步,说明一定存在有向环,并且无入度为2的点
return destoryRing(edges, father);
}
private:
bool deleteEdgeIsTree(vector<vector<int>>& edges, vector<int> deleteEdge, vector<int>& father) {
init(father);
for (auto edge : edges) {
if (edge[0] == deleteEdge[0] && edge[1] == deleteEdge[1]) continue;
if (isSame(edge[0], edge[1], father)) return false;
else join(edge[0], edge[1], father);
}
return true;
}
vector<int> destoryRing(vector<vector<int>>& edges, vector<int>& father) {
init(father);
for (auto edge : edges) {
if (isSame(edge[0], edge[1], father)) return edge;
else join(edge[0], edge[1], father);
}
return {};
}
//以下是并查集
void init(vector<int>& father) {
for (int i = 1; i < father.size(); i++) father[i] = i;
}
int find(int x, vector<int>& father) {
return x == father[x] ? x : father[x] = find(father[x], father);
}
bool isSame(int u, int v, vector<int>& father) {
u = find(u, father);
v = find(v, father);
return u == v;
}
void join(int u, int v, vector<int>& father) {
u = find(u, father);
v = find(v, father);
if (u == v) return;
father[v] = u;
}
};