|
|
#include <bits/stdc++.h>
|
|
|
using namespace std;
|
|
|
const int N = 2e5 + 10;
|
|
|
|
|
|
int n, m, l1, r1, l2, r2;
|
|
|
|
|
|
// 线段树维护区间最大连续子序列和
|
|
|
#define ls u << 1
|
|
|
#define rs u << 1 | 1
|
|
|
struct Node {
|
|
|
int l, r;
|
|
|
int lmax, rmax; // 从左开始的连续脑洞个数,从右开始的连续脑洞个数
|
|
|
int sum; // 区间正常脑组织个数
|
|
|
int len; // 区间长度
|
|
|
int ans; // 区间最大的连续脑洞数量
|
|
|
int tag; // 懒标记
|
|
|
} tr[N << 2];
|
|
|
|
|
|
void pushup(int u) {
|
|
|
tr[u].sum = tr[ls].sum + tr[rs].sum; // 区间正常脑组织个数
|
|
|
|
|
|
if (tr[ls].lmax == tr[ls].len) // 左边都是0
|
|
|
tr[u].lmax = tr[ls].len + tr[rs].lmax; // 父亲左最长=左子整个长+右子树左最长
|
|
|
else
|
|
|
tr[u].lmax = tr[ls].lmax;
|
|
|
|
|
|
if (tr[rs].rmax == tr[rs].len) // 右边都是0
|
|
|
tr[u].rmax = tr[rs].len + tr[ls].rmax; // 父亲右最长=右子整个长+左子树右最长
|
|
|
else
|
|
|
tr[u].rmax = tr[rs].rmax;
|
|
|
|
|
|
// 三种情况,分情况讨论
|
|
|
tr[u].ans = max({tr[ls].ans, tr[rs].ans, tr[ls].rmax + tr[rs].lmax});
|
|
|
}
|
|
|
|
|
|
void build(int u, int l, int r) {
|
|
|
tr[u].l = l, tr[u].r = r, tr[u].tag = -1; // 边界,懒标记初始化
|
|
|
tr[u].len = r - l + 1; // 区间长度,虽然记录了l,r,也可以现用现算,但一次计算还是代码更短,但内存会更大
|
|
|
if (l == r) {
|
|
|
tr[u].sum = 1; // 初始化时,是没有脑洞的,都是正常的脑组织
|
|
|
tr[u].ans = tr[u].lmax = tr[u].rmax = 0; // 因为默认值是1,所以不写也行,但好习惯是写就写全,不用默认值
|
|
|
/* ans 区间最大的连续脑洞数量 = 0
|
|
|
lmax 从左开始的连续脑洞个数 = 0
|
|
|
rmax 从右开始的连续脑洞个数 = 0
|
|
|
*/
|
|
|
return;
|
|
|
}
|
|
|
int mid = (l + r) >> 1;
|
|
|
build(ls, l, mid), build(rs, mid + 1, r);
|
|
|
pushup(u);
|
|
|
}
|
|
|
|
|
|
/* 功能:在u节点管控的区间完整命中的情况下,更新u节点的统计信息,并且变更u节点的懒标记为tag
|
|
|
Q:为什么要独立封装出来一个chang函数,我看其它线段树的题目中没有这个东西?
|
|
|
答:在有懒标记的区间修改题目中,会在三个地方使用到修改u管辖范围内的统计信息、记录懒标识的情况:
|
|
|
① modify中完整命中时,对于整体区间需要修改统计信息和下传懒标记
|
|
|
② 分裂pushdown时,如果u区间存在懒标记,需要同时修改左、右儿子的统计信息和懒标记
|
|
|
如此说来,这个change(u,tag)独立封装出来就完全合理了,因为可以复用,功能就是将当前节点的管辖区间修改统计信息和懒标记
|
|
|
这个change(u,tag)应该对于每个需要下传懒标记的线段树都是一样有用的,只不过有的逻辑太简单,一句话两句话就直接在modify、
|
|
|
pushdown里实现了,没有独立封成change(u,tag)函数,按道理来讲,其实全都封成change函数才上正道。
|
|
|
*/
|
|
|
void change(int u, int tag) {
|
|
|
// 根据懒标记的不同,结合业务逻辑,对于u管辖的区间进行统计信息的变更(tr[u].len不需要变更,它不是统计信息,它是属性)
|
|
|
// ① 单个懒标记:不同的数值表示不同的含义,处理逻辑不同,比如本题 √
|
|
|
// ② 单个懒标记:不同的数值表示相同的含义,处理逻辑相同,比如都是+tag
|
|
|
// ③ 多个懒标记:不同的tag代表不同的业务逻辑
|
|
|
if (tag == 0) {
|
|
|
tr[u].ans = tr[u].lmax = tr[u].rmax = tr[u].len;
|
|
|
tr[u].sum = 0;
|
|
|
}
|
|
|
if (tag == 1) {
|
|
|
tr[u].ans = tr[u].lmax = tr[u].rmax = 0;
|
|
|
tr[u].sum = tr[u].len;
|
|
|
}
|
|
|
// 记录当前区间的懒标记,以后可以向下进行传递
|
|
|
tr[u].tag = tag;
|
|
|
}
|
|
|
|
|
|
// 下传懒标记,让左右儿子进行统计信息修改和懒标记修改
|
|
|
void pushdown(int u) {
|
|
|
if (~tr[u].tag) {
|
|
|
// 递归处理左右子树
|
|
|
change(ls, tr[u].tag), change(rs, tr[u].tag);
|
|
|
// 下传完毕,懒标记清空
|
|
|
tr[u].tag = -1;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 将区间[L,R]修改为v
|
|
|
void modify(int u, int L, int R, int v) {
|
|
|
int l = tr[u].l, r = tr[u].r;
|
|
|
if (l >= L && r <= R) { // 如果完整命中,那么根据修改的值,对于整体区间的统计信息进行计算,同时,对于整体区间打加修改的懒标记v
|
|
|
change(u, v);
|
|
|
return;
|
|
|
}
|
|
|
if (l > R || r < L) return; // 如果与当前区间无交集,那么返回
|
|
|
|
|
|
// 下传懒标记
|
|
|
pushdown(u);
|
|
|
// 分裂
|
|
|
modify(ls, L, R, v), modify(rs, L, R, v);
|
|
|
// 向上汇总信息
|
|
|
pushup(u);
|
|
|
}
|
|
|
|
|
|
// 查询区间内数字0的个数
|
|
|
int query0(int u, int L, int R) {
|
|
|
int l = tr[u].l, r = tr[u].r;
|
|
|
if (l >= L && r <= R) return tr[u].len - tr[u].sum;
|
|
|
if (l > R || r < L) return 0;
|
|
|
pushdown(u);
|
|
|
return query0(ls, L, R) + query0(rs, L, R);
|
|
|
}
|
|
|
|
|
|
// 查询区间内数字1的个数
|
|
|
int query1(int u, int L, int R) {
|
|
|
int l = tr[u].l, r = tr[u].r;
|
|
|
if (l >= L && r <= R) return tr[u].sum;
|
|
|
if (l > R || r < L) return 0;
|
|
|
pushdown(u);
|
|
|
return query1(ls, L, R) + query1(rs, L, R);
|
|
|
}
|
|
|
|
|
|
// 查询区间内连续数字0的长度
|
|
|
int query2(int u, int L, int R) {
|
|
|
int l = tr[u].l, r = tr[u].r;
|
|
|
if (l >= L && r <= R) return tr[u].ans; // 区间最大的连续脑洞数量
|
|
|
if (l > R || r < L) return 0;
|
|
|
|
|
|
pushdown(u);
|
|
|
/*三种情况:
|
|
|
1、答案在左半边 : query2(ls, L, R)
|
|
|
2、答案在右半边 : query2(rs, L, R)
|
|
|
3、左半边的后面+右半边的前面(不能越界)
|
|
|
*/
|
|
|
return max({query2(ls, L, R), query2(rs, L, R), min(tr[ls].rmax, tr[rs].l - L) + min(tr[rs].lmax, R - tr[ls].r)});
|
|
|
}
|
|
|
|
|
|
// 操作2
|
|
|
void solve() {
|
|
|
cin >> l2 >> r2;
|
|
|
int sum = query1(1, l1, r1); // 数字1的个数
|
|
|
if (sum == 0) return; // 特判,如果没有数字1,就没有继续的必要了:你准备用一个区间里的1去填充别人家的空闲区域,结果,你一个都没有,那还填什么填
|
|
|
|
|
|
// 将区[l1,r1]所有位置修改为0,把所有数字1都取走
|
|
|
modify(1, l1, r1, 0);
|
|
|
/*
|
|
|
注意:懒标记=-1,默认值;懒标记=0,修改为0;懒标记=1,修改为1;
|
|
|
这里需要注意把懒标记在build时初始化为-1,否则,如果懒标记默认值是0,这里还需要将位置修改为0就有二义性了,
|
|
|
你就不知道一个tag=0到底是啥意思,是初始值没有修改过,还是修改过变成了0了? 初始化为-1是一下好习惯,以后要全面执行这个规则。
|
|
|
|
|
|
因为需要进行推平操作,我们知道左侧肯定是l2,那么,右侧到哪里结束呢?
|
|
|
因为[l2,r2]中可能有的位置是0,有的位置是1 ,我们现在有sum个1,假设最后的右端点位置是x, 那么执行推平操作后,
|
|
|
则[l2,x]必然都是1。
|
|
|
问题是如何找到这个右端点x呢?当然可以暴力,一个个找过去,发现是1就放过,发现是0就计数,然后一旦计数到达sum,就是找到了合适的位置x,
|
|
|
还有更聪明的办法,就是二分,因为这个一个个找过去的办法具有单调性,我们如果合理使用查找数字0的函数query0,获取指定区间[l2,x]范围内数
|
|
|
字0的个数,找出x的具体位置,然后再执行一下modify(1,l2,x,1)即可
|
|
|
*/
|
|
|
int l = l2, r = r2;
|
|
|
while (l < r) {
|
|
|
int mid = (l + r) >> 1;
|
|
|
if (query0(1, l2, mid) >= sum) // 在以root=1的树中,[l2,mid]这个区间内,查找数字0的个数,如果左边的0位置数量多于sum,则只用递归左侧即可
|
|
|
r = mid;
|
|
|
else
|
|
|
l = mid + 1;
|
|
|
}
|
|
|
modify(1, l2, l, 1);
|
|
|
}
|
|
|
|
|
|
int main() {
|
|
|
#ifndef ONLINE_JUDGE
|
|
|
freopen("P4344.in", "r", stdin);
|
|
|
#endif
|
|
|
// 加快读入
|
|
|
ios::sync_with_stdio(false), cin.tie(0);
|
|
|
cin >> n >> m;
|
|
|
// 构建线段树
|
|
|
build(1, 1, n);
|
|
|
|
|
|
int op;
|
|
|
while (m--) {
|
|
|
cin >> op >> l1 >> r1;
|
|
|
if (op == 0) modify(1, l1, r1, 0); // SHTSC 挖了一个范围为 [l, r]的脑洞。
|
|
|
if (op == 1) solve(); // 把操作2封装成一个solve()函数,可以更好的理清代码的逻辑
|
|
|
if (op == 2) cout << (query2(1, l1, r1)) << endl; // 连续数字0的最大长度
|
|
|
}
|
|
|
return 0;
|
|
|
} |