BIT与线段树专项练习

线段树专项练习


这个草稿存在于我的CSDN良久,最近一场div2的E题出现了一次线段树相关,所以开始填坑。
以下关于加法、乘法和根号线段树来自之前久远的某一天。


HH的项链
第二次做,20分钟切掉。但是因为有个快读…所以上网查了下但是发现了个假的快读模板最后三个test直接re(我也很疑惑啊…上了CF找了一些rank靠前的快读模版就ac了。

附上快读模版:

template <typename _T>
inline void read(_T &f) {
    f = 0; _T fu = 1; char c = getchar();
    while(c < '0' || c > '9') { if(c == '-') fu = -1; c = getchar(); }
    while(c >= '0' && c <= '9') { f = (f << 3) + (f << 1) + (c & 15); c = getchar(); }
    f *= fu;
}

AC代码


P1198 [JSOI2008]最大数
用Bit维护后缀区间上的最大值。
设置函数update用来将当前插入的值传入到儿子结点,设置函数query用来查询当前结点以及其所有的父结点拥有的最大的权值
这里的Bit与常见的前缀查询略有不同但是只要理解了本质也是个大水题。
AC代码


P2023 [AHOI2009]维护序列
裸的加法乘法线段树。
具体考虑可以参见之前暑训博客的线段树模版2
AC代码


补充一下根号线段树的处理办法:
SPOJ GSS4 (区间开根号 + 区间查询) (线段树)
线段树 从入门到进阶
具体的区间算法类似于普通的线段树。这里需要处理的问题是一个常数的优化,我们维护区间和与区间最大值两个量,显然,对于最大值为1的区间开根号已经无须修改了,而对于long long范围下的1e18开6次根号左右之后就可以得到1,从而整体的复杂度O(6NlogN)还算可以接受。
在Dijstra-Liu的博客中维护了区间的最大值与区间的最小值,同样也很显然的是,如果最大值开根号后与最小值开根号后的值相同,那么这一段区间可以直接用线段树进行修改而不需要单独考虑开根号后的取整问题。
两种优化办法其实本质类似,之后找到模版题的话可以实际操作一下。


UPDATED1: Vjudge NWU线段树练习

A 单点修改

B 区间置值 & 区间查询: 关键的变形就在于 s t st st t a g tag tag标记不是累加,而是直接覆盖为新值。

void down(int p, int l, int r){
    if(!tag[p]) return;
    st[ls] = tag[p] * (mid - l + 1); tag[ls] = tag[p];
    st[rs] = tag[p] * (r - mid); tag[rs] = tag[p];
    tag[p] = 0;
}

C
以线段树的存储结构进行的位运算。 对线段树分层,构造从节点 p p p到其对应层数的映射,r后根据层数和输入 n n n的奇偶性进行不同的位运算。

D 线段树区间合并

下午做的时候考虑了两种做法,一种是维护两个极值ST,分别记录每个点所对应的连续区间的左值和右值,不过这种做法在D的情况可以比较好地进行维护,在R的时候就比较麻烦了;另一种做法是二分求区间然后线段树区间置值,写起来也奇丑无比…

正确的办法是使用区间合并的办法。
开设结构体存储每个节点的左右端点序号、以左端点为起点的最长连续区间的长度、以右端点为终点的最长连续区间的长度以及该区间中最长连续区间的长度。

struct Tree{
    int l, r, ll, rr, all;
}tree[maxn << 2];

结合本题题意,需要做到单点修改、单点查询的操作。为此,我们给出如下变形的线段树操作办法:

//初始化区间线段树
void build(int p, int l, int r){
    tree[p].l = l; tree[p].r = r;
    tree[p].ll = tree[p].rr = tree[p].all = r - l + 1;
    if(r == l) return;
    
    build(ls, l, mid);
    build(rs, mid + 1, r);
}

void up(int p){
	//更新连续左区间
    if(tree[ls].ll == tree[ls].r - tree[ls].l + 1) tree[p].ll = tree[ls].ll + tree[rs].ll;
    else tree[p].ll = tree[ls].ll;
    
    //更新连续右区间
    if(tree[rs].rr == tree[rs].r - tree[rs].l + 1) tree[p].rr = tree[ls].rr + tree[rs].rr;
    else tree[p].rr = tree[rs].rr;
    
    //更新全区间
    tree[p].all = max(tree[ls].all, max(tree[rs].all, tree[ls].rr + tree[rs].ll));
}


//将x所在的元素置为k(k = 0/1);当前节点为p
void update(int p, int x, int k){
    if(tree[p].l == tree[p].r && tree[p].l == x){
        tree[p].all = tree[p].ll = tree[p].rr = k;
        return;
    }
    if(x <= (tree[p].l + tree[p].r) >> 1) update(ls, x, k);
    else update(rs, x, k);
    up(p);
}

ll que(int p, int x){
    if(tree[p].l == tree[p].r) return tree[p].all;
    
    //x处于中心位置的区间则更新之
    if(x >= tree[ls].r - tree[ls].rr + 1 && x <= tree[rs].l + tree[rs].ll - 1)
        return tree[ls].rr + tree[rs].ll;
    //x处于左边其他某个位置
    else if(x < tree[ls].r - tree[ls].rr + 1) return que(ls, x);
    //x处于右边其他某个位置
    else return que(rs, x);
}

此外这道hdu 1540需要多组数据输入,并且题目没说这件事…

总而言之,区间合并的线段树算法是利用了线段树的存储结构,将存储空间上相邻的儿子节点上的区间信息高效地利用起来,快速完成某些信息的区间操作与查询。

摘自CSDN博主「sunyutian1998」的原创文章:

下面是做过的一些类型题总结

1 求一块满足条件的最左边的空白空间 poj3667

2 求某个元素所在连续段的长度(也可求左右端点) hdu1540

3 求某个连续段的起始位置 hdu2871

4 区间合并在扫描线求周长中的应用 hdu1828

5 区间合并与异或操作结合 以及求整个区间内最长连续段的长度 hdu3911 hdu3397

6 求区间最长连续上升序列 hdu3308

7 求最大子段和 uva1400



参考博客:
线段树区间合并小结 (言简意赅👍)
hdu1540(线段树维护连续区间模型)

E
支持单点修改和区间查询的最值线段树。

F (HDU4302)
因为之前学习到了D题的 线段树区间合并 算法,这道题又如此的相像,我就开始了漫长(真的很自闭 各种错误一共WA6)的线段树区间合并实现。

我们设置那些没有食物的地方为1,有食物的地方为0,那么这道题就要找向左或向右的连续1区间中较短的那个。与D不同,我们不需要维护all区间(事实上all区间完全由left和right的值决定,只是为了方便起见才另存一遍),只需要记录left和right表示左端点区间和右端点区间。
然后因为这道题的查询点会不断移动,所以我们大体设计算法为:up()和update()函数用来更新区间线段树,query(int &x)函数用来返回移动到1区间的终点(注意不是移动直接吃到蛋糕)的步数,维护当前方向dir,同时更新当前位置x,在main函数中前进dir(可能为正负1)步吃掉蛋糕,更新ans.

有一个地方要注意(感谢样例3),一个位置可能出现多个蛋糕,这时如果用区间线段树维护会出现困难,所以我们另开设一个桶数组记录当前位置的蛋糕数量。在吃蛋糕前先检查自己脚下有没有蛋糕,如果没有的话才进行区间查询。

在区间查询中也有一个地方要注意。首先我们保证区间查询时地图上一定有一个蛋糕,然后我们系统地思考区间查询的所有分支:

  • 当前位置x处于中央区域,即x <= m + tree[rs].left && x >= m - tree[ls].right + 1:此时我们不能保证左右端点就是蛋糕,有可能是地图终点!如果忽视这个问题x就有可能会往地图外移动,最后的报错就是ACCESS_VIOLATE! 所以在这种情况下还要单独考虑左右某一段为全连续区间,此时不得不转向另一端区间。而又因为我们保证地图上一定有一个蛋糕,所以不可能左右两端都是全连续的1区间。当两端都是蛋糕时,只需要比较一下移动的步数再结合当前的朝向就可以得出移动策略。
  • 当x位于两端区域时,类似于D题的区间查询,递归地交给左右区间线段素后处理。
  • (终止条件)当x位于长度为1的单元素区间上,此时显然不能移动,返回0,dir不变即可。

区间合并算法


#define ls (p << 1)
#define rs (p << 1 | 1)
#define mid ((l + r) >> 1)
#define INF 0x3f3f3f3f

const int maxn = 1e5 + 10;

struct Tree{
    int l, r, left, right;
}tree[maxn << 2];
int n, l;
int cnt[maxn];

void build(int p, int l, int r){
    tree[p].l = l; tree[p].r = r;
    tree[p].left = tree[p].right = r - l + 1;
    if(l == r) return;
    build(ls, l, mid);
    build(rs, mid + 1, r);
}

void up(int p){
    tree[p].left = tree[ls].left;
    if(tree[ls].left == tree[ls].r - tree[ls].l + 1) tree[p].left += tree[rs].left;
    
    tree[p].right = tree[rs].right;
    if(tree[rs].right == tree[rs].r - tree[rs].l + 1) tree[p].right += tree[ls].right;
}

void update(int p, int x, int k){
    if(tree[p].l == tree[p].r && tree[p].l == x){
        tree[p].left = tree[p].right = k;
        return;
    }
    int m = (tree[p].l + tree[p].r) >> 1;
    if(x <= m) update(ls, x, k);
    else update(rs, x, k);
    up(p);
}

int dir;
//用query函数走到尽头,在main函数中吃糖果
int query(int p, int &x){
    if(tree[p].l == tree[p].r) return 0;
    
    int m = (tree[p].l + tree[p].r) >> 1, left, right;
    //x在中央区域的情况。
    if(x <= m + tree[rs].left && x >= m - tree[ls].right + 1){
        //特判一下左右两端某一段可能出现全部连续的情况  (保证不可能两端均全部连续!)
        if(m - tree[ls].right + 1 == tree[p].l){
            dir = 1; right = m + tree[rs].left - x;
            x = tree[rs].left + m;
            return right;
        }
        if(m + tree[rs].left == tree[p].r){
            dir = -1; left = x - (m - tree[ls].right + 1);
            x = m - tree[ls].right + 1;
            return left;
        }
        
        left = x - (m - tree[ls].right + 1); right = m + tree[rs].left - x;
        if(left == right){
            if(dir == 1) x += right;
            else x -= left;
            return left;
        }
        else if(left < right){
            dir = -1; x -= left;
            return left;
        }
        else{
            dir = 1; x += right;
            return right;
        }
    }
    else if(x < m - tree[ls].right + 1) return query(ls, x);
    else return query(rs, x);
}


int main(){
    Fast;
    int t; scanf("%d", &t);
    for(int _ = 1; _ <= t; _++){
        memset(cnt, 0, sizeof cnt); memset(tree, 0, sizeof tree);
        scanf("%d %d", &l, &n); l++;
        build(1, 1, l);
        ll ans = 0;
        int now = 1; dir = 1; int total = 0;
        for(int i = 0; i < n; i++){
            int op; scanf("%d", &op);
            if(op){
                if(total == 0) continue;
                total--;
                if(cnt[now] > 0) cnt[now]--;
                else{
                    ans += query(1, now) + 1;
                    now += dir;
                    cnt[now]--;
                }
                if(!cnt[now]) update(1, now, 1);
            }
            else{
                total++;
                int x; scanf("%d", &x); x++;
                update(1, x, 0);
                cnt[x]++;
            }
        }
        printf("Case %d: %lld\n", _, ans);
    }
    return 0;
}

除了这种看起来有点啰嗦的办法,我找了其他一些博客学习该题。

参考: HDU - 4302 :Holedox Eating ,线段树、树状数组+二分,优先队列

BIT/ST的二分
其实这个跟D题的二分做法思想相同,二分在 O ( l o g ) O(log) O(log)的时间完成跟ST一样的工作。

优先队列
这才应该是这道题干净漂亮的正解!

因为对于每个点而言,只会往最近的一个点移动,所以用两个优先队列维护小于(等于)当前位置的最大值和大于当前位置的最小值即可。
按这个算法来看,大概放在CF div2的C题都差不多,哪里需要我这么大费周章地WA了N发的区间线段树!还是呗 就当作练习区间线段树吧!

G
这道题的关键在于这是一串单调不减的序列,所以查询元素个数的时候并不需要像常规的桶查找那样,可以将一段相同的元素看成一串连续区间,从而就可以通过区间合并算法解决该问题。

因为这个1e6的数据范围…而我之前学的区间合并都是开一个五元的结构体线段树…所以我交了几发空代码测试如何在 65536 kB 内不会MLE…结论是结构体不存左右边界的位置,只存三个区间长度即可。

区间合并和查询都大同小异,这里主要不同之处在于要考虑合并点处的中央区域的长度,但并不是很困难。

关键代码

const int maxn = 1e6 + 10;

struct{
    int left, right, all;
}tree[maxn << 2];

int n, m;
int a[maxn];

void up(int p, int l, int r){
    tree[p].left = tree[ls].left;
    if(tree[ls].left == mid - l + 1 && a[mid] == a[mid + 1]) tree[p].left += tree[rs].left;
    
    tree[p].right = tree[rs].right;
    if(tree[rs].right == r - mid && a[mid] == a[mid + 1]) tree[p].right += tree[ls].right;
    
    tree[p].all = max(tree[ls].all, tree[rs].all);
    if(a[mid] == a[mid + 1]) tree[p].all = max(tree[p].all, tree[ls].right + tree[rs].left);
}

void build(int p, int l, int r){
    if(l == r){
        tree[p].left = tree[p].right = tree[p].all = 1;
        return;
    }
    build(ls, l, mid);
    build(rs, mid + 1, r);
    up(p, l, r);
}

int query(int p, int l, int r, int L, int R){
    if(L <= l && r <= R) return tree[p].all;
    
    int ans = 0;
    if(L <= mid) ans = max(ans, query(ls, l, mid, L, R));
    if(R > mid) ans = max(ans, query(rs, mid + 1, r, L, R));
    if(L <= mid && R > mid && a[mid] == a[mid + 1]){
        int left = max(L, mid - tree[ls].right + 1);
        int right = min(R, mid + tree[rs].left);
        ans = max(ans, right - left + 1);
    }

    return ans;
}

H
挺漂亮的一道题目…题意干净漂亮,题解也让人耳目一新,确实让人学到不少东西。

最开始我的想法是枚举中间项,在维护左右区间所有的两项和,然后查询云云…不过并不能维护成功…

正确做法一个关键在于,这是一个1到n的排列,每个数只出现一次,所以可以做一个等价变化:如果枚举中间项 a [ i ] a[i] a[i],那么如果不能在左右两边找某两个数使其之和恰为 2 ∗ a [ i ] 2*a[i] 2a[i],就等价于仅在其左区间取任意一个数均能再在左区间找到另一个数使得其和为 2 ∗ a [ i ] 2*a[i] 2a[i],这是由每个出现的数字的唯一性决定的。这样转化的好处是显而易见的。

之后我们维护一个权值线段树,当我们检验 a [ i ] a[i] a[i]是否可能成为等差中项时,只需要在权值线段树中检查是否对于每一对 a [ i ] ± d ( d a[i] ± d (d a[i]±d(d取遍所有可能的公差)的值都相同。如果不相同那么这一对数一定分布在 a [ i ] a[i] a[i]的左右两边,因而可以跳出检验输出Y

而如何利用权值线段树快速检验每一对可能的首尾项?如果要快速检验两串字符区间是否相同,最好的办法就是O(1)的利用hash进行检验;而如果要快速地动态检验字符区间,就可以利用线段树的区间修改、合并操作。又由于这两个串具有良好的对称性,所以为了比较的方便,我们对 a [ i ] − d a[i]-d a[i]d的部分采用正序求hash值,对 a [ i ] + d a[i] +d a[i]+d的部分采用倒序求hash值,那么如果两个串构成回文关系(即 a [ i ] a[i] a[i]不能作为等差中项!),其左右区间的hash值应该完全相同!所以,我们维护两个权值线段树,一个正序存哈希值,一个倒序存哈希值,遍历串的所有位置检验两个树查询的哈希值是否相同即可。

15WA -> AC

#define ls (p << 1)
#define rs (p << 1 | 1)
#define mid ((l + r) >> 1)

const int maxn = 1e4 + 10;
const int M = 1e9 + 7;
const ll p = 3;

ll pow2[maxn];
int a[maxn];
ll st1[maxn << 2], st2[maxn << 2];

void ini(){
    pow2[0] = 1; pow2[1] = p;
    for(int i = 2; i < maxn; i++) pow2[i] = pow2[i - 1] * p % M;
}

//维护权值线段树st1和st2。
void up(int p, int l, int r){
    st1[p] = (st1[ls] * pow2[r - mid]) % M + st1[rs]; st1[p] %= M;
    st2[p] = (st2[rs] * pow2[mid - l + 1]) % M + st2[ls]; st2[p] %= M;
}

void update(int p, int l, int r, int x){
    if(l == r){
        st1[p] = st2[p] = 1;
        return;
    }
    if(x <= mid) update(ls, l, mid, x);
    else update(rs, mid + 1, r, x);
    up(p, l, r);
}

ll query1(int p, int l, int r, int L, int R){
    if(L > R) return 0;
    if(L == l && r == R) return st1[p];
    
    if(L >= mid + 1) return query1(rs, mid + 1, r, L, R);
    else if(R <= mid) return query1(ls, l, mid, L, R);
    else return
        (query1(ls, l, mid, L, mid) * pow2[R - mid] % M + query1(rs, mid + 1, r, mid + 1, R)) % M;
    
}

ll query2(int p, int l, int r, int L, int R){
    if(L > R) return 0;
    if(L == l && r == R) return st2[p];
    
    if(L >= mid + 1) return query2(rs, mid + 1, r, L, R);
    else if(R <= mid) return query2(ls, l, mid, L, R);
    else return
        (query2(ls, l, mid, L, mid) + query2(rs, mid + 1, r, mid + 1, R) * pow2[mid - L + 1] % M) % M;
}



int main(){
//    Fast;
    ini();
    int _; scanf("%d", &_);
    while(_--){
        memset(st1, 0, sizeof st1); memset(st2, 0, sizeof st2);
        int n; scanf("%d", &n); for(int i = 1; i <= n; i++) scanf("%d", a + i);
        if(n <= 2){puts("N"); continue;}
        update(1, 1, n, a[1]);
        int flag = 0;
        for(int i = 2; i <= n - 1; i++){
            int d = min(a[i] - 1, n - a[i]);
            if(d >= 1 && query1(1, 1, n, a[i] - d, a[i] - 1) != query2(1, 1, n, a[i] + 1, a[i] + d)){
                flag = 1; break;
            }
            update(1, 1, n, a[i]);
        }
        puts(flag ? "Y" : "N");
    }
    return 0;
}

踩过的一些坑

  • 眼睛不要瞎,query1和query2里面不要出现不对应的函数方法。
  • 之前写过的所有线段树在query查询[L, R]时往儿子节点递归传入的都是不变的左右区间,终止条件设置为L <= l && r <= R;而在上述代码的query查询时,因为终止条件是严格的l == L && r == R,所以在递归传入时也要相应地将查询区间进行划分。按照我粗糙的理解这可能是因为前一种是对线段树上的区间进行不断划分,拼接到查询区间上进行贡献;后一种是将查询区间进行划分,一个个落到线段树上的那些左右位置完全相同的节点位置再进行贡献。两种办法殊途同归,但是具体应用的时候一定要一一对应。

贴一下前一种方法的查询函数: (摘自第二篇参考博客)

UL query1(int p, int l, int r, int ql, int qr) {
    if(ql > qr) return 0;
    if(ql <= l && r <= qr) return h1[p];
    int mid = l + r >> 1, lc = o << 1, rc = lc | 1;
    UL ans = 0;
    if(ql <= mid) ans = query1(lc, l, mid, ql, qr);
    if(qr > mid) ans = ans * idx[min(qr,r)-mid] + query1(rc, mid + 1, r, ql, qr);
    return ans;
}

这种方法在该题的hash值维护时有一个要注意的的地方,因为hash值是一个长度位置敏感的值,而这种方法不分割查询区间而只拼接那些线段树上的区间,所以在递归查询时当前区间(不妨仅先考虑右值)的右端点r可能超过查询右端点R,也可能不足右端点那么长而充当一个拼图的角色,这两种情况都要取较小的值作为哈希函数的计算值,即取最小min(qr, r)! 对应的在query2中取max(ql, l)


参考博客:
题解 P2757 【[国家集训队]等差子序列】
[BZOJ2124]等差子序列

此外,还有一个类似的没有被削弱的题目: bzoj3509
删去了1-n的排列的约束和,并且不仅仅问存在性而且要统计出现的所有对数。
好像是FFT和分块 等线段树这边全部刷完就补一下做做看


I (出自: CF 914D)
简单的线段树变形题。
因为gcd的运算满足结合律,所以在线段树上一段一段地找,找到那些不满足gcd % d == 0的位置并递归到叶子结点后计数即可。如果操作数大于1的话那么显然NO;如果操作数为1的话可以将操作的那个数就置为d,这样其区间gcd就不会是d的倍数;而当操作数为0时也无需考虑区间gcd为d的倍数的情况,这是因为可以用去一次修改能力将任意一个数置为d也就满足了条件。

TLE警告 因为查询时必须要搜索到叶子结点,所以还得进行适当的剪枝:当操作数已经大于1时,应该直接返回而无需再进行递归搜索,这样就可以顺利通过了。

#define ls (p << 1)
#define rs (p << 1 | 1)
#define mid ((l + r) >> 1)

const int maxn = 5e5 + 10;

int a[maxn];
ll st[maxn << 2];

void up(int p){
    st[p] = gcd(st[ls], st[rs]);
}

void build(int p, int l, int r){
    if(l == r){
        st[p] = a[l];
        return;
    }
    build(ls, l, mid);
    build(rs, mid + 1, r);
    up(p);
}

void update(int p, int l, int r, int x, int k){
    if(l == r){
        st[p] = k;
        return ;
    }
    if(x <= mid) update(ls, l, mid, x, k);
    else update(rs, mid + 1, r, x, k);
    up(p);
}

void query(int p, int l, int r, int L, int R, int d, int &op){
    if(l == r && st[p] % d){
        op++;
        return;
    }
    if(L <= l && r <= R){
        if(st[p] % d){
            if(st[ls] % d) query(ls, l, mid, L, R, d, op); if(op > 1) return;
            if(st[rs] % d) query(rs, mid + 1, r, L, R, d, op);
        }
        return;
    }
    if(L <= mid) query(ls, l, mid, L, R, d, op); if(op > 1) return;
    if(R > mid) query(rs, mid + 1, r, L, R, d, op);
}


int main(){
//    Fast;
    int n; scanf("%d", &n);
    for(int i = 1; i <= n; i++) scanf("%d", a + i);
    build(1, 1, n);
    int m; scanf("%d", &m);
    int op, x, y, z;
    for(int i = 0; i < m; i++){
        int cnt = 0;
        scanf("%d", &op);
        if(op == 1){
            scanf("%d%d%d", &x, &y, &z);
            query(1, 1, n, x, y, z, cnt);
            puts(cnt <= 1? "YES": "NO");
        }
        else{
            scanf("%d%d", &x, &y);
            update(1, 1, n, x, y);
        }
    }
    return 0;
}

J (出自CF 877E)
在树上对子树的翻转与查询问题。

容易注意到对某个子树进行翻转维护其对应的值是很容易实现的,其数值只会在两个数之间轮流变换;但是如何快速维护子树上的翻转信息就比较超出我的知识储备了…意识到这个问题后我上网去看了题解

其实这个知识点之前有学过,在学tarjan的时候学到过:dfs序
在dfn序上子树结点为连续的一段区间,因而我们只需要记录所有节点的子树在dfn序中的区间左右端点位置即可用线段树进行维护dfn序上的翻转操作。

#define ls (p << 1)
#define rs (p << 1 | 1)
#define mid ((l + r) >> 1)

const int maxn = 2e5 + 10;

struct star{
    int to, next;
}edge[maxn];
int head[maxn], top = 0;
int st[maxn << 2], tag[maxn << 2];
int sts[maxn], left[maxn], right[maxn], dfn[maxn], times = 1;

void add(int u, int v){
    edge[top].to = v;
    edge[top].next = head[u];
    head[u] = top++;
}

int vis[maxn];
void dfs(int p){
    vis[p] = 1; left[p] = times; dfn[times] = p; times++;
    for(int i = head[p]; ~i; i = edge[i].next)
        if(!vis[edge[i].to]) dfs(edge[i].to);
    right[p] = times - 1;
}

void up(int p){
    st[p] = st[rs] + st[ls];
}

void build(int p, int l, int r){
    if(l == r){
        st[p] = sts[dfn[l]];
        return;
    }
    build(ls, l, mid); build(rs, mid + 1, r);
    up(p);
}

void down(int p, int l, int r){
    if(tag[p]){
        st[ls] = mid - l + 1 - st[ls];
        st[rs] = r - mid - st[rs];
        tag[ls] ^= 1; tag[rs] ^= 1;
        tag[p] = 0;
    }
}

void update(int p, int l, itn r, int x){
    if(left[x] <= l && r <= right[x]){
        st[p] = r - l + 1 - st[p];
        tag[p] ^= 1;
        return;
    }
    down(p, l, r);
    if(left[x] <= mid) update(ls, l, mid, x);
    if(right[x] > mid) update(rs, mid + 1, r, x);
    up(p);
}

ll query(int p, int l, int r, int x){
    if(left[x] <= l && r <= right[x]) return st[p];
    down(p, l, r);
    ll ans = 0;
    if(left[x] <= mid) ans += query(ls, l, mid, x);
    if(right[x] > mid) ans += query(rs, mid + 1, r, x);
    return ans;
}

int main(){
//    Fast;
    memset(head, -1, sizeof head);
    int n; scanf("%d", &n);
    for(int i = 2, pi; i <= n; i++){
        scanf("%d", &pi);
        add(pi, i);
    }
    for(int i = 1; i <= n; i++) scanf("%d", sts + i);
    dfs(1); build(1, 1, n);
    
    int m; scanf("%d", &m);
    for(int i = 0; i < m; i++){
        char s[10]; scanf("%s", s); int x; scanf("%d", &x);
        if(s[0] == 'g')
            printf("%lld\n", query(1, 1, n, x));
        else update(1, 1, n, x);
    }
    return 0;
}

总而言之,这道题利用dfn序 & SegTree 来快速维护子树信息是值得学习的一种巧妙手法!

事实上,还有更多的一些动态数结构要学习,留坑待补:
动态树拓展相关 Top Tree & ETT
树链剖分和LCT相关知识 (这个应该是暑训落下的内容…)

K
理解题意后发现,之前比赛的区间覆盖优先级高于后面比赛的区间覆盖优先级,所以存入所有比赛情况,倒序线段树置数就可以了。

#define ls (p << 1)
#define rs (p << 1 | 1)
#define mid ((l + r) >> 1)

const int maxn = 3e5 + 10;

itn st[maxn << 2], tag[maxn << 2];

void down(int p, int l, int r){
    if(tag[p]){
        st[ls] = st[rs] = tag[ls] = tag[rs] = tag[p];
        tag[p] = 0;
    }
}

void update(int p, int l, itn r, int L, int R, int x){
    if(L <= l && r <= R){
        st[p] = x;
        tag[p] = x;
        return;
    }
    down(p, l, r);
    if(L <= mid) update(ls, l, mid, L, R, x);
    if(R > mid) update(rs, mid + 1, r, L, R, x);
}

int query(int p, int l, int r, int x){
    if(l == r && l == x) return st[p];
    down(p, l, r);
    if(x <= mid) return query(ls, l, mid, x);
    else return query(rs, mid + 1, r, x);
}

struct{
    int l, r, x;
}node[maxn];

int main(){
//    Fast;
    itn n, m; scanf("%d %d", &n, &m);
    for(int i = 0; i < m; i++) scanf("%d %d %d", &node[i].l, &node[i].r, &node[i].x);
    int pre = 0;
    for(int i = m - 1; i >= 0; i--){
        pre = query(1, 1, n, node[i].x);
        update(1, 1, n, node[i].l, node[i].r, node[i].x);
        update(1, 1, n, node[i].x, node[i].x, pre);
    }
    for(int i = 1; i <= n; i++) printf("%d ", query(1, 1, n, i));
    printf("\n");
    return 0;
}

L (来自CF438D)
简单的线段树区间求和、区间取模和单点修改问题。

不过在CF上TLE在了39上…本以为卡scanf需要快读但是快读也死在39上…事实证明假算法永远是假算法,不存在优化读入或者其他乱七八糟的卡过去的…

T飞的原因在于我的剪枝不够好,我的剪枝办法是区间和大于等于模数就进行递归搜索,但是这只是其中某个一个元素需要取模的必要条件并不是一个充分条件,所以我们实际上进行了很多次冗余的搜索。为了解决这个问题,我们另外维护一个权值线段树,如果某段区间的最大值大于等于模数,那么这段区间就需要进行取模处理了。这样子就可以剪去许多不必要的情况,跑得飞快地通过这道题。

#define ls (p << 1)
#define rs (p << 1 | 1)
#define mid ((l + r) >> 1)

const int maxn = 1e5 + 10;
int a[maxn];

ll st[maxn << 2], ms[maxn << 2];

void up(int p){
    st[p] = st[ls] + st[rs];
    ms[p] = std::max(ms[ls], ms[rs]);
}

void build(int p, int l, int r){
    if(l == r){
        st[p] = a[l];
        ms[p] = a[l];
        return;
    }
    build(ls, l, mid);
    build(rs, mid + 1, r);
    up(p);
}

void update2(int p, int l, int r, int L, int R, int m){
    if(l == r){st[p] %= m; ms[p] %= m; return;}
    if(L <= l && r <= R){
        if(ms[p] >= m){
            if(ms[ls] >= m) update2(ls, l, mid, L, R, m);
            if(ms[rs] >= m) update2(rs, mid + 1, r, L, R, m);
            up(p);
        }
        return;
    }
    if(L <= mid && ms[ls] >= m) update2(ls, l, mid, L, R, m);
    if(R > mid && ms[rs] >= m) update2(rs, mid + 1, r, L, R, m);
    up(p);
}

void update3(int p, int l, int r, int x, int k){
    if(l == r && l == x){
        st[p] = k; ms[p] = k;
        return;
    }
    if(x <= mid) update3(ls, l, mid, x, k);
    else update3(rs, mid + 1, r, x, k);
    up(p);
}

ll query(int p, int l, int r, int L, int R){
    if(L <= l && r <= R) return st[p];
    ll ans = 0;
    if(L <= mid) ans += query(ls, l, mid, L, R);
    if(R > mid) ans += query(rs, mid + 1, r, L, R);
    return ans;
}

int main(){
//    Fast;
    int n, m; scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++) scanf("%d", a + i);
    build(1, 1, n);
    int x, y, M;
    for(int i = 0; i < m; i++){
        int op; scanf("%d", &op);
        if(op == 1){
            scanf("%d %d", &x, &y);
            printf("%lld\n", query(1, 1, n, x, y));
        }
        else if(op == 2){
            scanf("%d%d%d", &x, &y, &M);
            update2(1, 1, n, x, y, M);
        }
        else{
            scanf("%d %d", &x, &y);
            update3(1, 1, n, x, y);
        }
    }
    return 0;
}

M
与之前等差数列的搜索题类似,只需要用线段树 & hash就可以快速地动态维护字符串、比较两个字符串是否相同。

1Try

#define ls (p << 1)
#define rs (p << 1 | 1)
#define mid ((l + r) >> 1)

const int maxn = 1e5 + 10;
const int p = 3;
const int M = 1e9 + 7;

char s[maxn];
int a[maxn];
ll pow2[maxn];
ll st1[maxn << 2], st2[maxn << 2];

void up(int p, int l, int r){
    st1[p] = st1[ls] * pow2[r - mid] + st1[rs]; st1[p] %= M;
    st2[p] = st2[rs] * pow2[mid - l + 1] + st2[ls]; st2[p] %= M;
}

void build(int p, int l, int r){
    if(l == r){
        st2[p] = st1[p] = a[l];
        return;
    }
    build(ls, l, mid); build(rs, mid + 1, r);
    up(p, l, r);
}

void update(int p, int l, int r, int x, itn k){
    if(l == r){
        st1[p] = st2[p] = k;
        return;
    }
    if(x <= mid) update(ls, l, mid, x, k);
    else update(rs, mid + 1, r, x, k);
    up(p, l, r);
}

ll query1(int p, int l, int r, int L, itn R){
    if(l == L && r == R) return st1[p];
    
    if(R <= mid) return query1(ls, l, mid, L, R);
    else if(L >= mid + 1) return query1(rs, mid + 1, r, L, R);
    else return (query1(ls, l, mid, L, mid) * pow2[R - mid] % M + query1(rs, mid + 1, r, mid + 1, R)) % M;
}

ll query2(int p, int l, int r, int L, int R){
    if(L > R) return 0;
    if(l == L && r == R) return st2[p];
    
    if(R <= mid) return query2(ls, l, mid, L, R);
    else if(L >= mid + 1) return query2(rs, mid + 1, r, L, R);
    else return (query2(ls, l, mid, L, mid) + (pow2[mid - L + 1] * query2(rs, mid + 1, r, mid + 1, R)) % M) % M;
}

int main(){
//    Fast;
    
    pow2[0] = 1;for(int i = 1; i < maxn; i++) pow2[i] = pow2[i - 1] * p % M;
    
    scanf("%s", s + 1); int n = (int)strlen(s + 1), m; scanf("%d", &m);
    for(itn i = 1; i <= n; i++) a[i] = s[i] - 'a';
    build(1, 1, n);
    char op[20]; int x, y; char c;
    for(int i = 0; i < m; i++){
        scanf("%s", op);
        if(op[0] == 'p'){
            scanf("%d %d", &x, &y);
            if(((x + y)) & 1){
                if(query1(1, 1, n, x, (x + y) >> 1) == query2(1, 1, n, ((x + y) >> 1) + 1, y)) puts("Yes");
                else puts("No");
            }
            else{
                if(query1(1, 1, n, x, (x + y) >> 1) == query2(1, 1, n, (x + y) >> 1, y)) puts("Yes");
                else puts("No");
            }
        }
        else{
            scanf("%d %c", &x, &c);
            update(1, 1, n, x, c - 'a');
        }
    }
    return 0;
}

N
自己写的时候本以为线段树已经不算暴力了,结果线段树与其他 O ( l o g n ) O(logn) O(logn)的运算组合仍然可能成为一个非常暴力的算法,所以我们应该想个办法优化这个线段树上求gcd的 O ( l o g 2 n ) O(log^2n) O(log2n)的算法。但我不会

正解: 因子分解 & 状态压缩 & 线段树

题目保证,原序列中的每个数均不会大于100,所以我们可以对每个数分解因子,对因子的每个指数进行状态压缩,查询LCM和GCD时只需要根据指数的情况进行按位与按位或即可。

对于大于10的每个素数,其指数最高只可能为1,因而为此分配1bit存储空间;对于小于10的每个素数(即2、3、5、7),分别可以求出应该分配的存储空间: 2 6 = 64 2^6=64 26=64, 3 4 = 81 3^4=81 34=81, 因而为其分配3bit; 5 2 = 25 5^2=25 52=25, 7 2 = 49 7^2=49 72=49,因而为其分配2bit。而100以内一共有25个素数,所以我们可以把这些素数的只素后连在一起,它们一共仅占用了31位(0到30位),可以用一个int存储表示其指数。当我们考虑2、3、5、7这四个素数的幂次时需要移位后比较其真值,而更大的素数只需要在对应的1bit上进行|&即可。

根据LCM和GCD的定义,(以LCM为例)我们需要对每个素数都取两个数中较高的那个幂次,但是我们并不需要笨办法地(笨人亲测会T)遍历一共25个幂次,只需要单独考虑前四个素数的指数情况并对剩余的所有素数整体取就可以了。

LCM

int LCM(int x, int y){
    return MAX(x&0x70000000, y&0x70000000) | MAX(x&0x0e000000   , y&0x0e000000) | MAX(x&0x01800000, y&0x01800000) | MAX(x&0x00600000, y&0x00600000) | (x&0x001fffff) | (y&0x001fffff);
}

上述的0x70000000为28-30位为1、其余位为0的int值,利用这个数就可以截取xy在第一个素数即2时的指数,剩余三种情况同理。
可以在高位直接比较而无需再右移是因为要比较前四个素数的指数,不需要具体地比较其指数真实的大小,而可以等价地比较它们同时左移相同位后的数值大小,这样子节约了操作成本降低了时间消耗。

其余的线段树操作就没有什么新奇的了,注意下合并LCM和GCD时候中间变量的初始化即可。


#define ls (p << 1)
#define rs (p << 1 | 1)
#define mid ((l + r) >> 1)

const int maxn = 1e5 + 10;
int M;

int prime[25], pos[25];
int top = 0;
int pow2[] = {1, 2, 4, 8, 16, 32, 64};
int pow3[] = {1, 3, 9, 27, 81};
int pow5[] = {1, 5, 25};
int pow7[] = {1, 7, 49};

int a[maxn], b[maxn];
int st1[maxn << 2], st2[maxn << 2];

int isprime(int x){
    for(int i = 2; i * i <= x; i++) if(x % i == 0) return 0;
    return 1;
}

void ini(){
    for(int i = 2; i <= 97; i++) if(isprime(i)) prime[top++] = i;
    for(int i = top - 1; i >= 4; i--) pos[i] = top - 1 - i;
    pos[0] = 28; pos[1] = 25; pos[2] = 23; pos[3] = 21;
}

int get(int x){
    int res = 0;
    for(int i = 0; i < top; i++){
        if(x % prime[i] == 0){
            int cnt = 0;
            while(x % prime[i] == 0){
                cnt++;
                x /= prime[i];
            }
            res |= cnt << pos[i];
        }
    }
    return res;
}

int back(int x){
    ll ans = 1, temp;
    temp = x >> pos[0]; ans *= pow2[temp]; x ^= temp << pos[0]; ans %= M;
    temp = x >> pos[1]; ans *= pow3[temp]; x ^= temp << pos[1]; ans %= M;
    temp = x >> pos[2]; ans *= pow5[temp]; x ^= temp << pos[2]; ans %= M;
    temp = x >> pos[3]; ans *= pow7[temp]; x ^= temp << pos[3]; ans %= M;
    
    for(int i = 4; i < top; i++){
        if((x & (1 << pos[i])) == 0) continue;
        ans *= prime[i]; ans %= M;
    }
    return ans % M;
}

#define MAX(x, y) ((x) > (y)? (x): (y))
#define MIN(x, y) ((x) < (y)? (x): (y))

void debug(int x){
    printf("x's bit: ");
    for(itn i = 31; i >= 0; i--){
        printf("%d", x >> i & 1);
        if(i % 4 == 0) printf(" ");
        if(i == 16) printf("| ");
    }
    printf("\n");
}

int LCM(int x, int y){
    return MAX(x&0x70000000, y&0x70000000) | MAX(x&0x0e000000   , y&0x0e000000) | MAX(x&0x01800000, y&0x01800000) | MAX(x&0x00600000, y&0x00600000) | (x&0x001fffff) | (y&0x001fffff);
}

int GCD(int x, int y){
    return MIN(x&0x70000000, y&0x70000000) | MIN(x&0x0e000000, y&0x0e000000) | MIN(x&0x01800000, y&0x01800000) | MIN(x&0x00600000, y&0x00600000) | (x&0x001fffff & y&0x001fffff);
}

void up(int p){
    st1[p] = GCD(st1[ls], st1[rs]);
    st2[p] = LCM(st2[ls], st2[rs]);
}

void build(int p, int l, itn r){
    if(l == r){
        st1[p] = st2[p] = b[l];
        return;
    }
    build(ls, l, mid); build(rs, mid + 1, r);
    up(p);
}

void update(int p, int l, int r, int x, int k){
    if(l == r){
        st1[p] = st2[p] = k;
        return;
    }
    if(x <= mid) update(ls, l, mid, x, k);
    else update(rs, mid + 1, r, x, k);
    up(p);
}


int query1(int p, int l, itn r, int L, int R){
    if(L <= l && r <= R) return st1[p];
    int d1 = 0x7fffffff, d2 = 0x7fffffff;
    if(L <= mid) d1 = query1(ls, l, mid, L, R);
    if(R > mid) d2 = query1(rs, mid + 1, r, L, R);
    return GCD(d1, d2);
}

int query2(int p, int l, itn r, itn L, int R){
    if(L <= l && r <= R) return st2[p];
    int d1 = 0, d2 = 0;
    if(L <= mid) d1 = query2(ls, l, mid, L, R);
    if(R > mid) d2 = query2(rs, mid + 1, r, L, R);
    return LCM(d1, d2);
}

int main(){
//    Fast;
    ini();
    int n, m;
    char op[3]; int x, y;
    while(~scanf("%d%d", &n, &m)){
        for(int i = 1; i <= n; i++){
            scanf("%d", a + i);
            b[i] = get(a[i]);
        }
        build(1, 1, n);
        for(int i = 0; i < m; i++){
            scanf("%s", op);
            if(op[0] == 'C'){
                scanf("%d %d", &x, &y);
                update(1, 1, n, x, get(y));
            }
            else if(op[0] == 'L'){
                scanf("%d%d%d", &x, &y, &M);
                printf("%d\n", back(query2(1, 1, n, x, y)) % M);
            }
            else{
                scanf("%d%d%d", &x, &y, &M);
                printf("%d\n", back(query1(1, 1, n, x, y)) % M);
            }
        }
    }
    return 0;
}

小结
题目还是蛮超出知识储备和算法思维的…
不过确实可以看到,线段树在区间处理方面的优势使得它能与很多可以转化为求解区间信息的问题相结合,所以遇到区间问题、状态压缩问题可以多往线段树这个工具上思考。

参考博客:
HDU_3071 Gcd & Lcm game 【素数分解 + 线段树 + 状压】


O
之前因为某个子树修改题而接触到了一小部分dfn序和括号序(进栈出栈序),所以想到如果用括号序就可以很容易地实现单点修改和路径求和的操作,但是如何还能够对子树进行区间修改我没有想到;而如果用dfn序就不能比较快速的求路径和了…

我最终用(学)的办法还是括号序,通过一些辅助手段成功在括号序中更新子树。因为每个节点会出现两次,我们为其赋上一个符号位,之后再开设一个数组cnt[]记录符号位的前缀和。有了cnt数组的辅助我们就可以很好的完成本来很困难的任务:当更新的区间包含某些进栈又出栈的元素时,只需要根据cnt[r] - cnt[l - 1]的数值就可以知道这段区间中有几个净进栈(当差分为负时即为净出栈)的元素,从而只需要加上(或减去)这些个数个k即可;而当递归到某些单元素节点后,此时前缀和差分就变成了某个位置的符号位,就可以根据符号位正负号对当前位置进行单点修改操作。
总而言之,在线段树维护括号序前缀路径和的时候,如果要区间修改某个子树的所有节点,精髓就在于前缀和数组cnt的使用上。

其实还是对线段树维护的东西、对线段树本身的理解不够透彻,如果能够想到线段树维护的是区间和并且区间中会被修改到的地方只是那些净入栈、出栈的元素的贡献,那么其实应该可以想到cnt数组的构造。不过只是大概有这个概念,具体地写不出来,所以只能较快地理解,却不能场上自己发现并构造出来…

参考博客:
【题解】 Luogu3178 [HAOI2015]树上操作

其他办法

P3178 [HAOI2015]树上操作 题解 里面还是百花齐放的…
不过大体应该是三种做法:括号序,dfn序和树链剖分

括号序在上面已经实现过,dfn序的关键在于一种“神奇”的做法。

具体算法参加luogu题解第一篇。
总体的思路大概如下: 记dis[i]表示第i个节点到根之间的距离, sum[i]表示第i个节点到根之间的总权值。

当修改某个节点时,将子树所有节点的sum值均加上k即可;
当修改某个节点x以及其子树时,对于某个子树结点y而言其sum变化值为k * (dis[y] - dis[x] + 1),上式可变形为k * dis[y] + k * (-dis[x] + 1)。这样的变形是很有意义的,是因为对于某一个确定的节点y,dis[y]是不变的,因而变形后的式子就可以看成是一个kx + y (k为常数)形式的表达式;又因为不同修改时上式具有线性性,所以我们发现可以用线段树维护两个值,最后在询问时只需要根据这个线性关系就可以得到sum值了!为了简洁美,我们尝试统一两种情况,会发现只修改某个节点时,其实就是上述线性表达形式只修改常数项的一种特殊形式!
至此,我们就漂亮地解决了如何维护、查询要求信息的问题。

这种做法是我做的NWU线段树套题中第一次遇到的维护一个二元线性关系的题目;想到这种做法其实不算困难,因为这道题推完公式后(用敏锐的洞察力)就可以发现二元线性关系,以后遇到类似的题目要善于发现其中奥妙…

有一说一,提到二元线性关系的维护,我就想到了qko在BIT中讲到的某个做法…可谓温故而知新…


P

是个水题啊!想个办法利用两个标记维护区间和和区间长度就好了啊!但是我今天居然在这个傻逼题上写了靠近一天??不我是傻逼。。

利用两个tag,一个是置值标记,一个是翻转标记。在pushdown的时候考虑优先置值,如果置值标记为空再考虑翻转标记。为此,在inverse和set函数中也要根据当前节点的情况综合地修改两个tag的情况。当区间置值时,不管当前节点的翻转标记为多少,只需要修改置值标记并且把翻转标记置为0就可以了;而在区间翻转时,如果当前节点置值标记不为空,则应该将置值标记翻转,并令翻转标记为空(这一步可有可无因为在down时只要置值标记不空就会无视翻转标记),而如果当前节置值标记为空,那么翻转标记就应该翻转一次(累加的效果)。

上述的操作即可正确地更新那些完全被某次区间更新包含的线段树区间的tag值,不过我本以为这样就可以了!实际上在down中如果传递父节点的标记来修改左右节点的信息,也需要跟之前讨论的一样才行!不能只进行简单地加、等!这是因为我们之前对于tag的维护(尽管只涉及到了顶层的tag维护)都是严格地遵循着优先置值,再翻转的原则进行修改的,如果不考虑左右儿子的两个标记的具体情况就生搬硬套地与父节点的两个标记结合,那么在下传的时候显然不能满足我们的运算要求!

就因为这个地方我WA了一天并熟练地掌握了线段树的打印、调试技巧(雾

丑陋的代码


#define ls (p << 1)
#define rs (p << 1 | 1)
#define mid ((l + r) >> 1)

const int maxn = 1e5 + 10;

inline int max(int x, int y){return x > y? x: y;}
int n, m;
int a[maxn];
int st[maxn << 2], tag1[maxn << 2], tag2[maxn << 2];
int left1[maxn << 2], right1[maxn << 2], all1[maxn << 2];
int left0[maxn << 2], right0[maxn << 2], all0[maxn << 2];

void up(int p, int l, int r){
    st[p] = st[ls] + st[rs];
    left1[p] = left1[ls]; right1[p] = right1[rs];
    if(left1[ls] == mid - l + 1) left1[p] += left1[rs];
    if(right1[rs] == r - mid) right1[p] += right1[ls];
    all1[p] = max(all1[ls], max(all1[rs], right1[ls] + left1[rs]));
    
    left0[p] = left0[ls]; right0[p] = right0[rs];
    if(left0[ls] == mid - l + 1) left0[p] += left0[rs];
    if(right0[rs] == r - mid) right0[p] += right0[ls];
    all0[p] = max(all0[ls], max(all0[rs], right0[ls] + left0[rs]));
}

void build(int p, int l, itn r){
    if(l == r){
        left0[p] = right0[p] = all0[p] = 1 - a[l];
        left1[p] = right1[p] = all1[p] = st[p] = a[l];
        return;
    }
    build(ls, l, mid); build(rs, mid + 1, r);
    up(p, l, r);
}

void Swap(int p){
    std::swap(all1[p], all0[p]);
    std::swap(left1[p], left0[p]);
    std::swap(right1[p], right0[p]);
}

void down(int p, int l, int r){
//    先翻转、再置值。
    if(tag1[p] == -1){
//        没有置值操作只有翻转操作p
        if(tag2[p]){
            if(tag1[ls] != -1){
                tag1[ls] ^= 1;
                left0[ls] = right0[ls] = all0[ls] = tag1[ls]? 0: mid - l + 1;
                left1[ls] = right1[ls] = all1[ls] = st[ls] = tag1[ls]? mid - l + 1: 0;
                st[ls] = tag1[ls] * (mid - l + 1);
                tag2[ls] = 0;
            }
            else{
                Swap(ls);
                st[ls] = mid - l + 1 - st[ls];
                tag2[ls] ^= 1;
            }
            if(tag1[rs] != -1){
                tag1[rs] ^= 1;
                left0[rs] = right0[rs] = all0[rs] = tag1[rs]? 0: r - mid;
                left1[rs] = right1[rs] = all1[rs] = st[rs] = tag1[rs]? r - mid: 0;
                st[rs] = tag1[rs] * (r - mid);
                tag2[rs] = 0;
            }
            else{
                Swap(rs);
                st[rs] = r - mid - st[rs];
                tag2[rs] ^= 1;
            }
            tag2[p] = 0;
        }
        else return;
    }
    //置值不为空,那么就可以不考虑翻转情况
    else{
        if(tag1[p] == 0){
            st[rs] = st[ls] = 0;
            left1[ls] = left1[rs] = right1[ls] = right1[rs] = all1[ls] = all1[rs] = 0;
            left0[ls] = right0[ls] = all0[ls] = mid - l + 1;
            left0[rs] = right0[rs] = all0[rs] = r - mid;
        }
        else{
            st[ls] = mid - l + 1; st[rs] = r - mid;
            left0[ls] = left0[rs] = right0[ls] = right0[rs] = all0[ls] = all0[rs] = 0;
            left1[ls] = right1[ls] = all1[ls] = mid - l + 1;
            left1[rs] = right1[rs] = all1[rs] = r - mid;
        }
        tag1[ls] = tag1[rs] = tag1[p]; tag1[p] = -1;
        tag2[ls] = tag2[rs] = tag2[p] = 0;
    }
}

void set(int p, int l, int r, int L, int R, int k){
    if(L <= l && r <= R){
        left1[p] = right1[p] = all1[p] = st[p] = k? r - l + 1: 0;
        left0[p] = right0[p] = all0[p] = k? 0: r - l + 1;
        tag1[p] = k; tag2[p] = 0;
        return ;
    }
    down(p, l, r);
    if(L <= mid) set(ls, l, mid, L, R, k);
    if(R > mid) set(rs, mid + 1, r, L, R, k);
    up(p, l, r);
}

void inverse(int p, int l, int r, int L, int R){
    if(L <= l && r <= R){
        st[p] = r - l + 1 - st[p];
        Swap(p);
        if(tag1[p] != -1){
            tag1[p] ^= 1;
        }
        tag2[p] ^= 1;
        return;
    }
    down(p, l, r);
    if(L <= mid) inverse(ls, l, mid, L, R);
    if(R > mid) inverse(rs, mid + 1, r, L, R);
    up(p, l, r);
}

int query(int p, int l, itn r, itn L, int R){
    if(L <= l && r <= R){
        return st[p];
    }
    down(p, l, r);
    int ans = 0;
    if(L <= mid) ans += query(ls, l, mid, L, R);
    if(R > mid) ans += query(rs, mid + 1, r, L, R);
    
    return ans;
}

int getlen(int p, int l, int r, int L, int R){
    if(L > R) return 0;
    if(L <= l && r <= R){
        return all1[p];
    }
    down(p, l, r);
    
    if(R <= mid) return getlen(ls, l, mid, L, R);
    if(L > mid) return getlen(rs, mid + 1, r, L, R);
    int ans = max(getlen(ls, l, mid, L, R), getlen(rs, mid + 1, r, L, R));
    ans = max(ans, std::min(right1[ls], mid - L + 1) + std::min(left1[rs], R - mid));
    return ans;
}

int main(){
//    Fast;
    memset(tag1, -1, sizeof tag1);
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++) scanf("%d", a + i);
    build(1, 1, n);
    int op, x, y;
    for(int i = 0; i < m; i++){
        scanf("%d%d%d", &op, &x, &y);
        x++; y++;
        if(op == 0) set(1, 1, n, x, y, 0);
        else if(op == 1) set(1, 1, n, x, y, 1);
        else if(op ==2) inverse(1, 1, n, x, y);
        else if(op == 3) printf("%d\n", query(1, 1, n, x, y));
        else printf("%d\n", getlen(1, 1, n, x, y));
    }
    return 0;
}

大概是这样一个丑法
虽然WA了一天但是对线段树的理解还是加深了许多…特别是双标记的维护方式…


小结

因为某个P题导致我有点恶心线段树…NWU的线段树套题还有4个没有过掉…虽然有点强迫症不太舒服但是还是先算了吧!线段树的各种变形情况我感觉写起来已经不算生疏了,之后的训练就应该在发现线段树、构造线段树上面花费心思… 主要是有点腻线段树了(菜


之后看点别的东西准备一下晚上的CF下分之旅,明天继续开始补以前欠下的无数套题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值