特此记录!注意以后用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
做缓存+去重,以及对一些情况加了些特判,才勉强通过。
非常艰难的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++中如果不加&
,会默认进行数据拷贝,所以会非常吃性能!
下次一定要注意,从容器中取数据时,尽可能的使用引用!
但同时也要注意,使用引用取出的数据,修改就是直接在原数据上修改!而不是在数据的副本上修改。