力扣刷题笔记 44. 通配符匹配 C#

昨日签到题,题目如下

给定一个字符串 (s) 和一个字符模式 (p) ,实现一个支持 '?' 和 '*' 的通配符匹配。

'?' 可以匹配任何单个字符。
'*' 可以匹配任意字符串(包括空字符串)。
两个字符串完全匹配才算匹配成功。

说明:

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

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

输入:
s = "aa"
p = "*"
输出: true
解释: '*' 可以匹配任意字符串。
示例 3:

输入:
s = "cb"
p = "?a"
输出: false
解释: '?' 可以匹配 'c', 但第二个 'a' 无法匹配 'b'。
示例 4:

输入:
s = "adceb"
p = "*a*b"
输出: true
解释: 第一个 '*' 可以匹配空字符串, 第二个 '*' 可以匹配字符串 "dce".
示例 5:

输入:
s = "acdcb"
p = "a*c?b"
输出: false

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

这几天的签到题都有点难,不看完题解理解一遍,就没办法写出能够提交成功的代码。

一开始不知道为什么,我的思路居然是查找 s 中是否存在能够匹配 p 的子字符串,然后写到一半就写不下去了。

重新审题后才捋清题意,首先肯定是想尝试暴力解题。解题过程中,我发现自己无法处理出现 '*' 的情况,因为我无法确定 '*' 可能匹配的长度。我尝试过这种思路:使用标记 start 记录遇到 '*' 的起始位置(好像 start 么有什么用),然后向下寻找 s 中第一个与 p[start + 1]匹配的字符之后,假设结束这个 '*' 的判定,用 end 标记结束位置,继续向下寻找匹配,如果中途匹配失败,则回到这个 end 位置,重新开始向下寻找 s 中第一个与 p[start + 1]匹配的字符。如果枚举完所有可能性还是未果,则匹配失败。

思路中问题很多,比如:如果假设匹配的过程中遇到 了新的 '*' ,那么还要进行新的假设,如果 '*' 较多,那会产生极其可怕的重复计算,而且还有产生较多的 end 标记,显然不可取。

无果,看官方题解,给出了两个解法:

  1. 动态规划
  2. 贪心算法

嗯,又是该死的动态规划,不过这次理解完题解之后,好像对动态规划的认知提高了一点。这里不思考贪心算法,因为我看完动态规划的解法就花了太多时间,没有心情接着看下去了,看了个大概,没有捋清许多细节。

动态规划

先分割状态,假设 s 长度为m,p 长度为 n,题解即检索 s 中前 m 个字符 与 p 中前 n 个字符是否匹配,当匹配到 s[m-1] 和 p[n- 1](因为在C#中集合首位为 0,所以-1),需要满足 s[m-2] 和 p[n- 2]匹配,以此类推,s[i] 和 p[j] 的结果基于 s[i - 1] 和 p[j - 1] 的结果。

另外,又有存在 '?' 和 '*'。'?' 等价于一个万能字符,与任意字符匹配,但是匹配法则还是基于前文规律,'*' 比较特殊,它可能匹配任意长度的字符串,但是,也可能匹配一个空字符串。由于 '*' 的存在,又基于前文规律,求解过程中 s 中的字符未必匹配 p 中相同下标位置,所以需要枚举 s 中字符对 p 中任意字符的匹配情况。

综上,需要枚举 s 任意位置字符对 p 中任意字符的匹配情况,每种匹配情况又会基于之前的匹配情况,所以需要建立一个 m * n 的二维 bool 数组 dp,去记录这些匹配情况。由于空字符串的存在,所以实际建立的的是 (m + 1)*(n + 1) 长度的二维数组,两个方向上的 0 下标表示 s 或者 p 为空的枚举情况。基于前文规律,数组中 dp[i][j] 需要基于 dp[i][j] 前一位,类推能知道 dp[i][j] 即表示 s 中前 i 位字符串与 p 中前 j 位字符串的匹配情况(即前文规则在数组中的表达)。

以上在官方题解中只是一笔带过,但是我捋清楚这个模型的实际作用花费了些许时间,故此写下自己理解的大致思路。

分割状态后需要开始对每个状态进行判断。

因为 s 中只存在 'a'-'z',但是 p 中含有 '?' 和 '*',所以在对 dp[i][j] 的状态判断中分三种情况(这里先不考虑空字符串,即dp的下标分别于 s 和 p 的下标一一对应):

  • p[j] 为小写字母,此时直接与 s[i] 判断是否相等得到结果 nowAns,如果 nowAns 为真,则从 dp[i-1][j-1] 转移状态,计算表达为 dp[i][j] = nowAns && dp[i-1][j-1];
  • p[j] 为 '?',此时即情况 1 中 nowAns 恒为真,即计算表达为 dp[i][j] = dp[i-1][j-1];
  • p[j] 为 '*',此种情况下含有两种分支情况:

p[j] 表达为空字符串

即 p[j] 不占位,不与 s[i] 比较,此时 dp[i][j] 从 dp[i][j - 1] 转移状态。

这里我捋了半天,一直认为此时也应该基于 dp[i-1][j-1] 转移状态。但是,在此种情况下 dp[i][j] 没有实际匹配意义(个人认为)。又由于 dp[i + 1][j + 1] 的状态转移自 dp[i][j],但由于 dp[i][j] 中 p[j] 表达空字符串,p[j-1] 与 s[i] 的匹配情况,至此我才理解为什么从 dp[i][j - 1] 转移状态。

p[j] 表达为 1 个或多个字符

由于 '*' 的万能性,实际上此时也不与 s[i] 比较,此时 dp[i][j] 从 dp[i - 1][j] 转移状态。

这里我又捋了半天,同也也是认为还是应该基于 dp[i-1][j-1] 转移状态(至少对 '*' 开始匹配的情况而言)。实际可以认为 p[j] 刚开始检索匹配的状态就是基于 dp[i-1][j-1] 转移,仅仅是刚开始的状态。

先讨论 dp[i][j] 不为 p[j] 刚开始检测匹配的情况,可以很容易理解,此时 p[j] 也需要匹配 s[i-1],所以上述表达式成立。

再讨论 dp[i][j] 为 p[j] 刚开始检测匹配的情况,此时即 p[j] 只表示一个字符,dp[i][j] 从 dp[i - 1][j] 转移状态(此时先假设表达式成立,接下来进行验证情况成立),由于 i -1 ,我们可以知道 dp[i - 1][j] 中 p[j] 表示为空字符串,所以 dp[i - 1][j] 从 dp[i - 1][j - 1] 转移状态。综上 dp[i][j] 刚开始检测匹配的时候状态转移自 dp[i - 1][j - 1]。

 

综上,我们已经枚举枚举出所有的状态表达式。

另外,我们还需要确定边界条件,即 dp[0][j] 和 dp[i][0] 的所有情况。dp[0][j] 中,除非 p 的前 j 个字符都为 '*',否则不可能匹配。dp[i][0] 中,p 的前 0 位始终为空字符串,所以恒不成立。

复杂度分析:

需要对 dp 中的每一种情况进行枚举,故时间复杂度为 O(MN)。由于使用了数组 dp,故空间复杂度为 O(MN)。

以下为代码:

public class Solution {
    public bool IsMatch(string s, string p) {
        int m = s.Length;
        int n = p.Length;
        bool[,] dp = new bool[m + 1,n + 1];
        dp[0,0] = true;
        for (int i = 0;i < n;i++)
        {
            if (p[i] == '*')
            {
                dp[0,i + 1] = true;
            }
            else
            {
                break;
            }
        }
        for (int i = 1;i < m + 1;i++)
        {
            for(int j = 1;j < n + 1;j++)
            {
                if (p[j - 1] == '*')
                {
                    dp[i,j] = dp[i - 1,j] || dp[i,j - 1];
                }
                else if (p[j - 1] == '?' || p[j-1] == s[i - 1])
                {
                    dp[i,j] = dp[i - 1,j - 1];
                }
            }
        }
        return dp[m,n];
    }
}

 

你可能感兴趣的:(基础算法)