0
点赞
收藏
分享

微信扫一扫

每日一题 —— LC. 792 匹配子序列的单词数


特此记录!注意以后用C++写题时,能用引用就尽量用引用!

特此记录!注意以后用C++写题时,能用引用就尽量用引用!

特此记录!注意以后用C++写题时,能用引用就尽量用引用!

792. 匹配子序列的单词数

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

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

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

提示

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

示例

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

思路

假设待匹配的单词里面有一个字母​​a​​​,那么我们需要在主串​​s​​​中找到一个字母​​a​​​来与之匹配,如果​​s​​​中存在多个​​a​​​,那么我们用哪个来匹配呢?答案是最左侧的​​a​​,即最早出现的那一个。

因为选择越靠左侧的​​a​​​,能让其右侧的字母尽可能的多。对于待匹配单词中字母​​a​​​的下一个字母,假设是​​b​​​,我们需要从​​s​​​中,我们选择的字母​​a​​​后面,再找到一个​​b​​​来与之匹配。选择最靠左的​​a​​​,能让其右侧可供选择的字母数量最多,就越有可能匹配到字母​​b​​。

举个简单的例子,加入​​s = cabcade​​​,待匹配单词​​w = ac​​,

我们匹配时,先取​​w​​​的第一个字母,即​​a​​​,然后尝试在​​s​​​中找到一个​​a​​​来与之匹配,而此时​​s​​​中有2个​​a​​​,我们应当取哪个呢?当然应该取最左侧的那个。因为这样能让​​s​​​中右侧剩余部分最长,剩余部分的字母也就越多,就越有可能匹配上​​w​​​后续的字母。同时需要注意,当我们匹配了第一个​​a​​​后,在匹配​​w​​​的第二个字母​​c​​​时,必须要求​​s​​​中选择的​​c​​​,一定要在​​s​​​中选择的​​a​​​的右侧。从上面的例子来说,就是我们不能选择​​s​​​中的第一个​​c​​​,因为其不在​​a​​的右侧。

于是我们的思路就比较清晰了。我们依次遍历待匹配单词​​w​​​的每个字符,对于当前字符​​c​​​,每次尝试在主串​​s​​​中查找这个字符,但是需要注意,查找的​​c​​​的下标,必须要大于前一个匹配的字符在​​s​​中的位置。

即,对于​​w​​​中每个字符​​c​​​,我们需要维护一个左边界​​left​​​,我们在主串​​s​​​中,查找下标大于​​left​​​的第一次字符​​c​​​,设其下标为​​idx​​​,找到后,我们更新​​left = idx​​​,此时的​​left​​是作为下一个待匹配字符的左边界。

只要对于​​w​​​中的每个字符,我们都能在​​s​​​中完成匹配。那么该​​w​​​就是​​s​​的一个子序列。

要完成上述操作,我们需要先遍历一次主串​​s​​​,将每个字符出现的位置保存下来(某个字符出现多次的话,就保存多个下标)。而每次我们查找字符时,直接取该字符在​​s​​​中出现的下标数组,并通过二分,查找到第一个大于​​left​​的下标即可。

// C++ 144ms
class Solution {
public:

// 在idx中找到第一个大于left的位置
int find(vector<int>& idx, int left) {
// idx可能为空
if (idx.size() == 0) return -1;
int l = 0, r = idx.size() - 1;
while (l < r) {
int mid = l + r >> 1;
if (idx[mid] > left) r = mid;
else l = mid + 1;
}
if (idx[l] > left) return idx[l];
return -1;
}

int numMatchingSubseq(string s, vector<string>& words) {
// 由于全是小写字母, 可以直接开一个大小为26的vector
vector<vector<int>> idx(26);
// 保存每个字符出现的下标
for (int i = 0; i < s.size(); i++) {
idx[s[i] - 'a'].push_back(i);
}
int ans = 0;
for (auto& w : words) {
int l = -1;
bool yes = true;
for (int i = 0; i < w.size(); i++) {
vector<int>& v = idx[w[i] - 'a']; // 注意这里要加引用 & , 否则会超时
// 不加引用的话每次取都会拷贝一次, 会非常消耗性能
l = find(v, l); // 这里可以直接用 upper_bound 函数替代
if (l == -1) {
yes = false;
break;
}
}
if (yes) ans++;
}
return ans;
}
};

也可以用STL中的​​upper_bound​​函数来替代自实现的二分查找

// C++ 144ms
class Solution {
public:
int numMatchingSubseq(string s, vector<string>& words) {
// 由于全是小写字母, 可以直接开一个大小为26的vector
vector<vector<int>> idx(26);
// 保存每个字符出现的下标
for (int i = 0; i < s.size(); i++) {
idx[s[i] - 'a'].push_back(i);
}
int ans = 0;
for (auto& w : words) {
int l = -1;
bool yes = true;
for (int i = 0; i < w.size(); i++) {
vector<int>& v = idx[w[i] - 'a']; // 注意这里要加引用 & , 否则会超时
// 不加引用的话每次取都会拷贝一次, 会非常消耗性能
vector<int>::iterator it = upper_bound(v.begin(), v.end(), l);
// upper_bound是查找第一个 > 某个数的
// lower_bound是查找第一个 >= 某个数的
if (it == v.end()) {
// 没找着
yes = false;
break;
}
l = *it; //找着了, 更新left边界
}
if (yes) ans++;
}
return ans;
}
};

今早做这道题时,我自己想出了二分的做法,但是提交一直TLE。最后针对几个样例数据,加了​​set​​做缓存+去重,以及对一些情况加了些特判,才勉强通过。

每日一题 —— LC. 792 匹配子序列的单词数_算法

非常艰难的AC!!下面是我TLE的代码

class Solution {
public:

// 在idx中找到第一个大于left的位置
int find(vector<int>& idx, int left) {
int l = 0, r = idx.size() - 1;
while (l < r) {
int mid = l + r >> 1;
if (idx[mid] > left) r = mid;
else l = mid + 1;
}
if (idx[l] > left) return idx[l];
return -1;
}

// 4 × 10^6
int numMatchingSubseq(string s, vector<string>& words) {
// 只包含小写字母, 可以存储一下每个字母第一次出现时的位置, 哈哈哈
unordered_map<int, vector<int>> idx;
for (int i = 0; i < s.size(); i++) {
int u = s[i] - 'a';
idx[u].push_back(i);
}
// for (auto& [k, v] : idx) {
// char c = k + 'a';
// printf("char = %c : -> ", c);
// for (auto& i : v) printf("%d, ", i);
// printf("\n");
// }

int ans = 0;
for (auto& w : words) {
// printf("word = %s, ", w.c_str());
int cnt = 0, l = -1;
for (int i = 0; i < w.size(); i++) {
int u = w[i] - 'a';
if (idx.find(u) == idx.end()) break;
vector<int> v = idx.find(u)->second;
l = find(v, l);
// printf("char = %c, idx = %d; ", w[i], l);
if (l == -1) break; // 没找着
cnt++;
}
if (cnt == w.size()) {
// printf("yes!");
ans++;
}
// printf("\n");
}
return ans;
}
};

可以看到我加了很多​​print​​​进行反复调试。下面是我加了​​set​​做缓存,加了一些特判后勉强AC的代码。

// C++ 1248ms
class Solution {
public:

// 在idx中找到第一个大于left的位置
int find(vector<int>& idx, int left) {
int l = 0, r = idx.size() - 1;
while (l < r) {
// printf("%d次二分", c);
if (left == -1) break; // 第一次, 直接break, 取l = 0
int mid = l + r >> 1;
if (idx[mid] > left) r = mid;
else l = mid + 1;
}
if (idx[l] > left) return idx[l];
return -1;
}

// 4 × 10^6
int numMatchingSubseq(string s, vector<string>& words) {
// 只包含小写字母, 可以存储一下每个字母第一次出现时的位置, 哈哈哈
unordered_map<char, vector<int>> idx;
for (int i = 0; i < s.size(); i++) {
idx[s[i]].push_back(i);
}
// for (auto& [k, v] : idx) {
// printf("char = %c : -> ", k);
// for (auto& i : v) printf("%d, ", i);
// printf("\n");
// }
// 缓存已判断过的单词
unordered_set<string> valid;
unordered_set<string> invalid;
int ans = 0;
for (auto& w : words) {
if (valid.count(w)) {
ans++;
continue;
}
if (invalid.count(w)) continue;
// printf("word = %s, ", w.c_str());
int cnt = 0, l = -1;
for (int i = 0; i < w.size(); i++) {
char u = w[i];
if (idx.find(u) == idx.end()) break;
vector<int> v = idx.find(u)->second;
l = find(v, l);
// printf("char = %c, idx = %d; ", w[i], l);
if (l == -1) break; // 没找着
cnt++;
}
if (cnt == w.size()) {
// printf("yes!");
ans++;
valid.emplace(w);
} else {
invalid.emplace(w);
}
// printf("\n");
}
return ans;
}
};

后来看题解,发现别人的思路和我的一样,但是用别人的代码提交,就运行非常快。我非常纳闷,一行一行代码的进行对比,删除调试。

最后发现,是因为我后面取​​vector​​时,没有加引用导致的!

然后我把上面那份TLE的代码,只多加了一个引用符号 ​​&​​,就AC了,跑了180ms

就是这句

vector<int> v = idx.find(u)->second;

多加了引用符号,改成了

vector<int>& v = idx.find(u)->second;

C++中如果不加​​&​​,会默认进行数据拷贝,所以会非常吃性能!

下次一定要注意,从容器中取数据时,尽可能的使用引用!

但同时也要注意,使用引用取出的数据,修改就是直接在原数据上修改!而不是在数据的副本上修改。


举报

相关推荐

0 条评论