D 小红的01子序列构造(easy)

两种写法,这里都介绍一下:

方法1:双指针

先考虑一个区间内的 子序列如何统计,我们只需要对于每个 ,看它之前有几个 ,就是它的贡献。

例如对于序列 都是 ,他们的贡献依次为 ,所以最后的 子序列数为

用双指针枚举区间的左右端点,假设当前区间是 ,区间内的 子序列为 个。

如果 则直接输出当前区间。

如果 说明当前 子序列过多,需要把左端点往右靠。

  • 如果把 给排出序列,说明后面的所有 的贡献都减少了 ,所以总共减少了 个( 表示当前区间内 的个数)
  • 如果把 给排出序列,对个数毫无影响

如果 说明当前 子序列过少,需要把右端点往右扩展。

  • 如果加进来一个 ,对答案毫无影响,但是 需要加
  • 如果加进来一个 ,之前的 贡献无影响,新加进来的 贡献为

时间复杂度

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;

int main() {
    LL n, k;
    cin >> n >> k;
    string s; cin >> s;
    int cnt0 = 0, cnt1 = 0;
    int l = 0, r = 0;
    if (s[0] == '0') cnt0 ++;
    if (s[0] == '1') cnt1 ++;
    LL num = 0;
    while (l < n && r < n) {
        if (num == k) {
            cout << l + 1 << ' ' << r + 1 << endl;
            return 0;
        }
        if (num < k) {
            r ++;
            if (s[r] == '0') cnt0 ++;
            else {
                num += cnt0;
                cnt1 ++;
            }
        } else {
            if (s[l] == '0') {
                num -= cnt1;
                cnt0 --;
            } else cnt1 --;
            l ++;
        }
    }
    cout << -1 << endl;
    return 0;
}

方法2:前缀和+二分

我们思考下如何快速算出一段指定区间 的贡献。

经过上面的过程,我们可以看出贡献只由区间内的 产生,每个 产生的贡献就是它们之前的 的个数。所以我们需要求一段区间内的 的个数,这个可以由前缀和实现。我们也可以预处理出每个 的贡献,然后计算出区间内的贡献和即可。这个也可以由前缀和实现。

具体来说,我们可以定义几个前缀和数组

  • 为前 个数中 的个数
  • 为前 个数中 的个数
  • 为区间 的贡献

所以,对于区间 来说,总贡献就是

例如对于 来说,它们每个数的贡献就是 ,对应的前缀和数组就是

假设我们要求 的贡献,可能同学们会诈以为是 ,但是这样的话我们没有想到前面少的两个0对后面1的影响,我们来看看少了多少。

如果前面两个0没了,那么后面的贡献会变成 ,对应的前缀和数组变成了 。前面每少掉一个 ,后面的所有 的贡献都少了 。所以少掉的贡献就是 ,所以正确的贡献应该是原来区间和的基础上,减去少掉的贡献,即

那么也简单了,我们可以枚举区间左端点,然后二分找出贡献大于等于 的最小贡献值,看看是不是刚好等于 即可。时间复杂度

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e5 + 10;
int pre0[N], pre1[N];
LL sum[N];

LL calc(int l, int r) {
    return sum[r] - sum[l - 1] - 1ll * pre0[l - 1] * (pre1[r] - pre1[l - 1]);
}

int main() {
    LL n, k;
    cin >> n >> k;
    string s; cin >> s;
    for (int i = 1; i <= n; i ++ ) {
        pre0[i] = pre0[i - 1];
        pre1[i] = pre1[i - 1];
        if (s[i - 1] == '0') pre0[i] ++;
        if (s[i - 1] == '1') pre1[i] ++;
    }
    for (int i = 1; i <= n; i ++ ) {
        sum[i] = sum[i - 1];
        if (s[i - 1] == '1') sum[i] += pre0[i];
    }
    for (int i = 1; i <= n; i ++ ) {
        int l = i, r = n;
        // 二分查找i开头,贡献大于等于k个区间的最小贡献
        int ans = -1;
        while (l <= r) {
            int mid = (l + r) / 2;
            if (calc(i, mid) >= k) {
                ans = mid;
                r = mid - 1;
            } else l = mid + 1;
        }
        if (ans == -1) break;
        if (calc(i, ans) == k) {
            cout << i << ' ' << ans << endl;
            return 0;
        }
    }
    cout << -1 << endl;
    return 0;
}