刷题使我快乐,满脸开心.jpg

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

题目

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

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

示例 1:

输入:s = "aa", p = "a"
输出:false
解释:"a" 无法匹配 "aa" 整个字符串。

示例 2:

输入:s = "aa", p = "a*"
输出:true
解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。

示例 3:

输入:s = "ab", p = ".*"
输出:true
解释:".*" 表示可匹配零个或多个('*')任意字符('.')。

提示:

  • 1 <= s.length <= 20
  • 1 <= p.length <= 20
  • s 只包含从 a-z 的小写字母。
  • p 只包含从 a-z 的小写字母,以及字符 .*
  • 保证每次出现字符 * 时,前面都匹配到有效的字符

思路

这道困难题,感觉难度一小半在确定动规思路,一小半在动规状态转移方程

动规思路

正则匹配我们其实都熟悉,都会注意到几个点

  • 遇到 '.' 则可以匹配任意字符,那么就需要关注 p 字符串当前判断字符是否为 '.'
  • 遇到 '*' 则可以将前一个字符从 0无限 重复,所以需要关注 p 字符串 前一个字符
  • 如果 '.''*' 相邻,则可以匹配所有字符,那么还需要关注之后两个字符串的 尾巴 是否匹配

这些思路如果要直接用双指针之类的方式实现,其实要考虑的边界等情况会很繁复,不敢说不能实现,但是应该会比较复杂

所以其实也是基于这个思路,但是形式上更加清晰的动态规划就更为合适了:

  • 后面的结果依赖于前面字符的匹配情况
  • 基于上面的思路可以得出状态转移方程
  • 无后效性,之前字符串是否匹配的结果,不会影响增加字符后,整体字符串的匹配情况

动规要素

  • dp 方程定义:dp[i][j] 表示 s[:i]p[:j] 是否匹配
  • 初始化:
    • dp[0][0] = truesp 均为空串时是匹配的
    • 因为 '*' 可以取 0 个循环,所以当 p[i-1]=='*' 时,dp[0][j]=dp[0][j-2]
  • 状态转移方程
    • p[i-1] == '*'
      • s 字符串不变,此时 p 字符串结尾是 'a*' 或者 '.*',都可以匹配: dp[i][j] = dp[i][j-2]
      • s 字符串 s[i-1]='a',同时 p[i-2]='a',可以匹配: dp[i][j] = dp[i-1][j] && s[i-1] == p[i-2]
      • s 字符串 s[i-1]='a',同时 p[i-2]='.',可以匹配: dp[i][j] = dp[i-1][j] && p[i-2] == '.'
    • p[i-1] != *
      • s 字符串 s[i-1]='a',同时 p[i-2]='a' 或者 p[i-2]='.',都可以匹配: dp[i][j] = dp[i][j-2]

整体思路就是这样啦,直接上代码,结合注释会更加清晰。

代码

func isMatch(s string, p string) bool {
	sLen, pLen := len(s), len(p)

	// 定义dp数组 dp[i][j] 表示 s[:i] 和 p[:j] 是否匹配
	dp := make([][]bool, sLen+1)
	for i := 0; i <= sLen; i++ {
		dp[i] = make([]bool, pLen+1)
	}

	// 初始化
	// dp[0][0] 即 s 和 p 均为空串时是否匹配
	dp[0][0] = true
	// 这个初始化条件,其实就是因为 * 可以取 '0个XXX' 的含义,
	// 使得 s=="" && p != "" 也可能是匹配的
	for j := 1; j <= pLen; j++ {
		if p[j-1] == '*' {
			dp[0][j] = dp[0][j-2]
		}
	}

	// 状态转移方程
	// p[i-1] == * 时
	//   s字符串不变,此时p字符串结尾是 'a*' 或者 '.*',都可以匹配: dp[i][j] = dp[i][j-2]
	// 	 s字符串 s[i-1]='a',同时 p[i-2]='a',可以匹配: dp[i][j] = dp[i-1][j] && s[i-1] == p[i-2]
	// 	 s字符串 s[i-1]='b',同时 p[i-2]='.',可以匹配: dp[i][j] = dp[i-1][j] && p[i-2] == '.'
	// p[i-1] != * 时
	// 	 s字符串 s[i-1]='a',同时 p[i-2]='a' 或者 p[i-2]='.',都可以匹配: dp[i][j] = dp[i][j-2]
	for i := 1; i <= sLen; i++ {
		for j := 1; j <= pLen; j++ {
			if p[j-1] == '*' {
				dp[i][j] = dp[i][j-2] || dp[i-1][j] && (s[i-1] == p[j-2] || p[j-2] == '.')
			} else {
				dp[i][j] = dp[i-1][j-1] && (p[j-1] == '.' || s[i-1] == p[j-1])
			}
		}
	}
	return dp[sLen][pLen]
}