LeetCode 面试题 16.18. 模式匹配
题目描述

题意分析
模式只含
a、b,每种字符对应一个固定字符串,将它们按模式顺序拼接后必须等于value。映射可以为空,但两种字符都出现时不能对应同一个字符串;同一种字符重复出现,必须始终使用相同内容。
解法:枚举子串长度 + 线性验证
核心思路
[!blue]
设两种字符出现次数为
countA、countB,映射长度为lenA、lenB,值串长度为N。任何合法方案都满足countA * lenA + countB * lenB = N。因此只需枚举lenA:当countA > 0时范围是0..N/countA;当没有a时,令lenA = 0即可,不必枚举一个未使用的长度。固定
lenA后,令rest = N - countA * lenA。如果存在b,只有rest能被countB整除时才有候选,且lenB唯一等于这个商;没有b时,则必须让rest = 0,不能作除法。这覆盖了所有可能的长度组合。长度确定后,字符串内容也不需要另行枚举:用游标
idx按模式从左到右切片,某种字符首次出现时,对应片段就是它必须映射的字符串,之后每次都与首次片段比较。出现内容不一致,或两种已出现映射相同,就排除这组长度;全部通过则得到合法方案。长度等式已经保证总消费量恰好为N,且每段长度非负,所以切片不会超过末尾。空串要先处理:
value为空时,模式中出现的每种字符都只能映射为空;两种都出现便违反映射不同的要求,只出现一种或模式也为空则可行。value非空而模式为空时无法生成任何字符,应返回false。
解题步骤
- 统计
countA、countB,先处理空值串和空模式。- 从零开始枚举
lenA,根据剩余长度和countB确定lenB,跳过不能满足长度等式的候选。- 验证时从
idx = 0开始,按当前模式字符对应的长度取片段,首次保存、以后比较,并推进游标。- 两种映射都已确定后,检查它们不能相等。Java 用
null区分未出现,Go 用hasA、hasB,不能把空字符串直接当作未初始化标记。- 任一候选通过就返回
true,所有长度都失败才返回false。
代码实现
class Solution {
public boolean patternMatching(String pattern, String value) {
int countA = 0;
int countB = 0;
for (char c : pattern.toCharArray()) {
if (c == 'a') {
countA++;
} else {
countB++;
}
}
if (value.isEmpty()) {
return countA == 0 || countB == 0;
}
if (pattern.isEmpty()) {
return false;
}
int n = value.length();
int maxLenA = countA == 0 ? 0 : n / countA;
for (int lenA = 0; lenA <= maxLenA; lenA++) {
int rest = n - lenA * countA;
// 没有第二种字符时不能作除数,第一种片段必须刚好铺满值串。
if (countB == 0) {
if (rest == 0 && check(pattern, value, lenA, 0)) {
return true;
}
continue;
}
if (rest < 0 || rest % countB != 0) {
continue;
}
int lenB = rest / countB;
if (check(pattern, value, lenA, lenB)) {
return true;
}
}
return false;
}
private boolean check(String pattern, String value, int lenA, int lenB) {
// 消费位置由映射片段长度累加,与模式下标不同。
int idx = 0;
String wordA = null;
String wordB = null;
for (char c : pattern.toCharArray()) {
int len = (c == 'a') ? lenA : lenB;
String sub = value.substring(idx, idx + len);
if (c == 'a') {
if (wordA == null) {
wordA = sub;
} else if (!wordA.equals(sub)) {
return false;
}
} else {
if (wordB == null) {
wordB = sub;
} else if (!wordB.equals(sub)) {
return false;
}
}
if (wordA != null && wordB != null && wordA.equals(wordB)) {
return false;
}
idx += len;
}
return true;
}
}
func patternMatching(pattern string, value string) bool {
countA, countB := 0, 0
for _, c := range pattern {
if c == 'a' {
countA++
} else {
countB++
}
}
if len(value) == 0 {
return countA == 0 || countB == 0
}
if len(pattern) == 0 {
return false
}
n := len(value)
check := func(lenA, lenB int) bool {
// 消费位置由映射片段长度累加,与模式下标不同。
idx := 0
wordA, wordB := "", ""
hasA, hasB := false, false
for _, c := range pattern {
length := lenB
if c == 'a' {
length = lenA
}
sub := value[idx : idx+length]
if c == 'a' {
if !hasA {
wordA = sub
hasA = true
} else if wordA != sub {
return false
}
} else {
if !hasB {
wordB = sub
hasB = true
} else if wordB != sub {
return false
}
}
if hasA && hasB && wordA == wordB {
return false
}
idx += length
}
return true
}
maxLenA := 0
if countA > 0 {
maxLenA = n / countA
}
for lenA := 0; lenA <= maxLenA; lenA++ {
rest := n - lenA*countA
// 没有第二种字符时不能作除数,第一种片段必须刚好铺满值串。
if countB == 0 {
if rest == 0 && check(lenA, 0) {
return true
}
continue
}
if rest < 0 || rest%countB != 0 {
continue
}
lenB := rest / countB
if check(lenA, lenB) {
return true
}
}
return false
}
复杂度分析
设模式长度为
P、值串长度为N、枚举的长度候选数为M。有a时M = N/countA + 1,没有a时为 1。
- 时间复杂度:上界为 $O(P+M(P+N+1))$。每个候选最多遍历整个模式,并切分、比较总长为线性级别的字符串片段;即使片段为空,仍需扫描模式字符。
- 空间复杂度:Java 验证会创建模式字符数组和子串副本,上界为 $O(P+N)$;Go 仅保存字符串切片和标量,辅助空间为 $O(1)$。
关键点总结
[!green]
- 长度等式消掉一个枚举维度,首次出现的片段又确定了映射内容。
- 合法性同时要求总长度吻合、同字符内容一致、两种已出现映射不同。
- 空映射是一种合法取值,与“这个字符尚未出现”是两种状态。
易错点总结
[!yellow]
- 枚举从 1 开始,会漏掉某一种映射为空的合法方案。
- 只验证长度不验证片段内容,无法保证同一模式字符始终表示同一个字符串。
- 两种映射都出现后必须检查不同,不能仅凭拼接结果相同就接受。
- 某种字符出现次数为零时需要单独处理,不能把零作为除数。
- 值串游标按片段长度推进,不是每处理一个模式字符就只增加 1。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 290. 单词规律 | 简单 | 原题单词边界由空格给出,本题未知映射子串长度,需要枚举并验证拼接。 |
| 291. 单词规律 II | 中等 | 原题模式字符更多且映射非空,本题只有a、b并允许空串,长度枚举的起点与约束不同。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!