线段树学习笔记
目录
- 基本线段树
- 线段树的一些奇技淫巧
- 权值线段树
- 线段树上二分
- 基本势能线段树
- 进阶势能线段树(吉司机线段树)
- 李超线段树
- 可持久化线段树
- 树套树
- 线段树优化 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 觉得第一题太水了,不屑于写第一题,所以他又玩起了新的游戏。在游戏中,他发现,这个游戏的伤害计算有一个规律,规律如下:
- 拥有一个伤害串,是一个长度为 \(n\) 的只含字符
0
和字符1
的字符串。规定这个字符串的首字符是第一个字符,即下标从 \(1\) 开始。- 给定一个范围 \([l,r]\),伤害为伤害串的这个范围内中字符
1
的个数。- 会修改伤害串中的数值,修改的方法是把 \([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\) 拆开,即
此时应有一个基本思路:由于当 \(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\) 的开根历程如下:
只需 \(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)
题目大意
同样是求区间和,给定两种区间修改操作
- 询问区间和
- 区间l到r所有值加上其highbit
- 区间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 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具