题解 P4428 【[BJOI2018]二进制】
是时候来一篇码风正常的题解了
题目地址
前置知识:线段树
Description
给定一个长度为
- 将第
i 个位置0/1 反转(0 变成1 ,1 变成0 ) - 求区间
[l, r] 之间有多少个连续子序列,满足重排以后是3 的倍数。
Solution
子问题
先来解决一个子问题,什么样的序列重排是
设
换成
可以观察出(或者随便证明一下 )这是一个
不难发现,当
假设我们权值为
充要条件就是:
经过完这步转化,我们需要做的就是构造出一组合理的
由于
因为
分类讨论
-
我们考虑
s 是偶数的情况下,显然取x = y ,即保持两个权贡献一样的拿,那么肯定是可以满足的。 -
我们考虑
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 显然就是负数了搞不出来。 -
-
-
至此,我们发现一个序列是否合法取决于它的长度
补集思想
显然,符合要求的
然后发现后面两个条件可以合并,让合法性与
然后用总的方案减去不合法方案就可以求出合法方案了~
优化算法复杂度!
方向:线段树!
现在,问题变成了支持单点修改,然后
咋做咋做?我也不会
满足条件的连续子序列,可以看做是枚举左右端点,或者是二元计数。想想我们之前学过的算法啥能计算二元计数?相信你想到了,就是归并排序,他求解逆序对的原理就是说合并两个区间,然后把两个区间内各自选一个数,然后把贡献记录到答案上。
然后这题还要支持修改,通过看题解我们就非常直接的想到线段树,然后每次合并两个线段的时候,答案
那么我们只要支持用常数时间复杂度计算合并两个线段对答案的贡献就行了!
怎么合并?
想象一下,你现在有了两个线段
对于第一种条件咋搞?
不妨把那个
首先你需要分类讨论那个
然后再左边的话是对称的,就不赘述了。
综上你需要维护:
- 每条线段的前缀
0 个数L_0 以及 后缀0 个数R_0 - 前缀满足只有一个
1 的个数L_1 ,后缀满足只有一个1 的个数R_1
- 如果
B 的全串只有1 个1 ,即B.C_1 = 1 ,那么跨越过来还可以搞几个0 :C.R_1 \gets A.R_0
第二个条件
比较显然的是我们只关注
设
那么你枚举
- 若
a+c \le 1 且b + d 为奇数,那么则可以产生对答案有他们俩乘积的贡献。
维护这俩破玩意挺恶心的,以
-
C.R_{i, j} \gets B.R_{i, j} -
C.R_{i, j} \gets A.R_{i - B.C_0, j - B.C_1}
重复计算了咋办?
分类讨论一下:
- 重复计算的
s = 1 ,0 的个数为0 的情况,即只有一个'1' 的形式。这个好办,不会再合并的时候统计(因为合并长度至少\ge 2 ,直接一开始的时候统计即可)。 - 重复计算的
s = 1 ,0 的个数为1 的情况。这种情况应该就是mid, mid + 1 两个位置成为01 或者10 的情况,特判一下减掉就行。
然后我们就解决了重复计算的麻烦。
时间复杂度
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;
}