题目
你有两个字符串,即 pattern 和 value。
pattern 字符串由字母 "a" 和 "b" 组成,用于描述字符串中的模式。例如,字符串 "catcatgocatgo" 匹配模式 "aabab"(其中 "cat" 是 "a","go" 是 "b"),该字符串也匹配像 "a"、"ab" 和 "b" 这样的模式。但需注意 "a" 和 "b" 不能同时表示相同的字符串。编写一个方法判断 value 字符串是否匹配 pattern 字符串。
示例 1:
输入: pattern = "abba", value = "dogcatcatdog"
输出: true
示例 2:
输入: pattern = "abba", value = "dogcatcatfish"
输出: false
示例 3:
输入: pattern = "aaaa", value = "dogcatcatdog"
输出: false
示例 4:
输入: pattern = "abba", value = "dogdogdogdog"
输出: true
解释: "a"="dogdog",b="",反之也符合规则
提示:
1 <= len(pattern) <= 10000 <= len(value) <= 1000- 你可以假设
pattern只包含字母"a"和"b",value仅包含小写字母。
思路
我们要判断:
能不能给 a 和 b 分别对应两个(可以为空的)字符串,使得:
- 把 pattern 中每个
'a'替换成字符串 A - 把 pattern 中每个
'b'替换成字符串 B - 恰好拼出
value - 且 A 和 B 不能是同一个字符串(包括不能都等于相同的非空串,一般也不允许都为
"",除非其中一个完全没出现)
思路核心是:
-
数出 pattern 里
'a'和'b'的个数:countA、countB。 -
枚举一种字符的长度(比如
lenA),因为总长度是固定的value.size():lenA * countA + lenB * countB == value.size()在给定
lenA的情况下,lenB就是一个唯一的解(如果能整除的话)。 -
对每个可行的
(lenA, lenB),按照 pattern 的顺序从 value 里切子串,检查:- 所有
'a'对应的子串是否一致(同一个映射) - 所有
'b'对应的子串是否一致 - 且
'a'对应的串和'b'对应的串不相等
- 所有
-
如果存在任何一组
(lenA, lenB)满足上述条件,就返回 true,否则 false。
注意特殊情况:
- 交换 a / b 保证 pattern 以
a开头 :- 减少分类情况,否则你可能得分别讨论 pattern 以 a 或 b 开头的不同情况。
- pattern 为空:只有当 value 也为空时返回 true。
- value 为空 :
- pattern 必须只包含同一种字符(全
a或全b),否则需要 a 和 b 都映射成"",导致冲突。
- pattern 必须只包含同一种字符(全
- pattern 全是
a:- 那就只有一种变量,
lenA由m / countA决定(能整除才可能)。
- 那就只有一种变量,
- 任意一方可以映射为空串 :
- 题目给出的例子 4 明确说明:
"a"="dogdog", "b"=""是允许的。 - 我们通过枚举
lenA、计算lenB,自然能覆盖lenA == 0或lenB == 0的情况。
- 题目给出的例子 4 明确说明:
代码
cpp
class Solution {
public:
bool patternMatching(string pattern, string value) {
// 特判 pattern 为空或 value 为空的情况
if(pattern.size() == 0) return value.size() == 0;
if(value.size() == 0) {
char first = pattern[0];
for(auto c : pattern) {
if(c != first) return false;
}
return true;
}
// 简化处理,保证 pattern 以 a 开头
if(pattern[0] == 'b') {
for(auto &c : pattern) {
c = (c == 'a' ? 'b' : 'a');
}
}
// 统计 a 和 b 的数量
int cntA = 0, cntB = 0;
for(auto &c : pattern) {
if(c == 'a') ++ cntA;
else ++ cntB;
}
// 枚举 a 的长度
int n = pattern.size(), m = value.size();
for(int lenA = 0; lenA * cntA <= m; lenA ++ ) {
// 判断 lenB 否合法: cntA * lenA + cntB * lenB == m
int sizeB = m - cntA * lenA;
int lenB = 0;
if(cntB == 0) {
if(sizeB != 0) continue;
}
else {
if(sizeB % cntB != 0) continue;
lenB = sizeB / cntB;
}
// 尝试构造 a 和 b
string subA, subB;
bool hasA = false, hasB = false;
bool ok = true;
int pos = 0;
for(auto c : pattern) {
// 注意在这里我们不用担心 substr(pos, lenA/lenB) 越界
// 因为通过前面的计算我们保证 cntA*lenA+cntB*lenB==value.size()
if(c == 'a') {
string cur = value.substr(pos, lenA);
if(!hasA) { subA = cur; hasA = true; }
else if(subA != cur) { ok = false; break; }
pos += lenA;
}
else {
string cur = value.substr(pos, lenB);
if(!hasB) { subB = cur; hasB = true; }
else if(subB != cur) { ok = false; break; }
pos += lenB;
}
}
if(ok && subA != subB) return true;
}
return false;
}
};
时间复杂度分析
记:
n = pattern.length()m = value.length()
-
统计
countA/countB:- 一次遍历 pattern → 时间
O(n)。
- 一次遍历 pattern → 时间
-
枚举
lenA:lenA从 0 到m / countA(极端情况下countA = 1,则接近m),所以约O(m)次枚举。
-
每一次枚举
(lenA, lenB)后:- 要遍历一遍 pattern(长度为
n),同时在 value 上用substr切子串。 - pattern 长度固定为
n,但substr开销取决于实现:- 理论上,一次
substr是O(子串长度)的拷贝。 - 遍历 pattern 时,总共切出的子串长度之和刚好等于
m(因为我们把 value 从头到尾分块用掉),所以单次枚举的总 substring 拷贝成本是O(m)。
- 理论上,一次
- 因此:每个
(lenA, lenB)的检查成本约为O(m)(或者,你也可以认为是O(n + m),但其中n ≤ 1000,同量级)。
- 要遍历一遍 pattern(长度为
-
总时间:
- 枚举次数大约
O(m),每次检查O(m)→ 总时间O(m^2)。 - 由于
m ≤ 1000,m^2 = 10^6,再乘上常数完全可以接受。
- 枚举次数大约
在很多讲解里,会粗略写成:
- 时间复杂度 :
O(n * m)或O(m^2),这两种说法都能接受(因为n和m同数量级上界为 1000),严格一点按上面的分析是O(m^2)。
空间复杂度分析
- 额外变量:
subA、subB、若干整型和循环变量。
subA/subB最长可能到m(整个 value)。- 所以额外空间复杂度 为
O(m)(或者说O(|value|))。 - 不存在递归或大型辅助数组,空间占用很小