题解 P4428 【[BJOI2018]二进制】

· · 题解

是时候来一篇码风正常的题解了

题目地址

前置知识:线段树

Description

给定一个长度为 n01 串,m 次操作:

Solution

子问题

先来解决一个子问题,什么样的序列重排是 3 的倍数?

s 为序列上 1 的个数,序列长度为 len。将二进制下每位下的权列列举出来,分别是 1, 2, 4, 8, 16, 32....

换成 \bmod 3 意义下的权:1, 2, 1, 2, 1, 2, 1...

可以观察出(或者随便证明一下 )这是一个 1, 2 循环的权值。设 1 的权数量为 a2 的权数量为 b

不难发现,当 len 为偶数时,a = b = \dfrac{len}{2};当 len 为奇数时,a = \dfrac{len + 1}{2}, b = \dfrac{len - 1}{2}

假设我们权值为 1 的位置上有 x 个是 1,权值为 2 的位置上有 y 个是 1,要满足 x + y = s

充要条件就是:

3 | x + 2y \Leftrightarrow x+2y \equiv 0 \pmod 3\Leftrightarrow 3y+x-y \equiv 0 \pmod 3\Leftrightarrow x-y \equiv 0 \pmod 3 \Leftrightarrow x \equiv y \pmod 3

经过完这步转化,我们需要做的就是构造出一组合理的 x, y。使得 x, y 能填到位置上(显然顺序已经无关)。

由于 x \ge y, a \ge b,不妨让 x 取填到 a 个位置上,y 填到 b 个位置上,即要满足 0 \le x \le a, 0 \le y \le b

因为 s, a, b 是有限的,不难想到贪心情况下,x, y 要尽量接近,这样容错率更大(比如 x = y 的情况可以支持 s = 0x = y + 3 的情况势必 s, x \ge 3x = y + 6 的情况势必 s, x \ge 6。或者另外一种解释,任意 x \equiv y \pmod 3 的情况都可以通过大的那一项 -3,小的那一项 +3,最终变为 x = y + 3 或者是 x = y 的形式,不妨手玩一下)

分类讨论

  1. 我们考虑 s 是偶数的情况下,显然取 x = y,即保持两个权贡献一样的拿,那么肯定是可以满足的。

  2. 我们考虑 s 是奇数的情况下,因为若 x = y,那么 x + y = s 一定是奇数所以不能这么取。所以一定得是 x = y + 3 的形式,这种情况下 x = \dfrac{s + 3}{2}, y = \dfrac{s - 3}{2},在这种形式下, 取带入刚才的 x, y 需要满足的区间式,讨论 s 在何时合法:

    • 首先需要 s > 1,因为若 s = 1,那么 y 显然就是负数了搞不出来。

至此,我们发现一个序列是否合法取决于它的长度 len 奇偶性以及 1 的数量 s

补集思想

显然,符合要求的 s 非常多,不好统计,也不好优化,反过来,考虑不合法数量:

然后发现后面两个条件可以合并,让合法性与 len 无关,并且 s = 1 和后面两个条件有一部分重叠,即 s = 10 的个数 \le 1 时,所以再优化一下:

然后用总的方案减去不合法方案就可以求出合法方案了~

优化算法复杂度!

方向:线段树!

现在,问题变成了支持单点修改,然后 O(1) 或者 O(\log) 的时间在 [l, r] 查询满足条件(上一步总结的)的连续子序列个数。

咋做咋做?我也不会

满足条件的连续子序列,可以看做是枚举左右端点,或者是二元计数。想想我们之前学过的算法啥能计算二元计数?相信你想到了,就是归并排序,他求解逆序对的原理就是说合并两个区间,然后把两个区间内各自选一个数,然后把贡献记录到答案上。

然后这题还要支持修改,通过看题解我们就非常直接的想到线段树,然后每次合并两个线段的时候,答案 = 左边线段的答案 + 右边线段的答案 + 左端点在左边这条线段、右端点在右边这条线段的产生的合法连续子序列。

那么我们只要支持用常数时间复杂度计算合并两个线段对答案的贡献就行了!

怎么合并?

想象一下,你现在有了两个线段 A:[l, mid]B:[mid + 1, r],合并后新的线段为 C,你如何算出跨越两条线段产生的答案贡献?换句话说你需要维护哪些信息?比较显然的是,跨越显然过 midmid + 1。即这样的线段一定是 A 的一个后缀和 B 的一个前缀构成的,所以信息一定要设立前缀后缀的信息。

对于第一种条件咋搞?

不妨把那个 0 的个数 \ge 2 这个坑爹的条件去掉,仅考虑计算 s = 1 的贡献,后面减掉就行。

首先你需要分类讨论那个 1 所在的位置,不妨设他在右边这条线段上,那么你需要记录 [l, mid] 的后缀 0 个数,以及 B 线段的前缀满足只有一个 1 的个数,然后两者的乘积就是答案。

然后再左边的话是对称的,就不赘述了。

综上你需要维护:

  1. 每条线段的前缀 0 个数 L_0 以及 后缀 0 个数 R_0
  2. 前缀满足只有一个 1 的个数 L_1,后缀满足只有一个 1 的个数 R_1
* $C.R_1 \gets B.R_1$ 即原来 $B$ 的后缀也是 $C$ 的后缀 * 讨论那个唯一的 $1$ 在 $A$,如果$B.C_0 = 0$, $C.R_1 \gets A.R_1

第二个条件

比较显然的是我们只关注 1 的数量的奇偶性,以及 0 的数量(而且只能为 0/1)。

R_{i, j} 为线段后缀中满足 0 的数量为 i1 的数量 \bmod 2 = j 的数量。L 统计前缀,对称地是类似的。

那么你枚举 A.R_{a, b}, B.L_{c, d} 考虑能否产生贡献:

维护这俩破玩意挺恶心的,以 R 为例,要考虑跨域整个 B,左端点到 A 的新后缀。:

重复计算了咋办?

分类讨论一下:

  1. 重复计算的 s = 10 的个数为 0 的情况,即只有一个 '1' 的形式。这个好办,不会再合并的时候统计(因为合并长度至少 \ge 2,直接一开始的时候统计即可)。
  2. 重复计算的 s = 10 的个数为 1 的情况。这种情况应该就是 mid, mid + 1 两个位置成为 01 或者 10 的情况,特判一下减掉就行。

然后我们就解决了重复计算的麻烦。

时间复杂度

O(N\log_2N)

Tips

对于这种及其毒瘤的东西可以自己写个结构体啥的,这样把代码分治,这样不会写的很乱。

Code

我相信这个码风一点都不毒瘤。。。。

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

typedef long long LL;

const int N = 100005;

int n, m, w[N];

// 线段结构体
struct Seg{
    int L0, R0, L1, R1, C0, C1, R[2][2], L[2][2];
    LL res;
    // 数组清零啥的
    void init() {
        L0 = R0 = L1 = R1 = C0 = C1 = res = 0;
        memset(R, 0, sizeof R), memset(L, 0, sizeof L);
    }
    Seg(){}
    // 线段长度为 1,这个元素是 x 的时候的初始化
    Seg(int x){
        init();
        if (x) L1 = R1 = C1 = L[0][1] = R[0][1] = res = 1;
        else L0 = R0 = C0 = L[1][0] = R[1][0] = 1; 
    }
    // 合并两个区间,同时对 ans 产生贡献, mid 是线段 A 的右端点
    Seg(Seg A, Seg B, int mid) {
        init();
        C0 = A.C0 + B.C0, C1 = A.C1 + B.C1;
        L0 = A.L0 + (!A.C1 ? B.L0 : 0), R0 = B.R0 + (!B.C1 ? A.R0 : 0);
        L1 = A.L1 + (!A.C1 ? B.L1 : 0) + (A.C1 == 1 ? B.L0 : 0);
        R1 = B.R1 + (!B.C1 ? A.R1 : 0) + (B.C1 == 1 ? A.R0 : 0);
        for (int i = 0; i < 2; i++) {
            for (int j = 0; j < 2; j++) {
                L[i][j] = A.L[i][j] + (i >= A.C0 ? B.L[i - A.C0][j ^ (A.C1 & 1)] : 0);
                R[i][j] = B.R[i][j] + (i >= B.C0 ? A.R[i - B.C0][j ^ (B.C1 & 1)] : 0);
            }
        }
        res = A.res + B.res;
        // 条件 1
        res += (LL)A.R0 * B.L1 + (LL)A.R1 * B.L0;
        // 条件 2
        res += (LL)A.R[0][0] * (B.L[0][1] + B.L[1][1]) + (LL)A.R[0][1] * (B.L[0][0] + B.L[1][0]); 
        res += (LL)A.R[1][0] * B.L[0][1] + (LL)A.R[1][1] * B.L[0][0]; 
        // 减掉重复统计
        if (w[mid] + w[mid + 1] == 1) res--;
    }   
} v[N << 2];

void build(int p, int l, int r) {
    if (l == r) { v[p] = Seg(w[l]); return; }
    int mid = (l + r) >> 1;
    build(p << 1, l, mid);
    build(p << 1 | 1, mid + 1, r);
    v[p] = Seg(v[p << 1], v[p << 1 | 1], mid);
}

void change(int p, int l, int r, int x) {
    if (l == r) { v[p] = Seg(w[l]); return; }
    int mid = (l + r) >> 1;
    if (x <= mid) change(p << 1, l, mid, x);
    else change(p << 1 | 1, mid + 1, r, x);
    v[p] = Seg(v[p << 1], v[p << 1 | 1], mid);
}

Seg query(int p, int l, int r, int x, int y) {
    if (x <= l && r <= y) return v[p];
    int mid = (l + r) >> 1;
    if (y <= mid) return query(p << 1, l, mid, x, y);
    if (mid < x) return query(p << 1 | 1, mid + 1, r, x, y);
    return Seg(query(p << 1, l, mid, x, y), query(p << 1 | 1, mid + 1, r, x, y), mid);
}

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%d", w + i);
    build(1, 1, n);
    scanf("%d", &m);
    while (m--) {
        int opt; scanf("%d", &opt);
        if (opt == 1) {
            int x; scanf("%d", &x);
            w[x] ^= 1, change(1, 1, n, x);
        } else {
            int l, r; scanf("%d%d", &l, &r);
            LL sum = (LL)(r - l + 1) * (r - l + 2) / 2; 
            printf("%lld\n", sum - query(1, 1, n, l, r).res);
        }
    }
    return 0;
}