本文内容是基于小象学院——林沐 《面试算法 LeetCode 刷题班》,后期仍将对相关内容进行不定期更新!
8.搜索
文章目录
LeetCode 200 岛屿数量(M)
问题描述:
用一个二维数组代表一张地图,全由“0”和“1”组成,其中“0”代表水域,“1”代表小岛,小岛“1”被水域“0”所包围,当小岛土地“1”在水平和垂直方向相连接时,认为是同一块土地。求这张地图中小岛的数量。
![](https://i.imgur.com/0UfkjLw.png)
算法思路1: DFS
1.标记当前搜索位置已被搜索
2.按照方向数组的4个方向,拓展4个新位置 newx,newy
3.若新位置不在地图范围内,则忽略。
4.如果新位置未曾到达过、且是陆地,继续DFS该位置。
class Solution {
public:
void DFS(vector<vector<int>> &mark, vector<vector<char>> &grid, int x, int y)
{
mark[x][y] = 1;
static const int dx[] = { -1,1,0,0 };
static const int dy[] = { 0,0,-1,1 };
for (int i = 0; i < 4; i++)
{
int newx = dx[i] + x;
int newy = dy[i] + y;
if (newx<0 || newx >= mark.size() ||
newy<0 || newy >= mark[newx].size())
{
continue;
}
if (mark[newx][newy] == 0 && grid[newx][newy] == '1')
{
DFS(mark, grid, newx, newy);
}
}
}
int numIslands(vector<vector<char>>& grid) {
int island_num = 0;
vector<vector<int>> mark;
for (int i = 0; i < grid.size(); i++)
{
mark.push_back(vector<int>());
for (int j = 0; j < grid[i].size(); j++)
{
mark[i].push_back(0);
}
}
for (int i = 0; i < grid.size(); i++)
{
for (int j = 0; j < grid[i].size(); j++) {
if (grid[i][j] == '1' && mark[i][j] == 0)
{
DFS(mark, grid, i, j);
island_num++;
}
}
}
return island_num;
}
};
算法思路2: BFS
1.设置搜索队列Q,标记 mark[x][y] = 1, 并将待搜索的位置 (x,y) push 进队列Q
2.只要队列不为空,即取队头元素,按照方向数组的4个方向,拓展4个新位置,newx,newy
3.若新位置不在地图范围内,则忽略
4.如果新位置未曾到达过、且是陆地,将该位置push 进入队列,并标记 mark[newx][newy] = 1
class Solution {
public:
void BFS(vector<vector<int>> &mark, vector<vector<char>> &grid, int x, int y)
{
static const int dx[] = { -1,1,0,0 };
static const int dy[] = { 0,0,-1,1 };
queue<pair<int, int>> Q;
Q.push(make_pair(x, y));
mark[x][y] = 1;
while (!Q.empty())
{
x = Q.front().first;
y = Q.front().second;
Q.pop();
for (int i = 0; i < 4; i++)
{
int newx = dx[i] + x;
int newy = dy[i] + y;
if (newx<0 || newx >= mark.size() ||
newy<0 || newy >= mark[newx].size())
{
continue;
}
if (mark[newx][newy] == 0 && grid[newx][newy] == '1')
{
Q.push(make_pair(newx, newy));
BFS(mark, grid, newx, newy);
}
}
}
}
int numIslands(vector<vector<char>>& grid) {
int island_num = 0;
vector<vector<int>> mark;
for (int i = 0; i < grid.size(); i++)
{
mark.push_back(vector<int>());
for (int j = 0; j < grid[i].size(); j++)
{
mark[i].push_back(0);
}
}
for (int i = 0; i < grid.size(); i++)
{
for (int j = 0; j < grid[i].size(); j++) {
if (grid[i][j] == '1' && mark[i][j] == 0)
{
BFS(mark, grid, i, j);
island_num++;
}
}
}
return island_num;
}
};
LeetCode 127 词语阶梯 (M)
问题描述:
已知两个单词(一个为起始单词与结束单词),一个单词词典,根据转换规则计算从起始单词到结束单词最短转换步数:
转化规则如下:
1.在转化时,只能转换单词中的1个字符
2.转换得到的新词必须在单词词典中
Example 1:
Input:
beginWord = "hit",
endWord = "cog",
wordList = ["hot","dot","dog","lot","log","cog"]
Output: 5
Explanation: As one shortest transformation is "hit" -> "hot" -> "dot" -> "dog" -> "cog",
return its length 5.
算法思路
单词和单词之间的转换,可以理解为一张图,图的顶点是单词,若两个单词之间可以相互转换,则这两个单词所代表的顶点间有一条边,求图中节点hit(开始单词)到节点cog(结束单词)的所有路径中,最少包括对少个节点。即图的宽度优先搜索。
可以先基于上述关系使用 map 构建邻接表表示的图,将开始单词作为key,对任意两个单词,若他们只差一个字符,则将其相连。
首先构建图的邻接表:
bool connect(const string &word1, const string &word2)
{
int cnt = 0;
for (int i = 0; i < word1.length(); i++)
{
if (word1[i] != word2[i]) {
cnt++;
}
}
return cnt == 1;
}
void connect_graph(string &beginWord, vector<string> &wordList,map<string,vector<string>> &graph)
{
wordList.push_back(beginWord);
for (int i = 0; i < wordList.size(); i++)
{
graph[wordList[i]] = vector<string>();
}
for (int i = 0; i < wordList.size(); i++)
{
for (int j = i+1; j < wordList.size(); j++)
{
if (connect(wordList[i],wordList[j]))
{
graph[wordList[i]].push_back(wordList[j]);
graph[wordList[j]].push_back(wordList[i]);
}
}
}
}
给定了起始单词,终点单词,图,从开始单词宽度优先搜索图graph,搜索过程中记录到达步数:
- 设置搜索队列Q,队列节点为 pair<顶点,步数>;设置集合 visit,记录搜索过的顶点;将<beginWord,1>添加到队列;
- 只要队列不空,取出队列头部元素,若等于结束单词则返回步数,否则拓展该节点,将与之相邻的未添加到visit中的节点与步数同时添加到队列Q,并将其拓展节点加入 visit;
- 若最终都无法搜索到endWord,返回0;
实现图的宽度优先搜索:
int BFS_graph(string &beginWord, string &endWord,map<string,vector<string>>&graph)
{
queue<pair<string, int>> Q;
std::set<string> visit;
Q.push(make_pair(beginWord, 1));
visit.insert(beginWord);
while (!Q.empty())
{
string node = Q.front().first;
int step = Q.front().second;
Q.pop();
if (node == endWord)
{
return step;
}
const vector<string> &neighbors = graph[node];
for (int i = 0; i < neighbors.size(); i++)
{
if (visit.find(neighbors[i]) == visit.end())
{
Q.push(make_pair(neighbors[i], ++step));
visit.insert(neighbors[i]);
}
}
}
return 0;
}
最终提交代码:
bool connect(const string &word1, const string &word2)
{
int cnt = 0;
for (int i = 0; i < word1.length(); i++)
{
if (word1[i] != word2[i]) {
cnt++;
}
}
return cnt == 1;
}
void connect_graph(string &beginWord, vector<string> &wordList,map<string,vector<string>> &graph)
{
wordList.push_back(beginWord);
for (int i = 0; i < wordList.size(); i++)
{
graph[wordList[i]] = vector<string>();
}
for (int i = 0; i < wordList.size(); i++)
{
for (int j = i+1; j < wordList.size(); j++)
{
if (connect(wordList[i],wordList[j]))
{
graph[wordList[i]].push_back(wordList[j]);
graph[wordList[j]].push_back(wordList[i]);
}
}
}
}
int BFS_graph(string &beginWord, string &endWord,map<string,vector<string>>&graph)
{
queue<pair<string, int>> Q;
std::set<string> visit;
Q.push(make_pair(beginWord, 1));
visit.insert(beginWord);
while (!Q.empty())
{
string node = Q.front().first;
int step = Q.front().second;
Q.pop();
if (node == endWord)
{
return step;
}
const vector<string> &neighbors = graph[node];
for (int i = 0; i < neighbors.size(); i++)
{
if (visit.find(neighbors[i]) == visit.end())
{
Q.push(make_pair(neighbors[i], ++step));
visit.insert(neighbors[i]);
}
}
}
return 0;
}
class Solution {
public:
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
map<string, vector<string>> graph;
connect_graph(beginWord, wordList, graph);
return BFS_graph(beginWord, endWord, graph);
}
};
LeetCode 126 词语阶梯2(H)
问题描述:
已知两个单词(一个为起始单词与结束单词),一个单词词典,根据转换规则计算从起始单词到结束单词所有的最短路径:
转化规则如下:
1.在转化时,只能转换单词中的1个字符
2.转换得到的新词必须在单词词典中
主要思考问题:
- 在宽度优先搜索是,如何保存宽度搜索时的路径
- 如果起点与中点间有多条路径,如何将多条路径全部搜索出
- 在建立连接图时,若单词表中已包含 beginWord ,还按 LeetCode 200 中的方法,会出现什么问题?
需要使用新的结构来构建队列中的元素,到达某一个位置可能存在多条路劲,使用映射记录到达每个位置的最短需要的步数,新拓展到的位置只要未曾到达或到达步数与最短步数相同,即将该位置添加到队列中,从而存储了从不同前驱到达该位置的情况。
struct Qitem {
string node;
int parent_pos;
int step;
Qitem(string node,int parent_pos, int step):node(node),parent_pos(parent_pos),step(step){}
};
class Solution {
public:
bool connect(const string &word1, const string &word2)
{
int cnt = 0;
for (int i = 0; i < word1.length(); i++)
{
if (word1[i] != word2[i]) {
cnt++;
}
}
return cnt == 1;
}
void connect_graph(string &beginWord, vector<string>& wordList,map<string,vector<string>> &graph)
{
int has_begin_word = 0;
for (int i = 0; i < wordList.size(); i++)
{
if (wordList[i] == beginWord) {
has_begin_word = 1;
}
graph[wordList[i]] = vector<string>();
}
for (int i = 0; i < wordList.size(); i++)
{
for (int j = i+1; j < wordList.size(); j++)
{
if (connect(wordList[i],wordList[j]))
{
graph[wordList[i]].push_back(wordList[j]);
graph[wordList[j]].push_back(wordList[i]);
}
}
if (has_begin_word == 0 && connect(beginWord, wordList[i])) {
graph[beginWord].push_back(wordList[i]);
}
}
}
void BFS_graph(string &beginWord, string &endWord,map<string,vector<string>>&graph,
vector<Qitem> &Q,vector<int> &end_word_pos)
{
map<string, int> visit;
int min_step = 0;
Q.push_back(Qitem(beginWord.c_str(), -1, 1));
visit[beginWord] = 1;
int front = 0;
while (front !=Q.size())
{
const string &node = Q[front].node;
int step = Q[front].step;
if (min_step != 0 && min_step < step)
{
break;
}
if (node == endWord)
{
min_step = step;
end_word_pos.push_back(front);
}
const vector<string> &neighbors = graph[node];
for (int i = 0; i < neighbors.size(); i++)
{
if (visit.find(neighbors[i]) == visit.end() ||
visit[neighbors[i]]== step+1) {
Q.push_back(Qitem(neighbors[i], front, step + 1));
visit[neighbors[i]] = step + 1;
}
}
front++;
}
}
vector<vector<string>> findLadders(string beginWord, string endWord, vector<string>& wordList) {
map<string, vector<string>> graph;
connect_graph(beginWord, wordList, graph);
vector<Qitem> Q;
vector<int> end_word_pos;
BFS_graph(beginWord, endWord, graph, Q, end_word_pos);
vector<vector<string>> result;
for (int i = 0; i < end_word_pos.size(); i++)
{
int pos = end_word_pos[i];
vector<string> path;
while (pos != -1)
{
path.push_back(Q[pos].node);
pos = Q[pos].parent_pos;
}
result.push_back(vector<string>());
for (int j = path.size()-1; j >= 0; j--)
{
result[i].push_back(path[j]);
}
}
return result;
}
};
LeetCode 473 火柴棍摆正方形
问题描述:
已知一个数组,保存了n个(n<=15)火柴棍,问可否使用这n个火柴棍摆成1个正方形?
问题思考:
暴力搜索(回溯法)最多有 4^15种可能,肯定不可取,所以:
- 回溯算法如何设计
- 递归的回溯搜索(DFS)何时返回真,何时返回假
- 普通的回溯搜索(DFS)可否解决该问题,如何对深度优先搜索进行优化(剪枝)
正常算法如下:
将正方形的4条边视作4个捅,将每个火柴回溯的放置在每个桶中,在放完n个火柴杆后,检查4个桶中的火柴杆长度和是否相同,相同返回真,否则返回假,在回溯过程中,如果当前所有可能向后的回溯,都无法满足条件,即递归函数最终返回假。
优化1: n个火柴杆的总和对4取余需要为 0 ,否则返回假
优化2:火柴杆按照从大到小的顺序排序,先尝试大的减少回溯可能
优化3:每次放置时,每条边上不可放置超过总和的1/4长度的火柴杆
class Solution {
public:
bool generate(int i , vector<int> & nums, int target, int bucket[])
{
if (i >=nums.size())
{
return bucket[0] == target &&
bucket[1] == target &&
bucket[2] == target &&
bucket[3] == target;
}
for (int j = 0; j < 4; j++)
{ // 4 个桶中分别尝试
if (bucket[j] + nums[i] > target) {
continue;
}
bucket[j] += nums[i];
if (generate(i + 1, nums, target, bucket)) {
return true;
}
bucket[j] -= nums[i];
}
return false;
}
bool makesquare(vector<int>& nums) {
if (nums.size()<4)
{
return false;
}
int sum = 0;
for (int i = 0; i < nums.size(); i++)
{
sum += nums[i];
}
if (sum % 4 != 0)
{
return false;
}
sort(nums.rbegin(), nums.rend()); // 从大到小排序
int bucket[4] = { 0 };
return generate(0, nums, sum / 4, bucket);
}
};
LeetCode 407 收集雨水2 (H)
问题描述:
已知一个 m*n 的二维数组,数组存储正整数,代表一个个单元的高度(立方体),将这些立方体想象成水槽,问如果下雨这些立方体中会有多少积水。
算法思路:
- 搜索队列使用优先级队列(堆),越低矮的点优先级越高(最小堆),越优先进行搜索。
- 以矩形四周的点作为起始点进行广度优先搜索
- 使用一个二维数组堆push进入队列的点进行标记,之后搜索到该点后,不再push到队列中
- 只要优先级队列不空,即取出优先级队列队头元素进行搜索,按照上下左右四个方向进行拓展,在这个过程中忽略超出边界与已入队列的点
- 当对某点(x,y,h)进行拓展时(h即为(x,y)位置的高度,heightMap[x][y]),得到新点(newx,newy),高度为height[newx][newy],如果h大于height[newx][newy]: 最终结果 + = h - height[newx][newy];将height[newx][newy]赋值为h。 将(newx,newy,heightMap[newx][newy])push进入优先级队列,并做标记。
n
struct Qitem
{
int x;
int y;
int h;
Qitem(int _x, int _y, int _h):x(_x),y(_y),h(_h){}
}
struct cmp {
bool operator()(const Qitem &a, const Qitem &b) {
return a.h > b.h;
}
};
class Solution {
public:
int trapRainWater(vector<vector<int>>& heightMap) {
priority_queue<Qitem, vector<Qitem>, cmp> Q;
if (heightMap.size() < 3 || heightMap[0].size() <3)
{
return 0;
}
int row = heightMap.size();
int column = heightMap[0].size();
vector<vector<int>> mark;
for (int i = 0; i < row; i++)
{
mark.push_back(vector<int>());
for (int j = 0; j < column; j++)
{
mark[i].push_back(0);
}
} // 至此先初始化一个与 heightMap 等维度的全零数组
for (int i = 0; i < row; i++)
{
Q.push(Qitem(i, 0, heightMap[i][0]));0 //加入第一列的元素
mark[i][0] = 1;
Q.push(Qitem(i, column - 1, heightMap[i][column - 1])); //加入最后一列的元素
mark[i][column - 1] = 1;
}
for (int i = 1; i < column - 1; i++)
{
Q.push(Qitem(0, i, heightMap[0][i])); //加入第一行的元素
mark[0][i] = 1;
Q.push(Qitem(row - 1, i, heightMap[row - 1][i])); //加入最后一行的元素
mark[row - 1][i] = 1;
}
// 设置方向数组进行遍历
static const int dx[] = { -1,1,0,0 };
static const int dy[] = { 0,0,-1,1 };
int result = 0;
while (!Q.empty())
{
int x = Q.top().x;
int y = Q.top().y;
int h = Q.top().h;
Q.pop();
for (int i = 0; i < 4; i++)
{
int newx = x + dx[i];
int newy = y + dy[i];
if (newx<0 || newx >=row ||
newy<0 || newy >=column || mark[newx][newy])
{
continue;
}
if (h>heightMap[newx][newy])
{
result += h - heightMap[newx][newy];
heightMap[newx][newy] = h;
}
Q.push(Qitem(newx, newy, heightMap[newx][newy]));
mark[newx][newy] = 1;
}
}
return result;
}