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