dfs和回溯

2023-02-21  本文已影响0人  欢西西西

1. 火车进站

求N列火车进出站的顺序组合

重点:每次可以选择1列进站或1列出站,当选择情况1或情况2时,它们的初始条件应该是相同的,这也是为什么每种情况操作完之后需要进行条件恢复的原因

let trains = ['1', '2', '3'] // 求3列火车进出站的顺序组合

let lth = trains.length;
let stack = [],
    res = []; // 存放出站顺序
let dfs = function (leftArr) {
    if (res.length === lth) {
        console.log(res);
        return;
    }
    // 因为进站和出站这2种选择是并列的,
    // 所以当选了第一种时,操作完后需要将数据恢复,以保证选2和选1的初始条件是相同的
    if (leftArr.length) { // 可以选择一个进站
        let v = leftArr.shift();
        stack.push(v); // 火车进站
        // console.log(v, '进')
        dfs(leftArr);
        leftArr.unshift(stack.pop()); // 恢复
    }
    if (stack.length) { // 或者选择一个出站
        let v = stack.pop(); // 出站
        // console.log(v, '出')
        res.push(v);
        dfs(leftArr);
        stack.push(v); // 恢复
        res.pop(); // 注意res也要恢复
    }
}
dfs(trains);

2. 迷宫问题

let [row, col] = arr.shift(); // 迷宫行和列
let res = [];
function dfs(i, j) {
    if (i === row - 1 && j === col - 1) { // 确定终止条件
        res.push([i, j]);
        res.forEach(r => {
            console.log(`(${r[0]},${r[1]})`);
        });
        return;
    }
    // [i][j]
    let options = []; // 收集本轮的可选值
    if (i > 0 && arr[i - 1][j] === '0') {
        // [i - 1][j] // 上
        options.push([i - 1, j]);
    }
    if (j < col - 1 && arr[i][j + 1] === '0') {
        // [i][j + 1] // 右
        options.push([i, j + 1]);
    }
    if (j > 0 && arr[i][j - 1] === '0') {
        // [i][j - 1] // 左
        options.push([i, j - 1]);
    }
    if (i < row - 1 && arr[i + 1][j] === '0') {
        // [i + i][j]  // 下
        options.push([i + 1, j]);
    }
    if (res.length) { // 过滤可选值——上一步走过的下一步就不走了
        let last = res[res.length - 1];
        options = options.filter(op => {
            return !(op[0] == last[0] && op[1] == last[1]);
        });
    }
    if (options.length) {
        options.forEach((op) => {
            let [m, n] = op;
            res.push([i, j]); // 记录当前格子
            dfs(m, n); // 从下个格子开始
            res.pop(); // res恢复
        });
    }
}
dfs(0, 0)

3. 数独

依次往每个空格子里填入1-9,这个题需要注意dfs的返回值

function dfs() { // 返回一个布尔值
    for (let i = 0; i < 9; i++) {
        for (let j = 0; j < 9; j++) {
            if (arr[i][j] == 0) { // 如果当前是空格子
                for (let k = 1; k <= 9; k++) { // 依次校验1-9是否可以填入,可以就填
                    if (checkIsValid(i, j, k)) { // 校验arr[i][j]是否可以放入k
                        arr[i][j] = k; // 尝试放入
                        let res = dfs(); // 如果返回true,表示这个格子填了k之后其他格子都填数成功,那么不需要再往后走了
                        if (res) { return true }
                        arr[i][j] = 0; // 否则就恢复数据,并继续下一轮尝试                  
                    }
                }
                // 表示填1-9都不行,表示这个数独本身无解
                return false;
            }
        }
    }
    return true; // 表示每一个格子都有数
}

function checkIsValid(i, j, k) { // 检验arr[i][j]的格子是否可以放入k
    if (arr[i].find(r => r == k)) { // 同行不能重复
        return false;
    }
    for (let r = 0; r < 9; r++) {
        if (arr[r][j] == k) { // 同列不能重复
            return false;
        }
    }
    // 9宫格内不重复
    let rStart = Math.floor(i / 3) * 3, 
        cStart = Math.floor(j / 3) * 3;

    let rEnd = rStart + 2, cEnd = cStart + 2;
    while (rStart <= rEnd) {
        let cs = cStart;
        while (cs <= cEnd) {
            if (arr[rStart][cs] == k) {
                return false;
            }
            cs++
        }
        rStart++;
    }
    return true
}

4. 岛屿数量

image.png

这个题最重要的一步:要想统计1聚集的堆数而不重复统计,那每次找到一堆相邻的1,就将其全部改成0

let count = 0;
function dfs(rIndex, cIndex) {
    let row = grid[rIndex]
    grid[rIndex][cIndex] = 0; // 为了不重复读取1,就把已经读取过的1置为0
    // 左
    if (cIndex > 0 && row[cIndex - 1] == 1) {
        dfs(rIndex, cIndex - 1);
    }
    // 右
    if (cIndex + 1 < row.length && row[cIndex + 1] == 1) {
        dfs(rIndex, cIndex + 1);
    }
    // 上
    if (rIndex > 0 && grid[rIndex - 1][cIndex] == 1) {
        dfs(rIndex - 1, cIndex);
    }
    // 下
    if (rIndex + 1 < grid.length && grid[rIndex + 1][cIndex] == 1) {
        dfs(rIndex + 1, cIndex);
    }
}
grid.forEach((row, rIndex) => {
    row.forEach((item, cIndex) => {
        if (item == 1) {
            // 之所以在这里就能增加count是因为下面的dfs会把和这个1相连的岛屿都置为0
            count++; 
            dfs(rIndex, cIndex);
        }
    });
});


console.log(count)

5. N皇后问题

image.png

这个题要一行一行尝试(而不是在格子维度上),在一行中选一个格子放皇后,再dfs下一行

let grid = [];
let res = [];
let count = 0;
function dfs(rowIndex) {
    if (res.length == n) {
        count++;
        return;
    }
    if (rowIndex === n) { // 边界
        return;
    }
    for (let j = 0; j < n; j++) { // 在当前行中选一个格子
        if (isValid(rowIndex, j)) { // 如果能放的话
            grid[rowIndex][j] = 1; // 放入皇后
            res.push([rowIndex, j]) // 收集结果
            dfs(rowIndex + 1); // 在当前结果下进入下一行去选格子
            grid[rowIndex][j] = 0; // 恢复
            res.pop(); // 恢复
        }
    }
}

for (let i = 0; i < n; i++) {
    grid[i] = new Array(n);
}

dfs(0);

检查是否能在grid[i][j]位置上放皇后

function isValid(i, j) {
    let row = grid[i];
    if (row[j] == 1) { // 格子已经是皇后了
        return false;
    }
    if (row.find(r => r == 1)) {
        // 不能在同一列
        return false;
    }
    for (let r of grid) {
        // 不能在同一行
        if (r[j] == 1) {
            return false;
        }
    }

    // 以下校验不能在同一条斜线
    let level = 1;
    while (i - level >= 0 && j - level >= 0) {
        // 左上
        if (grid[i - level][j - level] == 1) {
            return false;
        }
        level += 1;
    }

    level = 1;
    while (i - level >= 0 && j + level < n) {
        // 右上 i-1 j+1 i-2 j+2
        if (grid[i - level][j + level] == 1) {
            return false;
        }
        level++;
    }

    level = 1;
    while (j - level >= 0 && i + level < n) {
        // 左下 i+1 j-1 i+2 j-2
        if (grid[i + level][j - level] == 1) {
            return false;
        }
        level++;
    }

    level = 1;
    while (j + level < n && i + level < n) {
        // 右下 i+1 j+1 i+2 j+2
        if (grid[i + level][j + level] == 1) {
            return false;
        }
        level++;
    }
    return true;
}

6. 全排列

var arr = ['A', 'B', 'V', ];
var res = [];
function dfs() {
    if (res.length === 3) {
        console.log(res.join(' '));
        return;
    }
    for (let item of arr) { // 每一次可选值都是arr里剩余的元素
        res.push(arr.shift());
        dfs();
        arr.push(res.pop());
    }
}
dfs();

7. 传球

M个人相互传球,由甲开始,经过N次传球后,球仍回到甲手中,则不同的传球方式共有多少种?

/**
 * @param {Array} members 队员 
 * @param {Number} count 传球次数
 */
function pass(members, count) {
    const target = members[0]; // 从第一人开始传,最后回到第一个手上
    let result = [target]; // 球从第1个人手上传出去
    let lth = count + 1; // 例如传3次球,result里会有4个人
    const dfs = function (arr, self) {
        if (result.length === lth) { // 次数到了,如果最后一个人是target,就输出
            if (result[result.length - 1] === target) {
                console.log(result.join(' > '));
            }
            return;
        }
        arr.forEach(item => {
            if (item === self) { return; } // 不能传递给自己
            result.push(item);
            count--; // push了一项就表示传了1次,则总次数-1
            dfs(arr, item);
            result.pop();
            count++;
        })
    }
    dfs(members, target);
}


pass(['甲', '乙', '丙'], 5)

上一篇 下一篇

猜你喜欢

热点阅读