Luogu P2572 [SCOI2010]序列操作(线段树)题解
细节狂魔题
题意:维护一个\(01\)序列,支持区间修改(全部变\(0\)或\(1\)),区间取反,区间求和,区间求最长1序列
做法
显然应该用线段树维护。
线段树需要维护:每段区间内\(0\)的数量,\(1\)的数量,最长\(1\)序列长度,最长\(0\)序列长度,从左端点开始的0/1序列长度,从右端点开始的\(0/1\)序列长度。很少吧
注意!在标记下传时会有先后,赋值标记会覆盖翻转标记,而翻转标记会影响赋值标记
为什么我觉得网上的题解都写的好复杂,其实区间修改与取反可以写在一起,求序列与求和也可以很方便地写出来。
代码
#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;
inline int read(){
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9'){if(c == '-') f = -1; c = getchar();}
while(c >= '0' && c <= '9'){x = x * 10 + c - '0'; c = getchar();}
return x * f;
}
const int maxn = 1e5 + 10;
int n,m;
struct Seg_Tree{
#define lc(x) x << 1
#define rc(x) x << 1 | 1
struct node{
int c,c0,sum,lm,lm0,rm,rm0,tag,re;
}t[maxn << 2];
// c->num of continued 1; c0->num of continued 0;
void update(int l, int r, int p){
int mid = (l + r) >> 1;
t[p].sum = t[rc(p)].sum + t[lc(p)].sum;
t[p].c = max( max(t[lc(p)].c, t[rc(p)].c), t[lc(p)].rm + t[rc(p)].lm );
t[p].c0 = max( max(t[lc(p)].c0, t[rc(p)].c0), t[lc(p)].rm0 + t[rc(p)].lm0 );
if(t[lc(p)].lm == mid - l + 1) t[p].lm = mid - l + 1 + t[rc(p)].lm;
else t[p].lm = t[lc(p)].lm;
if(t[rc(p)].rm == r - mid) t[p].rm = r - mid + t[lc(p)].rm;
else t[p].rm = t[rc(p)].rm;
if(t[lc(p)].lm0 == mid - l + 1) t[p].lm0 = mid - l + 1 + t[rc(p)].lm0;
else t[p].lm0 = t[lc(p)].lm0;
if(t[rc(p)].rm0 == r - mid) t[p].rm0 = r - mid + t[lc(p)].rm0;
else t[p].rm0 = t[rc(p)].rm0;
}
void build(int l, int r, int p){
t[p].tag = 0;
if(l == r){
t[p].lm = t[p].rm = t[p].c = t[p].sum = read();
t[p].lm0 = t[p].rm0 = t[p].c0 = t[p].c ^ 1;
return;
}
int mid = (l + r) >> 1;
build(l, mid, lc(p)); build(mid + 1, r, rc(p));
update(l, r, p);
}
void f(int l, int r, int p, int typ){ //1 -> to 0; 2 -> to 1; 3 -> to ~
if(typ == 1){
t[p].sum = t[p].c = t[p].lm = t[p].rm = 0;
t[p].c0 = t[p].lm0 = t[p].rm0 = r - l + 1;
t[p].tag = 1; t[p].re = 0;
}
if(typ == 2){
t[p].sum = t[p].c = t[p].lm = t[p].rm = r - l + 1;
t[p].c0 = t[p].lm0 = t[p].rm0 = 0;
t[p].tag = 2; t[p].re = 0;
}
if(typ == 3){
t[p].sum = r - l + 1 - t[p].sum;
swap(t[p].c0, t[p].c); swap(t[p].lm, t[p].lm0); swap(t[p].rm, t[p].rm0);
if(t[p].tag == 1) t[p].tag = 2;
else if(t[p].tag == 2) t[p].tag = 1;
else t[p].re ^= 1;
}
}
void downdate(int l, int r, int p){
if(t[p].tag){
int mid = (l + r) >> 1;
f(l, mid, lc(p), t[p].tag);
f(mid + 1, r, rc(p), t[p].tag);
t[p].tag = 0; t[p].re = 0;
}
if(t[p].re){
int mid = (l + r) >> 1;
f(l, mid, lc(p), 3);
f(mid + 1, r, rc(p), 3);
t[p].re = 0;
}
}
void change(int L, int R, int l, int r, int p, int typ){
if(L <= l && R >= r){
f(l, r, p, typ);
return;
}
downdate(l, r, p);
int mid = (l + r) >> 1;
if(mid >= L) change(L, R, l, mid, lc(p), typ);
if(mid < R) change(L, R, mid + 1, r, rc(p), typ);
update(l, r, p);
}
int query(int L, int R, int l, int r, int p){ //get the num of continued 1
if(L <= l && R >= r) return t[p].c;
downdate(l, r, p);
int mid = (l + r) >> 1; int ans = 0;
if(mid >= L) ans = query(L, R, l, mid, lc(p));
if(mid < R) ans = max(ans, query(L, R, mid + 1, r, rc(p)));
if(mid >= L && mid < R) ans = max(ans, min(t[lc(p)].rm, mid - L + 1) + min(t[rc(p)].lm, R - mid));
return ans;
}
int get_sum(int L, int R, int l, int r, int p){
if(L <= l && R >= r) return t[p].sum;
downdate(l, r, p);
int mid = (l + r) >> 1; int ans = 0;
if(mid >= L) ans += get_sum(L, R, l, mid, lc(p));
if(mid < R) ans += get_sum(L, R, mid + 1, r, rc(p));
return ans;
}
}tree;
int main(){
n = read(), m = read();
tree.build(1, n, 1);
for(int i = 1; i <= m; ++ i){
int opt = read(), x = read(), y = read();
x += 1, y += 1;
if(opt == 0) tree.change(x, y, 1, n, 1, 1);
if(opt == 1) tree.change(x, y, 1, n, 1, 2);
if(opt == 2) tree.change(x, y, 1, n, 1, 3);
if(opt == 3) printf("%d\n", tree.get_sum(x, y, 1, n, 1));
if(opt == 4) printf("%d\n", tree.query(x, y, 1, n, 1));
}
return 0;
}
真是一点也不难写呢
总结
这类涉及带修区间求子序列的题基本上都是这种思路,考虑三种情况:答案序列在左边,答案序列在右边,答案序列横跨\(mid\),然后就可以很方(nan)便(tiao)地用线段树维护了。
难想的主要是:\(update\)函数,\(downdate\)函数