牛客寒假算法基础集训营3 I 智乃的密码(二分、尺取)
题目大意:
给定字符串 \(s\) 、\(L\) 、\(R\) ,求满足长度为 \([L, R]\) 且至少包含四类字符中的三种的子串数量。
思路:
当固定了区间左端点时,随着右端点向右移动对答案的贡献具有单调性。同样,固定右端点,向右移动左端点,对答案的贡献也有单调性。我们考虑使用尺取。
固定区间的左端点为 \(i\) 时,找可行的右端点的取值范围。
Code:
int main() {
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
int n, L, R; cin >> n >> L >> R;
string s; cin >> s;
map<int, int> cnt; // 四类字符的数量
auto upd = [&](char ch, int dx) {
if (isupper(ch)) {
cnt[0] += dx;
} else if (islower(ch)) {
cnt[1] += dx;
} else if (isdigit(ch)) {
cnt[2] += dx;
} else {
cnt[3] += dx;
}
return;
};
ll ans = 0;
for (int i = 0, j = 0; i < n; i++) { // [, )
while (j <= n && (cnt[0] > 0) + (cnt[1] > 0) + (cnt[2] > 0) + (cnt[3] > 0) < 3) {
if (j < n)
upd(s[j], 1);
j++;
}
int l = max(j, i + L); // 此时j是刚好满足出现次数的位置,i + L表示符合条件的右端点至少距离长度为L
int r = min(n, i + R); // 右端点不能取越界,且距离最多不能超过R
ans += max(0, r - l + 1);
upd(s[i], -1);
}
cout << ans << "\n";
return 0;
}
/*
11 3 6
11111bAAA**
7 3 6
11111bA
*/
左端点已经固定,假设此时第一个满足条件的右端点下标为 \(idx\),那么根据单调性在这之后的下标也可以当作右端点。我们可以二分求出第一个满足条件的右端点。这里使用前缀和加速对区间内子串数量的查询。
int main() {
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
int n, L, R; cin >> n >> L >> R;
string s; cin >> s;
vector<vector<int>> pre(4, vector<int>((int)s.length() + 1));
for (int i = 0; i < n; i++) {
for (int j = 0; j < 4; j++)
pre[j][i + 1] = pre[j][i];
if (isupper(s[i]))
pre[0][i + 1]++;
else if (islower(s[i]))
pre[1][i + 1]++;
else if (isdigit(s[i]))
pre[2][i + 1]++;
else
pre[3][i + 1]++;
}
auto check = [&](int l, int r) {
int cnt = 0;
for (int i = 0; i < 4; i++) {
cnt += (pre[i][r] - pre[i][l - 1] > 0);
}
return cnt >= 3;
};
ll ans = 0;
for (int i = 1; i <= n; i++) {
int l = i, r = min(i + R, n + 1), idx = 0;
while (l < r) {
int mid = (l + r) >> 1; // 找第一个满足条件的右端点
if (check(i, mid)) {
idx = mid;
r = mid;
} else {
l = mid + 1;
}
}
if (idx == 0) // 一个满足条件的都没找到
continue;
l = max(idx, i + L - 1);
r = min(n, i + R - 1);
// cerr << "l = " << l << ", r = " << r << endl;
ans += max(0, r - l + 1);
}
cout << ans << "\n";
return 0;
}