特此记录!注意以后用C++写题时,能用引用就尽量用引用!
特此记录!注意以后用C++写题时,能用引用就尽量用引用!
特此记录!注意以后用C++写题时,能用引用就尽量用引用!
给定字符串 s 和字符串数组 words, 返回 words[i] 中是s的子序列的单词个数 。
字符串的 子序列 是从原始字符串中生成的新字符串,可以从中删去一些字符(可以是none),而不改变其余字符的相对顺序。
例如, “ace” 是 “abcde” 的子序列。
提示
1 <= s.length <= 5 * 10^41 <= words.length <= 50001 <= words[i].length <= 50words[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& v = idx.find(u)->second;
C++中如果不加&,会默认进行数据拷贝,所以会非常吃性能!
下次一定要注意,从容器中取数据时,尽可能的使用引用!
但同时也要注意,使用引用取出的数据,修改就是直接在原数据上修改!而不是在数据的副本上修改。