LC-792.匹配子序列的单词数

题目描述

leetcode 中等题

给定字符串 s 和字符串数组 words, 返回  words[i] 中是s的子序列的单词个数 。

字符串的 子序列 是从原始字符串中生成的新字符串,可以从中删去一些字符(可以是none),而不改变其余字符的相对顺序。

  • 例如, “ace” 是 “abcde” 的子序列。

示例1:

1
2
3
输入: s = "abcde", words = ["a","bb","acd","ace"]
输出: 3
解释: 有三个是 s 的子序列的单词: "a", "acd", "ace"

示例2:

1
2
输入: s = "dsahjpjauf", words = ["ahjpjau","ja","ahbwzgqnuk","tnmlanowax"]
输出: 2

提示1:

1
2
3
4
1 <= s.length <= 5 * 10^4
1 <= words.length <= 5000
1 <= words[i].length <= 50
words[i]和 s 都只由小写字母组成。

分桶 + 二分查找 (贪心)

最容易想到的方法就是对于每个 $word$ ,通过双指针的方法和 $s$ 进行匹配,指针 $i$ 和 $j$ 初始时分别指向 $word$ 和 $a$ 的第 $0$ 位字符,如果匹配成功则将两个指针分别向后移动,否则只移动 $j$,如果 $i$ 能匹配结束的话则 $ans$ 加一。

但是该方法在该题的数据规模下会 TLE,所以需要进行优化。

一种方法是我们可以将 $s$ 按照每个字符进行分桶,每个桶内存储该字符在 $s$ 内从小到大排列后的索引值。那么对于每个 $word$ 我们都会进行一轮遍历,每一轮都独立维护一个 $next$ 值,并在每次 $i$ 指针后移时,尝试通过二分查找得到当前 $i$ 所指向字符的对应桶内第一个大于 $next$ 的索引值(贪心思路,目的是让每次的 $next$ 都尽量小),然后将 $next$ 更新为该索引值,过程中若不存在大于 $next$ 的索引值则匹配失败,若能匹配结束则 $ans$ 加一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Solution {
public int numMatchingSubseq(String s, String[] words) {
Map<Character, List<Integer>> map = new HashMap<>();
for(int i = 0; i < s.length(); i++){
char c = s.charAt(i);
List<Integer> list = map.computeIfAbsent(c, ArrayList::new);
list.add(i);
map.put(c, list);
}
int ans = 0;
for(int i = 0; i < words.length; i++){
char[] item = words[i].toCharArray();
int next = Integer.MIN_VALUE;
boolean flag = true;
for(int j = 0; j < item.length; j++){
List<Integer> list = map.getOrDefault(item[j], new ArrayList<>());
// 二分查找第一个大于 target 的数
// 也就是右边界 + 1, 右边界为 left - 1, 右边界 <= target
int left = 0, right = list.size();
while(left < right){
int mid = left + right >> 1;
if(list.get(mid) <= next){
left = mid + 1;
}else if(list.get(mid) > next){
right = mid;
}
}
if(left < list.size()){
next = list.get(left);
}else{
flag = false;
break;
}
}
if(flag){
ans++;
}
}
return ans;
}
}

类似上面的思路,我们也可以对于每个 $word$ 进行逆序遍历,只需要在二分查找时改成找到第一个小于 $i$ 的索引值即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Solution {
public int numMatchingSubseq(String s, String[] words) {
Map<Character, List<Integer>> map = new HashMap<>();
for(int i = 0; i < s.length(); i++){
char c = s.charAt(i);
List<Integer> list = map.computeIfAbsent(c, ArrayList::new);
list.add(i);
map.put(c, list);
}
int ans = 0;
for(int i = 0; i < words.length; i++){
char[] item = words[i].toCharArray();
int next = Integer.MAX_VALUE;
boolean flag = true;
for(int j = item.length - 1; j >=0; j--){
List<Integer> list = map.getOrDefault(item[j], new ArrayList<>());
// 二分查找第一个小于 target 的数
// 也就是左边界 - 1, 左边界为 left, 左边界 >= target
int left = 0, right = list.size();
while(left < right){
int mid = left + right >> 1;
if(list.get(mid) < next){
left = mid + 1;
}else if(list.get(mid) >= next){
right = mid;
}
}
if(left - 1 >= 0 && left - 1 < list.size()){
next = list.get(left - 1);
}else{
flag = false;
break;
}
}
if(flag){
ans++;
}
}
return ans;
}
}

分桶 + 多指针

上面的做法的思路是对 $s$ 按字符进行分桶,从而加速每一个 $word$ 与 $s$ 之间的匹配速度,相当于每个 $word$ 都会独立匹配一次。

而另一种做法的思路是将 $s$ 同时与所有 $word$ 进行匹配。具体来说每一个 $word$ 都有着独立的指针 $i$ ,并且初始值为 0,那么对每一个 $word$ 按照当前匹配到的字符,也就是各自 $i$ 指针指向的字符进行分桶。那么我们就可以直接遍历 $s$,每次只需要将 $j$ 所指向字符的对应桶内的所有 $word$ 拿出来并将它们的 $i$ 指针往后移动一位,然后重新维护桶即可,而若当前取出的 $word$ 对应的 $i$ 已经到达结尾,则 $ans$ 加一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public int numMatchingSubseq(String s, String[] words) {
Queue<int[]>[] bucket = new Queue[26];
for (int i = 0; i < 26; ++i) {
bucket[i] = new ArrayDeque<int[]>();
}
for (int i = 0; i < words.length; ++i) {
bucket[words[i].charAt(0) - 'a'].offer(new int[]{i, 0});
}
int ans = 0;
for (int i = 0; i < s.length(); ++i) {
char c = s.charAt(i);
int size = bucket[c - 'a'].size();
while (size > 0) {
int[] item = bucket[c - 'a'].poll();
if (item[1] == words[item[0]].length() - 1) {
++ans;
} else {
++item[1];
bucket[words[item[0]].charAt(item[1]) - 'a'].offer(item);
}
--size;
}
}
return ans;
}
}

LC-792.匹配子序列的单词数
https://wecgwm.github.io/2022/11/18/LC-792-匹配子序列的单词数/
作者
yichen
发布于
2022年11月18日
许可协议