I'm Lyw_and_Segmen|

Laiyiwen_01

园龄:11个月粉丝:0关注:2

线段树学习笔记

目录

  • 基本线段树
  • 线段树的一些奇技淫巧
  • 权值线段树
  • 线段树上二分
  • 基本势能线段树
  • 进阶势能线段树(吉司机线段树)
  • 李超线段树
  • 可持久化线段树
  • 树套树
  • 线段树优化 dp

基本线段树

在开始讲线段树之前,我们要思考,为什么选择线段树呢?

首先,线段树是一种相当好理解的数据结构,其码量固然有点大,但是代码相当好理解,不过调试难度较大,考验选手一遍写对的能力。

其次,在进行区间操作时,线段树拥有优秀的 \(O(\log n)\) 复杂度,即使在 \(n = 10 ^ 6\) 时,\(O(\log n)\) 也仅有大约 20 左右,并且即使正解是 \(O(n)\) 的,线段树在经过一些较为基本的卡常之后,也能够踩着时间通过,是一种非常高效的数据结构。

并且,相比起树状数组,虽然代码量大了,常数也大了,但是线段树足以应付几乎任何的区间操作问题,比树状数组用途大了很多。

而比起更加强大的平衡树,线段树的码量属于较小的,并且大多平衡树能够维护的,线段树也能维护(线段树本身就是一种比较弱的平衡树),并且常数上讲,线段树的常数比很多平衡树的常数要小得多。

线段树1(洛谷 P3372)

给定一个长度为 \(n\) 的序列 \(a\),有 \(q\) 次操作,可以进行如下几种操作:

  • 1 x y k 表示将 \([x, y]\) 中的每个数都加上 \(k\)
  • 2 x y 表示求出 \([x, y]\) 的和

对于 \(100 \%\) 的数据,保证 \(1 \le n, q \le 10 ^ 5\),并且在任意时刻都有 \(1 \le |a_i| \le 10 ^ {18}\)

首先我们明确题意,从数据范围来看,就是要你快速维护区间加和区间和两种操作,此时直接暴力或是前缀和都是没用的,会稳稳地 TLE 掉,那么如何快速操作呢?

那么这个时候就要引入线段树的概念,因为这些操作都是区间的,我们考虑来维护一些区间的信息,使得能够快速回答询问,而为了保证时间效率,我们也不能对每个区间进行维护,也就是说我们只考虑对部分区间进行维护,然后考虑通过区间拼凑的操作来高效的进行区间查询,而对于一个数列 \(d = \{ 10, 11, 12, 13, 14 \}\),它所代表的线段树如下:

观察,很好发现线段树为了保证效率会把一个区间分为两半,直到不能再分为止。这样树高能够维持在 \(\log n\) 左右,并且我们很好发现,对于一个节点 \(t\),其左孩子(如有)是 \(2t\),右孩子(如有)是 \(2t + 1\)

而对于这道题,线段树上要维护每个区间的区间和、左端点、右端点、以及懒标记(后面会讲这个懒标记是做什么的),我们使用结构体来进行存储,代码如下:

struct segment {
    ll l, r, sum, lzy = 0;
} w[400005];

对于数组大小,严格的来说是不超过 \(4n - 5\) 的,但为了方便且防止被卡掉,一般数组大小都会开到 \(4n + 5\) 及以上

而对于建树,我们使用递归,在建到 \(u\) 这个子节点时,我们先建立 \(2u\)\(2u + 1\) 这两个节点,然后进行信息的合并,代码如下:

void build(ll u, ll l, ll r) {
    w[u].l = l, w[u].r = r;
    if(l == r) {
        w[u].sum = a[l]; return ;
    }
    ll mid = l + ((r - l) >> 1);
    build(u * 2, l, mid), build(u * 2 + 1, mid + 1, r);
    pushup(u);
}

其中 pushup(u) 函数表示合并 \(u\) 节点所代表的信息,代码如下:

void pushup(ll u) {
    w[u].sum = w[u * 2].sum + w[u * 2 + 1].sum;
}

此时我们引入懒标记的概念:

即对一个区间 \([l, r]\) 进行区间加时,我们先不急着直接加上,而是先存着,直到该区间或该区间的左儿子或右儿子被查询到时,再一并加上。首先这样做的正确性是显然的,那么这样做有什么好处呢?

有的,若 \([l, r] = [1, n]\),则若每次操作都对每一个节点进行相加,则时间复杂度会退化成建树的 \(O(n)\) 复杂度,但如果加上了懒标记,则无论 \([l, r]\) 如何变化,都可以保证 \(O(\log n)\) 的复杂度。

那么如何建立一个懒标记呢?

我们可以使用一个函数 maketag(u, ...) 来表示对 \(u\) 节点建立一个懒标记,后面的参数则是所对应需要的一些东西。

在这个题目中,我们的 maketag() 函数需要有三个参数 u, len, x 表示对于 \(u\) 这个节点,加上 \(len \times x\)

代码如下:

void maketag(ll u, ll len, ll x) {
    w[u].lzy += x, w[u].sum += len * x;
}

而此时,有了懒标记,当然也必须有下放的方式:

  • 对于一个节点 \(u\),其懒标记 w[u].lzy 在下放时需要给其左儿子和右儿子,在下放完成之后,清空当前层的懒标记。

这部分非常好理解,上代码:

void pushdown(ll u, ll l, ll r) {
    ll mid = l + ((r - l) >> 1);
    maketag(u * 2, mid - l + 1, w[u].lzy), maketag(u * 2 + 1, r - mid, w[u].lzy);
    w[u].lzy = 0;
}

接下来是重点的查询与修改:

  • 查询

线段树查询的核心在于线段的拆分,即一个线段 \([l, r]\) 维护的信息(如和、最大值、最小值)可以通过对 \([l, x], [x, r]\) 这两个线段所维护的信息进行处理得来,其中一般情况下 \(x = l + \dfrac{r - l}{2}\)

比如说上面的线段树中,有 \([3, 5] = [3, 3] + [4, 5]\),那么有了这一个性质,我们就可以考虑进行递归式的查询,具体步骤如下:

假设当前要查询的区间为 \([l, r]\) 当前线段为 \([x, y]\),则:

  • \(l \le x\)\(y \le r\)(即 \([x, y]\)\([l, r]\) 完全包含),此时我们返回该线段的和。

  • \([l, r]\)\([x, y]\) 的左儿子相交,则递归下去查询左儿子,并累加和。

  • \([l, r]\)\([x, y]\) 的右儿子相交,则递归下去查询右儿子,并累加和。

  • 最后返回累加的和即可。

不要忘记在继续递归前要下传懒标记,防止信息错误。

根据如上步骤,可写出如下代码:

ll query(ll u) {
    if(l <= w[u].l && w[u].r <= r) {
        return w[u].sum;
    }
    
    ll res = 0, mid = w[u].l + ((w[u].r - w[u].l) >> 1);
    
    pushdown(u, w[u].l, w[u].r);
    
    if (l <= mid) {
        res += query(u * 2);
    }
    
    if (r > mid) {
        res += query(u * 2 + 1);
    }
    
    return res;
}
  • 修改

修改的步骤与查询很像,只不过没有了累加和,转而直接递归修改,并且当 \(l \le x\)\(y \le r\) 时,进行打懒标记即可,最后因为信息发生了改变,所以要进行 pushup,至此,整个修改的步骤就结束了。

根据上述步骤,我们可以写出如下代码:

void update(ll u) {
    if (l <= w[u].l && w[u].r <= r) {
        maketag(u, w[u].r - w[u].l + 1, x);
        return ;
    }
    
    ll mid = w[u].l + ((w[u].r - w[u].l) >> 1);
    
    pushdown(u, w[u].l, w[u].r);
    
    if (l <= mid) {
        update(u * 2);
    }
    
    if (r > mid) {
        update(u * 2 + 1);
    }
    
    pushup(u);
    
    return ;
}

上完整代码:

#include <bits/stdc++.h>
#define ll long long
#define db double
#define endl "\n"

using namespace std;

ll n, m, a[100005], opt, l, r, x;

struct segment {
	ll l, r, sum, lzy;
} w[400005] ;

void pushup(ll u) {
	w[u].sum = w[u * 2].sum + w[u * 2 + 1].sum;
}

void build(ll u, ll l, ll r) {
	w[u].l = l, w[u].r = r;
	if (l == r) {
		w[u].sum = a[l];
		return;
	}
	ll mid = l + ((r - l) >> 1);
	build(u * 2, l, mid), build(u * 2 + 1, mid + 1, r);
	pushup(u);
}

void maketag(ll u, ll len, ll x) {
	w[u].sum += len * x, w[u].lzy += x;
}

void pushdown(ll u, ll l, ll r) {
	ll mid = l + ((r - l) >> 1);
	maketag(u * 2, mid - l + 1, w[u].lzy), maketag(u * 2 + 1, r - mid, w[u].lzy);
	w[u].lzy = 0;
}

ll query(ll u) {
	if (l <= w[u].l && w[u].r <= r) {
		return w[u].sum;
	}
	
	pushdown(u, w[u].l, w[u].r);
	
	ll mid = w[u].l + ((w[u].r - w[u].l) >> 1), res = 0;
	
	if (l <= mid) {
		res += query(u * 2);
	}
	
	if (r > mid) {
		res += query(u * 2 + 1);
	}
	
	return res;
}

void update(ll u) {
	if (l <= w[u].l && w[u].r <= r) {
		maketag(u, w[u].r - w[u].l + 1, x);
		return ;
	}
	
	pushdown(u, w[u].l, w[u].r);
	
	ll mid = w[u].l + ((w[u].r - w[u].l) >> 1);
	
	if (l <= mid) {
		update(u * 2);
	}
	
	if (r > mid) {
		update(u * 2 + 1);
	}
	
	pushup(u);
	
	return ;
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	
	cin >> n >> m;
	
	for (ll i = 1; i <= n; i++) {
		cin >> a[i];
	}
	
	build(1, 1, n);
	
	while (m--) {
		cin >> opt;
		
		if (opt == 1) {
			cin >> l >> r >> x;
			update(1);
		} else {
			cin >> l >> r;
			cout << query(1) << endl;
		}
	}
}

线段树2(洛谷 P3373)

给定一个长度为 \(n\) 的序列 \(a\),有 \(q\) 次操作,可以进行如下几种操作:

  • 1 x y k 表示将 \([x, y]\) 中的每个数都加上 \(k\)
  • 2 x y k 表示将 \([x, y]\) 中的每个数都乘上 \(k\)
  • 2 x y 表示求出 \([x, y]\) 的和

对于 \(100 \%\) 的数据,\(1 \le n \le 10 ^ 5, 1 \le q \le 10 ^ 5, m = 571373\)

这回的题目要维护区间同乘一个数了。怎么办?

我们知道区间同乘会对区间加的 lazy 有影响,这是棘手的地方。但是我们惊奇的发现有一个东西叫乘法分配律,于是我们在乘法的时候可以直接顺带把 lazy 乘一下,美滋滋。但是问题来了,乘法会影响所有的 lazy,所以看起来又要暴力了,没事,谁跟你说 lazy 只能有一个的?

但是这里就有一个细节问题了。算答案时,是优先计算乘法的 lazy 还是加法的 lazy?注意到先加后乘在加法操作是可能出现浮点甚至浮点误差,所以建议先乘后加。

上代码。注意初始时乘法的 lazy 要设成 \(1\)

#include <bits/stdc++.h>
#define ll long long
#define endl "\n"
using namespace std;

ll n, mod, q, a[100005];

ll opt, l, r, x;

struct segment {
	ll sum = 0, l, r, lzy_add = 0, lzy_mul = 0;
} w[400005] ;

void pushup(ll u) {
	w[u].sum = w[u * 2].sum + w[u * 2 + 1].sum; w[u].sum %= mod;
}

void build(ll u, ll l, ll r) {
	w[u].l = l, w[u].r = r, w[u].lzy_mul = 1;
	if (l == r) {
		w[u].sum = a[l];
		return ;
	}
	ll mid = l + ((r - l) >> 1);
	build(u * 2, l, mid), build(u * 2 + 1, mid + 1, r);
	pushup(u);
	return ;
}

void maketag(ll u, ll l, ll r, ll x, ll type) {
	if (type == 1) {
		w[u].lzy_add *= x; w[u].lzy_add %= mod;
		w[u].lzy_mul *= x; w[u].lzy_mul %= mod;
		w[u].sum *= x; w[u].sum %= mod;
	} else {
		w[u].lzy_add += x; w[u].lzy_add %= mod;
		w[u].sum += (r - l + 1) * x; w[u].sum %= mod;
	}
}

void pushdown(ll u, ll l, ll r) {
	ll mid = l + ((r - l) >> 1);
	maketag(u * 2, l, mid, w[u].lzy_mul, 1), maketag(u * 2, l, mid, w[u].lzy_add, 2);
	maketag(u * 2 + 1, mid + 1, r, w[u].lzy_mul, 1), maketag(u * 2 + 1, mid + 1, r, w[u].lzy_add, 2);
	w[u].lzy_mul = 1, w[u].lzy_add = 0; 
}

ll query(ll u) {
	if(l <= w[u].l && w[u].r <= r) {
		return w[u].sum;
	}
	ll mid = w[u].l + ((w[u].r - w[u].l) >> 1), res = 0;
	pushdown(u, w[u].l, w[u].r);
	if (l <= mid) {
		res += query(u * 2) % mod;
	}
	if (r > mid) {
		res += query(u * 2 + 1) % mod;
	}
	res %= mod; 
    return res;
}

void update(ll u, ll type) {
	if (l <= w[u].l && w[u].r <= r) {
		maketag(u, w[u].l, w[u].r, x, type);
		return ;
	}
	ll mid = w[u].l + ((w[u].r - w[u].l) >> 1);
	pushdown(u, w[u].l, w[u].r);
	if (l <= mid) {
		update(u * 2, type);
	}
	if (r > mid) {
		update(u * 2 + 1,type);
	}
	pushup(u);
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	cin >> n >> mod;
	for (ll i = 1; i <= n; i++) {
		cin >> a[i]; a[i] %= mod;
	}
	cin >> q; build(1, 1, n);
	while (q--) {
		cin >> opt >> l >> r;
		if (opt == 1 || opt == 2) {
			cin >> x; update(1, opt);
		} else {
			cout << query(1) << endl;
		}
	}
}

守墓人(洛谷 P2357)

给定一个长度为 \(n\) 的序列 \(a\),有 \(q\) 次操作,可以进行如下几种操作:

  • \([l,r]\) 增加 \(k\)
  • \(a_1\) 增加 \(k\)
  • \(a_1\) 减少 \(k\)
  • 求出 \([l, r]\) 的和
  • 求出 \(a_1\) 的值

tips : 单点修改和单点查询可以当成 \(l, r\) 相等的区间操作。

code :

#include<bits/stdc++.h>
#define ll long long
#define endl "\n"
using namespace std;
ll n, m, a[200005], op, l, r, x;
struct segment {
    ll l, r, sum, lzy;
} w[800005];
void pushup(ll u) {
    w[u].sum = w[u * 2].sum + w[u * 2 + 1].sum;
}
void build(ll u, ll l, ll r) {
    w[u].l = l, w[u].r = r;
    if(l == r) {
        w[u].sum = a[l]; return ;
    }
    ll mid = l + ((r - l) >> 1);
    build(u * 2, l, mid), build(u * 2 + 1, mid + 1, r);
    pushup(u);
}
void maketag(ll u, ll len, ll x) {
    w[u].lzy += x, w[u].sum += len * x;
}
void pushdown(ll u, ll l, ll r) {
    ll mid = l + ((r - l) >> 1);
    maketag(u * 2, mid - l + 1, w[u].lzy), maketag(u * 2 + 1, r - mid, w[u].lzy);
    w[u].lzy = 0;
}
ll query(ll u) {
    if(l <= w[u].l && w[u].r <= r) {
        return w[u].sum;
    }
    ll res = 0, mid = w[u].l + ((w[u].r - w[u].l) >> 1);
    pushdown(u, w[u].l, w[u].r);
    if (l <= mid) {
        res += query(u * 2);
    }
    if (r > mid) {
        res += query(u * 2 + 1);
    }
    return res;
}
void update(ll u) {
    if (l <= w[u].l && w[u].r <= r) {
        maketag(u, w[u].r - w[u].l + 1, x);
        return ;
    }
    ll mid = w[u].l + ((w[u].r - w[u].l) >> 1);
    pushdown(u, w[u].l, w[u].r);
    if (l <= mid) {
        update(u * 2);
    }
    if (r > mid) {
        update(u * 2 + 1);
    }
    pushup(u);
    return ;
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> a[i];
    build(1, 1, n);
    while(m--) {
        cin >> op;
        if(op == 1) {
            cin >> l >> r >> x;
            update(1);
        } else if (op == 2) {
            cin >> x; l = r = 1;
            update(1);
        } else if (op == 3) {
            cin >> x; l = r = 1, x = -x;
            update(1);
        } else if (op == 4) {
            cin >> l >> r; 
            cout << query(1) << endl;
        } else {
            l = r = 1;
            cout << query(1) << endl;
        }
    }
}

扶苏的问题(洛谷 P1253)

给定一个长度为 \(n\) 的序列,要求支持如下三个操作:

  • 给定区间 \([l,r]\),将区间内每个数都修改为 \(k\)

  • 给定区间 \([l,r]\),将区间内每个数加上 \(k\)

  • 给定区间 \([l,r]\),求区间内所有数的最大值

数据范围:\(1 \leq n, q \leq 10^6\)\(1 \leq l, r \leq n\)\(op \in \{1, 2, 3\}\)\(|a_i|, |x| \leq 10^9\)

\(\texttt{Solution}\)

维护最大值的方式与维护和的方式是基本相同的,只不过最大值的维护通过 std::max() 完成,此外,为了维护区间推平为 \(x\) 的操作,我们需要额外维护一个懒标记,不妨设为 lzy2,在常态下,lzy2 = inf,这里 inf 代指一个特别大的数,表示该标记不存在,而当 lzy2 != inf 时,表示该区间被进行了区间推平,并且整个区间都被加上了 lzy2,注意此时要把 lzy1 设为 \(0\),表示不存在和的标记,在下放标记时也要记得特判该区间是什么状态。

最后注意这题数据都比较大,记得加上快读才能过。

code :

#include <bits/stdc++.h>
#define ll long long
#define db double
#define inf 1000000000000000000
#define endl "\n"

namespace fastio {
	char buf[1 << 21], *p1 = buf, *p2 = buf;
	
	const ll getc() {
	    return p1 == p2 && ( p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1 ++;
	}
	
	const ll read() {
		ll x = 0, f = 1;
		
		char ch = getc();
		
		while (ch < '0' || ch > '9') {
			if (ch == '-') f = -1; ch = getc();
		}
		
		while (ch >= '0' && ch <= '9') {
			x = (x << 1) + (x << 3) + (ch ^ 48), ch = getc();
		}
		
		return x * f;
	}
	
	const void write(ll x) {
		if (x < 0) {
			putchar('-'), x = -x;
		}
		
	    ll sta[35], top = 0;
	    
	    do {
	        sta[top++] = x % 10, x /= 10;
	    } while (x);
	    
	    while (top) putchar(sta[--top] + 48);
	}
}

using namespace std;

ll n, q, a[1000005], l, r, x;

struct segment {
    ll l, r, len, sum, mx, mn, lzy1, lzy2 = inf;
    // lzy1 是区间和 tag, lzy2 是区间修改 tag
} w[4000005];

void pushup(ll u) { 
    w[u].sum = w[u * 2].sum + w[u * 2 + 1].sum;
    w[u].mx = max(w[u * 2].mx, w[u * 2 + 1].mx);
    w[u].mn = min(w[u * 2].mn, w[u * 2 + 1].mn);
}

void build(ll u, ll l, ll r) {
	w[u].l = l, w[u].r = r, w[u].len = r - l + 1;

	if (l == r) {
		w[u].sum = w[u].mx = w[u].mn = a[l];

		return ;
	}

	ll mid = l + ((r - l) >> 1);

	build(u * 2, l, mid), build(u * 2 + 1, mid + 1, r);

	pushup(u);
}

void maketag1(ll u, ll x) { // 维护区间和的 tag  
    if(w[u].lzy2 == inf) {
        w[u].lzy1 += x;
    } else {
        w[u].lzy2 += x;
    }

    w[u].sum += w[u].len * x, w[u].mx += x, w[u].mn += x;
}

void maketag2(ll u, ll x) { // 维护全部修改的 tag
    w[u].sum = w[u].len * x, w[u].lzy2 = x, w[u].mx = w[u].mn = x;

	w[u].lzy1 = 0;
}

void pushdown(ll u) {
    if(w[u].lzy2 == inf) {
        maketag1(u * 2, w[u].lzy1);
        maketag1(u * 2 + 1, w[u].lzy1);
        w[u].lzy1 = 0;
    } else {
        maketag2(u * 2, w[u].lzy2);
        maketag2(u * 2 + 1, w[u].lzy2);
        w[u].lzy2 = inf;
    }
}

ll query(ll u) {
	if (l <= w[u].l && w[u].r <= r) {
		return w[u].mx;
	}

	pushdown(u);

	ll mid = w[u].l + ((w[u].r - w[u].l) >> 1), res;

	res = LLONG_MIN; // max

	if (l <= mid) {
		res = max(res, query(u * 2));
	}

	if (r > mid) {
		res = max(res, query(u * 2 + 1));
	}

	return res;
}

void update(ll u, ll opt) {
	if (l <= w[u].l && w[u].r <= r) {
		if (opt == 1) maketag1(u, x);
		
		if (opt == 2) maketag2(u, x);
		
		return ;
	}

	pushdown(u);

	ll mid = w[u].l + ((w[u].r - w[u].l) >> 1);

	if (l <= mid) {
		update(u * 2, opt);
	}

	if (r > mid) {
		update(u * 2 + 1, opt);
	}

	pushup(u);
}

int main() {
    n = fastio::read(), q = fastio::read();

	for (ll i = 1; i <= n; i++) {
		a[i] = fastio::read();
	}

	build(1, 1, n);

	while (q--) {
		ll opt = fastio::read(); l = fastio::read(), r = fastio::read();
		
		if (opt == 1) {
			x = fastio::read(), update(1, 2);
		} else if (opt == 2) {
			x = fastio::read(), update(1, 1);
		} else if (opt == 3) {
			fastio::write(query(1)), puts("");
		}
	}
}

线段树的一些奇技淫巧

维护特殊信息

XOR的艺术(洛谷 P2574)

AKN 觉得第一题太水了,不屑于写第一题,所以他又玩起了新的游戏。在游戏中,他发现,这个游戏的伤害计算有一个规律,规律如下:

  1. 拥有一个伤害串,是一个长度为 \(n\) 的只含字符 0 和字符 1 的字符串。规定这个字符串的首字符是第一个字符,即下标从 \(1\) 开始。
  2. 给定一个范围 \([l,r]\),伤害为伤害串的这个范围内中字符 1 的个数。
  3. 会修改伤害串中的数值,修改的方法是把 \([l,r]\) 中所有原来的字符 0 变成 1,将 1 变成 0

对于这道题,我们可以考虑维护区间长度 \(len\),以及区间中 \(1\) 的个数 \(sum\),每次取反只需要让 \(sum = len - sum\) 即可,然后对于懒标记,可以发现我们直接对 \(1\) 取个异或即可。然后注意本题表示懒标记不存在的标志是懒标记为 \(0\),以为根据异或的性质,可以发现 \(0 ^ 1 = 1, 1 ^ 1 = 0\),什么意思呢?就是说如果原来没有懒标记,那么异或之后为 \(1\) 就能表示需要翻转,如果原来懒标记为 \(1\),然后进行异或 \(1\) 变为 \(0\),这是对同个区间进行两次翻转操作之后这个区间相当于没有被翻转。

最后,注意只有当前节点存在懒标记的时候我们才进行 pushdown() 操作。

code :

#include <bits/stdc++.h>
#define ll long long
#define endl "\n"
using namespace std;

ll n, m, a[200005];

ll opt, l, r;

struct segment {
	ll l, r, sum, len, lzy = 0;
} w[800005] ;

void pushup(ll u) {
	w[u].sum = w[u * 2].sum + w[u * 2 + 1].sum;
}

void build(ll u, ll l, ll r) {
	w[u].l = l, w[u].r = r, w[u].len = r - l + 1;
	if (l == r) {
		w[u].sum = a[l];
		return ;
	}
	ll mid = l + ((r - l) >> 1);
	build(u * 2, l, mid), build(u * 2 + 1, mid + 1, r);
	pushup(u);
	return ;
}

void maketag(ll u) {
	w[u].sum = w[u].len - w[u].sum; w[u].lzy ^= 1;
}

void pushdown(ll u) {
	maketag(u * 2), maketag(u * 2 + 1);
	w[u].lzy = 0;
}

bool InRange(ll u, ll l, ll r) {
	return (l <= w[u].l) && (w[u].r <= r);
}

bool OutofRange(ll u, ll l, ll r) {
	return  (l > w[u].r) || (r < w[u].l);
}

ll query(ll u) {
	if(InRange(u, l, r)) {
		return w[u].sum;
	} else if (!OutofRange(u, l, r)) {
		if(w[u].lzy) pushdown(u);
		return query(u * 2) + query(u * 2 + 1);
	} else return 0;
}

void update(ll u) {
	if (InRange(u, l, r)) {
		maketag(u); return ;
	} else if (!OutofRange(u, l, r)) {
		if(w[u].lzy) pushdown(u);
		update(u * 2), update(u * 2 + 1);
		pushup(u);
	}
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	cin >> n >> m; string s; cin >> s;
	for (ll i = 1; i <= n; i++) {
	    a[i] = s[i - 1] - '0';
	}
	build(1, 1, n);
	while (m--) {
		cin >> opt >> l >> r;
		if (opt == 0) {
			update(1);
		} else {
			cout << query(1) << endl;
		}
	}
}

方差(洛谷 P1471)

蒟蒻 HansBug 在一本数学书里面发现了一个神奇的数列,包含 \(N\) 个实数。他想算算这个数列的平均数和方差。

操作 \(1\)1 x y k ,表示将第 \(x\) 到第 \(y\) 项每项加上 \(k\)\(k\) 为一实数。
操作 \(2\)2 x y ,表示求出第 \(x\) 到第 \(y\) 项这一子数列的平均数。
操作 \(3\)3 x y ,表示求出第 \(x\) 到第 \(y\) 项这一子数列的方差。

很明显方差这玩意没法直接维护,只有一种可能是推式子。

所以我们只需要维护平均数(其实也就是和)和平方和。

和很好维护,但是平方和好像有点难。问题是我们不知道每次区间加平方和会多多少,于是继续推式子。

这样平方和也解决了。

序列操作(洛谷 P2572)

前四个都好操作。但问题是怎么维护有多少个连续的1.首先我们肯定要维护区间里连续的1,然后由于区间取反的存在还要维护区间里的0。但是还要考虑到合并区间,所以还要考虑左/右起的0/1.加上0的个数和1的个数,总共要维护八个东西,不愧是 SCOI,恐怖如斯.

区间和区间 \(\sin\) 和(洛谷 P6327)

给出一个长度为 \(n\) 的整数序列 \(a_1,a_2,\ldots,a_n\),进行 \(m\) 次操作,操作分为两类。

操作 \(1\):给出 \(l,r,v\),将 \(a _ l, a _ {l+1}, \ldots, a _ r\) 分别加上 \(v\)

操作 \(2\):给出 \(l,r\),询问 \(\displaystyle \sum_{i = l} ^ {r} \sin(a_i)\)

一眼看起来就恐怖如斯

做法是三角和差公式。就来 \(\sin\) 来说吧,\(\sin(A+B) = \sin A \times \cos B + \cos A \times \sin B\)\(B\) 是固定的所以只要在原来的 \(\sin\) 和上乘上\(\cos B\) 再加上 \(\cos\) 和乘上 \(\sin B\)

  • 直接维护线段

对于一个线段 \([l, r]\),我们直接在线段树上进行覆盖,即 update(1, l, r),这样可以快速知道一个点 \(x\) 被几条线段包含,具体应用在 P1712 区间这个题中结合双指针解决

序列末尾增/删一个数

假设有这样一道题目:

Sequence

有一个初始为空的序列,你需要进行 \(q\) 次操作,每次操作可能为如下几种类型之一:

  • 1 l r x 表示将 \([l, r]\) 这个区间中的每个数加上 \(x\),保证该区间存在

  • 2 x 表示在该序列末端添上一项,该项的值为 \(x\)

  • 3 x 表示删除该序列最后一项

  • 4 l r 表示求出 \([l, r]\) 这个区间的和,保证该区间存在。由于这个值比较大,所以你只需要输出其对 \(998244353\) 取模之后的结果

对于 \(100 \%\) 的数据,\(1 \le q \le 10 ^ 5, 1 \le x \le 10 ^ {18}\)

读完题目,我们会发现,1、4 操作都是线段树经典操作,那么如何处理操作 2、3 呢?

其实也很简单,考虑最基础的一种办法:用 update 函数(即区间修改函数)来新增节点,为了表示出 \(l, r\),还需要额外全局维护一个 \(cnt\) 变量表示当前元素总数。

贴一下实现:

...
if (opt == 2) {
    x = read() % mod, cnt ++, l = cnt, r = cnt, update(1);
}
...

删除同理,代码如下:

...
if (opt == 3) {
    l = cnt, r = cnt, x = (-(query(1) % mod) + mod) % mod, update(1), cnt --;
}
...

需要注意的是这只适合在末尾增/删元素的情况,并且要能够实现确定线段树这棵树可能的 最大 大小。

最后贴一下代码:

#include<bits/stdc++.h>
#define ll long long
#define endl "\n"
using namespace std;

const ll mod = 998244353;

ll q, cnt = 0, a[200005], opt, l, r, x;
struct segment {
	ll l, r, sum, lzy;
} w[800005];
void pushup(ll u) {
	w[u].sum = w[u * 2].sum % mod + w[u * 2 + 1].sum % mod;
	w[u].sum %= mod;
}
void build(ll u, ll l, ll r) {
	w[u].l = l, w[u].r = r;
	if(l == r) {
		w[u].sum = a[l] % mod; return ;
	}
	ll mid = l + ((r - l) >> 1);
	build(u * 2, l, mid), build(u * 2 + 1, mid + 1, r);
	pushup(u);
}
void maketag(ll u, ll len, ll x) {
	w[u].lzy += x % mod, w[u].sum += len % mod * x % mod;
	w[u].lzy %= mod, w[u].sum %= mod;
}
void pushdown(ll u, ll l, ll r) {
	ll mid = l + ((r - l) >> 1); w[u].lzy %= mod;
	maketag(u * 2, mid - l + 1, w[u].lzy), maketag(u * 2 + 1, r - mid, w[u].lzy);
	w[u].lzy = 0;
}
ll query(ll u) {
	if(l <= w[u].l && w[u].r <= r) {
		return w[u].sum;
	}
	ll res = 0, mid = w[u].l + ((w[u].r - w[u].l) >> 1);
	pushdown(u, w[u].l, w[u].r);
	if (l <= mid) {
		res += query(u * 2) % mod, res %= mod;
	}
	if (r > mid) {
		res += query(u * 2 + 1) % mod, res %= mod;
	}
	return res;
}
void update(ll u) {
	if (l <= w[u].l && w[u].r <= r) {
		maketag(u, w[u].r - w[u].l + 1, x);
		return ;
	}
	ll mid = w[u].l + ((w[u].r - w[u].l) >> 1);
	pushdown(u, w[u].l, w[u].r);
	if (l <= mid) {
		update(u * 2);
	}
	if (r > mid) {
		update(u * 2 + 1);
	}
	pushup(u);
	return ;
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	cin >> q; build(1, 1, q);
	while (q--) {
		cin >> opt;
		if (opt == 1) {
			cin >> l >> r >> x, x %= mod;
			update(1);
		} else if (opt == 2) {
			cin >> x; x %= mod, cnt ++; l = cnt, r = cnt;
			update(1);
		} else if (opt == 3) {
			l = cnt, r = cnt, x = (-(query(1) % mod) + mod) % mod;
			update(1); cnt --;
		} else {
			cin >> l >> r;
			cout << (query(1) % mod) << endl;
		}
	}
}
  • 简单 chkmax / chkmin 操作

注意,这个部分所给出的算法最坏 \(O(n)\),想得到正确复杂度请移步“吉司机线段树”

\(\forall l \le i \le r, a_i = \max(a_i, x)\) 这个操作为例,考虑把 \(\max\) 拆开,即

\[\max(a_i, x) = \begin{cases} a_i & a_i \gt x \\ x & a_i \le x \end{cases} \]

此时应有一个基本思路:由于当 \(a_i \le x\)\(\max(a_i, x) = x\),所以我们维护一个区间最大值,设为 w[u].mx,当我们扫到有一个 w[u].mx <= x 的时候直接打一个区间推平的懒标记然后返回即可,然后我们再从当 \(a_i \gt x\)\(\max(a_i, x) = a_i\) 入手,我们维护一个区间最小值 w[u].mn,只有扫到有 w[u].mn > x 说明无需修改,直接返回,其他情况暴力递归其左子树和右子树即可。

下面给出一个代码实现的范例:

// l, r, x 均已在代码外定义,maketag() 是区间推平的懒标记

void upd(ll u) {
	if (w[u].mn >= x) {
		return ;
	}
	
	if (w[u].mx <= x) {
		maketag(u, x); return ;
	}
	
	pushdown(u);
	
	upd(u * 2), upd(u * 2 + 1);
	
	pushup(u);
	
	return ;
}

void modify(ll u) {
	if (l <= w[u].l && w[u].r <= r) {
		upd(u); return ;
	}
	
	pushdown(u);

	ll mid = w[u].l + ((w[u].r - w[u].l) >> 1);

	if (l <= mid) {
		modify(u * 2);
	}

	if (r > mid) {
		modify(u * 2 + 1);
	}

	pushup(u);	
} 

对于 \(\forall l \le i \le r, a_i = \min(a_i, x)\) 处理的方法也是类似的,拆开 \(\min\) 然后分讨即可。

可以发现,这个办法最坏需要遍历整棵树,所以最坏是 \(O(n)\),但是看似如此,面对比较随机的数据,它还是能交出令人满意的答卷,具体如下:

在紫题 P10638 和 P10639 中,有着不俗的表现,战绩如下:

  • P10638 91 pts
  • P10639 AC

贴一下代码:

P10638 code :

#include <bits/stdc++.h>
#define ll long long
#define db double
#define inf 1000000000000000000
#define endl "\n"

namespace fastio {
	char buf[1 << 21], *p1 = buf, *p2 = buf;
	
	const ll getc() {
	    return p1 == p2 && ( p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1 ++;
	}
	
	const ll read() {
		ll x = 0, f = 1;
		
		char ch = getc();
		
		while (ch < '0' || ch > '9') {
			if (ch == '-') f = -1; ch = getc();
		}
		
		while (ch >= '0' && ch <= '9') {
			x = (x << 1) + (x << 3) + (ch ^ 48), ch = getc();
		}
		
		return x * f;
	}
	
	const void write(ll x) {
		if (x < 0) {
			putchar('-'), x = -x;
		}
		
	    ll sta[35], top = 0;
	    
	    do {
	        sta[top++] = x % 10, x /= 10;
	    } while (x);
	    
	    while (top) putchar(sta[--top] + 48);
	}
}

using namespace std;

ll n, q, a[500005], l, r, x;

struct segment {
    ll l, r, len, sum, mx, mn, cmn, lzy1, lzy2 = inf;
    // lzy1 是区间和 tag, lzy2 是区间修改 tag
} w[2000005];

void pushup(ll u) { 
    w[u].sum = w[u * 2].sum + w[u * 2 + 1].sum;
    w[u].mx = max(w[u * 2].mx, w[u * 2 + 1].mx);
    w[u].mn = min(w[u * 2].mn, w[u * 2 + 1].mn);
    
    if (w[u * 2].mn == w[u * 2 + 1].mn) {
        w[u].cmn = w[u * 2].cmn + w[u * 2 + 1].cmn;
    } else {
        w[u].cmn = (w[u * 2].mn > w[u * 2 + 1].mn ? w[u * 2 + 1].cmn : w[u * 2].cmn);
    }
}

void build(ll u, ll l, ll r) {
	w[u].l = l, w[u].r = r, w[u].len = r - l + 1;

	if (l == r) {
		w[u].sum = w[u].mx = w[u].mn = a[l], w[u].cmn = 1;
		return ;
	}

	ll mid = l + ((r - l) >> 1);

	build(u * 2, l, mid), build(u * 2 + 1, mid + 1, r);

	pushup(u);
}

void maketag1(ll u, ll x) { // 维护区间加的 tag  
    if(w[u].lzy2 == inf) {
        w[u].lzy1 += x;
    } else {
        w[u].lzy2 += x;
    }

    w[u].sum += w[u].len * x, w[u].mx += x, w[u].mn += x;
}

void maketag2(ll u, ll x) { // 维护全部修改的 tag
    w[u].sum = w[u].len * x, w[u].lzy2 = x, w[u].mx = w[u].mn = x, w[u].cmn = w[u].len;

	w[u].lzy1 = 0;
}

void pushdown(ll u) {
    if(w[u].lzy2 == inf) {
        maketag1(u * 2, w[u].lzy1);
        maketag1(u * 2 + 1, w[u].lzy1);
        w[u].lzy1 = 0;
    } else {
        maketag2(u * 2, w[u].lzy2);
        maketag2(u * 2 + 1, w[u].lzy2);
        w[u].lzy2 = inf;
    }
}

ll query(ll u) {
	if (l <= w[u].l && w[u].r <= r) {
		return (w[u].mn == 0 ? w[u].cmn : 0);
	}

	pushdown(u);

	ll mid = w[u].l + ((w[u].r - w[u].l) >> 1), res = 0;

	if (l <= mid) {
		res += query(u * 2);
	}

	if (r > mid) {
		res += query(u * 2 + 1);
	}

	return res;
}

void update(ll u, ll opt) {
	if (l <= w[u].l && w[u].r <= r) {
		if (opt == 1) maketag1(u, x);
		
		if (opt == 2) maketag2(u, x);
		
		return ;
	}

	pushdown(u);

	ll mid = w[u].l + ((w[u].r - w[u].l) >> 1);

	if (l <= mid) {
		update(u * 2, opt);
	}

	if (r > mid) {
		update(u * 2 + 1, opt);
	}

	pushup(u);
}

void upd(ll u) { //a[i] = max(a[i], x)
	if (w[u].mn >= x) {
		return ;
	}
		
	if (w[u].mx <= x) {
		maketag2(u, x); return ;
	}
	
	pushdown(u);
	
	upd(u * 2), upd(u * 2 + 1);
	
	pushup(u);
	
	return ;
}

void modify(ll u) {
	if (l <= w[u].l && w[u].r <= r) {
		upd(u); return ;
	}
	
	pushdown(u);

	ll mid = w[u].l + ((w[u].r - w[u].l) >> 1);

	if (l <= mid) {
		modify(u * 2);
	}

	if (r > mid) {
		modify(u * 2 + 1);
	}

	pushup(u);	
}

int main() {
    n = fastio::read(), q = fastio::read();

	for (ll i = 1; i <= n; i++) {
		a[i] = fastio::read();
	}

	build(1, 1, n);

	while (q--) {
		ll opt = fastio::read(); l = fastio::read(), r = fastio::read();
		
		if (opt == 1) {
		    x = fastio::read(), update(1, 2);
		} else if (opt == 2) {
		    x = fastio::read(), update(1, 1);
		    x = 0, modify(1);
		} else if (opt == 3) {
		    fastio::write(query(1)), puts("");
		}
	}
}

P10639 code :

#include <bits/stdc++.h>
#define ll long long
#define db double
#define inf 1000000000000000000
#define endl "\n"

namespace fastio {
	char buf[1 << 21], *p1 = buf, *p2 = buf;
	
	const ll getc() {
	    return p1 == p2 && ( p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1 ++;
	}
	
	const ll read() {
		ll x = 0, f = 1;
		
		char ch = getc();
		
		while (ch < '0' || ch > '9') {
			if (ch == '-') f = -1; ch = getc();
		}
		
		while (ch >= '0' && ch <= '9') {
			x = (x << 1) + (x << 3) + (ch ^ 48), ch = getc();
		}
		
		return x * f;
	}
	
	const void write(ll x) {
		if (x < 0) {
			putchar('-'), x = -x;
		}
		
	    ll sta[35], top = 0;
	    
	    do {
	        sta[top++] = x % 10, x /= 10;
	    } while (x);
	    
	    while (top) putchar(sta[--top] + 48);
	}
}

using namespace std;

ll n, q, a[500005], l, r, x;

struct segment {
    ll l, r, len, sum, mx, mn, lzy1, lzy2 = inf;
    // lzy1 是区间和 tag, lzy2 是区间修改 tag
} w[2000005];

void pushup(ll u) { 
    w[u].sum = w[u * 2].sum + w[u * 2 + 1].sum;
    w[u].mx = max(w[u * 2].mx, w[u * 2 + 1].mx);
    w[u].mn = min(w[u * 2].mn, w[u * 2 + 1].mn);
}

void build(ll u, ll l, ll r) {
	w[u].l = l, w[u].r = r, w[u].len = r - l + 1;

	if (l == r) {
		w[u].sum = w[u].mx = w[u].mn = a[l];

		return ;
	}

	ll mid = l + ((r - l) >> 1);

	build(u * 2, l, mid), build(u * 2 + 1, mid + 1, r);

	pushup(u);
}

void maketag1(ll u, ll x) { // 维护区间和的 tag  
    if(w[u].lzy2 == inf) {
        w[u].lzy1 += x;
    } else {
        w[u].lzy2 += x;
    }

    w[u].sum += w[u].len * x, w[u].mx += x, w[u].mn += x;
}

void maketag2(ll u, ll x) { // 维护全部修改的 tag
    w[u].sum = w[u].len * x, w[u].lzy2 = x, w[u].mx = w[u].mn = x;

	w[u].lzy1 = 0;
}

void pushdown(ll u) {
    if(w[u].lzy2 == inf) {
        maketag1(u * 2, w[u].lzy1);
        maketag1(u * 2 + 1, w[u].lzy1);
        w[u].lzy1 = 0;
    } else {
        maketag2(u * 2, w[u].lzy2);
        maketag2(u * 2 + 1, w[u].lzy2);
        w[u].lzy2 = inf;
    }
}

ll query(ll u, ll opt) {
	if (l <= w[u].l && w[u].r <= r) {
		if (opt == 1) {
			return w[u].sum;
		} else if (opt == 2) {
			return w[u].mx;
		} else if (opt == 3) {
			return w[u].mn;
		}
	}

	pushdown(u);

	ll mid = w[u].l + ((w[u].r - w[u].l) >> 1), res;

	if (opt == 1) res = 0; // sum

	if (opt == 2) res = LLONG_MIN; // max

	if (opt == 3) res = LLONG_MAX; // min

	if (l <= mid) {
		if (opt == 1) {
			res += query(u * 2, opt);
		} else if (opt == 2) {
			res = max(res, query(u * 2, opt));
		} else if (opt == 3) {
			res = min(res, query(u * 2, opt));
		}
	}

	if (r > mid) {
		if (opt == 1) {
			res += query(u * 2 + 1, opt);
		} else if (opt == 2) {
			res = max(res, query(u * 2 + 1, opt));
		} else if (opt == 3) {
			res = min(res, query(u * 2 + 1, opt));
		}
	}

	return res;
}

void update(ll u) {
	if (l <= w[u].l && w[u].r <= r) {
		maketag1(u, x); return ;
	}

	pushdown(u);

	ll mid = w[u].l + ((w[u].r - w[u].l) >> 1);

	if (l <= mid) {
		update(u * 2);
	}

	if (r > mid) {
		update(u * 2 + 1);
	}

	pushup(u);
}

void upd(ll u, ll opt) {
	if (opt == 1) { //a[i] = max(a[i], x)
		if (w[u].mn >= x) {
			return ;
		}
		
		if (w[u].mx <= x) {
			maketag2(u, x); return ;
		}
	}
	
	if (opt == 2) { // a[i] = min(a[i], x)
		if (w[u].mx <= x) {
			return ;
		}
		
		if (w[u].mn >= x) {
			maketag2(u, x); return ;
		}
	}
	
	pushdown(u);
	
	upd(u * 2, opt), upd(u * 2 + 1, opt);
	
	pushup(u);
	
	return ;
}

void modify(ll u, ll opt) {
	if (l <= w[u].l && w[u].r <= r) {
		upd(u, opt); return ;
	}
	
	pushdown(u);

	ll mid = w[u].l + ((w[u].r - w[u].l) >> 1);

	if (l <= mid) {
		modify(u * 2, opt);
	}

	if (r > mid) {
		modify(u * 2 + 1, opt);
	}

	pushup(u);	
}

int main() {
    n = fastio::read();

	for (ll i = 1; i <= n; i++) {
		a[i] = fastio::read();
	}

	build(1, 1, n);

	q = fastio::read();

	while (q--) {
		ll opt = fastio::read(); l = fastio::read(), r = fastio::read();
		
		if (opt == 1) {
			x = fastio::read(), update(1);
		} else if (opt == 2) {
			x = fastio::read(), modify(1, 1);
		} else if (opt == 3) {
			x = fastio::read(), modify(1, 2);
		} else if (opt == 4) {
			fastio::write(query(1, 1)), puts("");
		} else if (opt == 5) {
			fastio::write(query(1, 2)), puts("");
		} else if (opt == 6) {
			fastio::write(query(1, 3)), puts("");
		}
	}
}

结合基本算法维护信息

例题小白逛公园。

离线处理

例题 HH 的项链。使用离线的技巧可以快速处理。

权值线段树

权值线段树的思想很暴力,就是以具体的数为下标,值表示一些特殊的信息(如所对应的 dp 值,该数的出现次数等)

例题 1:

有一个长度为 \(n\) 的序列 \(\{a_i\}\),你需要求出该序列最长上升子序列的长度。

数据范围:\(1 \le n, a_i \le 5 \times 10 ^ 5\)

闲话:本来这题应该归入线段树优化 dp 的章节中,但由于本题的 dp 实在是过于简单,故出现在权值线段树这一章节。

好的回到正题,例题 1 做法如下:

\(\large \texttt{Solution}\)

首先可以快速写出 dp 的转移方程,即:\(dp_i = (\max\limits_{a_j \lt a_i\ \text{and}\ 1 \le j \lt i} dp_j) + 1\),然后考虑如何消除限制。

其次 \(1 \le j \lt i\) 的限制只需要按顺序加入即可。然后 \(a_j \lt a_i\) 这个限制也只需要权值线段树维护 \((a_i, dp_i)\) 这个点即可。其中 \(a_i\) 是下标,\(dp_i\) 为所维护的值。

最后是实现,我们顺序枚举 \(i\),然后先在线段树上查询 \(1 \sim a_i - 1\) 的 dp 值最大值,存入 \(dp_i\),再然后加入 \((a_i, dp_i)\) 这个点即可。最后输出 \(\max_{i = 1}^{n} dp_i\),问题得以解决。时间复杂度 \(O(n \log n)\)。记得注意一些边界的处理。

code :

#include <bits/stdc++.h>
#define ll long long
#define db double
#define endl "\n"

using namespace std;

ll n, a[1000005], dp[1000005];

struct segment {
    ll l, r, mx = 0;
} w[4000005] ; 

void pushup(ll u) {
    w[u].mx = max(w[u * 2].mx, w[u * 2 + 1].mx);
}

void build(ll u, ll l, ll r) {
    w[u].l = l, w[u].r = r;
    
    if (l == r) {
        w[u].mx = 0; return ;
    }
    
    ll mid = l + ((r - l) >> 1);
    
    build(u * 2, l, mid), build(u * 2 + 1, mid + 1, r);
    
    pushup(u);
}

ll query(ll u, ll l, ll r) {
    if (l <= w[u].l && w[u].r <= r) {
        return w[u].mx;
    }
    
    ll mid = w[u].l + ((w[u].r - w[u].l) >> 1), res = LLONG_MIN;
    
    if (l <= mid) {
        res = max(res, query(u * 2, l, r));
    }
    
    if (r > mid) {
        res = max(res, query(u * 2 + 1, l, r));
    }
    
    return res;
}

void update(ll u, ll pos, ll x) {
    if (w[u].l == w[u].r && w[u].l == pos) {
        w[u].mx = x; return ;
    }
    
    ll mid = w[u].l + ((w[u].r - w[u].l) >> 1);
    
    if (pos <= mid) {
        update(u * 2, pos, x);
    } else {
        update(u * 2 + 1, pos, x);
    }
    
    pushup(u);
}

int main() {
    cin >> n; ll ans = 0;
    
    for (ll i = 1; i <= n; i++) {
        cin >> a[i], dp[i] = 1;
    }
    
    build(1, 1, 1000000);
    
    for (ll i = 1; i <= n; i++) {
        ll now;

        if (a[i] == 1) now = 0;
        else now = query(1, 1, a[i] - 1);

        dp[i] = max(dp[i], now + 1);
        update(1, a[i], dp[i]);
        
        ans = max(ans, dp[i]);
    }
    
    cout << ans;
}

例题2

一个 \(n\) 个数的序列,\(m\) 次询问,每次询问 \([l, r]\) 区间中小于等于 \(v\) 的元素的数量。

数据范围:\(1 \le n, m, l, r, v \le 2 \times 10 ^ 6\)

\(\large \texttt{Solution}\)

很显然,可以直接把所有询问离线下来,然后按照 \(v\) 的大小先排个序,扔到权值线段树上逐一添加就做完了。

code :

#include <bits/stdc++.h>
#define ll int
#define db double
#define endl "\n"

namespace fastio {
	char buf[1 << 21], *p1 = buf, *p2 = buf;
	
	const ll getc() {
	    return p1 == p2 && ( p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1 ++;
	}
	
	const ll read() {
		ll x = 0, f = 1;
		
		char ch = getc();
		
		while (ch < '0' || ch > '9') {
			if (ch == '-') f = -1; ch = getc();
		}
		
		while (ch >= '0' && ch <= '9') {
			x = (x << 1) + (x << 3) + (ch ^ 48), ch = getc();
		}
		
		return x * f;
	}
	
	const void write(ll x) {
		if (x < 0) {
			putchar('-'), x = -x;
		}
		
	    ll sta[35], top = 0;
	    
	    do {
	        sta[top++] = x % 10, x /= 10;
	    } while (x);
	    
	    while (top) putchar(sta[--top] + 48);
	}
}

using namespace std;

#define read fastio::read
#define write fastio::write 

ll n = read(), m = read(), mx = INT_MIN, a[2000005], c[2000005];

struct node {
	ll l, r, x, id, ans;
	
	friend bool operator < (node x, node y) {
		if (x.x == y.x) {
			return x.id < y.id;
		}
		
		return x.x < y.x;
	}
} ask[4000005] ; 

struct segment {
	ll l, r, len, sum;
} w[8000005] ;

void pushup(ll u) {
	w[u].sum = w[u * 2].sum + w[u * 2 + 1].sum; 
}

void build(ll u, ll l, ll r) {
	w[u].l = l, w[u].r = r, w[u].len = r - l + 1;
	
	if (l == r) {	
		return ;
	}
	
	ll mid = l + ((r - l) >> 1);
	
	build(u * 2, l, mid), build(u * 2 + 1, mid + 1, r);
	
	pushup(u);
}

ll query(ll u, ll l, ll r) {
	if (l <= w[u].l && w[u].r <= r) {
		return w[u].sum;
	} 
	
	ll mid = w[u].l + ((w[u].r - w[u].l) >> 1), res = 0;
	
	if (l <= mid) {
		res += query(u * 2, l, r);
	}
	
	if (r > mid) {
		res += query(u * 2 + 1, l, r);
	}
	
	return res;
}

void update(ll u, ll l, ll x) {
	if (w[u].l == w[u].r && w[u].l == l) {
		w[u].sum += x; return ;
	}
	
	ll mid = w[u].l + ((w[u].r - w[u].l) >> 1);

	if (l <= mid) {
		update(u * 2, l, x);
	} else {
		update(u * 2 + 1, l, x);
	}

    pushup(u);
}

int main() {
//	ios::sync_with_stdio(false);
//	cin.tie(0), cout.tie(0);

	for (ll i = 1; i <= n; i++) {
		a[i] = read(); 
	}
	
	for (ll i = 1; i <= m; i++) {
		ll l = read(), r = read(), x = read();
		
		ask[i] = (node) {l, r, x, i};
		
		mx = max(mx, x);
	}
	
	for (ll i = 1; i <= n; i++) {
		ask[i + m] = (node) {i, i, a[i]};
	}
	
	sort(ask + 1, ask + n + m + 1);
	
	build(1, 1, 2000000);
	
	ll now = 1;
	
	for (ll i = 1; i <= n + m; i++) {
		if (ask[i].id) c[ask[i].id] = i;
	}
	
	while (now <= n + m && ask[now].x <= mx) {
		if (!ask[now].id) {
			update(1, ask[now].l, 1);
		} else {
			ask[now].ans = query(1, ask[now].l, ask[now].r);
		}
		
		now ++;
	}
	
	for (ll i = 1; i <= m; i++) {
		write(ask[c[i]].ans), puts(""); 
	}

	return 0;
} 

线段树上二分

例题 1

给出一个长度为 \(n\) 的序列 \(\{a_i\}\)\(q\) 次操作,每次或询问 \([l, r]\) 中第一个 \(a_i \ge x\) 的位置 \(i\),若不存在,输出 -1;或进行区间加。

数据范围:\(1 \le n, q \le 5 \times 10 ^ 5, 1 \le a_i, x \le 10 ^ 9\)

\(\texttt{Solution}\)

可以发现,由于我们查找的是第一个位置,所以,其要么在左子树,要么在右子树(这是一句正确的废话,但是有点用)。

所以,我们维护一个区间最大值,然后对区间进行分讨,情况如下:

  • w[u].l == w[u].r,即该节点为叶子节点,那么我们判断该节点的值是否 \(\ge x\),是,就返回 w[u].l,反之返回 -1
  • 否则,若 w[u * 2].mx >= x,那么答案就在左子树中,向其递归,反之向右子树递归。
  • 代码比较简单就不放了

基本势能线段树

区间取模

首先我们知道,当 \(x < y\) 时,\(x \bmod y = x\),并且也可以发现,当 \(x >y\) 时,\(x \bmod y \lt \dfrac{x}{2}\),可以感性理解一下,因为当 \(y\) 增大时,\(x \bmod y\) 一定是变小的,因为取模本质上是一个减法的过程。当 \(y\) 变小的时候,\(x \bmod y\) 也是变小,因为 \(x \bmod y < y\),所以 \(y\)\(\lfloor \dfrac{x}{2} \rfloor\) 或者 \(\lfloor \dfrac{x}{2} \rfloor + 1\) 的时候最大。

综上所述,一个数 \(a_i\) 最多经过 \(\lceil \log_2 a_i \rceil\) 次取模操作(需满足模数 \(m \lt a_i\))必会变为 \(1\)

所以只需要维护一个区间最大值,对于一个 \(\forall l \le i \le r, a_i \rightarrow a_i \bmod m\) 的操作,我们只需要判断是否有 w[u].mx < m,若是,直接返回,因为无需修改;反之继续递归至子节点直到递归至叶节点,并暴力修改。

时间复杂度 \(O(q \log n \log \max a_i)\)

代码有空会补。

附:对我们关于取模所推断结论的测试结果

区间开根号

这部分比较好懂。

假设 \(1 \le a_i \le 10 ^ {12}\),那么 \(a_i\) 的开根历程如下:

\[10 ^ {6} \rightarrow 10 ^ 3 \rightarrow 10 ^ 2 \rightarrow 10 ^ 1 \rightarrow 10 \rightarrow [3, 4] \rightarrow [1, 2] \rightarrow 1 \]

只需 \(7\) 次就能开到 \(1\)

所以仍然是维护一个区间最大值,当 w[u].mx <= 1 时返回,反之继续递归并暴力修改。

时间复杂度 \(O(q \log n \log \log \max a_i)\)

code :

#include <bits/stdc++.h>
#define ll long long
#define db double
#define endl "\n"

using namespace std;

ll n, m, a[100005], l, r;

struct segment {
    ll l, r, sum, mx;
} w[400005] ;

void pushup(ll u) {
    w[u].sum = w[u * 2].sum + w[u * 2 + 1].sum;
    w[u].mx = max(w[u * 2].mx, w[u * 2 + 1].mx);
}

void build(ll u, ll l, ll r) {
    w[u].l = l, w[u].r = r;
    
    if (l == r) {
        w[u].sum = w[u].mx = a[l];
        
        return ;
    }
    
    ll mid = l + ((r - l) >> 1);
    
    build(u * 2, l, mid), build(u * 2 + 1, mid + 1, r);
    
    pushup(u);
}

ll query(ll u) {
    if (l <= w[u].l && w[u].r <= r) {
        return w[u].sum;
    }
    
    ll mid = w[u].l + ((w[u].r - w[u].l) >> 1), res = 0;
    
    if (l <= mid) {
        res += query(u * 2);
    }
    
    if (r > mid) {
        res += query(u * 2 + 1);
    }
    
    return res;
}

void update(ll u) {
    if (w[u].mx <= 1) {
        return ;
    }
    
    if (w[u].l == w[u].r) {
        w[u].sum = sqrt(w[u].sum), w[u].mx = sqrt(w[u].mx);
        
        return ;
    }
    
    ll mid = w[u].l + ((w[u].r - w[u].l) >> 1);
    
    if (l <= mid) {
        update(u * 2);
    }
    
    if (r > mid) {
        update(u * 2 + 1);
    }
    
    pushup(u);
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    
    cin >> n;
    
    for (ll i = 1; i <= n; i++) cin >> a[i];
    
    build(1, 1, n), cin >> m;
    
    while (m--) {
        ll opt; cin >> opt >> l >> r;
        
        if (l > r) swap(l, r);
        
        if (opt == 0) {
            update(1);
        } else {
            cout << query(1) << endl;
        }
    }
}

区间取因数个数

这一类操作即令 \(\forall l \le i \le r, a_i = d(a_i)\),其中 \(d(x)\) 即为 \(x\) 的正因数个数。

可以发现在区间最大值 \(\le 2\) 的时候不用修改,然后预处理一下值域范围内的 \(d(x)\) 即可。

做完了。

code :

#include<bits/stdc++.h> 
#define endl '\n'
using namespace std;
typedef long long ll;
typedef long double ld;
const double eps = 1e-6;
const ll N = 3e5 + 10;
const ll M = 1e6;
const ll INF = 1e18+10;
const ll mod = 1e9+7;
#define ywh666 std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
#define all(a) a.begin(),a.end()
struct node{
	int l, r, mx;
	ll sum;
}tree[4 * N];
int a[M + 7];
int b[N];
void push_up(int id){
	tree[id].mx = max(tree[id << 1].mx, tree[id << 1 | 1].mx);
	tree[id].sum = tree[id << 1].sum + tree[id << 1 | 1].sum;
}
void build(int id, int l, int r){
	tree[id].l = l;
	tree[id].r = r;;
	if(l == r){
		tree[id].sum = b[l];
		tree[id].mx = b[l];
		return ;
	}
	int mid = (l + r) >> 1;
	build(id << 1, l, mid);
	build(id << 1 | 1, mid + 1, r);
	push_up(id);
}
void modify(int id, int l, int r){
	int L = tree[id].l;
	int R = tree[id].r;
	if(tree[id].mx <= 2) return;
	if(L == R){
		tree[id].sum = a[tree[id].sum];
		tree[id].mx = tree[id].sum;
		return;
	}
	int mid = (L + R) >> 1;
	if(l <= mid) modify(id << 1, l, r);
	if(r > mid) modify(id << 1 | 1, l, r);
	push_up(id);
}
ll qurry(int id, int l, int r){
	int L = tree[id].l;
	int R = tree[id].r;
	if(L >= l && R <= r) return tree[id].sum;
	ll sum = 0;
	if(tree[id << 1].r >= l) sum += qurry(id << 1, l, r);
	if(tree[id << 1 | 1].l <= r) sum += qurry(id << 1 | 1, l, r);
	return sum;
}

int main(){
	ywh666;
	for(int i = 1 ; i <= M ; i ++){
		for(int j = i; j <= M ; j += i){
			a[j] ++;
		}
	}
	int n, m;
	cin >> n >> m;
	for(int i = 1 ; i <= n ; i ++) cin >> b[i];
	build(1, 1, n);
	while(m --){
		int op, l, r;
		cin >> op >> l >> r;
		if(op == 1){
			modify(1, l, r);
		}else{
			cout << qurry(1, l, r) << endl;
		}
	}

	return 0 ;
}

区间按位与

先写代码吧,讲评有空会补上。

code :

#include<bits/stdc++.h> 
#define endl '\n'
using namespace std;
typedef long long ll;
typedef long double ld;
const double eps = 1e-9;
const ll N = 4e5 + 10;
const ll INF = 1e18+10;
const ll mod = 1e9+7;
#define ywh666 std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
#define all(a) a.begin(),a.end()
struct node{
	int l, r, orsum,mx;
}tree[N << 2];
int a[N];
void push_up(int id){
	tree[id].mx = max(tree[id << 1].mx, tree[id << 1 | 1].mx);
	tree[id].orsum = tree[id << 1].orsum | tree[id << 1 | 1].orsum;
}
void build(int id, int l, int r){
	tree[id].l = l;
	tree[id].r = r;
	if(l == r){
		tree[id].mx = a[l];
		tree[id].orsum = a[l];
		return;
	}
	int mid = (l + r) >> 1;
	build(id << 1, l, mid);
	build(id << 1 | 1, mid + 1, r);
	push_up(id);
}
void modify(int id, int l, int r, int x){
	if((tree[id].orsum & x) == tree[id].orsum) return ;
	if(tree[id].l == tree[id].r){
		tree[id].mx &= x;
		tree[id].orsum &= x;
		return;
	}
	if(tree[id << 1].r >= l) modify(id << 1, l, r, x);
	if(tree[id << 1 | 1].l <= r) modify(id << 1 | 1, l, r, x);
	push_up(id);
}
void change(int id, int x, int v){
	if(tree[id].l == tree[id].r){
		tree[id].mx = v;
		tree[id].orsum = v;
		return ;
	}
	if(tree[id << 1].r >= x) change(id << 1, x, v);
	if(tree[id << 1 | 1].l <= x) change(id << 1 | 1, x, v);
	push_up(id);
}
int qurry(int id, int l, int r){
	if(tree[id].l >= l && tree[id].r <= r) return tree[id].mx;
	int val = -1;
	if(tree[id << 1].r >= l) val = max(val, qurry(id << 1, l, r));
	if(tree[id << 1 | 1].l <= r) val = max(val, qurry(id << 1 | 1, l, r));
	return val;
}
int main(){
	ywh666;
	int n, q ;
	cin >> n >> q;
	for(int i = 1 ; i <= n ; i ++) cin >> a[i];
	build(1, 1, n);
	while(q --){
		string s;
		cin >> s;
		if(s == "AND"){
			int l, r, x;
			cin >> l >> r >> x;
			modify(1, l, r, x);
		}else if(s == "UPD"){
			int x, v;
			cin >> x >> v;
			change(1, x, v);
		}else{
			int l, r;
			cin >> l >> r;
			cout << qurry(1, l, r) << endl;
		}
	}
	return 0 ;
}

区间乘区间询问欧拉函数和

利用性质就很一眼了。

code :

#include<bits/stdc++.h> 
#define endl '\n'
using namespace std;
typedef long long ll;
typedef long double ld;
const double eps = 1e-9;
const ll N = 1e5 + 10;
const ll INF = 1e18+10;
const ll mod = 998244353;
const ll maxm = 110;
#define ywh666 std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
#define all(a) a.begin(),a.end()
struct node{
	int l, r;
	ll sum, lz;
	bitset<30> bt;
}tree[N << 2];
int a[N], phi[maxm];
bitset<30> sat[maxm];
int bh[maxm];
int bh2[maxm];
void init(){
	bh[2] = 1;
	bh[3] = 2;
	bh2[1] = 2;
	bh2[2] = 3;
	int st = 3;
	for(int i = 4; i <= 100 ; i ++){
		bool f = 1;
		for(int j = 2 ; j * j <= i ; j ++){
			if(i % j == 0){
				f = 0;
				break;
			}
		}
		if(f){
			bh[i] = st ;
			bh2[st] = i;
			st ++;
		}
	}
	for(int i = 2; i <= 100 ; i ++){
		if(bh[i] != 0){
			for(int j = i ; j <= 100 ; j += i){
				sat[j][bh[i]] = 1;
			}
		}
	}

}
void euler(int n = 100){
	for(int i = 2 ; i <= n ; i ++) phi[i] = i;
	for(int i = 2 ; i <= n ; i ++){
		if(phi[i] == i){
			for(int j = i ; j <= n ; j += i){
				phi[j] = phi[j] / i * (i - 1);
			}
		}
	}
	phi[1] = 1;
}
void push_up(int id){
	tree[id].bt = tree[id << 1].bt & tree[id << 1 | 1].bt;
	tree[id].sum = tree[id << 1].sum  + tree[id << 1 | 1].sum ;
	tree[id].sum %= mod;
}
void push_down(int id){
	tree[id << 1].sum =tree[id << 1].sum * tree[id].lz  % mod ;
	tree[id << 1 | 1].sum =tree[id << 1 | 1].sum * tree[id].lz % mod ;
	tree[id << 1].lz = tree[id << 1].lz * tree[id].lz % mod;
	tree[id << 1 | 1].lz =tree[id << 1 | 1].lz * tree[id].lz % mod;
	tree[id].lz = 1;
}

void build(int id, int l, int r){
	tree[id].l = l;
	tree[id].r = r;
	tree[id].lz = 1;
	if(l == r){
		tree[id].sum = phi[a[l]];
		tree[id].bt = sat[a[l]];
		return;
	}
	int mid = (l + r) >> 1;
	build(id << 1, l, mid);
	build(id << 1 | 1, mid + 1, r);
	push_up(id);
}

void modify(int id, int l, int r, int x){
	if(tree[id].l >= l && tree[id].r <= r){
		if(tree[id].bt[bh[x]]){
			tree[id].lz = 1ll * tree[id].lz * x % mod;
			tree[id].sum = 1ll * tree[id].sum * x % mod;
			return;
		}
		if(tree[id].l == tree[id].r){
			tree[id].lz = 1ll * tree[id].lz * (x - 1) % mod;
			tree[id].sum = 1ll * tree[id].sum * (x - 1) % mod;
			tree[id].bt[bh[x]] = 1;
			return;
		}
	}
	push_down(id);
	if(tree[id << 1].r >= l) modify(id << 1, l, r, x);
	if(tree[id << 1 | 1].l <= r) modify(id << 1 | 1, l, r, x);
	push_up(id);
}
int qurry(int id, int l, int r){
	if(tree[id].l >= l && tree[id].r <= r) return tree[id].sum % mod;
	ll val = 0;
	push_down(id);
	if(tree[id << 1].r >= l) val += qurry(id << 1, l, r);
	if(tree[id << 1 | 1].l <= r) val += qurry(id << 1 | 1, l, r);
	return val % mod;
}
int main(){
	ywh666;
	init();
	euler();
	int n, q ;
	cin >> n >> q;
	for(int i = 1 ; i <= n ; i ++) cin >> a[i];
	build(1, 1, n);
	while(q --){
		int op;
		cin >> op;
		if(op == 0){
			int l, r, x;
			cin >> l >> r >> x;
			while(x != 1){
				int nn = x;
				for(int i = 1; i <= 29 ; i ++){
					if(sat[x][i]== 1){
						modify(1, l, r, bh2[i]);
						nn /= bh2[i];
					}
				}
				x = nn;
			}
		}else{
			int l, r;
			cin >> l >> r;
			cout << qurry(1, l, r) % mod << endl;
		}
	}
	return 0 ;
}

区间和区间取 \(\gcd\)

给定一个长为 \(n\) 的序列 \(a\),有 \(m\) 次操作。

每次有两种操作:

1 l r x:对于区间 \([l,r]\) 内所有 \(i\),将 \(a_i\) 变成 \(\gcd(a_i,x)\)

2 l r:查询区间 \([l,r]\) 的和,答案对 \(2^{32}\) 取模后输出。

对于 \(100\%\) 的数据,满足 \(1\le n\le 2\cdot 10^5,1\le m\le 5 \cdot 10^5\),所有数值为 \([1,10^{18}]\) 内的整数。

\(\texttt{Solution}\)

势能线段树板子题。

每次 \(\gcd\)\(a_i\) 的值至少除以 \(2\),最多 \(\log\) 次就会变成 \(1\)。所以每次只操作在更新后会被变化作的 \(a_i\) 总的操作次数均摊是 \(\log\) 的。下面考虑用势能线段树去维护。

\(\text{lcm}_{i=l}^r a_i=A\),则一段区间内任意 \(a_i\) 都不会被修改当且仅当 \(x \equiv 0\pmod A\)

可是 \(A\) 显然已经超出了我们能存储的范围。但由于 \(x\not=0\),则当 \(A\) 足够大时上式不可能成立,直接将 \(A\) 设为一个极大的值即可。

code :

#include<bits/stdc++.h>
using namespace std;
const int NN=2e5+4;
typedef long long ll;
typedef unsigned int ui;
ll a[NN];
struct segment_tree
{
	int l,r;
	ui v;
	ll lcm;
}tr[NN<<2];
void pushup(int u)
{
	tr[u].v=tr[u<<1].v+tr[u<<1|1].v;
	tr[u].lcm=min((__int128)2e18,(__int128)tr[u<<1].lcm*tr[u<<1|1].lcm/__gcd(tr[u<<1].lcm,tr[u<<1|1].lcm));
}
void build(int u,int l,int r)
{
	tr[u]={l,r,0,0};
	if(tr[u].l==tr[u].r)
	{
		tr[u].v=tr[u].lcm=a[l];
		return;
	}
	int mid=l+(r-l)/2;
	build(u<<1,l,mid);
	build(u<<1|1,mid+1,r);
	pushup(u);
}
void modify(int u,int l,int r,ll v)
{
	if(!(v%tr[u].lcm))
		return;
	if(tr[u].l==tr[u].r)
	{
		tr[u].v=tr[u].lcm=__gcd(tr[u].lcm,v);
		return;
	}
	int mid=tr[u].l+(tr[u].r-tr[u].l)/2;
	if(l<=mid)
		modify(u<<1,l,r,v);
	if(r>mid)
		modify(u<<1|1,l,r,v);
	pushup(u);
}
ui query(int u,int l,int r)
{
	if(tr[u].l>=l&&tr[u].r<=r)
		return tr[u].v;
	ui res=0;
	int mid=tr[u].l+(tr[u].r-tr[u].l)/2;
	if(l<=mid)
		res+=query(u<<1,l,r);
	if(r>mid)
		res+=query(u<<1|1,l,r);
	return res;
}
int main()
{
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		scanf("%lld",&a[i]);
	build(1,1,n);
	while(m--)
	{
		int opt;
		scanf("%d",&opt);
		if(opt==1)
		{
			int l,r;
			ll v;
			scanf("%d%d%lld",&l,&r,&v);
			modify(1,l,r,v);
		}
		else
		{
			int l,r;
			scanf("%d%d",&l,&r);
			printf("%u\n",query(1,l,r));
		}
	}
	return 0;
}

2021杭电中超(8)D Counting Stars (HDU 7059

题目大意

同样是求区间和,给定两种区间修改操作

  1. 询问区间和
  2. 区间l到r所有值加上其highbit
  3. 区间l到r所有值减去其lowbit

解题思路

与黑龙江省赛大同小异,可以看出对于每个值其位权1的个数是一定的,而每进行一次操作3其数量会减一,操作2不会增加其数量。于是使用cnt来记录当前区间最多的位权1数量,若cnt为0,则不需要再修改。注意将每个数的hibit和剩下值分开存储,这样操作2就变成纯粹的区间乘操作,更好维护。

code :

#include <iostream>
#include <fstream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cstring>
#include <queue>
#include <stack>
#include <vector>
#include <map>
#include <set>
#pragma GCC optimize(2)
#pragma GCC optimize("Ofast","inline","-ffast-math")
#pragma GCC optimize(3)
#pragma GCC optimize(3,"Ofast","inline")
#pragma G++ optimize(2)
#pragma G++ optimize("Ofast","inline","-ffast-math")
#pragma G++ optimize(3)
#pragma G++ optimize(3,"Ofast","inline")
#pragma warning(disable:4996)
#define inr register int
#define ios ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define debug(a) cout << #a << " " << a << endl
using namespace std;
typedef long long ll;
const double pi = acos(-1.0);
const double eps = 1e-8;
const int inf = 0x3f3f3f3f;
const int maxn = 1000007;//1e5+7 
const ll mod = 998244353;//1e9+7

#define lson (o<<1)
#define rson (o<<1|1)

ll arr[maxn];
ll m2[maxn];

inline ll lowbit(ll x)
{
    return x & (-x);
}

inline ll hbit(ll x)
{
    for (inr i = 30; i >= 0; i--) {
        if (x >> i & 1) {
            return 1 << i;
        }
    }
}

inline void init()
{
    m2[0] = 1;
    for (inr i = 1; i < maxn; i++) {
        m2[i] = (m2[i - 1] * 2) % mod;
    }
}

struct node {
    ll hb, lb, cnt, lazy;
}tree[maxn << 2];

inline void pushup(int o)
{
    tree[o].lb = (tree[lson].lb + tree[rson].lb) % mod;
    tree[o].hb = (tree[lson].hb + tree[rson].hb) % mod;
    tree[o].cnt = max(tree[lson].cnt, tree[rson].cnt);
}

inline void pushdown(int o)
{
    if (tree[o].lazy) {
        tree[lson].lazy += tree[o].lazy;
        tree[rson].lazy += tree[o].lazy;
        tree[lson].hb = (tree[lson].hb * m2[tree[o].lazy]) % mod;
        tree[rson].hb = (tree[rson].hb * m2[tree[o].lazy]) % mod;//hb??lb 
        tree[o].lazy = 0;
    }
}

inline void build(int o, int l, int r)
{
    tree[o] = { 0,0,0,0 };//??????? 
    if (l == r) {
        tree[o].hb = hbit(arr[l]);
        tree[o].lb = arr[l] - tree[o].hb;
        tree[o].cnt = __builtin_popcount(arr[l]);
        return;
    }
    int mid = (l + r) >> 1;
    build(lson, l, mid);
    build(rson, mid + 1, r);
    pushup(o);
}

inline void modify(int o, int l, int r, int ml, int mr)
{
    if (!tree[o].cnt) {
        return;
    }
    if (l == r) {
        if (tree[o].cnt > 1) {
            tree[o].lb -= lowbit(tree[o].lb);
            tree[o].cnt--;;
        }
        else {
            tree[o].hb = tree[o].cnt = 0;
        }
        return;
    }
    pushdown(o);
    int mid = (l + r) >> 1;
    if (ml <= mid) {
        modify(lson, l, mid, ml, mr);
    }
    if (mid + 1 <= mr) {
        modify(rson, mid + 1, r, ml, mr);
    }
    pushup(o);
}

inline void update(int o, int l, int r, int ul, int ur)
{
    if (!tree[o].cnt) {
        return;
    }
    if (ul <= l && r <= ur) {
        tree[o].hb = tree[o].hb * 2 % mod;
        tree[o].lazy += 1;
        return;
    }
    pushdown(o);
    int mid = (l + r) >> 1;
    if (ul <= mid) {
        update(lson, l, mid, ul, ur);
    }
    if (mid + 1 <= ur) {
        update(rson, mid + 1, r, ul, ur);
    }
    pushup(o);
}

inline ll query(int o, int l, int r, int ql, int qr)
{
    if (!tree[o].cnt) {
        return 0ll;
    }
    if (ql <= l && r <= qr) {
        return tree[o].hb + tree[o].lb;
    }
    pushdown(o);
    int mid = (l + r) >> 1;
    ll res = 0;
    if (ql <= mid) {
        res = (res + query(lson, l, mid, ql, qr)) % mod;
    }
    if (mid + 1 <= qr) {//qr??r 
        res = (res + query(rson, mid + 1, r, ql, qr)) % mod;
    }
    return res;
}

int main()
{
    int T, n, m;
    init();
    scanf("%d", &T);
    while (T--) {
        scanf("%d", &n);
        for (int i = 1; i <= n; i++) {
            scanf("%lld", arr + i);
        }
        scanf("%d", &m);
        build(1, 1, n);
        int opt, opl, opr;
        while (m--) {
            scanf("%d%d%d", &opt, &opl, &opr);
            if (opt == 1) {
                printf("%lld\n", query(1, 1, n, opl, opr));
            }
            else if (opt == 2) {
                modify(1, 1, n, opl, opr);
            }
            else {
                update(1, 1, n, opl, opr);
            }
        }

    }
    return 0;
}

本文作者:Laiyiwen_01

本文链接:https://www.cnblogs.com/Laiyiwen-01/p/18716988

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Laiyiwen_01  阅读(4)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起