美文网首页
回溯算法和深度优先搜索(二)

回溯算法和深度优先搜索(二)

作者: 东方胖 | 来源:发表于2022-01-07 18:51 被阅读0次

先看一道题目:

括号生成。

输入一个整数 n,罗列出所有有效的括号组合。有效的括号组合是指 左括号开始,右括号结束,并且每个左括号都可以在右侧找到对应的右括号闭合,每个右括号在左边也有对应的左括号闭合。

例如:
((())) 有效;
)))((( 无效,因为它以 ) 开始;
(())() 有效;
())()) 无效 右括号不能闭合;
())(() 无效, 中间的 )(方向不对。

输入 n = 1, 只有一种组合 "()"
输入 n = 3, 有 5 种组合 ["((()))","(()())","(())()","()(())","()()()"]

这道题是典型的回溯穷举搜索。我们很容易想到一种方法: 把所有可能的组合搜索出来,然后检查每个结果是不是满足有效性的条件。
有效性检查,可以用一个栈数据结构来测试,将字符串从左到右依次放进一个栈里,如果是左括号,就入栈,如果是新的元素是右括号,就从栈里弹出一个元素。以上步骤重复,直到字符串结束。
这时候,有效组合会导致栈里的元素刚好是 0, 当然,栈自始至终的操作必须没有异常,比如要弹出元素时,检查栈是否为空,如果空说明这个字符串组合某个地方出现了右括号更多的情况——有效组合实际上是指,从左到右任何一个下标 i ,从0到i ,左括号的数量比右括号多,并且当 i 增长到结尾时,左右括号数量相等。

检测有效性的程序

bool isValidParenthesisStr(const string &ans) {
        std::stack<char> st;
        for (char c: ans) {
            if (c == '(') {
                st.push(c);
            } else if (c == ')') {
                if (st.empty()) {
                    return false;
                }
                st.pop();
            }
        }
        return st.empty();
    }

利用回溯法穷举所有组合,然后检查结果是否合格的算法

void backtrack(int n, int i, std::string& ans, vector<string>& result;) {
        if (i == n * 2) {
            if (isValidParenthesis(ans)) {
                result.push_back(ans);
            }
            return;
        }
        
        string optional_str = "()";
        for (char c: optional_str) {
            ans.push_back(c);
            backtrack(n, i + 1, ans, result);
            ans.pop_back();
        }
        
    }
    vector<string> generateParenthesis(int n) {
        string ans;
        vector<string> result;
        backtrack(n, 0, ans, result);
        return result;
    }

上一篇回溯法[]写过,回溯的效率关键在“剪枝” 环节,以上的解法我们没有做任何剪枝缩短穷举空间, 效率会大打折扣。

用一个简图说明这点:
下面这个图表示规模为 n = 2 的穷举空间树,最后我们在不符合的组合上打叉

剪枝空间示意图

上面的代码展示的算法,没有剪枝的过程,实际上是走完图中的所有路径,然后对每条路径产生的结果做一个检测。当 n 增加时,我们会发现路径树的规模是 2^{2n}

使用剪枝的策略,我们会发现有些路径早早地就能被否掉,如图所示的右枝,第一个符号是 '')" ,无论怎么凑,这种情况都是不合法的,所以我们可以把该节点下面的整棵树”剪掉” 不用考虑。

在左侧的树中,我们沿着树节点往下走,如果发现到了某个节点,")" 的数量超过了 ”(" 的数量,说明经过该节点的所有路径都“坏掉了” ,剪掉它。
如果发现 “(" 的数量过多——超过了 n ,那以这个节点的树也是坏的,需要剪掉。
通过这番操作,我们把遍历树的规模剪掉了一半还多。设想 n 增加到3,整棵树有 6 层,它的剪枝节点大同小异,只是 "(" 超过平衡点的地方会更深一些,随着树规模变大,剪枝的好处越发显现。

利用剪枝策略改写上面的算法:
关键是在每个节点,判断一下以该节点为根的子树是不是坏掉了。

void backtrack(vector<string>& ans, string& cur, int open, int close, int n) {
        if (cur.size() == n * 2) {
            ans.push_back(cur);
            return;
        }
        if (open < n) {
            cur.push_back('(');
            backtrack(ans, cur, open + 1, close, n);
            cur.pop_back();
        }
        if (close < open) {
            cur.push_back(')');
            backtrack(ans, cur, open, close + 1, n);
            cur.pop_back();
        }
    }

    vector<string> generateParenthesis(int n) {
        vector<string> result;
        string current;
        backtrack(result, current, 0, 0, n);
        return result;
    }

用 open 表示当前左括号的数量,close 表示当前右括号的数量。
如果 open 数量不到 n 就往 ans里填 "(" 如果 close 小于open 就往 ans 里填 ”)" ,注意上面代码的顺序,是一个深度优先搜索的顺序——尽可能先填满 ”(" ,填不下去之后就补 ")"

相关文章

  • 学习回溯法中的思考

    回溯法是一种用递归来实现的穷举法,是深度搜索优先算法。 我又联想到了,深度搜索优先和广度搜索优先,与栈和队...

  • 回溯算法和深度优先搜索(二)

    先看一道题目: 括号生成。 输入一个整数 ,罗列出所有有效的括号组合。有效的括号组合是指 左括号开始,右括号结束,...

  • 马踏棋盘算法

    最近在学习深度优先搜索算法,接触到了马踏棋盘,决定尝试一下。 涉及算法:递归,回溯法,深度优先搜索算法 题目需求:...

  • 高频题

    字符串 数学 数组 二叉树 二分查找 链表 动态规划 贪心算法 堆 广度优先搜索 深度优先 哈希表 回溯算法 设计

  • 递归、迭代、回溯、广度和深度优先

    在学习算法的时候,经常碰到递归、迭代、回溯、广度优先和深度优先算法,可是回溯使用了递归,深度优先也使用了递归,广度...

  • 北航算法复习笔记

    #算法复习笔记 一 决策和策略 二 回溯法使用深度优先(dfs)搜索状态空间树 三 快速排序 标准(常用)快速排序...

  • 算法汇总

    关于算法: 基础技巧:分治、二分、贪心排序算法:快速排序、归并排序、计数排序搜索算法:回溯、递归、深度优先遍历,广...

  • 算法与数据结构简介

    0x01 算法 基础技巧:分治、二分、贪心 排序算法:快速排序、归并排序、计数排序 搜索算法:回溯、递归、深度优先...

  • 算法之回溯算法详解

    回溯算法 定义 回溯算法实际上基于DFS(深度优先搜索)的一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问...

  • 深度优先搜索

    深度优先搜索思路 深度优先搜索 = 回溯法可以通过遍历或者分治法的思想实现深度优先搜索而递归和迭代 (非递归)两种...

网友评论

      本文标题:回溯算法和深度优先搜索(二)

      本文链接:https://www.haomeiwen.com/subject/avojcrtx.html