算法之回溯算法框架

回溯算法框架

前言

熟悉二叉树的同学都知道,二叉树遍历有三种:前序遍历、中序遍历以及后序遍历

不熟悉也不打紧,下面给你简单科普一下:

前序遍历的遍历顺序是:根节点–>左节点–>右节点

中序遍历的遍历顺序是:左节点–>根节点–>右节点

后序遍历的遍历顺序是:左节点–>右节点–>根节点

代码如下:

/* 基本的二叉树节点 */
struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int value) : val(value), left(nullptr), right(nullptr) {}
};

void NodeAction(TreeNode *node) {
    cout << node->val << endl;
}

// 前序遍历
void PreTraverse(TreeNode *root) {
    if (root == nullptr) {
        return;
    }
    NodeAction(root); // 当前节点需要做的操作,如:打印
    PreTraverse(root->left);
    PreTraverse(root->right);
}

// 中序遍历
void MiddleTraverse(TreeNode *root) {
    if (root == nullptr) {
        return;
    }
    MiddleTraverse(root->left);
    NodeAction(root); // 当前节点需要做的操作,如:打印
    MiddleTraverse(root->right);
}

// 后序遍历
void PostTraverse(TreeNode *root) {
    if (root == nullptr) {
        return;
    }
    PostTraverse(root->left);
    PostTraverse(root->right);
    NodeAction(root); // 当前节点需要做的操作,如:打印
}

从上面代码可以看出,二叉树的遍历核心思想是:递归、递归、递归!

算法分析

直接上干货,实际上回溯算法与二叉树遍历有异曲同工之妙!都是以递归为基础进行的,都是将一棵“树”完全遍历,也就是暴力穷举!但是回溯算法有个“做出选择”以及“撤销选择”的过程。

result = []
def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return

    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择

实际上回溯算法包含四大步骤就是:

  • 路径列表
  • 做出选择
  • 撤销选择
  • 终止条件

N 皇后问题

简单解释一下:给你一个 N×N 的棋盘,让你放置 N 个皇后,使得它们不能互相攻击。

PS:皇后可以攻击同一行、同一列、左上左下右上右下四个方向的任意单位。

如果将一个回溯问题看作是一棵决策树,回溯算法就是在这棵树上从上往下一层一层地做出选择,那N皇后这个问题本质实际上就是每一层的决策树表示棋盘上的每一行;每个节点可以做出的选择是,在该行的任意一列放置一个皇后。

直接套公式:

vector> res;

// 是否可以在 board[row][col] 放置皇后?
bool isValid(vector& board, int row, int col) {
    int n = board.size();
    // 检查列是否有皇后互相冲突
    for (int i = 0; i < n; i++) {
        if (board[i][col] == 'Q')
            return false;
    }
    // 检查右上方是否有皇后互相冲突
    for (int i = row - 1, j = col + 1;
         i >= 0 && j < n; i--, j++) {
        if (board[i][j] == 'Q')
            return false;
    }
    // 检查左上方是否有皇后互相冲突
    for (int i = row - 1, j = col - 1;
         i >= 0 && j >= 0; i--, j--) {
        if (board[i][j] == 'Q')
            return false;
    }
    return true;
}

// 路径:board 中小于 row 的那些行都已经成功放置了皇后
// 选择列表:第 row 行的所有列都是放置皇后的选择
// 结束条件:row 超过 board 的最后一行
void backtrack(vector& board, int row) {
    // 触发结束条件
    if (row == board.size()) {
        res.push_back(board);
        return;
    }

    int n = board[row].size();
    for (int col = 0; col < n; col++) {
        // 排除不合法选择
        if (!isValid(board, row, col))
            continue;
        // 做选择
        board[row][col] = 'Q';
        // 进入下一行决策
        backtrack(board, row + 1);
        // 撤销选择
        board[row][col] = '.';
    }
}

// 输入棋盘边长 n,返回所有合法的放置
vector> solveNQueens(int n) {
    // '.' 表示空,'Q' 表示皇后,初始化空棋盘。
    vector board(n, string(n, '.'));
    backtrack(board, 0);
    return res;
}

从上面是不是能看得到二叉树遍历的身影?是就没错了!

回溯算法这种暴力遍历的方法导致的一个问题就是时间复杂度贼大。一般来说题目都会让你在适当的时候终止遍历,如上面的N皇后问题,一般来说,只需要你得出其中一种摆放方式即可。

将上面的代码添加适当的终止条件:

vector> res;

// 是否可以在 board[row][col] 放置皇后?
bool isValid(vector& board, int row, int col) {
    int n = board.size();
    // 检查列是否有皇后互相冲突
    for (int i = 0; i < n; i++) {
        if (board[i][col] == 'Q')
            return false;
    }
    // 检查右上方是否有皇后互相冲突
    for (int i = row - 1, j = col + 1;
         i >= 0 && j < n; i--, j++) {
        if (board[i][j] == 'Q')
            return false;
    }
    // 检查左上方是否有皇后互相冲突
    for (int i = row - 1, j = col - 1;
         i >= 0 && j >= 0; i--, j--) {
        if (board[i][j] == 'Q')
            return false;
    }
    return true;
}

// 路径:board 中小于 row 的那些行都已经成功放置了皇后
// 选择列表:第 row 行的所有列都是放置皇后的选择
// 结束条件:row 超过 board 的最后一行
bool backtrack(vector& board, int row) {
    // 触发结束条件
    if (row == board.size()) {
        res.push_back(board);
        return true;
    }

    int n = board[row].size();
    for (int col = 0; col < n; col++) {
        // 排除不合法选择
        if (!isValid(board, row, col))
            continue;
        // 做选择
        board[row][col] = 'Q';
        // 进入下一行决策
        if (backtrack(board, row + 1)) {
            return true;
        }
        // 撤销选择
        board[row][col] = '.';
    }
    return false;
}

// 输入棋盘边长 n,返回所有合法的放置
vector> solveNQueens(int n) {
    // '.' 表示空,'Q' 表示皇后,初始化空棋盘。
    vector board(n, string(n, '.'));
    backtrack(board, 0);
    return res;
}

总某种程度上来说,动态规划的一种暴力解就是回溯算法;一般来说单单只套用回溯算法的实用性题目是比较少的,都是需要在某个情况做出适当的调整~

你可能感兴趣的:(算法杂谈,回溯算法,二叉树)