xxleyi/loop_invariants

leetcode 10. 正则表达式匹配

xxleyi opened this issue · 0 comments

题:

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。

'.' 匹配任意单个字符
'*' 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

说明:

s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/regular-expression-matching
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。


解:

这个题,我尝试了,但并没有靠自己的能力做出来。复盘原因的话,是没能找好「循环不变式」。或者说,对此题来说,没能找到描述状态转移的「递推方程」。

为什么没能找到呢?很简单,硬实力不够,积累不够,思维差点电火花,点不着。怎么办?还是得总结,得练。下面就是我用「循环不变式」视角进行的总结。

实现 . * 这两个正则匹配规则,其中 . 比较好办,但 * 的话,或许是因为我对正则不够熟悉的原因吧,我是没能想到实际的算法需要从后往前匹配,陷在了从前往后匹配的死胡同。

还有一点也挺关键,就是匹配时,以模式字符串为准,直到模式字符串为空,则匹配结束,得到结果。

剩下的就是准确定义初始值,以及循环的方式:

  • 字符串 s 为空时,匹配也会照常进行,故而,循环初始位置为 s.length
  • 对于模式字符串 p,我们应该从 p.length - 1 开始循环,但由于 * 匹配符的存在,我们同时也关心每一次当前循环位置之后的那个位置,也就是说,p.length 也会被使用到。

此外,循环之前的初始值不算在循环之内,所以我们 cache 变量的维度是 s.length + 2 x p.length + 2

var isMatch = function(s, p) {

  let genCache = () => {
    // initialize loop invariant related variable cache with dimension s.length + 2 x p.length + 2
    // cache[s.length][p.length] is true, all other should be false to maintain loop invariant
    let cache = Array(s.length + 2)
    for (let i = 0; i < cache.length; i++) cache[i] = Array(p.length + 2).fill(false)
    cache[s.length][p.length] = true
    return cache
  }

  // cache is the loop invariant related variable
  function loop(i = s.length, j = p.length - 1 , cache = genCache()) {

    // loop termination: cache[0][0] will be our answer
    if (i < 0 || j < 0) return cache[0][0]
    
    // single character match result (include .)
    let singleMatch = p[j] === '.' && !!s[i] || p[j] === s[i]
    // zero match result for *
    let zeroMatch = cache[i][j + 2]
    // one character match result for *
    let oneMoreMatch = singleMatch && cache[i + 1][j]
    // last match result (here we loop from right to left, the last match is index bigger than current)
    let lastMatch = cache[i + 1][j + 1]

    // update cache to maintain loop invariant depends on whether the current pattern is * or not
    if (p[j + 1] === '*') {
      cache[i][j] = zeroMatch || oneMoreMatch
    } else {
      cache[i][j] = singleMatch && lastMatch
    }

    // next loop
    if (j > 0) {
      return loop(i, j - 1, cache)
    } else {
      return loop(i - 1, p.length - 1, cache)
    }
  }

  return loop()
};

上面我有意循环递归,为了刻意训练自己「循环不变式」的视角,它同时适用于递归和迭代,但递归显得更好玩一些。

可以发现,在动态规划类型的问题中,这个「循环不变式」相关的变量,必然囊括那个 dp table

有了基于「循环不变式」的递归,转成循环就是比较容易的一件事了。

下面是我转为循环迭代的代码,你会发现,核心逻辑和注释无需任何修改。

var isMatch = function(s, p) {
  // initialize loop invariant related variable cache with dimension s.length + 2 x p.length + 2
  // cache[s.length][p.length] is true, all other should be false to maintain loop invariant
  let cache = Array(s.length + 2)
  for (let i = 0; i < cache.length; i++) cache[i] = Array(p.length + 2).fill(false)
  cache[s.length][p.length] = true

  for (let i = s.length; i >= 0; i--) {
    for (let j = p.length - 1; j >= 0; j--) {
      // single character match result (include .)
      let singleMatch = p[j] === '.' && !!s[i] || p[j] === s[i]
      // zero match result for *
      let zeroMatch = cache[i][j + 2]
      // one character match result for *
      let oneMoreMatch = singleMatch && cache[i + 1][j]
      // last match result (here we loop from right to left, the last match is index bigger than current)
      let lastMatch = cache[i + 1][j + 1]

      // update cache to maintain loop invariant depends on whether the current pattern is * or not
      if (p[j + 1] === '*') {
        cache[i][j] = zeroMatch || oneMoreMatch
      } else {
        cache[i][j] = singleMatch && lastMatch
      }
    }
  }
  
  // loop termination: cache[0][0] will be our answer
  return cache[0][0]
};

无论递归还是迭代,想要正确解题,三个阶段的「循环不变式」必须得到保证:

  • 循环之前,正确初始化相关变量
  • 每一次循环的开头和末尾,正确更新相关变量
  • 循环体的结束 后,「循环不变式」依然成立,且可从相关的变量中得到问题正确的解