算法练习-搜索算法
深度优先搜索
695. 岛屿的最大面积(easy)
题目:给你一个大小为 m x n 的二进制矩阵 grid 。
岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。
岛屿的面积是岛上值为 1 的单元格的数目。
计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0 。
输入:grid = [[0,0,1,0,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,1,1,0,1,0,0,0,0,0,0,0,0],[0,1,0,0,1,1,0,0,1,0,1,0,0],[0,1,0,0,1,1,0,0,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,1,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,0,0,0,0,0,0,1,1,0,0,0,0]]
输出:6
解释:答案不应该是 11 ,因为岛屿只能包含水平或垂直这四个方向上的 1 。
示例 2:
输入:grid = [[0,0,0,0,0,0,0,0]]
输出:0
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 50
grid[i][j] 为 0 或 1
分析
这个题目用深度递归的思路比较简单。首先他给了我一个二维数组,那么我可以去遍历,以当前的格子为基准,向四处发散,如果遇到了边缘处或者值为0的地方,返回0。走过的地方,赋值为0。采用递归的思想,当前的面积加上后续路径的面积是总面积,然后继续递归下去。最后从所有的结果里选出最大的那个,就是最大值了。
一个比较关键的点就是:先判断边界返回,再处理方向(或是在方向里继续递归)
var maxAreaOfIsland = function (grid) {
// 采用深度优先看一下
let ans = 0;
let row = grid.length;
let col = grid[0].length;
let dx = [-1, 1, 0, 0];
let dy = [0, 0, -1, 1];
function dfs(i, j) {
// 走不通或者是边界不符合条件
if (i < 0 || i >= row || j < 0 || j >= col || grid[i][j] == 0) return 0;
// 走过后赋值0
grid[i][j] = 0;
let len = 1;
for (let m = 0; m < dx.length; m++) {
len += dfs(i + dx[m], j + dy[m])
}
return len
}
for (let i = 0; i < row; i++) {
for (let j = 0; j < col; j++) {
ans = Math.max(ans, dfs(i, j))
}
}
return ans;
};
547. 省份数量(medium)
题目:有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。
返回矩阵中 省份 的数量。
示例 1:
输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]]
输出:2
输入:isConnected = [[1,0,0],[0,1,0],[0,0,1]]
输出:3
分析
这题需要思考,那么多的城市,需要怎么连成一片。在上题里面,重点要记录里面的面积。而这里只用把城市连接起来,不用进行状态的叠加。连起来的城市之间的值赋值0,而且,a-b和b-a是一样的,所以这里走一半表格就可以了。
**关键:**这里不是向四处散发了,以行或者列去查找关联的城市。原因是同一行或同一列的城市之间才会有连接。
var findCircleNum = function (isConnected) {
// 继续采用深度优先遍历
let row = isConnected.length;
let col = isConnected[0].length;
function dfs(i,j){
if(isConnected[i][j]==0) return 0;
isConnected[i][j]=0;
let len=1;
for(let m=0;m<=j;m++){
dfs(m,j)
}
for(let m=i;m<col;m++){
dfs(i,m)
}
return len;
}
let count=0
for(let i=0;i<row;i++){
for(let j=i;j<col;j++){
count += dfs(i,j)
}
}
return count;
};
417. 太平洋大西洋水流问题(medium)
题目:有一个 m × n 的矩形岛屿,与 太平洋 和 大西洋 相邻。 “太平洋” 处于大陆的左边界和上边界,而 “大西洋” 处于大陆的右边界和下边界。
这个岛被分割成一个由若干方形单元格组成的网格。给定一个 m x n 的整数矩阵 heights , heights[r][c] 表示坐标 (r, c) 上单元格 高于海平面的高度 。
岛上雨水较多,如果相邻单元格的高度 小于或等于 当前单元格的高度,雨水可以直接向北、南、东、西流向相邻单元格。水可以从海洋附近的任何单元格流入海洋。
返回网格坐标 result 的 2D 列表 ,其中 result[i] = [ri, ci] 表示雨水从单元格 (ri, ci) 流动 既可流向太平洋也可流向大西洋
示例 1:
输入: heights = [[1,2,2,3,5],[3,2,3,4,4],[2,4,5,3,1],[6,7,1,4,5],[5,1,1,2,4]]
输出: [[0,4],[1,3],[1,4],[2,2],[3,0],[3,1],[4,0]]
示例 2:
输入: heights = [[2,1],[1,2]]
输出: [[0,0],[0,1],[1,0],[1,1]]
分析
这题正着想,比较难。逆推着写,逻辑会简单很多。题目像要求得陆地的高地。也就是说从太平洋能流到的地方,从太平洋也能流过去。重叠的地方是既能流到太平洋,也能流到大西洋了。准备两个二维数组记录,有数据重叠的就是我们需要的地方。
var pacificAtlantic = function(heights) {
// 深度优先
let row=heights.length;
let col=heights[0].length;
let Pacific=new Array(row).fill(0).map(()=>new Array(col).fill(0));
let Atlantic=new Array(row).fill(0).map(()=>new Array(col).fill(0));
let ans=[];
let dx = [1,-1,0,0];let dy=[0,0,1,-1]
function dfs(x,y,arr){
if(arr[x][y]==1) return;
arr[x][y]=1;
for(let i=0;i<dx.length;i++){
let [x2,y2] = [x+dx[i],y+dy[i]]
if(0<=x2 && x2<row && 0<=y2 && y2<col && heights[x][y]<=heights[x2][y2]){
dfs(x2,y2,arr)
}
}
}
// 上边流动
for(let i=0;i<col;i++){
dfs(0,i,Pacific)
}
// 左边框流动
for(let i=0;i<row;i++){
dfs(i,0,Pacific)
}
// 右边流动
for(let i=0;i<row;i++){
dfs(i,col-1,Atlantic)
}
// 下边流动
for(let i=0;i<col;i++){
dfs(row-1,i,Atlantic)
}
for(let m=0;m<row;m++){
for(let n=0;n<col;n++){
if(Pacific[m][n]==Atlantic[m][n] && Pacific[m][n]==1){
ans.push([m,n])
}
}
}
return ans;
};
回溯法
46. 全排列(medium)
题目:给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]
示例 3:
输入:nums = [1]
输出:[[1]]
提示:
1 <= nums.length <= 6
-10 <= nums[i] <= 10
nums 中的所有整数 互不相同
分析
回溯法用的时候,会搭配深度优先或者广度优先的方法来写,但是不同的是,他需要记录每一步的记录,最后一条条记录里,就有可能会有我们需要的答案。前面几个题注重最后的搜索结果,每一步走的什么,并不关注。回溯法里有一个重要的变量path,用Array.from(),进行传递,可以不影响到原来的数组,并将数组传递下去。
var permute = function (nums) {
let ans = [];
let path = [];
allsort(nums, nums.length, [])
function allsort(nums, k, used) {
for (let i = 0; i < k; i++) {
if (path.length == k) {
ans.push(Array.from(path))
return
}
if (used[i]) continue;
used[i] = true;
path.push(nums[i]);
allsort(nums, k, used)
path.pop();
used[i] = false;
}
}
return ans;
};
77. 组合(medium)
题目:给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
示例 2:
输入:n = 1, k = 1
输出:[[1]]
提示:
1 <= n <= 20
1 <= k <= n
分析
从N个数中选择K个。首先找到循环的切入点。因为顺序是任意的,所以我们在选的时候就不要考虑乱序的问题了,从前向后一个一个找,不要返回去找参数,避免重复。那么第一个数的范围就是在[1,n-k+1],后面一定要留K-1个可选择的数,否则这条选择路径就是不可行的,后面再依次找下去,比如第一个的位置是1,那么第2个数的位置范围就是[2,n-k+2],依次类推。
var combine = function(n, k) {
let ans=[];
let path=[];
let arr=[];
for(let i=1;i<=n;i++){
arr.push(i)
}
back(arr,0,n)
function back(arr,start,end){
if(end-start<k-path.length) return
if(path.length==k){
ans.push(Array.from(path));
return;
}
for(let i=start;i<end;i++){
path.push(arr[i]);
back(arr,i+1,end);
path.pop();
}
}
return ans;
};
79. 单词搜索(medium)
题目:给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “ABCCED”
输出:true
分析
采用回溯法,还是深度优先递归的思路,从左上角开始走起,如果字符和判断的字符一样,那么就标记这里已经走过了,然后继续向四个方向走去递归,否则返回,标记为未走过。如果这条路后面的路径走不通,那么已走过的标记撤回,如果不撤回标记,走通后,返回true。
var exist = function (board, word) {
let row = board.length;
let col = board[0].length;
let dx = [1, -1, 0, 0];
let dy = [0, 0, 1, -1];
function back(x, y, index) {
if (index == word.length) {
return true;
}
if (x < 0 || x >= row || y < 0 || y >= col || board[x][y] == 'used') {
return false;
}
if (board[x][y] == word[index]) {
let temp = board[x][y]
board[x][y] = 'used';
let res = false
for (let i = 0; i < dx.length; i++) {
res = res || back(x + dx[i], y + dy[i], index + 1);
}
if (!res) {
board[x][y] = temp;
}else{
return true;
}
}
return false;
}
for (let i = 0; i < row; i++) {
for (let j = 0; j < col; j++) {
let res = back(i, j, 0);
if (res) return res;
}
}
return false
};
51. N 皇后(hard)
题目:按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
示例 2:
输入:n = 1
输出:[[“Q”]]
分析
这题呢,还是用深度递归,但是和前面的题有点区别。1.首先,在这个棋盘上再去遍历每一个位置作为第一个棋是没有必要的,因为他又不分顺序。所以我们只用遍历第一行,让第一行的每个位置都作为起始位置就行了,然后再一行一行接着向下找。2.判断是否可下,不可同行,不可同列,斜着同行或者同列也都不行。可以借助外在的变量去记录,首先同行可以排除,因为同行是不可能的,之后用数组记录是否同列即可。左斜与右斜也不可以,此时借助map去记录,左上到右下的每一条斜线,同一条线上x-y的差都相同的,可以用差值作为K,记录这条线上是否有棋子。从右上到左下,同一条线上x+y的只都是相同的,同理可记录。
当最后一行可以确定一个棋子的时候,说明这个走法是可行的。
走完之后一定要回溯,因为还要走其他的路径
var solveNQueens = function (n) {
// 每行或者每列,至少有一个棋子
let ans = new Array(n)
let result = []
// 斜角
let s1 = new Map()
let s2 = new Map()
let col = new Array(n).fill(0)
for (let i = 0; i < n; i++) {
ans[i] = new Array(n).fill('.')
}
function back(ans, col, row, index) {
if (row == n-1) {
ans[row][index] = 'Q'
let res = []
let temp=''
ans.forEach(item=>{
for(let i=0;i<item.length;i++){
temp += item[i]
}
res.push(temp)
temp=''
})
result.push(res)
ans[row][index] = '.'
return
}
// 占位
let temp1 = row - index;
let temp2 = row + index;
ans[row][index] = 'Q'
col[index] = 1;
s1.set(temp1, 1)
s2.set(temp2, 1)
// 看下一步能不能走
for (let i = 0; i < n; i++) {
if (!col[i]) {//不同列
let temp1 = row+1 - i;
let temp2 = row+1 + i;
if (!s1.has(temp1) && !s2.has(temp2)) {
back(ans, col, row + 1, i)
}
}
}
ans[row][index] = '.'
col[index] = 0;
s1.delete(temp1)
s2.delete(temp2)
}
for (let i = 0; i <n; i++) {
back(ans, col, 0, i)
}
return result
};
广度优先搜索
934. 最短的桥(medium)
题目:给你一个大小为 n x n 的二元矩阵 grid ,其中 1 表示陆地,0 表示水域。
岛 是由四面相连的 1 形成的一个最大组,即不会与非组内的任何其他 1 相连。grid 中 恰好存在两座岛 。
你可以将任意数量的 0 变为 1 ,以使两座岛连接起来,变成 一座岛 。
返回必须翻转的 0 的最小数目。
示例 1:
输入:grid = [[0,1],[1,0]]
输出:1
示例 2:
输入:grid = [[0,1,0],[0,0,0],[0,0,1]]
输出:2
示例 3:
输入:grid = [[1,1,1,1,1],[1,0,0,0,1],[1,0,1,0,1],[1,0,0,0,1],[1,1,1,1,1]]
输出:1
分析
广度优先搜索和深度优先搜索不太一样,深度优先搜索是找到一个循环条件,不停地向下去分析,到符合数组或某个条件终止。而广度优先搜索则是一层数据一层数据地判断,往往伴随着while嵌套for循环,并且在for循环之前重新赋值for循环的赋值,也就是代表新的层数。
本题可以通过不断扩展岛的面积来解决。在岛的边缘再加上一圈,从一个岛上上开始扩散。一圈一圈地加,直到最后出现重叠到另一个岛说明有了最小的路径,而翻转数就是层数。
var shortestBridge = function (grid) {
let n = grid.length;
let queue = [];
let dx = [1, -1, 0, 0];
let dy = [0, 0, -1, 1];
let isLand = [];
let step = 0;
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
if (grid[i][j] == 1) {
// 有地
queue.push([i, j]);
grid[i][j] = -1;
while (queue.length) {
let [x, y] = queue.shift();
// 把取出的岛存起来
isLand.push([x, y])
for (let s = 0; s < 4; s++) {
let nx = x + dx[s];
let ny = y + dy[s];
if (0 <= nx && nx < n && 0 <= ny && ny < n) {
if (grid[nx][ny] == 1) {
queue.push([nx, ny]);
grid[nx][ny] = -1;
}
}
}
}
for (let land of isLand) {
queue.push(land)
}
while (queue.length>0) {
let length = queue.length;
for (let i = 0; i < length; i++) {
let [x, y] = queue.shift();
for (let s = 0; s < 4; s++) {
let nx = x + dx[s];
let ny = y + dy[s];
if (0 <= nx && nx < n && 0 <= ny && ny < n) {
if (grid[nx][ny] == 0) {
queue.push([nx, ny]);
grid[nx][ny] = -1;
} else if (grid[nx][ny] == 1) {
return step;
}
}
}
}
step++;
}
}
}
}
return 0
};
练习
130. 被围绕的区域(medium)
题目:给你一个 m x n 的矩阵 board ,由若干字符 'X' 和 'O' ,找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O' 用 'X' 填充。
示例 1:
输入:board = [["X","X","X","X"],["X","O","O","X"],["X","X","O","X"],["X","O","X","X"]]
输出:[["X","X","X","X"],["X","X","X","X"],["X","X","X","X"],["X","O","X","X"]]
解释:被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。 任何不在边界上,或不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。
示例 2:
输入:board = [["X"]]
输出:[["X"]]
分析
这题和太平洋那题很像,从里向外找O很复杂。但是从边界处找到O,并且想里面延伸就相对简单,把从边界处扩展到的O标记一下,那么没有被标记到的O就是会被替换成为X。用深度优先递归写。
var solve = function (board) {
let m = board.length;
let n = board[0].length;
function dfs(row, col) {
if (row < 0 || row >= m || col < 0 || col >= n) return
if (board[row][col] == 'O') {
board[row][col] = 'NX';
dfs(row + 1, col);
dfs(row - 1, col);
dfs(row, col - 1);
dfs(row, col + 1);
}
}
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (i == 0 || j == 0 || i == m - 1 || j == n - 1) {
dfs(i, j)
}
}
}
for (let t = 0; t < m; t++) {
for (let k = 0; k < n; k++) {
if (board[t][k] == "NX") {
board[t][k] = 'O'
} else if (board[t][k] == "O") {
board[t][k] = 'X'
}
}
}
}
257. 二叉树的所有路径(easy)
题目:给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。
叶子节点 是指没有子节点的节点。
示例 2:
输入:root = [1]
输出:[“1”]
分析
这个题目示例有一个迷惑项,就是它让你以为root是个数组,其实root是根节点,直接拿来用。利用回溯+深度优先搜索左右子树搜出来并记录。如果左右子树都为空,说明这是一条路径。
var binaryTreePaths = function (root) {
let ans = []
function search(root, path) {
// return root.val.toString()
if (root) {
path.push(root.val)
if (root.left) {
search(root.left, Array.from(path))
}
if (root.right) {
search(root.right, Array.from(path))
}
if (!root.left && !root.right) {
let temp = path.join('->');
ans.push(temp)
}
}
}
search(root, [])
return ans
};
难度进阶
47. 全排列 II(medium)
题目:给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1:
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]
示例 2:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
分析
这题的核心思想,是构建一棵树,回溯递归。比如示例一:根节点有1,1,2三种情况,但是1和1是一样的,那我们就可以直接忽略重复的情况。从同一个节点出发去扩散,在每一层中将重复的子节点删除,可以使用map来记录重复的值,保证不会走向相同的路径。用过的节点,我们可以用一个新数组来记录他的位置used,为1表示数据已经被使用了。
var permuteUnique = function (nums) {
let n = nums.length;
let used = new Array(n).fill(0);
let ans = [];
function back(index, path) {
if (path.length == n) {
// used[index] = 0;
if (ans.indexOf(path) == -1) {
ans.push(path);
}
return
}
let res = new Map()
for (let i = 0; i < n; i++) {
if (!used[i]) {
if (!res.has(nums[i])) {
res.set(nums[i],1)
path.push(nums[i])
used[i] = 1;
back(i, Array.from(path));
path.pop();
used[i] = 0
}
}
}
// path.pop();
// used[index] = 0
}
let res = new Map()
for (let i = 0; i < n; i++) {
if (!res.has(nums[i])) {
res.set(nums[i],1);
used[i] = 1;
back(i, [nums[i]])
used[i] = 0
}
}
return ans
};
40. 组合总和 II (medium)
题目:给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]
提示:
1 <= candidates.length <= 100
1 <= candidates[i] <= 50
1 <= target <= 30
分析
这题也是用回溯,方法和上题类似,只是每个加数都是大于0的,可以提前结束某个不合适的路径。同样,他也有重复的数,每一层里,每个加数只让他出现一次。
var combinationSum2 = function (candidates, target) {
let n = candidates.length;
let used = new Array(n).fill(0);
let ans = [];
function dfs(index,path, sum) {
if (sum > target) {
return
}
if (sum == target) {
ans.push(path)
return
}
let temp = new Map();
for (let i = index+1;i<n;i++){
if(!used[i] && !temp.has(candidates[i])){
used[i]=1;
temp.set(candidates[i],1)
path.push(candidates[i])
dfs(i, Array.from(path),sum+candidates[i]);
used[i]=0;
path.pop();
}
}
}
candidates.sort((a,b)=>a-b)
dfs(-1,[],0)
return ans
};
310. 最小高度树(medium)
题目:树是一个无向图,其中任何两个顶点只通过一条路径连接。 换句话说,一个任何没有简单环路的连通图都是一棵树。
给你一棵包含 n 个节点的树,标记为 0 到 n - 1 。给定数字 n 和一个有 n - 1 条无向边的 edges 列表(每一个边都是一对标签),其中 edges[i] = [ai, bi] 表示树中节点 ai 和 bi 之间存在一条无向边。
可选择树中任何一个节点作为根。当选择节点 x 作为根节点时,设结果树的高度为 h 。在所有可能的树中,具有最小高度的树(即,min(h))被称为 最小高度树 。
请你找到所有的 最小高度树 并按 任意顺序 返回它们的根节点标签列表。
树的 高度 是指根节点和叶子节点之间最长向下路径上边的数量。
分析
这题我本来还想着用回溯法去做,找最小高度数。但是后面数据一旦多起来就会超时,所以要换个方法。由于这是个无向图,所以可以尝试用拓扑结构。看了一些分析,找到能让树最小的点,就要让权重小的子树,一层层砍掉。最后留下一个或者两个节点,那就是让树最低的节点。写法上还是用广度优先。但是要建立两个数组,一个存与A相关联的所有点,另一个存权重,有一条线,点的权重+1,砍掉一条线,权重-1.当权重为1的时候将该点相关联的线删除,关联的点权重-1.
var findMinHeightTrees = function(n, edges) {
let G = new Array(n).fill(0).map(()=>[]);
let dev = new Array(n).fill(0);
let queue = []
let res = []
if(n==1) return [0]
for(let [u,v] of edges){
G[u].push(v);G[v].push(u);
dev[u]++;dev[v]++;
}
for(let i=0;i<n;i++){
if(dev[i]==1){
queue.push(i)
}
}
while(queue.length>0){
res = [];
let size = queue.length;
for(let i=0;i<size;i++){
let node = queue.shift();
dev[node]--;
res.push(node)
for(let d of G[node]){
if(dev[d]>0){
dev[d]--;
if(dev[d]==1) queue.push(d)
}
}
}
}
return res
};