线段树
线段树本质上是一个二分区间的结构。或二分下标,或二分值域,又时也能二分时间轴。他们都有相应的思想、目的、适用场景。而利用线段树对区间划分的特性,可以使用它来维护各种信息,因此线段树具有着众多分支,但它们的本质相同。
作者并不擅长线段树的使用,故进行一些专项练习。
T1
题意:给定一个长度为 \(n\) 的小写字母串,有 \(m\) 次询问:区间 \([l,r]\) 中的子母如果能重组得到一个回文串,就保留修改。问最后的字符串。\(1\leq n,m\leq 10^5\)。请使用文件读写,从 "input.txt" 内读入,"output" 中输出。
Solution
考虑对每个子母开一个线段树,维护区间内这个子母出现过多少次。修改时,暴力记录每个子母有多少个,记作 \(b_i\)。如果有超过 \(1\) 个子母个数为奇数,就不可能组成回文串;如果有一个,就把那个子母放在正中间。其余部分按照子母序两边暴力放就行了。这是个明显的区间赋值操作,打个 tag 就行了。时间复杂度 \(O(26(n + m)\log n)\)。
\(\\\)
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>
#include <fstream>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 1e5 + 1, kA = 26;
ifstream fin("input.txt");
ofstream fout("output.txt");
int n, m;
string s;
struct Segment {
struct Node {
int w, t;
} t[4 * kN];
void PU(int x) { t[x].w = t[2 * x].w + t[2 * x + 1].w; }
void PD(int x, int l, int r) {
if (t[x].t == -1) {
return;
}
t[2 * x].t = t[2 * x + 1].t = t[x].t;
int m = (l + r) / 2;
t[2 * x].w = t[x].t * (m - l + 1);
t[2 * x + 1].w = t[x].t * (r - m);
t[x].t = -1;
}
void B(int c, int x = 1, int l = 1, int r = n) {
t[x] = {0, -1};
if (l == r) {
t[x].w = (s[l] - 'a' == c);
return;
}
int m = (l + r) / 2;
B(c, 2 * x, l, m);
B(c, 2 * x + 1, m + 1, r);
PU(x);
}
void U(int L, int R, int w, int x = 1, int l = 1, int r = n) {
if (r < L || R < l) {
return;
} else if (L <= l && r <= R) {
t[x].t = w;
t[x].w = w * (r - l + 1);
return;
}
int m = (l + r) / 2;
PD(x, l, r);
U(L, R, w, 2 * x, l, m);
U(L, R, w, 2 * x + 1, m + 1, r);
PU(x);
}
int Q(int L, int R, int x = 1, int l = 1, int r = n) {
if (r < L || R < l) {
return 0;
} else if (L <= l && r <= R) {
return t[x].w;
}
int m = (l + r) / 2;
PD(x, l, r);
return Q(L, R, 2 * x, l, m) + Q(L, R, 2 * x + 1, m + 1, r);
}
} t[26];
int b[kA], o, p;
int main() {
fin.tie(0)->sync_with_stdio(0);
fin >> n >> m;
fin >> s, s = '#' + s;
for (int i = 0; i < kA; i++) {
t[i].B(i);
}
for (int _ = 1, l, r; _ <= m; _++) {
fin >> l >> r;
fill_n(b, kA, 0), o = 0;
for (int i = 0; i < kA; i++) {
b[i] = t[i].Q(l, r);
if (b[i] % 2 == 1) {
o++, p = i;
}
}
if (o > 1) {
continue;
}
for (int i = 0; i < kA; i++) {
t[i].U(l, r, 0);
}
if (o == 1) {
int m = (l + r) / 2;
t[p].U(m, m, 1), b[p]--;
}
for (int i = 0; i < kA; i++) {
if (!b[i]) {
continue;
}
t[i].U(l, l + b[i] / 2 - 1, 1);
t[i].U(r - b[i] / 2 + 1, r, 1);
l += b[i] / 2, r -= b[i] / 2;
}
}
for (int i = 1; i <= n; i++) {
for (int c = 0; c < kA; c++) {
if (t[c].Q(i, i)) {
fout << char(c + 'a');
break;
}
}
}
return 0;
}
T2
题意:给定一个大小为 \(n\) 的序列 \(h\),进行 \(q\) 次操作:单点修改值;在 \(n\) 个数中选择其中几个,并把值 \(v\) 任意分配加到这些数中(可以不是整数),问这些数可能的最小最大值,不保留更改。\(1\leq n,q\leq 10^5\)
Solution
最大值最小,一眼二分答案。你发现我选择的这几个数一定在值域区间 \([1, r]\) 内全选。于是用权值线段树维护一下值域区间内有多少个数、这些数的和。然后暴力二分最大值。时间复杂度 \(O(n\log^2 V)\),足以通过此题。
但是作者认为这不够美丽。因为权值线段树本身就是一个按权值二分的 DS,可以直接线段树上二分。但是,直接二分需要支持实数域的线段树上二分,更不优雅了。考虑改变二分对象:你发现一定是把选中的数都加到一个定值,然后平均分,否则会浪费选中数数量。因此可以二分这个定值,计算出答案。考虑到无论何时序列中的数都是整数,就可以直接整数域二分了。时间复杂度 \(O(n\log V)\)。
\(\\\)
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>
#include <iomanip>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 1e5 + 1;
int n, q;
int h[kN];
struct Node {
int l, r, sz;
LL w;
} t[50 * kN];
int rt, tot;
void U(int h, int w = 1, int &x = rt, int l = 0, int r = 1e9) {
if (!x) {
x = ++tot;
}
t[x].sz += w, t[x].w += 1ll * h * w;
if (l == r) {
return;
}
int m = (l + r) / 2;
if (h <= m) {
U(h, w, t[x].l, l, m);
} else {
U(h, w, t[x].r, m + 1, r);
}
}
LL sz, ans;
void Q(LL lm, int x = 1, int l = 0, int r = 1e9) {
if (!x) {
return;
} else if (l == r) {
sz += t[x].sz, ans += t[x].w;
return;
}
int m = (l + r) / 2;
LL f = (t[t[x].l].sz + sz) * (m + 1) - (t[t[x].l].w + ans);
if (f > lm) {
Q(lm, t[x].l, l, m);
} else {
sz += t[t[x].l].sz, ans += t[t[x].l].w;
Q(lm, t[x].r, m + 1, r);
}
}
int main() {
cin.tie(0)->sync_with_stdio(0);
cin >> n >> q;
for (int i = 1; i <= n; i++) {
cin >> h[i];
U(h[i]);
}
for (int _ = 1, o; _ <= q; _++) {
cin >> o;
if (o == 1) {
int l, r;
cin >> l >> r;
U(h[l], -1), h[l] = r, U(r);
} else {
LL l;
cin >> l;
sz = ans = 0, Q(l);
cout << fixed << setprecision(5) << 1.0 * (l + ans) / sz << '\n';
}
}
return 0;
}
T3
题意:给定一个长度为 \(n\) 的字符串 \(s\),将进行 \(m + k\) 次操作:区间赋值、区间询问是否存在长度为 \(d\) 的循环节(可以是混周期,最后一个周期可以不全)。\(1\leq n, m + k\leq 10^5\)。
Solution
这里有一个经典的字符串结论,即 “有长度为 \(n - d\) 的 border” 和 “有长度为 \(d\) 的循环节” 互为充要条件。
Proof
首先由后者推出前者是显然可行的,这里试证前者推后者。
若 \(n - d > \frac{n}{2}\),即 border 存在重叠部分。设前面、后面的 border 为区间 \(s[1, i]\) 和 \(s(j, n]\)。由 border 定义有 \(s[1, j] = s(j, 2j], s(j, 2j] = s(2j, 3j] \cdots\) 如此反复,最后会剩下小于 \(j\) 个字符:如果他是后缀的末尾,那么一定也被前缀的末尾匹配,而前缀的末尾又一定是一个循环节的前缀。
若 \(n - d \leq \frac{n}{2}\),直接把 \(s[1, j]\) 当作循环节。因为 \(s[1, i] = s[j, n]\),所以 \(s[j, n]\) 和循环节的一个前缀相等,显然成立。
于是可以考虑开个线段树维护哈希值,需要支持区间赋值和区间查询哈希值。难点和细节点在于哈希值的转移、合并。可以预处理下 base 的次幂的前缀和,以便区间赋值时迅速算出新哈希值。
\(\\\)
Code
// stODDDDDDDDDDDDDDDDDDDDDDDDDDD hzt CCCCCCCCCCCCCCCCCCCCCCCCCOrz
#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using PII = pair<int, int>;
using LL = long long;
constexpr int kN = 1e5 + 1, kB = 131, kP = 777777773;
int n, m, k;
string s;
LL p[kN], sp[kN];
struct Node {
LL f;
char t;
} t[4 * kN];
void PU(int x, int l, int r) {
int m = (l + r) / 2;
t[x].f = (t[2 * x].f * p[r - m] % kP + t[2 * x + 1].f) % kP;
}
void PD(int x, int l, int r) {
if (t[x].t != -1) {
int m = (l + r) / 2;
t[2 * x].f = t[x].t * sp[m - l] % kP;
t[2 * x + 1].f = t[x].t * sp[r - m - 1] % kP;
t[2 * x].t = t[2 * x + 1].t = t[x].t;
t[x].t = -1;
}
}
void B(int x = 1, int l = 1, int r = n) {
t[x].t = -1;
if (l == r) {
t[x].f = s[l];
return;
}
int m = (l + r) / 2;
B(2 * x, l, m);
B(2 * x + 1, m + 1, r);
PU(x, l, r);
}
void U(int L, int R, char c, int x = 1, int l = 1, int r = n) {
if (R < l || r < L) {
return;
} else if (L <= l && r <= R) {
t[x].f = c * sp[r - l] % kP;
t[x].t = c;
return;
}
int m = (l + r) / 2;
PD(x, l, r);
U(L, R, c, 2 * x, l, m);
U(L, R, c, 2 * x + 1, m + 1, r);
PU(x, l, r);
}
LL Q(int L, int R, int x = 1, int l = 1, int r = n) {
if (R < l || r < L) {
return 0;
} else if (L <= l && r <= R) {
return t[x].f;
}
int m = (l + r) / 2;
PD(x, l, r);
LL al = Q(L, R, 2 * x, l, m), ar = Q(L, R, 2 * x + 1, m + 1, r);
return (al * p[max(0, min(r, R) - m)] + ar) % kP;
}
int main() {
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m >> k >> s, s = '#' + s;
p[0] = sp[0] = 1;
for (int i = 1; i <= n; i++) {
p[i] = kB * p[i - 1] % kP;
sp[i] = (sp[i - 1] + p[i]) % kP;
}
B();
for (int _ = 1, o, l, r, p; _ <= m + k; _++) {
cin >> o >> l >> r >> p;
if (o == 1) {
U(l, r, p + '0');
} else {
if (Q(l, r - p) == Q(l + p, r)) {
cout << "YES\n";
} else {
cout << "NO\n";
}
}
}
return 0;
}