【问题标题】:Is it possible to convert this recursive solution to DP for "Valid Parenthesis String" problem?对于“有效括号字符串”问题,是否可以将此递归解决方案转换为 DP?
【发布时间】:2020-04-18 00:35:29
【问题描述】:

上下文:我正在尝试通过提出递归解决方案然后缓存子问题的结果来学习动态编程。

我一直在为this LeetCode 问题苦苦挣扎。

给定一个只包含三种类型字符的字符串:'(', ')' 和 '*',编写一个函数来检查这个字符串是否有效。我们通过以下规则定义字符串的有效性:

任何左括号'('必须有一个对应的右括号')'。 任何右括号 ')' 必须有一个对应的左括号 '('。 左括号 '(' 必须在相应的右括号 ')' 之前。 '*' 可以被视为单个右括号 ')' 或单个左括号 '(' 或空字符串。 空字符串也是有效的。

示例 1:

Input: "()", Output: True

示例 2:

Input: "(*)", Output: True

示例 3:

Input: "(*))", Output: True

我设法想出了一个似乎可行的递归解决方案。但是,我无法将其转换为 DP,我什至不明白我应该从哪里开始或应该缓存什么,即我看不到递归子树的结果对于不同的子问题是如何相同的.我知道existing DP solution。但我无法将它与我的递归相关联。甚至可以将此特定解决方案转换为 DP 吗?非常感谢您的建议。

这是我的代码:

class Solution {
public:
    bool checkValidString(string s) {
        stack<int> paren_stack;
        int ind = 0;
        return check_valid_rec(s, ind, paren_stack);
    }

private:
    bool check_valid_rec(string& s, int ind, stack<int> paren_stack) {

        if (ind >= s.size())
            return paren_stack.empty();

        while (s[ind] != '*' && ind < s.size()) {
            if (s[ind] == '(')
                paren_stack.push('(');
            else {
                if (!paren_stack.empty() && paren_stack.top() == '(')
                    paren_stack.pop();
                else return false;
            }
            ind++;
        }

        if (ind >= s.size())
            return paren_stack.empty();

        // if '*', there are three options
        // 1. ignore '*'
        bool ignore = check_valid_rec(s, ind + 1, paren_stack);
        // 2. replace with '('
        s[ind] = '(';
        bool left_replace = check_valid_rec(s, ind, paren_stack);
        // 3. replace with ')'
        s[ind] = ')';
        bool right_replace = check_valid_rec(s, ind, paren_stack);

        if (ignore || left_replace || right_replace)
            return true;
        else 
            return false;
    }
};

【问题讨论】:

    标签: c++ recursion dynamic-programming


    【解决方案1】:

    您的解决方案是回溯的一个示例。它不服从overlapping subproblems property。您共享的 dp 解决方案链接包含正确的方法。这也很容易实现为 recursion 和 dp。

    如果你仍然想坚持你的方法(或类似的方法),你必须调整你的方法来遵守重叠子问题的属性。我想到了一种方法,您也许可以研究一下:

    1. Start from the end of the given string.
    2. Instead of a stack, maintain the count of left and right paranthesis.
    3. Psudocode for recursion:
    bool foo(pos,count_left_paranthesis,count_right_paranthesis):
        if count_left_paranthesis >  :
            return false;
        if (pos < 0)
            return count_left_paranthesis == count_right_paranthesis;
        if string[pos] == '(':
            return foo(pos-1,count_left_paranthesis+1,count_right_paranthesis);
        else if string[pos] == '):
            return foo(pos-1,count_left_paranthesis,count_right_paranthesis+1);
        else: // string[pos] = '*'
            return foo(pos-1,count_left_paranthesis,count_right_paranthesis) 
                      || foo(pos-1,count_left_paranthesis,count_right_paranthesis+1) 
                      || foo(pos-1,count_left_paranthesis+1,count_right_paranthesis)
    4. Memoize the results of the subproblems.
    

    虽然有更简单的解决方案。

    我自己尝试了上述方法,这是一个可行的解决方案。令人惊讶的是它甚至被接受了。

    class Solution {
    public:
        map<vector<int>,bool> dp;
        bool foo(const string& s, int pos, int lc, int rc)
        {
            if (lc > rc) return false;
            if (pos<0)
            {
                return lc == rc;
            }
            vector<int> v = {pos,lc,rc};
            if (dp.find(v) != dp.end()) return dp[v];
            if (s[pos] == '(')
            {
                return dp[v] = foo(s,pos-1,lc+1,rc);
            }
            else if (s[pos] == ')')
            {
                return dp[v] = foo(s,pos-1,lc,rc+1);
            }
            else
            {
                return dp[v] = (foo(s,pos-1,lc,rc) || foo(s,pos-1,lc+1,rc) || foo(s,pos-1,lc,rc+1));
            }
        }
        bool checkValidString(string s) {
            return foo(s,s.size()-1,0,0);
        }
    };
    
    • 另一个快速优化可能是使用向量来代替记忆,但我很懒。
    • 你甚至可以从字符串的开头开始。从后面开始对我来说更容易分析

    注意: 这绝不是最有效的解决问题的方法,这只是帮助 OP 用递归解决方案,然后用 memoization 的手段提出 dp 解决方案(或“缓存子问题的结果”)

    【讨论】:

    • 似乎比 dp,IMO 更具记忆力。
    • @Jarod42 OP 的问题:I am trying to learn dynamic programming by coming up with a recursive solution and then caching the results of sub-problems,即他要求记忆。
    • 谢谢,这很有帮助!我有一个后续问题,如果问题可以使用 DP 解决,是否总是意味着存在递归解决方案?此外,如果存在具有重叠子问题属性的递归解决方案,是否总是意味着存在 DP 解决方案?
    • 递归代码可以用迭代方式编写,循环可以用递归方式编写。
    • @user_185051 是的,每个 dp 解决方案都可以递归方式实现。对于要使用 dp 解决的递归问题,除了重叠子问题属性外,它还需要遵守 optimal substructure property afaik,这只不过是可以从较小的解决方案中推导出更大的解决方案。
    【解决方案2】:

    起初,您的堆栈仅包含'(',因此可以简单地用作计数器。

    s 是“几乎”不变的,所以你只有indleft_counter 作为 dp 的变量:

    std::vector&lt;std::vector&lt;bool&gt;&gt; dp(s.size() + 1, std::vector&lt;bool&gt;(s.size(), false));

    dp[0][0] 得到了你的最终结果。

    现在初始化:

    if (ind >= s.size())
        return paren_stack.empty();
    

    所以dp[s.size()][0] = true;

    现在,从一列传递到下一列(在我们的例子中是 prev :))(尽量保持你的逻辑):

    switch (s[ind]) {
        case '*':
            dp[ind][j] = dp[ind + 1][j] /* Empty */
                       | (j > 0 && dp[ind + 1][j - 1]) /* ) */
                       | (j + 1 < s.size() && dp[ind + 1][j + 1]) /* ( */
                       ;
                       break;
        case '(': dp[ind][j] = (j + 1 < s.size() && dp[ind + 1][j + 1]); break;
        case ')': dp[ind][j] = (j > 0 && dp[ind + 1][j - 1]); break;
    }
    

    最终结果:

    bool is_balanced(const std::string& s)
    {
        if (s.empty()) return true;
    
        std::vector<std::vector<bool>> dp(s.size() + 1, std::vector<bool>(s.size(), false));
    
        dp[s.size()][0] = true;
    
        for (int ind = s.size() - 1; ind >= 0; --ind) {
            for (std::size_t j = 0; j != s.size(); ++j) {
                switch (s[ind]) {
                    case '*':
                        dp[ind][j] = dp[ind + 1][j] /* Empty */
                                   | (j > 0 && dp[ind + 1][j - 1]) /* ) */
                                   | (j + 1 < s.size() && dp[ind + 1][j + 1]) /* ( */
                                   ;
                                   break;
                    case '(': dp[ind][j] = (j + 1 < s.size() && dp[ind + 1][j + 1]); break;
                    case ')': dp[ind][j] = (j > 0 && dp[ind + 1][j - 1]); break;
                }
            }
        }
        return dp[0][0];
    }
    

    Demo

    【讨论】:

    • 感谢您的回答!你能解释一下你如何在这里使用按位或运算符吗?
    • 按位 OR (|)(带布尔值)与常规或 (||) 相似,只是它不会短路(即使 lhs 为假,也要评估双方) .在当前代码中,您可以使用一个或另一个。当您不应该调用另一方时,短路很重要(如绑定检查j &gt; 0 或一般的 nullptr 检查,或者当 rhs 计算可能代价高昂时)。
    猜你喜欢
    • 2014-08-20
    • 1970-01-01
    • 2011-07-02
    • 1970-01-01
    • 2020-05-22
    • 1970-01-01
    • 1970-01-01
    • 2020-06-07
    • 2021-12-15
    相关资源
    最近更新 更多