参考:
刷题笔记:
回溯算法(DFS)
-
三个概念:
- 选择列表:表示你当前可以做出的选择
- 路径:记录你已经做过的选择
- 结束条件:到达叶子节点,无法再做选择的条件
如在根节点中,选择列表就是[1,2,3],而路径为[];
需要有一个
backtrack函数,在树的各个节点之间游走,进行路径和选择之间的抉择
-
核心思路:
- 用一个全局变量res保存结果
- 判断是否满足结束条件
- 对于选择列表中的每一个选择
- 做选择,加入路径列表
- 对选择的这个节点,继续递归,到下一层;
- 撤销选择,回到上一个节点
let res = [];//存储结果
function backtrack(path,selectList) {
if(结束条件满足){
res.push(path);
return;
}
for(select of selectList){
path.push(select); //做选择,将该选择从selectList中移除,得到newselectList
backtrack(path,newselectList);
path.pop(select);//撤销选择,回到上一个节点
}
}
- 适用场景
- 排列,组合
- 数组,字符串,给定一个特定的规则,尝试找到某个解
- 二维数组下的DFS搜索
【例题】 排列、组合、子集问题
常见形式:
形式一、元素无重不可复选,即 nums 中的元素都是唯一的,每个元素最多只能被使用一次
形式二、元素可重不可复选,即 nums 中的元素可以存在重复,每个元素最多只能被使用一次。
形式三、元素无重可复选,即 nums 中的元素都是唯一的,每个元素可以被使用若干次。
组合、子集问题及变种
子集和组合问题中,要通过 start 参数控制树枝的遍历,避免产生重复的子集
//通过 start 参数控制树枝的遍历,避免产生重复的子集
var subsets = function(nums) {
var res = [];
let backtrack = function(path,start) {
res.push(path);
for(let i = start; i<nums.length; i++) {
path.push(nums[i]);
backtrack(path.slice(),i+1);
path.pop(nums[i]);
}
}
backtrack([],0);
return res;
};
//相当于是子集问题,只不过需要对长度进行筛选
var combine = function(n, k) {
let res = [];
let path = [];
let backtrack = function(path,start) {
if(path.length === k){
res.push(path);
return;
}
for(let i=start; i<=n;i++) {
path.push(i);
backtrack(path.slice(),i+1);
path.pop(i);
}
}
backtrack([],1);
return res;
};
90. 子集 II(元素可重不可复选) - 力扣(LeetCode)
需要进行剪枝如果一个节点有多条值相同的树枝相邻,则只遍历第一条,剩下的都剪掉,不要去遍历。
体现在代码上,需要先进行排序,让相同的元素靠在一起,如果发现 nums[i] == nums[i-1],则跳过:
/**
* @param {number[]} nums
* @return {number[][]}
*/
var subsetsWithDup = function(nums) {
//首先对数组进行排序
nums.sort();
let res = [];
let backtrack = function(path,start) {
res.push(path);
for(let i=start;i<nums.length;i++) {
if(i>start && nums[i]===nums[i-1]){
continue;
}
path.push(nums[i]);
backtrack(path.slice(),i+1);
path.pop(nums[i]);
}
}
backtrack([],0);
return res;
};
40. 组合总和 II(元素可重不可复选) - 力扣(LeetCode)
/**
* @param {number[]} candidates
* @param {number} target
* @return {number[][]}
*/
var combinationSum2 = function(candidates, target) {
candidates.sort();
let res = [];
let backtrace = function(path,pathSum,start) {
// console.log(path);
if(pathSum===target) {
res.push(path);
return;
}
if(pathSum>target) return;
for(let i=start;i<candidates.length;i++) {
if(i>start && candidates[i]===candidates[i-1]){
continue;
}
path.push(candidates[i]);
pathSum+=candidates[i];
backtrace(path.slice(),pathSum,i+1);
path.pop(candidates[i]);
pathSum-=candidates[i];
}
}
backtrace([],0,0);
return res;
};
排列问题
/**
* @param {number[]} nums
* @return {number[][]}
*/
var permute = function(nums) {
let res = [];
let l = nums.length;
let backtrack = function(path) {
if(path.length===l) {
res.push(path);
return;
}
for(select of nums) {
// 当前这个数字不在path中
if(path.indexOf(select) === -1){
path.push(select);
// 传入的是path的副本,而不是path本身
backtrack(path.slice());
path.pop(select);
}
}
}
backtrack([]);
return res;
};
47. 全排列II(元素可重不可复选) - 力扣(LeetCode)
var permuteUnique = function(nums) {
nums.sort();
let res = [];
let len = nums.length;
let used = new Array(len).fill(false);
// console.log(used);
let backtrack = function(path) {
if(path.length === len) {
res.push(path);
return;
}
for(let i =0;i<len;i++) {
if(used[i]) continue;
if( i>0 && nums[i]===nums[i-1] && used[i-1]) {
continue;
}
path.push(nums[i]);
used[i] = true;
backtrack(path.slice());
path.pop(nums[i]);
used[i] = false;
}
}
backtrack([]);
return res;
};
DFS解决岛屿问题
- 每次遇到岛屿,用 DFS 算法把岛屿「淹了」
- 成片的陆地形成岛屿,因此只要遇到一个岛屿,将其上下左右,淹掉即可
var numIslands = function(grid) {
let res=0;
let row = grid.length;
let col = grid[0].length;
for(let i=0;i<row;i++) {
for(let j=0;j<col;j++) {
if(grid[i][j]==1) {
res++;
dfs(grid,i,j);
}
}
}
return res;
};
var dfs = function(grid,i,j) {
let row = grid.length;
let col = grid[0].length;
if(i<0 || j<0 || i>=row || j>= col) return;
if(grid[i][j]==0) return;
grid[i][j] = 0;
dfs(grid,i-1,j);
dfs(grid,i+1,j);
dfs(grid,i,j-1);
dfs(grid,i,j+1);
}
1254. 统计封闭岛屿的数目 - 力扣(LeetCode)
- 等价于把上一题中那些靠边的岛屿排除掉,剩下的就是「封闭岛屿」
var closedIsland = function(grid) {
let row = grid.length;
let col = grid[0].length;
let res = 0;
for(let i=0;i<row;i++) {
// 淹没左边靠岸岛屿
dfs(grid,i,0);
// 淹没右边靠岸岛屿
dfs(grid,i,col-1);
}
for(let j=0;j<col;j++) {
// 淹没上边靠岸岛屿
dfs(grid,0,j);
// 淹没下边靠岸岛屿
dfs(grid,row-1,j);
}
for(let i=0;i<=row-1;i++) {
for(let j=0;j<=col-1;j++) {
if(grid[i][j]==0) {
res++;
dfs(grid,i,j);
}
}
}
return res;
};
var dfs = function(grid,i,j) {
let row = grid.length;
let col = grid[0].length;
if(i<0 || i>=row || j<0 || j>= col) return;
if(grid[i][j]==1) return;
grid[i][j] = 1;
dfs(grid,i-1,j);
dfs(grid,i+1,j);
dfs(grid,i,j-1);
dfs(grid,i,j+1);
}
- 递归函数返回的是这块岛屿的面积
var maxAreaOfIsland = function(grid) {
let res=0;
let row = grid.length;
let col = grid[0].length;
for(let i=0;i<row;i++) {
for(let j=0;j<col;j++) {
if(grid[i][j]==1) {
res = Math.max(res,dfs(grid,i,j));
}
}
}
return res;
};
var dfs = function(grid,i,j) {
let row = grid.length;
let col = grid[0].length;
if(i<0 || j<0 || i>=row || j>= col) return 0;
if(grid[i][j]==0) return 0;
grid[i][j] = 0;
return 1+
dfs(grid,i-1,j)+
dfs(grid,i+1,j)+
dfs(grid,i,j-1)+
dfs(grid,i,j+1);
}
- 淹掉grid2中那些不在grid1中的岛屿
/**
* @param {number[][]} grid1
* @param {number[][]} grid2
* @return {number}
*/
var countSubIslands = function(grid1, grid2) {
let res=0;
let row = grid1.length;
let col = grid1[0].length;
for(let i=0;i<row;i++) {
for(let j=0;j<col;j++) {
if(grid2[i][j]==1 && grid1[i][j]==0) {
dfs(grid2,i,j);
}
}
}
for(let i=0;i<row;i++) {
for(let j=0;j<col;j++) {
if(grid2[i][j]==1) {
res++;
dfs(grid2,i,j);
}
}
}
return res;
};
var dfs = function(grid,i,j) {
let row = grid.length;
let col = grid[0].length;
if(i<0 || j<0 || i>=row || j>= col) return;
if(grid[i][j]==0) return;
grid[i][j] = 0;
dfs(grid,i-1,j);
dfs(grid,i+1,j);
dfs(grid,i,j-1);
dfs(grid,i,j+1);
}
其它
131. 分割回文串(子集问题)- 力扣(LeetCode)
/**
* @param {string} s
* @return {string[][]}
*/
var partition = function(s) {
let res = [];
//利用backtrack函数求子集
let backtrack = function(path,start){
if(start===s.length){
res.push(path);
}
for(let i=start;i<s.length;i++) {
if(!isBackString(s.slice(start,i+1))){
continue;
}
path.push(s.slice(start,i+1));
backtrack(path.slice(),i+1);
path.pop(s.slice(start,i+1));
}
}
backtrack([],0);
return res;
};
//判断回文串
let isBackString = function(s) {
let mid = Math.floor((s.length/2));
let flag = false;
let i = 0;
for(let i=0;i<mid;i++){
if(s[i] !== s[s.length-1-i]){
return false;
}
}
return true;
}
有关括号问题,你只要记住以下性质,思路就很容易想出来:
1、一个「合法」括号组合的左括号数量一定等于右括号数量,这个很好理解。
2、对于一个「合法」的括号字符串组合 p,必然对于任何 0 <= i < len(p) 都有:子串 p[0..i] 中左括号的数量都大于或等于右括号的数量。
/**
* @param {number} n
* @return {string[]}
*/
var generateParenthesis = function(n) {
let res = [];
// left,right 分别表示可以使用的左、右括号数量
let backtrack = function(left,right,path) {
// 若剩余的左括号大于剩余的右括号,不合法
if(left>right) return;
if (left < 0 || right < 0) return;
// 字符串合法,存入数组中
if(path.length === 2*n){
res.push(path);
return;
}
//尝试放左括号
path=path.concat("(");
backtrack(left-1,right,path.slice());
path=path.slice(0,-1);
//尝试放右括号
path=path.concat(")");
backtrack(left,right-1,path.slice());
path=path.slice(0,-1);
}
backtrack(n,n,"");
return res;
};
BFS
BFS 的核心思想应该不难理解的,就是把一些问题抽象成图,从一个点开始,向四周开始扩散。
这里注意这个 while 循环和 for 循环的配合,while 循环控制一层一层往下走,for 循环利用 sz 变量控制从左到右遍历每一层二叉树节点:
sz = queue.length
111. 二叉树的最小深度 - 力扣(LeetCode)
var minDepth = function(root) {
if(root===null) return 0;
var queue = [root];
var depth = 1;
var curNode;
while(queue.length!==0) {
//这里的for循环相当于遍历一层
let sz = queue.length;
for(let i=0;i<sz;i++) {
curNode = queue.shift();
if(curNode.left===null && curNode.right === null){
//求最小深度,即找到第一个叶子节点
return depth;
}
if(curNode.left!==null){
queue.push(curNode.left);
}
if(curNode.right!==null){
queue.push(curNode.right);
}
}
depth++;
}
return depth;
};
var openLock = function(deadends, target) {
//问题描述:从‘0000’开始到target-‘0202’所需的最小路径长度
// 但不能碰到deadends中的数字
const deads = new Set(deadends);
const visited = new Set();
let str = '0000';
let steps = 0;
let queue = [];
queue.push(str);
visited.add(str);
while(queue.length!=0) {
let sz = queue.length;
for(let i=0;i<sz;i++) {
let curStr = queue.shift();
if(deads.has(curStr)) continue;
if(curStr===target) return steps;
//将其相邻节点放入队列中
for(let j=0;j<4;j++) {
let upStr = plusOne(curStr,j);
let downStr = minsOne(curStr,j);
if(!visited.has(upStr)){
queue.push(upStr);
visited.add(upStr);
}
if(!visited.has(downStr)){
queue.push(downStr);
visited.add(downStr);
}
}
}
steps++;
}
return -1;
};
var plusOne = function(s,i) {
let nums = s.split("");
if(nums[i]==9) {
nums[i] = 0
}else{
nums[i]++;
}
return nums.join("");
}
var minsOne = function(s,i) {
let nums = s.split("");
if(nums[i]==0) nums[i] = 9;
else nums[i]--;
return nums.join("");
}