前端刷题笔记(三)回溯(DFS)+BFS

170 阅读5分钟

参考:

刷题笔记:

回溯算法(DFS)

  1. 三个概念:

    • 选择列表:表示你当前可以做出的选择
    • 路径:记录你已经做过的选择
    • 结束条件:到达叶子节点,无法再做选择的条件 如在根节点中,选择列表就是[1,2,3],而路径为[]; 需要有一个backtrack函数,在树的各个节点之间游走,进行路径和选择之间的抉择 image.png
  2. 核心思路:

    • 用一个全局变量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);//撤销选择,回到上一个节点
    }
}
  1. 适用场景
    • 排列,组合
    • 数组,字符串,给定一个特定的规则,尝试找到某个解
    • 二维数组下的DFS搜索

【例题】 排列、组合、子集问题

常见形式:

形式一、元素无重不可复选,即 nums 中的元素都是唯一的,每个元素最多只能被使用一次

形式二、元素可重不可复选,即 nums 中的元素可以存在重复,每个元素最多只能被使用一次

形式三、元素无重可复选,即 nums 中的元素都是唯一的,每个元素可以被使用若干次

组合、子集问题及变种

image.png 子集和组合问题中,要通过 start 参数控制树枝的遍历,避免产生重复的子集

78. 子集 - 力扣(LeetCode)

//通过 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;
};

77. 组合 - 力扣(LeetCode)

//相当于是子集问题,只不过需要对长度进行筛选
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],则跳过

image.png

/**
 * @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;
};

排列问题

image.png 46. 全排列 - 力扣(LeetCode)

/**
 * @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解决岛屿问题

200. 岛屿数量 - 力扣(LeetCode)

  • 每次遇到岛屿,用 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);
}

695. 岛屿的最大面积 - 力扣(LeetCode)

  • 递归函数返回的是这块岛屿的面积
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);
}

1905. 统计子岛屿 - 力扣(LeetCode)

  • 淹掉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;
}

22. 括号生成 - 力扣(LeetCode)

有关括号问题,你只要记住以下性质,思路就很容易想出来:

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 image.png 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;
};

752. 打开转盘锁 - 力扣(LeetCode)

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("");
}