You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

188 lines
8.3 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#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;
}