Files
python/TangDou/AcWing/SegmentTree/P4344_XDS.cpp
HuangHai 1f397eca87 'commit'
2025-08-30 18:35:01 +08:00

188 lines
8.3 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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