目录
leetcode 200 岛屿的个数——2018秋招竞技世界考过,2019春招竞技世界又考了一遍。。。
leetcode 127 单词接龙——2019秋招滴滴出行考过
leetcode 407 接雨水 II——竞技世界2019秋招真题
leetcode 200 岛屿的个数——2018秋招竞技世界考过,2019春招竞技世界又考了一遍。。。
给定一个由 '1'
(陆地)和 '0'
(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。
示例 1:
输入:
11110
11010
11000
00000
输出: 1
示例 2:
输入:
11000
11000
00100
00011
输出: 3
思路:
方法一:DFS
- 标记当前访问的位置为1
- 按照四个方向不断探索
- 若探索的位置不在地图的范围内,则忽略
- 如果新位置之前没有访问过,并且该位置是陆地,则继续DFS该位置
例如,按照上下左右的顺序深度搜索,从(1, 1)位置出发(绿色代表可达,紫色代表不可达)
方法二——BFD
- 设置搜索队列Q,标记mark[x][y]=1,并将带搜索的位置(x,y)push进入队列Q。(深搜是进入递归就马上做标记,宽搜是入队之后做标记)
- 只要队列不空,即取队头元素,按照方向数组的四个方向,扩展四个新位置newx、newy。
- 若新位置不在地图范围内,则忽略
- 如果新位置未被访问过,并且该位置是陆地,则将新位置push进入队列,并标记mark[newx][newy]=1
例如,按照上下左右的顺序深度搜索,从(1, 1)位置出发(蓝色代表入队完又拿出来的,绿色代表队列中的节点,紫色代表到不了的位置)
整体算法:
- 设置岛屿数量 island_num=0
- 设置mark数组,并初始化
- 遍历地图grid上的所有点,如果点是陆地并且未被访问,调用搜索函数search(mark, grid, i, j); ,search可以是DFS,也可以是BFS。完成所搜后 island_num++
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);
}
}
}
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));
mark[newx][newy]=1;
}
}
}
}
int numIslands(vector<vector<char>>& grid) {
if(grid.empty()) return 0;
int island_num=0;
//初始化mark数组
vector<vector<int>>mark(grid.size(),vector<int>(grid[0].size()));
for(int i=0; i<grid.size(); i++){
for(int j=0; j<grid[i].size(); j++){
if(mark[i][j]==0 && grid[i][j]=='1'){
//BFS(mark, grid, i, j);
DFS(mark, grid, i, j);
island_num++;
}
}
}
return island_num;
}
};
leetcode 127 单词接龙——2019秋招滴滴出行考过
给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord的最短转换序列的长度。转换需遵循如下规则:
- 每次转换只能改变一个字母。
- 转换过程中的中间单词必须是字典中的单词。
说明:
- 如果不存在这样的转换序列,返回 0。
- 所有单词具有相同的长度。
- 所有单词只由小写字母组成。
- 字典中不存在重复的单词。
- 你可以假设 beginWord 和 endWord 是非空的,且二者不相同。
示例 1:
输入:
beginWord = "hit",
endWord = "cog",
wordList = ["hot","dot","dog","lot","log","cog"]
输出: 5
解释: 一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 返回它的长度 5。
示例 2:
输入:
beginWord = "hit"
endWord = "cog"
wordList = ["hot","dot","dog","lot","log"]
输出: 0
解释: endWord "cog" 不在字典中,所以无法进行转换。
思考:
单词之间的转换,可以理解为一张图,图的顶点为单词,若两个单词可以互相转换,则将这两个单词所代表的的顶点之间连上一条边,求图中节点 hit 到节点 cog 的所有路径中,最少包含多少个节点,即图的宽度优先搜索。
使用map构造邻接表的表示图,以string为key,vector<string>为value,如图所示:
实现构建图的功能函数如下:
//计算两个单词之间不相等的字符个数
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++;
}
}
//若两个单词之间只有一个字符不同,则返回true
return cnt==1;
}
void construct_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++){
//若i和j只差一个字符,则将其相连
if(connect(wordList[i], wordList[j])){
graph[wordList[i]].push_back(wordList[j]);
graph[wordList[j]].push_back(wordList[i]);
}
}
}
}
思路:
指定图的起点 beginWord 和终点 endWord,图 graph,从 beginWord 开始宽度优先搜索图 graph ,搜索过程中记录达到的步数。
1、设置搜索队列Q,队列节点为pair<顶点,步数>;设置集合visit,记录搜索过的节点,将<beginWord, 1>添加至队列。
2、只要队不空,取出队列头部元素
- 若队列头部元素为endWord,返回到达当前节点的步数
- 若队列头部不是endWord,则继续扩展该节点,将该节点相邻的且尚未添加到visit中的节点与步数同时添加到队列Q中,并将扩展节点加入visit
3、若最终无法搜索到endWord,则返回0.
(蓝色指队列中的点,红色代表添加过的点)
宽搜的实现代码如下:
int BFS_graph(string &beginWord, string &endWord,
map<string, vector<string>>&graph){
//搜索队列
queue<pair<string, int>>Q;
//标记已访问节点
set<string>visit;
//添加其实节点,初始化访问步数为1
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;
}
//获取node的全部邻接点
const vector<string>&neighbors=graph[node];
for(auto i : neighbors){
//若该邻接点未被访问
if(visit.find(i)==visit.end()){
Q.push(make_pair(i, step+1));
visit.insert(i);
}
}
}
return 0;
}
整体实现代码:
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++;
}
}
//若两个单词之间只有一个字符不同,则返回true
return cnt==1;
}
void construct_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++){
//若i和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;
//标记已访问节点
set<string>visit;
//添加其实节点,初始化访问步数为1
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;
}
//获取node的全部邻接点
const vector<string>&neighbors=graph[node];
for(auto i : neighbors){
//若该邻接点未被访问
if(visit.find(i)==visit.end()){
Q.push(make_pair(i, step+1));
visit.insert(i);
}
}
}
return 0;
}
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
if(wordList.empty()) return 0;
map<string, vector<string>>graph;
//构造图
construct_graph(beginWord, wordList, graph);
return BFS_graph(beginWord, endWord, graph);
}
};
leetcode 126 单词接龙 II
给定两个单词(beginWord 和 endWord)和一个字典 wordList,找出所有从 beginWord 到 endWord 的最短转换序列。转换需遵循如下规则:
- 每次转换只能改变一个字母。
- 转换过程中的中间单词必须是字典中的单词。
说明:
- 如果不存在这样的转换序列,返回一个空列表。
- 所有单词具有相同的长度。
- 所有单词只由小写字母组成。
- 字典中不存在重复的单词。
- 你可以假设 beginWord 和 endWord 是非空的,且二者不相同。
示例 1:
输入:
beginWord = "hit",
endWord = "cog",
wordList = ["hot","dot","dog","lot","log","cog"]
输出:
[
["hit","hot","dot","dog","cog"],
["hit","hot","lot","log","cog"]
]
示例 2:
输入:
beginWord = "hit"
endWord = "cog"
wordList = ["hot","dot","dog","lot","log"]
输出: []
解释: endWord "cog" 不在字典中,所以不存在符合要求的转换序列。
思考:
这是记录路径的宽度优先搜索。
1、如何保存宽搜的路径?
2、如何保存多条路径?
步骤一:将普通队列更换为vector实现的队列,保存所有的搜索节点,pop操作并不会丢弃节点,而是将指针进行移动
步骤二:在队列中增加该节点的前驱节点在队列中的下标信息,可以通过该下标找到队列中是哪个节点开始搜索到当前节点的
(下图中每个节点的第二个值是节点的前驱节点的下标信息,第三值是到达该节点需要的最少步数)
步骤三:使用映射记录到达每个位置需要的最短步数,因为某一位置可能存在多条到达路径。新拓展到的位置只要是未曾访问过的或者到达的步数与最短的步数相同,则将该位置添加到队列中,从而我们存储了所有从不同前序节点到达该位置最短路径。从而使得宽搜保证到达的位置,只要是到达了,必然是最短的。(下图中visit记录到达节点所需的步数)
下图vector实现的队列中,经过拓展之后,获得红色和蓝色两条路径,分别是 7, 5, 3, 1, 0 和 6, 4, 2, 1, 0
节点7能被push进来的原因是他的路径长度和之前队列中6的路径长度相同
最终获得以vector实现的队列Q,终点下标6和7,即可输出全部搜索路径。
节点的结构体如下:
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){}
};
图的构建代码:
//计算两个单词之间不相等的字符个数
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++;
}
}
//若两个单词之间只有一个字符不同,则返回true
return cnt==1;
}
//构建图
void construct_graph(string &beginWord, vector<string>&wordList,
map<string, vector<string>>&graph){
//因为有可能遍历的时候重复的遍历到beginWord,此时将has_begin_word的值标记为1
int has_begin_word=0;
for(auto i: wordList){
if(i==beginWord){
has_begin_word=1;
}
graph[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]);
}
}
//如果路径中不存在重复的beginWord的访问且该点与beginWord相连
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;
//记录到达endWord的最小步数
int min_step=0;
//起始单词的前驱节点为 -1
Q.push_back(Qitem(beginWord.c_str(), -1, 1));
visit[beginWord]=1;
//队列头指针front,指向vector表示的队列头
int front=0;
//说明队列不空,如果front指向了队列的size,说明队列为空
while(front!=Q.size()){
const string &node=Q[front].node;
int step=Q[front].step;
//step > min_step时,代表所有到达终点的路径都搜索完成了
if(min_step!=0 && step >min_step){
break;
}
//搜索结束时,更新最小步数
if(node==endWord){
min_step=step;
end_word_pos.push_back(front);
}
//记录当前节点的所有邻接点
const vector<string>&neighbors=graph[node];
for(auto i : neighbors){
//若该邻接点未被访问过或者当前邻接点是另一条路径
//visit[i]==step+1 说明visit[i]上曾经记录过一个结果
//而该结果和step+1的值相等,说明这两个路径的长度是一样的
if(visit.find(i)==visit.end() || visit[i]==step+1){
//将该点压入队列中
Q.push_back(Qitem(i, front, step+1));
//标记到达邻接点 i的最小步数
visit[i]=step+1;
}
}
//向后移动指针
front++;
}
}
思路:
从所有的endWord所在的队列位置(end_word_pos),向前遍历到beginWord,遍历过程中保存路径上的单词。最后将保存的各个单词按照从尾巴到头的顺序存储到结果容器中即可。
整体实现代码:
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++;
}
}
//若两个单词之间只有一个字符不同,则返回true
return cnt==1;
}
//构建图
void construct_graph(string &beginWord, vector<string>&wordList,
map<string, vector<string>>&graph){
//因为有可能遍历的时候重复的遍历到beginWord,此时将has_begin_word的值标记为1
int has_begin_word=0;
for(auto i: wordList){
if(i==beginWord){
has_begin_word=1;
}
graph[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]);
}
}
//如果路径中不存在重复的beginWord的访问且该点与beginWord相连
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;
//记录到达endWord的最小步数
int min_step=0;
//起始单词的前驱节点为 -1
Q.push_back(Qitem(beginWord.c_str(), -1, 1));
visit[beginWord]=1;
//队列头指针front,指向vector表示的队列头
int front=0;
//说明队列不空,如果front指向了队列的size,说明队列为空
while(front!=Q.size()){
const string &node=Q[front].node;
int step=Q[front].step;
//step > min_step时,代表所有到达终点的路径都搜索完成了
if(min_step!=0 && step >min_step){
break;
}
//搜索结束时,更新最小步数
if(node==endWord){
min_step=step;
end_word_pos.push_back(front);
}
//记录当前节点的所有邻接点
const vector<string>&neighbors=graph[node];
for(auto i : neighbors){
//若该邻接点未被访问过或者当前邻接点是另一条路径
//visit[i]==step+1 说明visit[i]上曾经记录过一个结果
//而该结果和step+1的值相等,说明这两个路径的长度是一样的
if(visit.find(i)==visit.end() || visit[i]==step+1){
//将该点压入队列中
Q.push_back(Qitem(i, front, step+1));
//标记到达邻接点 i的最小步数
visit[i]=step+1;
}
}
//向后移动指针
front++;
}
}
vector<vector<string>> findLadders(string beginWord, string endWord,
vector<string>& wordList) {
map<string, vector<string>>graph;
construct_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 407 接雨水 II——竞技世界2019秋招真题
给定一个 m x n
的矩阵,其中的值均为正整数,代表二维高度图每个单元的高度,请计算图中形状最多能接多少体积的雨水。
说明:
m 和 n 都是小于110的整数。每一个单位的高度都大于0 且小于 20000。
示例:
给出如下 3x6 的高度图:
[
[1,4,3,1,3,2],
[3,2,1,3,2,4],
[2,3,3,2,3,1]
]
返回 4。
如上图所示,这是下雨前的高度图[[1,4,3,1,3,2],[3,2,1,3,2,4],[2,3,3,2,3,1]]
的状态。
下雨后,雨水将会被存储在这些方块中。总的接雨水量是4。
思路:
- 搜索队列使用优先级队列(STL的优先队列的底层实现默认是大顶堆),越低的点优先级越高(构造小顶堆),优先级越高的点越优先搜索。
- 以矩阵四周的点作为起始点进行广度优先搜索(这些点在搜索前需要push进队列中)。
- 使用一个二维数组对push进队列的点进行标记,如果该点被标记过,则不会被push到队列中。
- 只要优先级队列不空,即去除优先级队列的队头元素进行搜索,按照上下左右四个方向进行扩展,扩展过程中忽略超出边界的点和已经入队的点。
- 当某个点 (x, y, h) 进行拓展时,得到的新点 (newx, newy) 的高度 height 小于 h,则——积水高度+=h-height;,并更新 h 的值为 height 。
- 将(newx, newy, height)入队,并做上标记。
例如:
将四周的点添加至优先队列Q中,并做上标记
开始进行扩展:
蓝色代表正在搜索的节点,绿色代表队中的节点,紫色代表已完成节点,红色代表扩展的新节点(优先级队列中的元素:优先队列<行,列,高>)
实现代码:
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;
//行数或列数小于3,则无法积水
if(heightMap.size() < 3|| heightMap[0].size() < 3){
return 0;
}
int row = heightMap.size();
int col = heightMap[0].size();
vector<vector<int>> Mark(heightMap.size(),vector<int>(heightMap[0].size()));
//将四周的点添加至优先级队列,并做标记Mark
for(int i = 0 ; i <row ; i++){
Q.push(Qitem(i,0,heightMap[i][0]));
Mark[i][0] = 1 ;
Q.push(Qitem(i,col - 1 ,heightMap[i][col - 1]));
Mark[i][col - 1 ] = 1 ;
}
for(int j = 1 ; j <col - 1 ; j++){
Q.push(Qitem(0,j,heightMap[0][j]));
Mark[0][j] = 1 ;
Q.push(Qitem(row - 1 ,j,heightMap[row - 1][j]));
Mark[row - 1][j] = 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 >=col
|| 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;
}
};
走迷宫
下图表示迷宫,数字为0代表通路,数字1代表墙。左上角为入口,右下角为出口。需要使用广度优先搜索的方式来计算走到出口使用的最小步骤。
思路:
在起始位置时,有四个方向可以探索,因此从起始位置开始可以探索的点有四个。下图格子中的数字代表探索的步数。
未完待续