基础算法--搜索

基础算法–搜索

什么是搜索

搜索本质就是以某种特定的方法,枚举状态空间的状态。如果搜索空间是线性的,通常直接枚举

  • 直接枚举 – 从1n的正整数,每个整数都是一个状态
  • 枚举全排列 – 每个排列状态都是一个状态
  • 数/图搜索 – 每个节点都是一个状态

搜索的过程其实就是,从一个起始状态出发,通过给定的规则寻找后继状态,到达终止状态时停止继续扩展

搜索树

基础算法--搜索_第1张图片
以起点状态为根,每个状态向其后继状态连有向边,可以得到一棵有根数,终止状态对应这棵树的叶子节点。搜索过程可以被抽象成遍历这棵搜索树的过程,如果需要遍历整棵树,则复杂度至少正比于搜索树的节点数量。

搜索的时间复杂度

  • 记状态空间为Q,终止状态的集合为F
  • 如果可以用终止状态的数量估计搜索的复杂度:O(|Q|) = O(|F|)
  • 如果搜索树恰好有k层,每个非终止状态恰好有d个后继,那么终止状态的数量为|F| = d^k
  • 如果在每个状态上的额外计算成本为O(c),则搜索的总复杂度为O(c|Q|) = O(cd^k)
  • 搜索复杂度关于问题规模(这里指k)是指数级增长的
  • 枚举n次投骰子的结果
    • 初始状态为:1 种状态
    • 投一次:1+6 = 7种状态
    • 投二次:7+6*6 = 42种状态
    • 假设一台超级计算机每秒能算一百亿亿个状态(美国Frontier 1.102 EFlops),投40次的结果要算51万年

搜索策略

我们先定一简单的节点结构

struct Node {
    int val;
    std::vector<Node *> children;
    Node(int v) { val = v; };
};Ï
深度优先(Depth-First-Search)

优先遍历一个后继节点的子树内所有节点,更加通俗的理解就是,先一条路走到黑,再返回上一个分岔节点

void dfs(Node *node) {
    if (!node) return;
    std::cout << node->val << std::endl;
    for (int i = 0; i < node->children.size(); ++i) {
        dfs(node->children[i]);
    }
}	

如果节点是空,表明父节点是终止状态,因此停止此分支的搜索。如果节点不为空,表明节点存在后继状态,因此以某种顺序递归调用dfs即可

广度优先(Breadth-First-Search)

先遍历所有后继节点,再遍历后继节点的后继,更加通俗的理解就是,在分岔点分身,最总每个终止节点都有一个分身

void bfs(Node *node) {
    if (!node) return;
    std::queue<Node *> q;
    q.push(node);
    while (!q.empty()) {
        Node *n = q.front();
        std::cout << n->val << std::endl;
        for (int i = 0; i < n->children.size(); ++i) {
            q.push(n->children[i]);
        }
        q.pop();
    }
}

先使用一个队列保存状态,如果队列不为空,即对队列中的第一个状态做操作,然后将这个状态的所有后继状态全部加入到队列中,这个队列保证了只有当前层k上的节点和k+1层的节点。

DFS BFS
只需要存储从初始状态到当前状态的一条路径 需要存储所有尚待拓展的状态,空间开销大
当递归层数比较深时可能出现爆栈 可以动态使用堆内存
需要考虑回溯撤销的问题,细节可能比较麻烦 状态单向拓展,实现较为简单
搜索层数不确定时可能带来问题:无限拓展 可以知道从初始状态到每个状态的最少步数
子树中节点编号是连续的 同一层的节点编号是连续的

迭代加深搜索

通过上面我们知道,使用DFS搜索解时,有可能遇到层数不确定的问题。但是有时候由于空间限制等种种原因不适合使用BFS,但是又需要们求解出最小层数的解。这个时候我们可以考虑迭代加深搜索

对于迭代加深搜索,首先深度优先搜索k层,若没有找到可行解,再深度优先搜索k+1层,直到找到可行解为止。由于深度是从小到大逐渐增大的,所以当搜索到结果时可以保证搜索深度是最小的。这也是迭代加深搜索在一部分情况下可以代替广度优先搜索的原因。

迭代加深搜索的优势大体可归纳为以下三点

  • 时间复杂度只比深度优先搜索稍差一点,虽然搜索k+1层时会重复搜索k层,但是整体而言并不比广搜慢很多
  • 空间复杂度与深搜相同,却比广搜小很多
  • 利于剪枝

搜索剪枝

剪枝的目的是让效率更高,但是注意不要将最优解给剪掉

  • 可行性剪枝
    • 如果当前状态已经不满足题目要求,就不继续拓展
    • 可以用于最优问题,也可以用于统计解
  • 最优性剪枝
    • 如果从当前状态出发,可以得到的最优解一定不比已经得到的最优解优,则不继续拓展
    • 只能用于求解最优解问题

案例

N皇后问题

给定一个国际象棋棋盘,要求在上面放置N个皇后,使得两两之间不能相互攻击(皇后可以对同行,同列,同一对角线的棋子进行攻击)。这是一个典型的DFS案例

  • 暴力解法:枚举8个格子,终止状态是64*63*62*...*57= 4426165368
  • 可行性剪枝:每行只能放一个,每列也只能放一个,故只需要枚举排列8!=40320个状态
  • 可行性剪枝:可以先检测当前已经放置的皇后是否会对角攻击
bool check(std::vector<std::string> &rst, int i, int j) {
    for (int k = 0; k < i; ++k) {
        if (rst[k][j] == 'Q') return false;
    }
    int m = i - 1, n = j - 1;
    while (m >= 0 && n >= 0) {
        if (rst[m][n] == 'Q') return false;
        --m;
        --n;
    }
    m = i - 1, n = j + 1;
    while (m >= 0 && n < rst[i].size()) {
        if (rst[m][n] == 'Q') return false;
        --m;
        ++n;
    }
    return true;
}

void dfs(std::vector<std::vector<std::string>> &all_solve, std::vector<std::string> &solve, int n, int k) {
    if (n == k) {
        all_solve.push_back(solve);
        return;
    }
    for (int i = 0; i < n; ++i) {
        if (check(solve, k, i)) {
            solve[k][i] = 'Q';
            dfs(all_solve, solve, n, k + 1);
            solve[k][i] = '.';
        }
    }
}

std::vector<std::vector<std::string>> solve_n_queens(int n) {
    std::vector<std::vector<std::string>> rst;
    std::vector<std::string> solve(n, std::string(n, '.'));
    dfs(rst, solve, n, 0);
    return rst;
}

你可能感兴趣的:(基础算法,算法,深度优先,广度优先)