暴力递归转向动态规划

文章详细介绍了如何使用暴力递归、记忆化搜索和动态规划解决LeetCode中的正则表达式匹配问题。首先从暴力递归开始,分析边界条件和操作块,然后通过记忆化搜索减少重复计算,最后转换为动态规划优化算法效率。整个过程重点在于处理*字符的不同情况以及如何避免重复计算。
摘要由CSDN通过智能技术生成

题目

leetcode 10.正则表达式匹配

暴力递归

首先,我们先从简单的暴力递归开始解题。

1. 边界条件

递归的边界条件非常好得到,那就是 两个字符串为空时,递归就应该结束了:

  • 如果两个字符串此时都空了,那肯定返回 true,因为 空 匹配 空 肯定是 true。返回 true;
  • 如果 p 空了,而 s 没空,那此时肯定说明匹配不成功,显然 s 还有一段没匹配到。返回 false;
  • 如果 s 空了,而 p 没空,这个时候其实还没办法判断。等后面处理

这里为什么说 s 为空时没办法判断呢?主要是这个题的测试样例里面有这种情况:
s = "aa", p = "a*b*" ,此时结果返回 true。显然这和我们平常理解的得先有个 b 不同,*号意味着前面这个 b 也可有可无,所以我们这里没办法直接通过 s 为空来判断 返回 false,因为 p 剩下的部分可能是 "a*b*c*" 这种字符串,这一大串匹配 空 时是 true。

2. 操作块

有了边界条件,我们就需要思考递归的操作是怎样处理这两个字符串的。一共有几种情况:

  • 如果说 s 空了,而 p 没空(最外层的 else 部分):
    此时我们开始对 ‘*’ 进行判断。
    • 如果 p[1] == '*' (s == "", p == "a*b*c*"): 那说明 p[0] 这个字母是可有可无的。那我只需要接着判断 s 和 p.substr(2) 就行了。一个例子就是 s="", p="a*b*c*" 的比较结果和 s="", p="b*c*" 是相同的。
    • 如果 p[1] == nullptr || p[1] != '*'(s == "", p == "abc" 或者 p == "a"): 此时就是说明,p 不可继续消除了,那肯定是 false 了。
  • 如果说 s, p 都不为空,此时 p[0]s[0] 是相等的,即p[0] == '.' || p[0] == s[0]
    我们仍然对 ‘*’ 进行判断
    • 如果 p[1] == '*': 在这种情况下,我们实际上有多种选择
      • p == "a*b" 来当作 p == "" 来使用:此时,我们将 “a*” 当作 0 个 a 来看待,这和上面情况是一致的。那我们只需要比较 s 和 p.substr(2) 即可,即 s == "ab", p == "a*ab" 的比较结果和 s == "ab", p == "ab" 的比较结果相同。
      • p == "a*b" 来当作 p == "ab" 来使用: 此时,我们将 “a*” 当作 1 个 a 来看待。那我们只需要比较 s.substr(1) 和 p.substr(2) 即可,即 s == "ab", p == "a*ab" 的比较结果和 s == "b", p == "ab" 的比较结果相同。
      • p == "a*b" 来当作 p == "aa*b" 来使用: 此时,我们将 “a*” 当作 1 个以上 a 来看待。那我们只需要比较 s.substr(1) 和 p 即可,即 s == "ab", p == "a*ab" 的比较结果和 s == "b", p == "a*ab" 的比较结果相同。
    • 如果 p[1] != '*': 这种情况就非常好判断了,直接返回 s.substr(1) 和 p.substr(1) 的匹配结果就行了,即 s == "ab", p == "ac" 的匹配结果与 s == "b", p == "c" 的匹配结果相同。
  • s,p 都不为空,且p[0] s[0] 也不相等

有了这些结果,我们就可以撰代码了。

代码
class Solution {
public:
    bool isMatch(string s, string p) {
        return helper(s, 0, p, 0);
    }

    bool helper(string& s, int sbeg, string& p, int pbeg) {
        if (sbeg == s.size() && pbeg == p.size()) return true;
        else if (pbeg == p.size()) return false;

        if (sbeg != s.size() && (p[pbeg] == '.' || p[pbeg] == s[sbeg])) {
            if (pbeg < p.size() - 1 && p[pbeg + 1] == '*') 
                return helper(s, sbeg, p, pbeg + 2) || 
                       helper(s, sbeg + 1, p, pbeg + 2) || 
                       helper(s, sbeg + 1, p, pbeg);
            else 
                return helper(s, sbeg + 1, p, pbeg + 1);
        } else {
            if (pbeg < p.size() - 1 && p[pbeg + 1] == '*') 
                return helper(s, sbeg, p, pbeg + 2);
        }
        
        return false;
    }
};

这里因为需要获取子串,为了减少开销,我这里使用的 index 来替代获取子串的操作。这个代码无疑是超时的。

记忆化搜索

有了暴力递归,虽然超时,但是我们仍然可以将他朝着动态规划的方向进行优化。其实我们在上述代码中很容易就能发现递归的问题:对于同一个问题求解了多次。

你可以看到上面有很多 sbeg + 1, pbeg + 2 这种运算,这些值算出来很有可能相等,那么对于同一个 helper(s, 4, p, 5) 这种函数,可能会调用了多次。那如何来减少调用呢?可以选择使用一个全局的 map 来存储这些内容。每当第一次调用时就填充 map 后续如果还需要这个值就直接返回。

这里我的 map 使用 vector 来代替,大小是 (s.size() + 1) * (p.size() + 1) 的大小,每次取值时,通过 getIndex 来找出 vector 中对应的位置下标。

代码
class Solution {
public:
    bool isMatch(string s, string p) {
        row = s.size() + 1;
        col = p.size() + 1;
        v_map = vector<int>(row * col);
        return helper(s, 0, p, 0);
    }

    bool helper(string& s, int sbeg, string& p, int pbeg) {
        if (sbeg == s.size() && pbeg == p.size()) return true;
        else if (pbeg == p.size()) return false;

        if (sbeg != s.size() && (p[pbeg] == '.' || p[pbeg] == s[sbeg])) {
            if (pbeg < p.size() - 1 && p[pbeg + 1] == '*') 
            {
                if (v_map[getIndex(sbeg + 1, pbeg + 2)] == 0) v_map[getIndex(sbeg + 1, pbeg + 2)] = helper(s, sbeg + 1, p, pbeg + 2) ? 1 : -1;
                if (v_map[getIndex(sbeg + 1, pbeg + 2)] == 1) return true;

                if (v_map[getIndex(sbeg, pbeg + 2)] == 0) v_map[getIndex(sbeg, pbeg + 2)] = helper(s, sbeg, p, pbeg + 2) ? 1 : -1;
                if (v_map[getIndex(sbeg, pbeg + 2)] == 1) return true;


                if (v_map[getIndex(sbeg + 1, pbeg)] == 0) v_map[getIndex(sbeg + 1, pbeg)] = helper(s, sbeg + 1, p, pbeg) ? 1 : -1;
                if (v_map[getIndex(sbeg + 1, pbeg)] == 1) return true;

                return false;
            }
            else {
                if (v_map[getIndex(sbeg + 1, pbeg + 1)] == 0) v_map[getIndex(sbeg + 1, pbeg + 1)] = helper(s, sbeg + 1, p, pbeg + 1) ? 1 : -1;
                if (v_map[getIndex(sbeg + 1, pbeg + 1)] == 1) return true;
            }
        } else if (pbeg < p.size() - 1 && p[pbeg + 1] == '*') {
            if (v_map[getIndex(sbeg, pbeg + 2)] == 0) v_map[getIndex(sbeg, pbeg + 2)] = helper(s, sbeg, p, pbeg + 2) ? 1 : -1;
            if (v_map[getIndex(sbeg, pbeg + 2)] == 1) return true;
        }
        
        return false;
    }

private:
    vector<int> v_map;
    int row;
    int col;

    int getIndex(int a, int b) {
        return a * col + b;
    }
};

动态规划

最后,我们根据记忆化搜索的代码无脑转换成动态规划。

明确含义

首先,我们先明确 dp 的含义, dp[s.size() + 1][p.size() + 1]。 dp[i][j] 代表着 s.substr(i, end) 可以通过 p.substr(j, end) 来匹配 是 true 还是 false。

初始化 dp

然后我们根据递归的终止条件初始化 dp:

  • 如果两个字符串此时都空了,返回 true ==> dp[s.size()][p.size()] = true;
  • 如果 p 空了,而 s 没空, 返回 false ==> dp[*][p.size()] = false; 这一步不用做,因为 vector 默认初始化为 false
开始循环

然后我们开始写循环:

for (int i = s.size(); i > -1; --i) {
for (int j = p.size() - 1; j > -1; --j)

逆着遍历,因为你看到了,我们的终止条件在 dp[s.size()][p.size()] 处。

下面,我们需要为每一次循环来做点什么呢?仍然是照着递归来写。

首先,我们先判断 '*'。你会看到,如果说 p[j] == '*',此时我们是不需要这个 dp[i][j] 的值的。因为在递归中,我们处理 '*' 的方式是,要么取 '*' 前面的字符进行操作,要么是取 '*' 后面的字符进行操作,其实也就是 记忆化搜索里的 pbeg 或者 pbeg + 2。所以,我们在 dp 时,从当前视角来看我们不必处理 dp[i][j] 如果 p[j] == '*'。 continue 掉。

如果说 s 空了,而 p 没空:
- p[j + 1] == ‘': if (v_map[getIndex(sbeg, pbeg + 2)] == 1) return true; ==> dp[i][j] = dp[i][j + 2];
- p[j + 1] != '
’: dp[i][j] = false; 不用操作
如果说 s, p 都不为空:
- p[j + 1] == ‘':
if (v_map[getIndex(sbeg + 1, pbeg + 2)] == 1) return true; +
if (v_map[getIndex(sbeg, pbeg + 2)] == 1) return true; +
if (v_map[getIndex(sbeg + 1, pbeg)] == 1) return true; ==> dp[i][j] = dp[i][j + 2] || dp[i + 1][j + 2] || dp[i + 1][j];
- p[j + 1] != '
’: if (v_map[getIndex(sbeg + 1, pbeg + 1)] == 1) return true; ==> dp[i][j] = dp[i + 1][j + 1];

结果

结果就是 dp[0][0],直接返回就行了。

这样,我们就得到了完整的动态规划代码。

代码
class Solution {
public:
    bool isMatch(string s, string p) {
        vector<vector<bool>> dp(s.size() + 1, vector<bool>(p.size() + 1)); // dp[i][j] s.substr(i, end) 可以通过 p.substr(j, end) 来匹配
        dp[s.size()][p.size()] = true;

        for (int i = s.size(); i > -1; --i) {
            for (int j = p.size() - 1; j > -1; --j) {
                if (p[j] == '*') continue;
                if (i == s.size() || (p[j] != '.' && p[j] != s[i])) {
                    if (j < p.size() - 1 && p[j + 1] == '*') 
                        dp[i][j] = dp[i][j + 2];
                } else {
                    if (j < p.size() - 1 && p[j + 1] == '*') 
                        dp[i][j] = dp[i][j + 2] || dp[i + 1][j + 2] || dp[i + 1][j];
                    else
                        dp[i][j] = dp[i + 1][j + 1];
                }
            }
        }

        return dp[0][0];
    }
};
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值