Files
python/TangDou/AcWing/SegmentTree/P4344_XDS.cpp

188 lines
8.3 KiB
C++
Raw Normal View History

2025-08-30 18:35:01 +08:00
#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;
}