数据结构杂记 2025.7.17始
数据结构死去的夏天
本篇内容只涉及进阶篇,模板右转洛谷
%%% WTY 巨佬 @starrylasky 太帅啦!!!
加粗字为原题解,其余为补充
同时有个人评分 \(1 \sim 10\),相较其他数据结构题评分,谨慎食用。
1.1 树状数组 \(\textbf{BIT,Binnary Index Tree/FT,Fenwick Tree}\)
1.2 线段树 \(\textbf{Segment Tree}\)
1.2.1 CF803G Periodic RMQ Problem
给你一个整数序列 \(a\),你要执行 \(q\) 次操作,分两种:
1 l r x:将 \(a_l\sim a_r\) 赋值为 \(x\)。2 l r:求 \(a_l\sim a_r\) 中的最小值。我们认为这个问题太简单了,所以不直接给出 \(a\),而是给出长度为 \(n\) 的序列 \(b\),将 \(b\) 复制 \(k\) 份,拼在一起得到 \(a\)。
动态开点线段树解决修改,对于查询线段树上没有的节点直 接在 \(b_n\) 上查就行。
我觉得讲得已经很到位了。
有个细节,动态开点时,节点的值应该怎么求?
这里分三种情况讨论(节点区间表示为 \([l,r]\)):
- \(r-l+1>=n\) 说明覆盖完整 \(b_n\) 数组。
- \((l-1)/n==(r-1)/n\) 说明覆盖 \(b_{(i-1)\%n+1} \sim b_{(r-1)\%n+1}\) 。
- 跨越两块之间,覆盖 \(b_{(i-1)\%n+1} \sim n,1 \sim b_{(r-1)\%n+1}\) 。
于是用线段树维护区间最小值,复杂度为 \(O(q\log^2 nk)\),能过。
但请想一想,还有更快的方法。
由于原数组是静态的,用 \(\textbf{ST表}\) 更快。
复杂度 \(O(q \log nk)\)。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 100,LN = 18;
int n, k, b[N], q, tot, root;
class RMQ {
public:
int s[N][LN];
void Init() {
for (int i = 1; i <= n; i++) s[i][0] = b[i];
int p = log2(n);
for (int k = 1; k <= p; k++) {
for (int i = 1; i + (1 << k) <= n + 1; i++) {
s[i][k] = min(s[i][k - 1], s[i + (1 << (k - 1))][k - 1]);
}
}
}
int Query(int l, int r) {
int len = log2(r - l + 1);
return min(s[l][len], s[r - (1 << len) + 1][len]);
}
int SPquery(int l, int r) {
if (r - l + 1 >= n) return Query(1, n);
if ((l - 1) / n == (r - 1) / n) return Query((l - 1) % n + 1, (r - 1) % n + 1);
return min(Query((l - 1) % n + 1, n), Query(1, (r - 1) % n + 1));
}
}ST;
struct node {
int l, r, tag, mn;
}T[N * 256];
void build(int& u, int l, int r) {
if (u) return;
u = ++tot;
T[u].mn = ST.SPquery(l, r);
}
void pushup(int u) {
T[u].mn = min(T[T[u].l].mn, T[T[u].r].mn);
}
void pushdown(int u, int l, int r) {
int mid = (l + r) >> 1;
build(T[u].l, l, mid), build(T[u].r, mid + 1, r);
if (T[u].tag) {
build(T[u].l, l, mid), build(T[u].r, mid + 1, r);
T[T[u].l].tag = T[T[u].r].tag = T[u].tag;
T[T[u].l].mn = T[T[u].r].mn = T[u].tag;
T[u].tag = 0;
}
}
void change(int& u, int l, int r, int ql, int qr, int v) {
build(u, l, r);
if (ql <= l && r <= qr) {
T[u].tag = T[u].mn = v;
return;
}
pushdown(u, l, r);
int mid = (l + r) >> 1;
if (ql <= mid) change(T[u].l, l, mid, ql, qr, v);
if (qr > mid) change(T[u].r, mid + 1, r, ql, qr, v);
pushup(u);
}
int query(int& u, int l, int r, int ql, int qr) {
build(u, l, r);
if (ql <= l && r <= qr) return T[u].mn;
pushdown(u, l, r);
int mid = (l + r) >> 1, ans = 1e9;
if (ql <= mid) ans = min(ans, query(T[u].l, l, mid, ql, qr));
if (qr > mid) ans = min(ans, query(T[u].r, mid + 1, r, ql, qr));
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> k;
for (int i = 1; i <= n; i++) cin >> b[i];
ST.Init();
build(root, 1, n * k);
cin >> q;
for (int i = 1; i <= q; i++) {
int op, x, y, z;
cin >> op;
if (op == 1) {
cin >> x >> y >> z;
change(root, 1, n * k, x, y, z);
}
else {
cin >> x >> y;
cout << query(root, 1, n * k, x, y) << '\n';
}
}
return 0;
}
评分: \(\huge 5\)
1.2.2 洛谷P9596 [JOI Open 2018] 冒泡排序 2 / Bubble Sort 2
冒泡排序是一个对序列排序的算法。现在我们要将一个长度为 \(N\) 的序列 \(A_0,A_1,\ldots ,A_{N-1}\) 按不降顺序排序。当两个相邻的数没有按正确顺序排列时,冒泡排序会交换这两个数的位置。每次扫描这个序列就进行这种交换。更确切地说,在一趟扫描中,对于 \(i=0,1,\ldots ,N-2\),并按这个顺序,如果 \(A_i>A_{i+1}\),那么我们就交换这两个数的位置。众所周知任何序列经过有限趟扫描后一定可以按非降顺序排好序。对于一个序列 \(A\),我们定义用冒泡排序的扫描趟数为使用如上算法使得 \(A\) 排好序的情况下所扫描的趟数。
JOI 君有一个长度为 \(N\) 的序列 \(A\)。他打算处理 \(Q\) 次修改 \(A\) 的值的询问。更明确地说,在第 \((j+1)\ (0\le j\le Q-1)\) 次询问,\(A_{X_j}\) 的值会变为 \(V_j\)。
JOI 君想知道处理每次修改之后,用冒泡排序的扫描趟数。
对于 \(a_i\) ,每轮扫描会交换必然有一个 \(j<i,a_i > a_j\) 换到 \(a_i\) 后,所以 \(ans = \max_{i=1}^n \{ f_i = \sum_{j=1}^i [a_j > a_i] \}\)。
对 \(i<j,a_j > a_i\) 则有 \(f_i < f_j\) ,即 \(ans\) 一定在后缀最小值处。
考虑维护 \(g_i = \sum_{j=1}^n [a_j > a_i] -(n-i),g_i \le f_i\) ,故 \(ans = \max_{i=1}^n g_i\)。
权值线段树支持区间加,维护最大值即可。
这是啥玩意儿?
听不懂。。。
我只懂每轮扫描有一个比 \(a_i\) 大的数会换到它的前面。
因此有 \(ans = \max_{i=1}^n \{ f_i = \sum_{j=1}^i [a_j > a_i] \}\) 这一句话。
行吧,那就自己推着走。
我们通过瞪眼法得到 \(\textbf{性质1}\),\(f_i\) 表示在 \(a_i\) 之前有多少数比他大,那么在 \(a_i\) 之后又小于 \(a_i\) 的数,\(f_i\) 显然更大更优。
接下来的一步,真的有人想得到吗
将式子 \(f_i\) 变换为 \(F_i = \sum_{j=1}^n [a_j > a_i] - (n-i)\) 。
你会马上说:哎呀这两个式子并不等价啊?
但别着急,我们可以讨论,当 \(a_i\) 以后所有的数都更大,此时恒有 \(F_i = f_i\)。
但是其他情况不满足啊?
别忘了\(\textbf{性质1}\) ,若 \(a_i\) 后有更小的数,我们就不关心 \(a_i\) 了。
于是可以总结为:通过 \(F_i\) 求出的最大值一定是对的。
为了有更良好的代码细节,以及有题解可以贺,我们对 \(F_i\) 变换。
我们需要维护求逆序对,又要动态修改比 \(a_i\) 大的逆序对。
一开始并不知道怎么维护,因为有题解可以贺,所以可以想到使用权值线段树,将权值线段树值域拆成 \(n\) 个节点。
这样朴素权值线段树可以维护逆序对个数,对于比 \(a_i\) 大的数,直接使用区间加减可以记录。
为什么需要拆成 \(n\) 个点?
因为对于每一个比 \(a_i\) 大的值,区间加时都需要 \(+1\),但是你每个值个数不同,区间加很难维护,拆出来每个点只用加一次啦。
屎一样的实现:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 100, INF = 0x7fffffff;
int n, q, tot, num[N];
struct fk {
int first, second;
friend bool operator<(fk a, fk b) {
if (a.first == b.first) return a.second < b.second;
return a.first < b.first;
}
}inp[N], pv[N], lsh[N];
struct node {
int l, r, mx, sum, tag;
}T[N * 4];
void pushup(int u) {
T[u].sum = T[u * 2].sum + T[u * 2 + 1].sum;
T[u].mx = max(T[u * 2].mx, T[u * 2 + 1].mx);
}
void pushdown(int u) {
if (T[u].tag) {
if (T[u * 2].mx != -INF) {
T[u * 2].tag += T[u].tag;
T[u * 2].mx += T[u].tag;
}
if (T[u * 2 + 1].mx != -INF) {
T[u * 2 + 1].tag += T[u].tag;
T[u * 2 + 1].mx += T[u].tag;
}
T[u].tag = 0;
}
}
void build(int u, int l, int r) {
T[u].l = l, T[u].r = r;
if (l == r) {
T[u].mx = -INF;
T[u].sum = 0;
return;
}
int mid = (T[u].l + T[u].r) >> 1;
build(u * 2, l, mid), build(u * 2 + 1, mid + 1, r);
pushup(u);
}
void update(int u, int q, int v) {
if (T[u].l == T[u].r) {
if (v == -INF) T[u].sum = 0;
else T[u].sum = 1;
T[u].mx = v;
return;
}
pushdown(u);
int mid = (T[u].l + T[u].r) >> 1;
if (q <= mid) update(u * 2, q, v);
else update(u * 2 + 1, q, v);
pushup(u);
}
void sequpdate(int u, int ql, int qr, int v) {
if (ql <= T[u].l && T[u].r <= qr) {
if (T[u].mx == -INF) return;
T[u].mx += v;
T[u].tag += v;
return;
}
pushdown(u);
int mid = (T[u].l + T[u].r) >> 1;
if (ql <= mid) sequpdate(u * 2, ql, qr, v);
if (qr > mid) sequpdate(u * 2 + 1, ql, qr, v);
pushup(u);
}
int query(int u, int q) {
if (T[u].l == T[u].r) return T[u].sum;
pushdown(u);
int mid = (T[u].l + T[u].r) >> 1;
if (q <= mid) return query(u * 2, q);
else return T[u * 2].sum + query(u * 2 + 1, q);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> q;
for (int i = 1; i <= n; i++) {
cin >> inp[i].first;
inp[i].second = i;
lsh[++tot] = inp[i];
}
for (int i = 1; i <= q; i++) {
cin >> pv[i].second >> pv[i].first;
pv[i].second++;
lsh[++tot] = pv[i];
}
sort(lsh + 1, lsh + tot + 1);
build(1, 1, tot);
for (int i = 1; i <= n; i++) {
num[inp[i].second] = lower_bound(lsh + 1, lsh + tot + 1, inp[i]) - lsh;
update(1, num[inp[i].second], i - query(1, num[inp[i].second] - 1) - 1);
sequpdate(1, num[inp[i].second] + 1, tot, -1);
}
for (int i = 1; i <= q; i++) {
update(1, num[pv[i].second], -INF);
sequpdate(1, num[pv[i].second] + 1, tot, 1);
num[pv[i].second] = lower_bound(lsh + 1, lsh + tot + 1, pv[i]) - lsh;
update(1, num[pv[i].second], pv[i].second - query(1, num[pv[i].second] - 1) - 1);
sequpdate(1, num[pv[i].second] + 1, tot, -1);
cout << T[1].mx << '\n';
}
return 0;
}
评分:\(\huge 8\)
1.2.3 洛谷P4198 楼房重建
小 A 的楼房外有一大片施工工地,工地上有 \(N\) 栋待建的楼房。每天,这片工地上的房子拆了又建、建了又拆。他经常无聊地看着窗外发呆,数自己能够看到多少栋房子。
为了简化问题,我们考虑这些事件发生在一个二维平面上。小 A 在平面上 \((0,0)\) 点的位置,第 \(i\) 栋楼房可以用一条连接 \((i,0)\) 和 \((i,H_i)\) 的线段表示,其中 \(H_i\) 为第 \(i\) 栋楼房的高度。如果这栋楼房上存在一个高度大于 \(0\) 的点与 \((0,0)\) 的连线没有与之前的线段相交,那么这栋楼房就被认为是可见的。
施工队的建造总共进行了 \(M\) 天。初始时,所有楼房都还没有开始建造,它们的高度均为 \(0\)。在第 \(i\) 天,建筑队将会将横坐标为 \(X_i\) 的房屋的高度变为 \(Y_i\)(高度可以比原来大—修建,也可以比原来小—拆除,甚至可以保持不变—建筑队这天什么事也没做)。请你帮小 A 数数每天在建筑队完工之后,他能看到多少栋楼房?
\(k_i = \cfrac{y_i}{x_i}\),\(k_j < k_i\),\(\forall j <i\),才能看见考虑用线段树维护斜率最大值 \(mx\) 和答案。
如果改的是右儿子,就对左儿子没有影响。
如果改的是左儿子,就对右儿子的答案进行 \(\textbf{calc}\)。如果 \(k_i > mx_{lson}\),那对右儿子 \(\textbf{calc}\),左儿子的 s 全赋成 0,反之 \(\textbf{calc}\) 左儿子。
这题说得也挺详细,对于线段树每个节点,存入 \(mx,ans\) 两个值,
\(mx\) 很好维护,\(O(1)\) 转移。
\(ans\) 表示以当前左端点看得见多少栋楼。
很不好 \(pushup\),于是可以构造 \(O(\log n)\) 的转移。
显然有性质:对于左儿子,答案不变,对于右儿子,若其左儿子最大值比左儿子最大值 \(kl\) 更小,递归进右儿子,否则递归进左儿子。
有点绕。
解释 \(\textbf{pushup}\) 实现
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 100;
int n, m;
double k[N];
struct node {
int l, r, ans;
double mx;
}T[N * 4];
int SPpushup(double kl, int u) {
// 两行剪枝
if (kl > T[u].mx) return 0;
if (k[T[u].l] > kl) return T[u].ans;
//
if (T[u].l == T[u].r) return k[T[u].l] > kl;
if (T[u * 2].mx <= kl) return SPpushup(kl, u * 2 + 1);
else return SPpushup(kl, u * 2) + T[u].ans - T[u * 2].ans;
}
void pushup(int u) {
T[u].mx = max(T[u * 2].mx, T[u * 2 + 1].mx);
T[u].ans = T[u * 2].ans + SPpushup(T[u * 2].mx, u * 2 + 1);
}
void build(int u, int l, int r) {
T[u].l = l, T[u].r = r;
if (l == r) return;
int mid = (T[u].l + T[u].r) >> 1;
build(u * 2, l, mid), build(u * 2 + 1, mid + 1, r);
}
void update(int u, int q, double v) {
if (T[u].l == T[u].r) {
T[u].mx = v;
T[u].ans = 1;
return;
}
int mid = (T[u].l + T[u].r) >> 1;
if (q <= mid) update(u * 2, q, v);
else update(u * 2 + 1, q, v);
pushup(u);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> m;
build(1, 1, n);
for (int i = 1; i <= m; i++) {
int x, y;
cin >> x >> y;
k[x] = (double)y / x;
update(1, x, k[x]);
cout << T[1].ans << '\n';
}
return 0;
}
评分:\(\huge 5\)
1.2.4 洛谷P2048 [NOI2010] 超级钢琴
小 Z 是一个小有名气的钢琴家,最近 C 博士送给了小 Z 一架超级钢琴,小 Z 希望能够用这架钢琴创作出世界上最美妙的音乐。
这架超级钢琴可以弹奏出 \(n\) 个音符,编号为 \(1\) 至 \(n\)。第 \(i\) 个音符的美妙度为 \(A_i\),其中 \(A_i\) 可正可负。
一个“超级和弦”由若干个编号连续的音符组成,包含的音符个数不少于 \(L\) 且不多于 \(R\)。我们定义超级和弦的美妙度为其包含的所有音符的美妙度之和。两个超级和弦被认为是相同的,当且仅当这两个超级和弦所包含的音符集合是相同的。
小 Z 决定创作一首由 \(k\) 个超级和弦组成的乐曲,为了使得乐曲更加动听,小 Z 要求该乐曲由 \(k\) 个不同的超级和弦组成。我们定义一首乐曲的美妙度为其所包含的所有超级和弦的美妙度之和。小 Z 想知道他能够创作出来的乐曲美妙度最大值是多少。
这个贪心思想还是太玄妙了。
我们可以这样定义一个状态:\(f_{i,l,r}\) 表示以 \(i\) 为左端点,右端点在 \([l,r]\) 上的最大方案。
我们求一个前缀和,则 \(f_{i,l,r} = \max_{j=l}^r \{sum_j - sum_{i-1} \}\) ,此时只需保证 \(sum_j\) 最大。
最开始压入堆中的状态为 \(f_{i,i+L-1,i+R-1}\),\(j\) 提前查找出来,把答案一起丢进堆。
那么一个状态出队后,我们知道 \(j\) 是最优位置,剩下的情况只能在 \(f_{i,l,j-1},f_{i,j+1,r}\) 中产生(就是把当前最大位置除掉)。
可以用 \(\textbf{ST}\) 表维护区间最大值,\(O(1)\)。
但我用的线段树,所以最后复杂度 \(O(n \log^2 n )\)
\(\huge \mathscr{Code}\)
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e5 + 100;
int n, k, L, R, sum[N];
class Segment_Tree {
struct ele {
int ans, pos;
}T[N * 4];
public:
void update(int u, int l, int r, int q, int val) {
if (l == r) {
T[u].ans = val;
T[u].pos = l;
return;
}
int mid = (l + r) >> 1;
if (q <= mid) update(u * 2, l, mid, q, val);
else update(u * 2 + 1, mid + 1, r, q, val);
T[u].ans = max(T[u * 2].ans, T[u * 2 + 1].ans);
T[u].pos = T[u * 2].ans > T[u * 2 + 1].ans ? T[u * 2].pos : T[u * 2 + 1].pos;
}
pair<int,int> query(int u, int l, int r, int ql, int qr) {
if (ql <= l && r <= qr) return make_pair(T[u].ans, T[u].pos);
int mid = (l + r) >> 1, ans = -1e9;
pair<int, int> recL, recR;
if (ql <= mid) recL = query(u * 2, l, mid, ql, qr), ans = max(ans, recL.first);
if (qr > mid) recR = query(u * 2 + 1, mid + 1, r, ql, qr), ans = max(ans, recR.first);
return make_pair(ans, recL.first == ans ? recL.second : recR.second);
}
}ST;
struct node {
int u, l, r, val;
friend bool operator <(node a, node b) {
return sum[a.val] - sum[a.u - 1] < sum[b.val] - sum[b.u - 1];
}
};
priority_queue<node> q;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> k >> L >> R;
for (int i = 1; i <= n; i++) {
cin >> sum[i];
sum[i] += sum[i - 1];
ST.update(1, 1, n, i, sum[i]);
}
for (int i = 1; i + L - 1 <= n; i++) {
q.push(node{ i,i + L - 1,min(i + R - 1,n),ST.query(1,1,n,i + L - 1,min(i + R - 1,n)).second });
}
int ans = 0;
for (int i = 1; i <= k; i++) {
auto [u, l, r, val] = q.top();
q.pop();
ans += sum[val] - sum[u - 1];
if (l != val) q.push(node{ u,l,val - 1,ST.query(1,1,n,l,val - 1).second });
if (r != val) q.push(node{ u,val + 1,r,ST.query(1,1,n,val + 1,r).second });
}
cout << ans;
return 0;
}
评分:\(\huge 4\)
1.2.5 洛谷P6242【模板】线段树 3(区间最值操作、区间历史最值)
给出一个长度为 \(n\) 的数列 \(A\),同时定义一个辅助数组 \(B\),\(B\) 开始与 \(A\) 完全相同。接下来进行了 \(m\) 次操作,操作有五种类型,按以下格式给出:
1 l r k:对于所有的 \(i\in[l,r]\),将 \(A_i\) 加上 \(k\)(\(k\) 可以为负数)。2 l r v:对于所有的 \(i\in[l,r]\),将 \(A_i\) 变成 \(\min(A_i,v)\)。3 l r:求 \(\sum_{i=l}^{r}A_i\)。4 l r:对于所有的 \(i\in[l,r]\),求 \(A_i\) 的最大值。5 l r:对于所有的 \(i\in[l,r]\),求 \(B_i\) 的最大值。在每一次操作后,我们都进行一次更新,让 \(B_i\gets\max(B_i,A_i)\)。
\(\textbf{Part1}\)
非常数据结构的题目描述,但是非常难。
我将花费巨长的篇幅解释这几个操作的关系。
首先,大家应该都会 \(1,3,4\) 操作,不会的可以退了。
没错,前置知识就是一个懒标记线段树。
那么我们着手维护的就只是 \(2,5\) 操作了。
很多人都是像这样讲的,但今天我想换个角度,先抛开 \(1\) 操作,进行 \(2\) 操作的阐述。
但显然区间最值的推平如果不下放至子节点的话,很难进行普适性操作。
吉老师也在其论文中陈述到:鉴于这种做法太难以想到,故在此陈述一个通用转化方法。
下文均用 \(mx,se\) 代表最大、次大值。
现在有一个 \(\min(A_i,x)\) 的操作下放了。
- 对于 \(mx \le x\) 这次修改不影响子节点,退出。
- \(se < x <mx\),显然只影响了最大值,修改当前节点最值与 \(sum\) 后,退出。
- \(x \le se\),显然我们无法直接修改,于是直接递归。
挺简洁的构想,代码操作也不是太难。
但是复杂度怎么保证呢,我们来分析一下。
这里采用与 \(\textbf{Splay}\) 树相同的势能分析。
在极端情况下,一次全部推平操作 \(O(n \log n)\)。
均摊下放至 \(n\) 个节点。
但对于次大值,此时 \(se = -\inf\)。
下次再操作时,复杂度 \(O(1)\)。
平摊复杂度 \(\Theta (\log n)\)。
证毕 \(\square\)
\(\textbf{Part2}\)
现在,我们解决了 \(2,3,4\) 的操作,接下来加入 \(1\)。
这部分令人高兴的是不用分析复杂度了。
这只是一个区间加操作,懒标记就可以搞定。
请注意,我们刚才使用了 \(mx,se\) 区分区间操作。
也就是说,最大值的操作和其他数的操作是不一样的,需要分开记录 \(tag\)。
打两个 \(tag\) 就行了。
\(\textbf{Part3}\)
最后加上历史最值的操作。
注意到历史最值也需要懒标记,\(2\) 个。
至于懒标记更新的话呢用:父亲历史最大加标记 \(+\) 该节点的当前加标记,就行了。
怎么越讲越水
不管了,看代码吧。
\(\huge \mathscr{Code}\)
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e5 + 100;
int n, m, num[N];
struct node {
int l, r, mx, sum, se, rec, mxcnt;
int mxtag, exmxtag, rectag, exrectag;
}T[N * 4];
void pushup(int u) {
T[u].mx = max(T[u * 2].mx, T[u * 2 + 1].mx);
T[u].rec = max(T[u * 2].rec, T[u * 2 + 1].rec);
T[u].sum = T[u * 2].sum + T[u * 2 + 1].sum;
if (T[u * 2].mx == T[u * 2 + 1].mx) {
T[u].mxcnt = T[u * 2].mxcnt + T[u * 2 + 1].mxcnt;
T[u].se = max(T[u * 2].se, T[u * 2 + 1].se);
}
else {
T[u].mxcnt = T[u].mx == T[u * 2].mx ? T[u * 2].mxcnt : T[u * 2 + 1].mxcnt;
T[u].se = max({ T[u * 2].se, T[u * 2 + 1].se,min(T[u * 2].mx,T[u * 2 + 1].mx) });
}
}
void addtag(int u, int mxtag, int exmxtag, int rectag, int exrectag) {
T[u].sum += T[u].mxcnt * mxtag + (T[u].r - T[u].l + 1 - T[u].mxcnt) * exmxtag;
T[u].rec = max(T[u].rec, T[u].mx + rectag);
T[u].mx += mxtag;
if (T[u].se != -1e18) T[u].se += exmxtag;
T[u].rectag = max(T[u].rectag, T[u].mxtag + rectag);
T[u].exrectag = max(T[u].exrectag, T[u].exmxtag + exrectag);
T[u].mxtag += mxtag;
T[u].exmxtag += exmxtag;
}
void pushdown(int u) {
int maxn = max(T[u * 2].mx, T[u * 2 + 1].mx);
if (T[u * 2].mx == maxn) addtag(u * 2, T[u].mxtag, T[u].exmxtag, T[u].rectag, T[u].exrectag);
else addtag(u * 2, T[u].exmxtag, T[u].exmxtag, T[u].exrectag, T[u].exrectag);
if (T[u * 2 + 1].mx == maxn) addtag(u * 2 + 1, T[u].mxtag, T[u].exmxtag, T[u].rectag, T[u].exrectag);
else addtag(u * 2 + 1, T[u].exmxtag, T[u].exmxtag, T[u].exrectag, T[u].exrectag);
T[u].mxtag = T[u].exmxtag = T[u].rectag = T[u].exrectag = 0;
}
void build(int u, int l, int r) {
T[u].l = l, T[u].r = r;
if (l == r) {
T[u].mx = T[u].sum = T[u].rec = num[l];
T[u].se = -1e18;
T[u].mxcnt = 1;
return;
}
int mid = (T[u].l + T[u].r) >> 1;
build(u * 2, l, mid), build(u * 2 + 1, mid + 1, r);
pushup(u);
}
void addupd(int u, int ql, int qr, int v) {
if (ql <= T[u].l && T[u].r <= qr) {
T[u].sum += (T[u].r - T[u].l + 1) * v;
T[u].mx += v;
T[u].rec = max(T[u].rec, T[u].mx);
if (T[u].se != -1e18) T[u].se += v;
T[u].mxtag += v;
T[u].exmxtag += v;
T[u].rectag = max(T[u].rectag, T[u].mxtag);
T[u].exrectag = max(T[u].exrectag, T[u].exmxtag);
return;
}
pushdown(u);
int mid = (T[u].l + T[u].r) >> 1;
if (ql <= mid) addupd(u * 2, ql, qr, v);
if (qr > mid) addupd(u * 2 + 1, ql, qr, v);
pushup(u);
}
void mxupd(int u, int ql, int qr, int v) {
if (v >= T[u].mx) return;
if (ql <= T[u].l && T[u].r <= qr && v > T[u].se) {
T[u].sum -= T[u].mxcnt * (T[u].mx - v);
T[u].mxtag -= T[u].mx - v;
T[u].mx = v;
return;
}
pushdown(u);
int mid = (T[u].l + T[u].r) >> 1;
if (ql <= mid) mxupd(u * 2, ql, qr, v);
if (qr > mid) mxupd(u * 2 + 1, ql, qr, v);
pushup(u);
}
int mxque(int u, int ql, int qr) {
if (ql <= T[u].l && T[u].r <= qr) return T[u].mx;
pushdown(u);
int mid = (T[u].l + T[u].r) >> 1, ans = -1e18;
if (ql <= mid) ans = max(ans, mxque(u * 2, ql, qr));
if (qr > mid) ans = max(ans, mxque(u * 2 + 1, ql, qr));
return ans;
}
int smque(int u, int ql, int qr) {
if (ql <= T[u].l && T[u].r <= qr) return T[u].sum;
pushdown(u);
int mid = (T[u].l + T[u].r) >> 1, ans = 0;
if (ql <= mid) ans += smque(u * 2, ql, qr);
if (qr > mid) ans += smque(u * 2 + 1, ql, qr);
return ans;
}
int recque(int u, int ql, int qr) {
if (ql <= T[u].l && T[u].r <= qr) return T[u].rec;
pushdown(u);
int mid = (T[u].l + T[u].r) >> 1, ans = -1e18;
if (ql <= mid) ans = max(ans, recque(u * 2, ql, qr));
if (qr > mid) ans = max(ans, recque(u * 2 + 1, ql, qr));
return ans;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> num[i];
build(1, 1, n);
for (int i = 1; i <= m; i++) {
int op, x, y, z;
cin >> op;
if (op == 1) {
cin >> x >> y >> z;
addupd(1, x, y, z);
}
if (op == 2) {
cin >> x >> y >> z;
mxupd(1, x, y, z);
}
if (op == 3) {
cin >> x >> y;
cout << smque(1, x, y) << '\n';
}
if (op == 4) {
cin >> x >> y;
cout << mxque(1, x, y) << '\n';
}
if (op == 5) {
cin >> x >> y;
cout << recque(1, x, y) << '\n';
}
}
return 0;
}
还有卡常版:
#include<bits/stdc++.h>
namespace Fread { const int SIZE = (1 << 18); char buf[SIZE], * p1 = buf, * p2 = buf; inline char getchar() { return (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, SIZE, stdin), p1 == p2) ? EOF : *p1++); } }
namespace Fwrite { const int SIZE = (1 << 18); char buf[SIZE], * S = buf, * T = buf + SIZE; inline void flush() { fwrite(buf, 1, S - buf, stdout), S = buf; } struct NTR { ~NTR() { flush(); } }ztr; inline void putchar(char c) { *S++ = c; if (S == T) flush(); } }
namespace Fastio {
struct Reader { template <typename T> Reader& operator >> (T& x) { char c = Fread::getchar(); bool f = false; while (c < '0' or c > '9') { if (c == '-') f = true; c = Fread::getchar(); } x = 0; while (c >= '0' and c <= '9') { x = (x << 1) + (x << 3) + (c ^ 48); c = Fread::getchar(); } if (f) x = -x; return *this; }Reader& operator>>(char& c) { c = Fread::getchar(); while (c == '\n' || c == ' ' || c == '\r')c = Fread::getchar(); return *this; }Reader& operator>>(char* str) { int len = 0; char c = Fread::getchar(); while (c == '\n' || c == ' ' || c == '\r')c = Fread::getchar(); while (c != '\n' && c != ' ' && c != '\r')str[len++] = c, c = Fread::getchar(); str[len] = '\0'; return *this; }Reader() {} }cin;
struct Writer { template <typename T> Writer& operator << (T x) { if (x == 0) return Fwrite::putchar('0'), * this; if (x < 0) Fwrite::putchar('-'), x = -x; static int sta[45], top = 0; while (x) sta[++top] = x % 10, x /= 10; while (top) Fwrite::putchar(sta[top] + '0'), --top; return *this; } Writer& operator<<(char c) { Fwrite::putchar(c); return*this; }Writer& operator<<(const char* str) { int cur = 0; while (str[cur])Fwrite::putchar(str[cur++]); return *this; }Writer() {} }cout;
}
#define cin Fastio :: cin
#define cout Fastio :: cout
using namespace std;
const int N = 5e5 + 100;
int n, m, num[N];
struct node {
int l, r, mx, se, rec, mxcnt;
int mxtag, exmxtag, rectag, exrectag;
long long sum;
}T[N * 4];
inline void pushup(int u) {
T[u].mx = max(T[u * 2].mx, T[u * 2 + 1].mx);
T[u].rec = max(T[u * 2].rec, T[u * 2 + 1].rec);
T[u].sum = T[u * 2].sum + T[u * 2 + 1].sum;
if (T[u * 2].mx == T[u * 2 + 1].mx) {
T[u].mxcnt = T[u * 2].mxcnt + T[u * 2 + 1].mxcnt;
T[u].se = max(T[u * 2].se, T[u * 2 + 1].se);
}
else {
T[u].mxcnt = T[u].mx == T[u * 2].mx ? T[u * 2].mxcnt : T[u * 2 + 1].mxcnt;
T[u].se = max({ T[u * 2].se, T[u * 2 + 1].se,min(T[u * 2].mx,T[u * 2 + 1].mx) });
}
}
inline void addtag(int u, int mxtag, int exmxtag, int rectag, int exrectag) {
T[u].sum += 1ll * T[u].mxcnt * mxtag + 1ll * (T[u].r - T[u].l + 1 - T[u].mxcnt) * exmxtag;
T[u].rec = max(T[u].rec, T[u].mx + rectag);
T[u].mx += mxtag;
if (T[u].se != -2e9) T[u].se += exmxtag;
T[u].rectag = max(T[u].rectag, T[u].mxtag + rectag);
T[u].exrectag = max(T[u].exrectag, T[u].exmxtag + exrectag);
T[u].mxtag += mxtag;
T[u].exmxtag += exmxtag;
}
inline void pushdown(int u) {
int maxn = max(T[u * 2].mx, T[u * 2 + 1].mx);
if (T[u * 2].mx == maxn) addtag(u * 2, T[u].mxtag, T[u].exmxtag, T[u].rectag, T[u].exrectag);
else addtag(u * 2, T[u].exmxtag, T[u].exmxtag, T[u].exrectag, T[u].exrectag);
if (T[u * 2 + 1].mx == maxn) addtag(u * 2 + 1, T[u].mxtag, T[u].exmxtag, T[u].rectag, T[u].exrectag);
else addtag(u * 2 + 1, T[u].exmxtag, T[u].exmxtag, T[u].exrectag, T[u].exrectag);
T[u].mxtag = T[u].exmxtag = T[u].rectag = T[u].exrectag = 0;
}
void build(int u, int l, int r) {
T[u].l = l, T[u].r = r;
if (l == r) {
T[u].mx = T[u].sum = T[u].rec = num[l];
T[u].se = -2e9;
T[u].mxcnt = 1;
return;
}
int mid = (T[u].l + T[u].r) >> 1;
build(u * 2, l, mid), build(u * 2 + 1, mid + 1, r);
pushup(u);
}
void addupd(int u, int ql, int qr, int v) {
if (ql <= T[u].l && T[u].r <= qr) {
T[u].sum += 1ll * (T[u].r - T[u].l + 1) * v;
T[u].mx += v;
T[u].rec = max(T[u].rec, T[u].mx);
if (T[u].se != -2e9) T[u].se += v;
T[u].mxtag += v;
T[u].exmxtag += v;
T[u].rectag = max(T[u].rectag, T[u].mxtag);
T[u].exrectag = max(T[u].exrectag, T[u].exmxtag);
return;
}
pushdown(u);
int mid = (T[u].l + T[u].r) >> 1;
if (ql <= mid) addupd(u * 2, ql, qr, v);
if (qr > mid) addupd(u * 2 + 1, ql, qr, v);
pushup(u);
}
void mxupd(int u, int ql, int qr, int v) {
if (v >= T[u].mx) return;
if (ql <= T[u].l && T[u].r <= qr && v > T[u].se) {
T[u].sum -= 1ll * T[u].mxcnt * (T[u].mx - v);
T[u].mxtag -= T[u].mx - v;
T[u].mx = v;
return;
}
pushdown(u);
int mid = (T[u].l + T[u].r) >> 1;
if (ql <= mid) mxupd(u * 2, ql, qr, v);
if (qr > mid) mxupd(u * 2 + 1, ql, qr, v);
pushup(u);
}
int mxque(int u, int ql, int qr) {
if (ql <= T[u].l && T[u].r <= qr) return T[u].mx;
pushdown(u);
int mid = (T[u].l + T[u].r) >> 1, ans = -2e9;
if (ql <= mid) ans = max(ans, mxque(u * 2, ql, qr));
if (qr > mid) ans = max(ans, mxque(u * 2 + 1, ql, qr));
return ans;
}
long long smque(int u, int ql, int qr) {
if (ql <= T[u].l && T[u].r <= qr) return T[u].sum;
pushdown(u);
long long mid = (T[u].l + T[u].r) >> 1, ans = 0;
if (ql <= mid) ans += smque(u * 2, ql, qr);
if (qr > mid) ans += smque(u * 2 + 1, ql, qr);
return ans;
}
int recque(int u, int ql, int qr) {
if (ql <= T[u].l && T[u].r <= qr) return T[u].rec;
pushdown(u);
int mid = (T[u].l + T[u].r) >> 1, ans = -2e9;
if (ql <= mid) ans = max(ans, recque(u * 2, ql, qr));
if (qr > mid) ans = max(ans, recque(u * 2 + 1, ql, qr));
return ans;
}
signed main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> num[i];
build(1, 1, n);
for (int i = 1; i <= m; i++) {
int op, x, y, z;
cin >> op;
if (op == 1) {
cin >> x >> y >> z;
addupd(1, x, y, z);
}
if (op == 2) {
cin >> x >> y >> z;
mxupd(1, x, y, z);
}
if (op == 3) {
cin >> x >> y;
cout << smque(1, x, y) << '\n';
}
if (op == 4) {
cin >> x >> y;
cout << mxque(1, x, y) << '\n';
}
if (op == 5) {
cin >> x >> y;
cout << recque(1, x, y) << '\n';
}
}
return 0;
}
评分:\(\huge 9\)
1.2.6 洛谷P4314 CPU 监控
第一行一个正整数 \(T\),表示 Bob 需要监视 CPU 的总时间。
然后第二行给出 \(T\) 个数表示在你的监视程序执行之前,Bob 干的事让 CPU 在这段时间内每个时刻的使用率达已经达到了多少。
第三行给出一个整数 \(E\),表示 Bob 需要做的事和询问的总数。
接下来 \(E\) 行每行表示给出一个询问或者列出一条事件:
Q X Y:询问从 \(X\) 到 \(Y\) 这段时间内 CPU 最高使用率。A X Y:询问从 \(X\) 到 \(Y\) 这段时间内之前列出的事件使 CPU 达到过的最高使用率。P X Y Z:列出一个事件这个事件使得从 \(X\) 到 \(Y\) 这段时间内 CPU 使用率增加 \(Z\)。C X Y Z:列出一个事件这个事件使得从 \(X\) 到 \(Y\) 这段时间内 CPU 使用率变为 \(Z\)。
这只是一个 \(Trick\) 的问题,在这道题中,你需要学习两个 \(Trick\)。
-
对于区间赋值与区间相加,怎么维护其懒标记?
我们可以知道,一个区间赋值操作后,如果再有区间加,也可以视作区间赋值。
接着,只需要定义一个 \(vis\) 标记,记录当前是否有区间赋值操作就行了。
-
对于区间历史最值,我们怎么维护?
这里并不需要详细解释。
定义历史 \(tag\),包括:历史最值,区间加标记最值,区间赋值标记最值。
如果下传懒标记,用父节点的历史最值(或历史标记最值)+子节点现在最值来更新。
只要维护好节点的各个标记,就可以 \(\textbf{AC}\)。
\(\huge \mathscr{Code}\)
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 100;
int n, m, val[N];
struct node {
int l, r, mx, mxmx, tagp, taga, mxtagp, mxtaga, vis;
}T[N * 4];
void pushup(int u) {
T[u].mx = max(T[u * 2].mx, T[u * 2 + 1].mx);
T[u].mxmx = max(T[u * 2].mxmx, T[u * 2 + 1].mxmx);
}
void addtagp(int u, int a, int b) {
if (T[u].vis) {
T[u].mxtaga = max(T[u].mxtaga, T[u].taga + b);
T[u].mxmx = max(T[u].mxmx, T[u].mx + b);
T[u].mx += a;
T[u].taga += a;
}
else {
T[u].mxtagp = max(T[u].mxtagp, T[u].tagp + b);
T[u].mxmx = max(T[u].mxmx, T[u].mx + b);
T[u].mx += a;
T[u].tagp += a;
}
}
void addtaga(int u, int a, int b) {
if (T[u].vis) {
T[u].mxtaga = max(T[u].mxtaga, b);
T[u].mxmx = max(T[u].mxmx, b);
}
else {
T[u].vis = 1;
T[u].mxtaga = b;
T[u].mxmx = max(T[u].mxmx, b);
}
T[u].mx = T[u].taga = a;
}
void pushdown(int u) {
addtagp(u * 2, T[u].tagp, T[u].mxtagp);
addtagp(u * 2 + 1, T[u].tagp, T[u].mxtagp);
T[u].mxtagp = T[u].tagp = 0;
if (T[u].vis) {
addtaga(u * 2, T[u].taga, T[u].mxtaga);
addtaga(u * 2 + 1, T[u].taga, T[u].mxtaga);
T[u].vis = T[u].taga = T[u].mxtaga = 0;
}
}
void build(int u, int l, int r) {
T[u].l = l, T[u].r = r;
if (l == r) {
T[u].mx = T[u].mxmx = val[l];
return;
}
int mid = (T[u].l + T[u].r) >> 1;
build(u * 2, l, mid), build(u * 2 + 1, mid + 1, r);
pushup(u);
}
void update(int u, int ql, int qr, int v) {
if (ql <= T[u].l && T[u].r <= qr) {
addtagp(u, v, v);
return;
}
pushdown(u);
int mid = (T[u].l + T[u].r) >> 1;
if (ql <= mid) update(u * 2, ql, qr, v);
if (qr > mid) update(u * 2 + 1, ql, qr, v);
pushup(u);
}
void align(int u, int ql, int qr, int v) {
if (ql <= T[u].l && T[u].r <= qr) {
addtaga(u, v, v);
return;
}
pushdown(u);
int mid = (T[u].l + T[u].r) >> 1;
if (ql <= mid) align(u * 2, ql, qr, v);
if (qr > mid) align(u * 2 + 1, ql, qr, v);
pushup(u);
}
int querymx(int u, int ql, int qr) {
if (ql <= T[u].l && T[u].r <= qr) return T[u].mx;
pushdown(u);
int mid = (T[u].l + T[u].r) >> 1, ans = -1e9;
if (ql <= mid) ans = max(ans, querymx(u * 2, ql, qr));
if (qr > mid) ans = max(ans, querymx(u * 2 + 1, ql, qr));
return ans;
}
int querymxmx(int u, int ql, int qr) {
if (ql <= T[u].l && T[u].r <= qr) return T[u].mxmx;
pushdown(u);
int mid = (T[u].l + T[u].r) >> 1, ans = -1e9;
if (ql <= mid) ans = max(ans, querymxmx(u * 2, ql, qr));
if (qr > mid) ans = max(ans, querymxmx(u * 2 + 1, ql, qr));
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n;
for (int i = 1; i <= n; i++) cin >> val[i];
build(1, 1, n);
cin >> m;
for (int i = 1; i <= m; i++) {
char op;
int x, y, z;
cin >> op;
if (op == 'Q') {
cin >> x >> y;
cout << querymx(1, x, y) << '\n';
}
if (op == 'A') {
cin >> x >> y;
cout << querymxmx(1, x, y) << '\n';
}
if (op == 'P') {
cin >> x >> y >> z;
update(1, x, y, z);
}
if (op == 'C') {
cin >> x >> y >> z;
align(1, x, y, z);
}
}
return 0;
}
评分:\(\huge 6.5\)
1.3 分块
1.4 莫队
1.4.1 洛谷P5355 [Ynoi Easy Round 2017] 由乃的玉米田
由乃在自己的农田边散步,她突然发现田里的一排玉米非常的不美。
这排玉米一共有 \(N\) 株,它们的高度参差不齐。
由乃认为玉米田不美,所以她决定出个数据结构题
这个题是这样的:
给你一个序列 \(a\),长度为 \(n\),有 \(m\) 次操作,每次询问一个区间是否可以选出两个数它们的差为 \(x\),或者询问一个区间是否可以选出两个数它们的和为 \(x\),或者询问一个区间是否可以选出两个数它们的乘积为 \(x\) ,或者询问一个区间是否可以选出两个数它们的商为 \(x\)(没有余数) ,这四个操作分别为操作 \(1,2,3,4\)。
选出的这两个数可以是同一个位置的数。
难点主要在于除法操作。
题目中的值都只与数值有关,自然可以想到用桶来维护。
考虑 \(\textbf{bitset}\)。
接下来分 \(4\) 种操作:
-
差为 \(x\),用 \((bitset << x) \& bitset\) 处理,查找其中有没有 \(1\) 就行了。
-
和为 \(x\) ,此时 \(a + b = x \to a - x = -b\) 值域范围 \([1,10^5]\),考虑维护 \(reversed\_bitset\) 维护 \(10^5 - num\) 的桶。
此时有 \((bitset << (10^5 - x)) \& reversed\_bitset\) ,查找其中有没有 \(1\) 就行了。
-
积为 \(x\),不要被骗了,直接暴力枚举 \([1,\sqrt{x}]\),查找 \(i,x/i\) 过了。
-
商为 \(x\),分两种情况讨论:
-
\(x > \sqrt{10^5}\) ,此时暴力枚举,查找 \(i,i \times x\),复杂度正确。
-
\(x < \sqrt{10^5}\),此时单独拿出来,不用莫队处理,对每一个 \(x\),暴力扫一遍 \([1,n]\),记录每个数 \(num\) 出现的位置。查找之前是否有 \(num \times x,num ÷ x\),记录下来,如果该位置小于查询左端点,则没有,相反则成立。
-
\(\huge \mathscr{Code}\)
// This code must be compiled in c++17 and above
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 100, Host = 1e5;
int n, m, num[N], blk[N], blksiz, cnt[N], tot;
bool ans[N];
bitset<N> pos, revpos;
struct node {
int op, l, r, x, id;
friend bool operator<(node a, node b) {
if (blk[a.l] == blk[b.l]) return a.r < b.r;
return blk[a.l] < blk[b.l];
}
}qs[N];
vector<int> ql[N], qr[N], qid[N];
int pre[N], last[N];
void del(int x) {
cnt[num[x]]--;
if (!cnt[num[x]]) pos.set(num[x], 0), revpos.set(Host - num[x], 0);
}
void add(int x) {
cnt[num[x]]++;
if (cnt[num[x]] == 1) pos.set(num[x], 1), revpos.set(Host - num[x], 1);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> num[i];
// MDui-pre
blksiz = sqrt(n);
for (int i = 1; i <= n; i++) blk[i] = (i - 1) / blksiz + 1;
for (int i = 1; i <= m; i++) {
int op, l, r, x;
cin >> op >> l >> r >> x;
if (op == 4 && x <= sqrt(Host)) {
ql[x].push_back(l);
qr[x].push_back(r);
qid[x].push_back(i);
}
else qs[++tot] = node{ op,l,r,x,i };
}
sort(qs + 1, qs + tot + 1);
// Run
int L = 1, R = 0;
for (int i = 1; i <= tot; i++) {
auto [op, l, r, x, id] = qs[i];
// Mdui
while (L > l) add(--L);
while (R < r) add(++R);
while (L < l) del(L++);
while (R > r) del(R--);
// bitset
if (op == 1) {
ans[id] = ((pos << x) & pos).any();
}
if (op == 2) {
ans[id] = ((pos << (Host - x)) & revpos).any();
}
if (op == 3) {
for (int j = 1; j * j <= x; j++) {
if (x % j) continue;
if (pos.test(j) && pos.test(x / j)) {
ans[id] = 1;
goto Kid;
}
}
ans[id] = 0;
Kid:;
}
if (op == 4) {
for (int j = 1; j * x <= Host; j++) {
if (!pos.test(j)) continue;
if (pos.test(j) && pos.test(x * j)) {
ans[id] = 1;
goto Kids;
}
}
ans[id] = 0;
Kids:;
}
}
// Violenly-Run
for (int x = 1; x <= sqrt(Host); x++) {
if (ql[x].empty()) continue;
memset(pre, 0, sizeof(pre));
memset(last, 0, sizeof(last));
int L = 0;
for (int i = 1; i <= n; i++) {
pre[num[i]] = i;
if (x * num[i] <= Host) L = max(L, pre[x * num[i]]);
if (num[i] % x == 0) L = max(L, pre[num[i] / x]);
last[i] = L;
}
for (int i = 0; i < ql[x].size(); i++) {
ans[qid[x][i]] = (ql[x][i] <= last[qr[x][i]]);
}
}
for (int i = 1; i <= m; i++) cout << (ans[i] ? "yuno" : "yumi") << '\n';
return 0;
}
评分:\(\huge 7.5\)
1.4.2 洛谷P7708 「Wdsr-2.7」八云蓝自动机 Ⅰ
八云蓝自动机维护了一个长度为 \(n\) 的序列 \(A\) ,每个元素都有一个初始值。同时自动机会支持以下三种操作:
\(\textbf{1 x k}\) :将 \(A_x\) 的值修改为 \(k\)。
\(\textbf{2 x y}\) :交换 \(A_x\) 与 \(A_y\) 的值。
\(\textbf{3 x}\) :查询 \(A_x\) 的值。
一道莫队好题。本题最有价值的地方在于对单点修改的转化,以及对交换两个数的处理:维护原来每个位置现在的位置,以及现在每个位置原来的位置。
注意到单点修改并不方便实现,将其转化为交换两个数。对于 \(a_x\gets k\),我们新建 \(a_c = k\),并将其看做 \(a_x\) 与 \(a_c\) 交换。这一步非常巧妙,因为它消灭了单点修改这一类麻烦的操作。
多次询问一段区间的操作结果,一般使用莫队实现。因此,考虑区间在伸缩时需要维护哪些信息。为了支持在操作序列最前面加入交换两个数的操作,可以想到维护:
- 序列 \(a\) 在操作后的形态。
- \(pos_i\) 表示 原 位置 \(i\) 的 现 位置。
- \(rev_i\) 表示 现 位置 \(i\) 的 原 位置。
- \(add_i\) 表示 现 位置 \(i\) 上的数被查询了多少次。
- 当右端点右移 \(r - 1\to r\) 时:
- 若第 \(r\) 个操作是交换 \(x, y\),则交换 \(a_x\) 和 \(a_y\),\(rev_x\) 和 \(rev_y\),\(pos_{rev_x}\) 和 \(pos_{rev_y}\)。
- 若第 \(r\) 个操作是查询 \(x\),则令 \(ans\gets ans + a_x\),\(add_x\gets add_x + 1\)。
- 当左端点左移 \(l+1\to l\) 时:
- 若第 \(l\) 个操作是交换 \(x,y\),注意我们相当于 交换原位置 上的两个数,因此对答案有影响。先交换 \(a_{pos_x}\) 和 \(a_{pos_y}\),\(rev_{pos_x}\) 和 \(rev_{pos_y}\),\(pos_x\) 和 \(pos_y\)。由于交换原位置上的两个数并不影响现位置被查询的数的次数(因为我们已经交换了 \(a_{pos_x}\) 和 \(a_{pos_y}\),或者说 \(a\) 和 \(add\) 当中只要交换一个即可描述本次操作,多交换反而会让答案错误),因此答案加上 交换后 的 \((a_{pos_x} - a_{pos_y})(add_{pos_x} - add_{pos_y})\),相当于把每个数原来的贡献减掉,加上新的贡献。
- 若第 \(l\) 个操作是查询 \(x\),则令 \(ans\gets ans + a_{pos_x}\),\(add_{pos_x} \gets add_{pos_x} + 1\)。
右端点左移和左端点右移的情况分别与上述两种情况相似,仅是符号相反,此处不再赘述。时间复杂度 \(\mathcal{O}(n\sqrt n)\)。
\(\huge \mathscr{Code}\)
#include<bits/stdc++.h>
#define uint unsigned int
using namespace std;
const int N = 4e5+100;
int n,m,q,val[N],B;
uint pos[N],add[N],rev[N],ans[N];
struct node{
int op,x,y;
}ask[N];
struct bear{
int l,r,blk,id;
friend bool operator<(bear a,bear b){
if(a.blk!=b.blk) return a.blk<b.blk;
return a.r>b.r;
}
}que[N];
signed main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>val[i];
for(int i=1;i<=m;i++){
int op,x,y,k;
cin>>op;
if(op==1){
cin>>x>>k;
val[++n] = k;
ask[i] = {2,x,n};
}
if(op==2){
cin>>x>>y;
ask[i] = {2,x,y};
}
if(op==3){
cin>>x;
ask[i] = {3,x,0};
}
}
for(int i=1;i<=n;i++) pos[i] = rev[i] = i;
B = 666;
cin>>q;
for(int i=1;i<=q;i++){
cin>>que[i].l>>que[i].r;
que[i].blk = (que[i].l-1)/B + 1;
que[i].id = i;
}
sort(que+1,que+q+1);
int L = 1,R = 0;
uint cur = 0;
for(int i=1;i<=q;i++){
auto [l,r,blk,id] = que[i];
while(R<r){
R++;
if(ask[R].op==2){
swap(val[ask[R].x],val[ask[R].y]);
swap(rev[ask[R].x],rev[ask[R].y]);
swap(add[ask[R].x],add[ask[R].y]);
swap(pos[rev[ask[R].x]],pos[rev[ask[R].y]]);
}
else add[ask[R].x]++,cur += val[ask[R].x];
}
while(L>l){
L--;
if(ask[L].op==2){
swap(pos[ask[L].x],pos[ask[L].y]);
swap(val[pos[ask[L].x]],val[pos[ask[L].y]]);
swap(rev[pos[ask[L].x]],rev[pos[ask[L].y]]);
cur += add[pos[ask[L].x]]*(val[pos[ask[L].x]]-val[pos[ask[L].y]]);
cur += add[pos[ask[L].y]]*(val[pos[ask[L].y]]-val[pos[ask[L].x]]);
}
else add[pos[ask[L].x]]++,cur += val[pos[ask[L].x]];
}
while(R>r){
if(ask[R].op==2){
swap(val[ask[R].x],val[ask[R].y]);
swap(rev[ask[R].x],rev[ask[R].y]);
swap(add[ask[R].x],add[ask[R].y]);
swap(pos[rev[ask[R].x]],pos[rev[ask[R].y]]);
}
else add[ask[R].x]--,cur -= val[ask[R].x];
R--;
}
while(L<l){
if(ask[L].op==2){
cur -= add[pos[ask[L].x]]*(val[pos[ask[L].x]]-val[pos[ask[L].y]]);
cur -= add[pos[ask[L].y]]*(val[pos[ask[L].y]]-val[pos[ask[L].x]]);
swap(pos[ask[L].x],pos[ask[L].y]);
swap(val[pos[ask[L].x]],val[pos[ask[L].y]]);
swap(rev[pos[ask[L].x]],rev[pos[ask[L].y]]);
}
else add[pos[ask[L].x]]--,cur -= val[pos[ask[L].x]];
L++;
}
ans[id] = cur;
}
for(int i=1;i<=q;i++) cout<<ans[i]<<'\n';
return 0;
}
评分:\(\huge 7\)
1.5 平衡树
其实平衡树一般不用自己写。。。
1.5.1 洛谷P3201 [HNOI2009] 梦幻布丁
\(n\) 个布丁摆成一行,进行 \(m\) 次操作。每次将某个颜色的布丁全部变成另一种颜色的,然后再询问当前一共有多少段颜色。
例如,颜色分别为 \(1,2,2,1\) 的四个布丁一共有 \(3\) 段颜色.
- 若 \(op = 1\),则后有两个整数 \(x, y\),表示将颜色 \(x\) 的布丁全部变成颜色 \(y\)。
- 若 \(op = 2\),则表示一次询问。
注意到 \(x \to y\) 的操作,可以进行启发式合并。
因为要合并 \(A,B\) 的话,假设 \(|A|<|B|\) ,那么 \(A\) 可以看做大小翻了一倍。
因此每个集合最多合并 \(\log n\) 次。
总复杂度 \(O(n \log n)\)。
\(\huge \mathscr{Code}\)
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 100;
int n, m, col[N], res;
set<int> q[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> col[i], q[col[i]].insert(i);
for (int i = 1; i <= n; i++) res += col[i] != col[i - 1];
for (int i = 1; i <= m; i++) {
int op, x, y;
cin >> op;
if (op == 2) cout << res << '\n';
else {
cin >> x >> y;
if (x == y) continue;
if (q[x].size() > q[y].size()) swap(q[x], q[y]);
for (int i : q[x]) res -= q[y].count(i - 1) + q[y].count(i + 1);
for (int i : q[x]) q[y].insert(i);
q[x].clear();
}
}
return 0;
}
评分:\(\huge 3.5\)
1.5.2 CF1098D Eels
Vasya 是一个非常喜欢鱼的人,他的父母在新年时送给了他一个水族箱。Vasya 并没有鱼类学的学位,所以他认为把新水族箱装满鳗鱼是个好主意。不幸的是,鳗鱼是食肉动物,因此 Vasya 决定了解一下这个想法有多危险。
当多条鳗鱼被放进同一个水族箱时,它们会互相争斗,直到只剩下一条鱼。每当两条鳗鱼争斗时,体型较大的会吃掉体型较小的(如果它们体重相等,也会有一条吃掉另一条)。具体来说,假设水族箱里最初有 \(n\) 条鳗鱼,第 \(i\) 条的体重为 \(x_i\)。那么它们之间会发生 \(n-1\) 场战斗,最终只剩下一条鳗鱼。每当两条体重分别为 \(a\) 和 \(b\) 的鳗鱼(\(a \le b\))争斗时,体重为 \(a\) 的鳗鱼会被吃掉并从水族箱中消失,而体重为 \(b\) 的鳗鱼体重会增加到 \(a+b\)。
当两条体重为 \(a\) 和 \(b\) 的鳗鱼(\(a \le b\))争斗时,如果 \(b \le 2a\),则这场战斗被认为是危险的。对于给定的一组鳗鱼,危险值定义为:如果将这些鳗鱼放入同一个水族箱中,可能发生的危险战斗的最大数量。
现在 Vasya 正在计划要往水族箱里放哪些鳗鱼。他有一个鳗鱼集合(初始为空)。他会对这个集合进行一系列操作。每次操作,他要么向集合中加入一条鳗鱼,要么从集合中移除一条鳗鱼。每次操作后,Vasya 都会询问你当前集合的危险值。
只能说思维难度极高。
感性理解一下,如果选两只最小的合并,不管危不危险,都是最优的(我不会证。
接下来对所有危险值分组,\([2^i,2^{i+1})\) 为一组。
危险只会在组内发生~~。
用一个 \(\textbf{multiset}\) 维护。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 100, M = 33;
int n, x, q, sum[M];
char op;
multiset<int> num[M];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> q;
for (int T = 1; T <= q; T++) {
cin >> op >> x;
int part = __lg(x) + 1;
if (op == '+') {
sum[part] += x;
num[part].insert(x);
}
else {
sum[part] -= x;
num[part].erase(num[part].find(x));
}
int siz = 0, ans = 0;
for (int i = 1; i <= 32; i++) {
if (num[i].empty()) continue;
ans += num[i].size() - (*num[i].begin() > 2 * siz);
siz += sum[i];
}
cout << ans << '\n';
}
return 0;
}
评分:\(\huge 5\)
1.6 K-D树
1.7 LCT
1.8 块状链表
1.9 其他
1.9.1 洛谷P7603 [THUPC 2021] 鬼街
那条街有“鬼街”之称,十年前是 A 市最繁华的地段之一,然而如今这里已无活人居住。
街边七零八落地排着 \(n\) 座房子,每栋房子都有一个 \(1\) 到 \(n\) 之间的独一无二的编号,用仿佛来自地狱的黑漆涂在破瓦残砖上,在黄尘中隐隐若现。
传说,这条街上的鬼是与别处的鬼是不同的,它们喜欢研究数论,会根据数字的性质来选择自己的生活,所以它们才为每一栋房子都画上了编号。
新上任的 A 市市长并不相信魑魅魍魉的传言,为了探清真相,他决定为这条街装上灵异事件监控器。
下面有 \(m\) 个事件依次发生。
- 灵异事件:在以 \(x\) 的所有质因子为编号的房子里,都发生了 \(y\) 次闹鬼。由于神秘的原因,次数 \(y\) 可能为 \(0\)。
- 监控事件:有一个监控器被安装,其监控以 \(x\) 的所有质因子为编号的房子,当累计的闹鬼总次数达到阈值 \(y\) 时,该监控器会触发报警(若 \(y = 0\),则不论被监控的房子是哪几栋,下一次灵异事件都会立即触发该监控器的报警)。不同房子发生的灵异事件次数会被分开统计,不同的监控器互不影响。所有的监控器被从 \(1\) 开始依次编号。
请将所有的报警反馈给市长,即每个灵异事件之后,有哪些监控器被触发。
• 容易发现 \(ω(n)≤6\),单点加是容易,问题在于如何维护监控。
• 将每个监控器的阈值平均分到其能监控到的房间上,若一个房间闹鬼次数达到这个平均阈值,则将这个监控器拎出来判断是否会响。
• 考虑对每个监控阈值维护,每次拿出剩余阈值最小的,判断是否需要重新分配警报。拿两个堆维护就行了,支持删除插入。
• 这样保证每次实现每个监控的阈值减半。
还是看不懂,但我们知道一个数的质因子数 \(ω(n)≤6\)。因为 \(2 \times 3 \times 5 \times 7 \times 11 \times 13 \times17 > 10^5\)。
暴力预处理质因子,\(O(n \sqrt n)\),可以忽略。
根据鸽巢原理,对于每个警报器,至少有一个点的报警次数大于等于 \(\lceil \cfrac{y}{ω(n)} \rceil\)。
这里我们就已经阐明减半警报器的思想了:
对于报警器管辖的每一个地方,设置阈限 \(\lceil \cfrac{y}{ω(n)} \rceil\)
对于每个警报器,其阈限触发后,对其剩下的阈值暴力重构,阈限\(\lceil \cfrac{y}{ω(n)} \rceil \to \lceil \cfrac{(ω(n)-1) \times y}{ω(n)} \rceil\),(自己思考)。
设一个警报器最多被拉出来 \(n\) 次,阈限最大值为 \(V\)。
所以警报器最多被拉出来 \(\log_{\tfrac{6}{5}} V\) 次。
此时我们再用小根堆维护最先超出阈值的警报器。
每次修改警报器时,需要枚举每个位置。
因此,总复杂度 \(O(q \omega(x) \log q \log_{\tfrac{6}{5}} V)\)
理解代码吧。。。
\(\huge \mathscr{Code}\)
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 100;
long long n, m, tag[N], vis[N], tot, last, lim[N];
vector<int> D[N], ans;
struct ques {
long long x, y;
}qs[N];
struct node {
long long id, val;
friend bool operator<(node a, node b) { return a.val > b.val; }
};
priority_queue<node> q[N];
unordered_map<int, int> lst[N];
class EulerSieve {
public:
vector<int> prime;
bool vis[N] = {};
void Sieve(int n) {
for (int i = 2; i <= n; i++) {
if (!vis[i]) prime.push_back(i);
for (int p : prime) {
if (i * p > n) break;
vis[i * p] = 1;
if (i % p == 0) break;
}
}
}
}ES;
void pre() {
ES.Sieve(n);
for (int i = 2; i <= n; i++) {
int tmp = i;
for (int p : ES.prime) {
if (p * p > tmp) break;
if (tmp % p == 0) {
D[i].push_back(p);
while (tmp % p == 0) tmp /= p;
}
}
if (tmp > 1) D[i].push_back(tmp);
}
}
long long calc(int x) {
long long s = 0;
for (int p : D[x]) s += tag[p];
return s;
}
void add(int id) {
int x = qs[id].x;
long long threshold = (lim[id] - calc(x) + (int)D[x].size() - 1) / D[x].size();
for (int p : D[x]) q[p].push(node{ id,tag[p] + threshold });
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> m;
pre();
for (int i = 1; i <= m; i++) {
long long op, x, y;
cin >> op >> x >> y;
y ^= last;
if (op == 0) {
for (int p : D[x]) {
tag[p] += y;
while (q[p].size() && q[p].top().val <= tag[p]) {
node now = q[p].top();
q[p].pop();
if (vis[now.id]) continue;
if (calc(qs[now.id].x) >= lim[now.id]) {
vis[now.id] = 1;
ans.push_back(now.id);
continue;
}
if (lst[now.id][p] == i) continue;
lst[now.id][p] = i;
add(now.id);
}
}
sort(ans.begin(), ans.end());
last = ans.size();
cout << last << ' ';
for (int e : ans) cout << e << ' ';
cout << '\n';
ans.clear();
}
else {
qs[++tot] = ques{ x,y };
if (!y) {
ans.push_back(tot);
continue;
}
lim[tot] = calc(x) + y;
add(tot);
}
}
return 0;
}
评分:\(\huge 7\)
1.9.2 CFgym102331F Fast Spanning Tree
王秀涵有一个最初为空的无向图,图中有 \(n\) 个顶点。
每个顶点都有一个权重,权重是一个非负整数。
此外,他还有 \(m\) 个图元 (\(a_i,b_i,s_i\)) ,其中 \(1≤a_i,b_i≤n\) 、 \(a_i≠b_i\) 和 \(s_i\) 为非负整数。
之后,他开始了下面的过程:
- 如果没有 \(i\) 使 \(a_i\) 和 \(b_i\) 位于图的不同连通部分,且\(( a_i 部分的顶点总重)+( b_i 部分的顶点总重) ≥ s_i\) ,则结束该过程。
- 否则,选择最小的 \(i\) ,在 \(a_i\) 和 \(b_i\) 之间添加一条边,在记事本中写下 \(i\) ,然后重复上述过程(但现在是在更大的图形上)。
这个过程完成后,不幸发生了......有人偷走了他的记事本!你能帮助他有效地恢复所有数字吗?
这个才是真正的减半警报器板子。
因为一个警报器管辖的是 \(a_i,b_i\),所以复杂度是 \(O(q \log n \log V)\)。
\(\huge \mathscr{Code}\)
#include<bits/stdc++.h>
using namespace std;
const int N = 3e5 + 100;
int n, m, val[N], fa[N], tot, rec[N], vis[N];
struct node {
int val, id;
friend bool operator<(node a, node b) { return a.val > b.val; }
};
priority_queue<node> q[N];
priority_queue<int, vector<int>, greater<int>> ans;
struct dat {
int x, y, s;
}qs[N];
int find(int x) {
if (x == fa[x]) return x;
return fa[x] = find(fa[x]);
}
void add(int id) {
int fx = find(qs[id].x), fy = find(qs[id].y);
if (fx == fy) return;
if (val[fx] + val[fy] >= qs[id].s) {
ans.push(id);
return;
}
int thres = (qs[id].s - val[fy] - val[fx] + 1) >> 1;
q[fx].push(node{ val[fx] + thres,id });
q[fy].push(node{ val[fy] + thres,id });
}
signed main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> val[i];
fa[i] = i;
}
for (int i = 1; i <= m; i++) {
cin >> qs[i].x >> qs[i].y >> qs[i].s;
add(i);
}
while (ans.size()) {
int id = ans.top();
ans.pop();
int fx = find(qs[id].x), fy = find(qs[id].y);
if (fx == fy) continue;
rec[++tot] = id;
if (q[fx].size() > q[fy].size()) swap(fx, fy);
fa[fx] = fy;
val[fy] += val[fx];
if (val[fy] > 1e9) val[fy] = 1e9;
while (q[fx].size()) {
node t = q[fx].top();
q[fx].pop();
if (t.val <= val[fy]) add(t.id);
else q[fy].push(t);
}
while (q[fy].size()) {
node t = q[fy].top();
q[fy].pop();
if (t.val <= val[fy]) add(t.id);
else {
q[fy].push(t);
break;
}
}
}
cout << tot << '\n';
for (int i = 1; i <= tot; i++) cout << rec[i] << ' ';
return 0;
}
评分:\(\huge 5\)
1.9.3 洛谷P9968 [THUPC 2024 初赛] 二进制
今天也是喜欢二进制串的一天,小 F 开始玩二进制串的游戏。
小 F 给出了一个这里有一个长为 \(n\) 的二进制串 \(s\),下标从 \(1\) 到 \(n\),且 \(\forall i\in[1,n],s_i\in \{0,1\}\),他想要删除若干二进制子串。
具体的,小 F 做出了 \(n\) 次尝试。
在第 \(i\in[1,n]\) 次尝试中,他会先写出正整数 \(i\) 的二进制串表示 \(t\)(无前导零,左侧为高位,例如 \(10\) 可以写为 \(1010\))。
随后找到这个二进制表示 \(t\) 在 \(s\) 中从左到右 第一次 出现的位置,并删除这个串。
注意,删除后左右部分的串会拼接在一起 形成一个新的串,请注意新串下标的改变。
若当前 \(t\) 不在 \(s\) 中存在,则小 F 对串 \(s\) 不作出改变。
你需要回答每一次尝试中,\(t\) 在 \(s\) 中第一次出现的位置的左端点以及 \(t\) 在 \(s\) 中出现了多少次。
定义两次出现不同当且仅当出现的位置的左端点不同。
对于第 \(i\) 次尝试,关于 \(i\) 的二进制串长度不是固定的。
这令我们非常头痛,所以可以马上知道一个小Trick:二进制分组。
将二进制长度相同的一起处理,运用可删堆来维护答案。
代码实现比较难,可以参考一下我的代码。
\(\huge \mathscr{Code}\)
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 100;4
int n;
string bits;
class delable_q {
public:
priority_queue<int, vector<int>, greater<int>> q, d;
void insert(int x) {
q.push(x);
}
void del(int x) {
d.push(x);
}
int size() {
return q.size() - d.size();
}
int top() {
while (!d.empty() && q.top() == d.top()) q.pop(), d.pop();
return q.top();
}
}delq[N];
class BIT {
public:
int tree[N];
void update(int x, int v) {
while (x <= n) {
tree[x] += v;
x += x & -x;
}
}
int query(int x) {
int ans = 0;
while (x) {
ans += tree[x];
x -= x & -x;
}
return ans;
}
}T;
struct node {
int pre, nxt;
}nlist[N];
inline int highbit(int x) { return __lg(x) + 1; }
void update(int pos, int opt) {
if (bits[pos] == '0') return;
int now = 0;
for (int i = pos, len = 1; i && len <= highbit(n); i = nlist[i].nxt, len++) {
now *= 2, now += bits[i] - '0';
if (now > n) break;
if (opt) delq[now].del(pos);
else delq[now].insert(pos);
}
}
void erase(int pos, int len) {
int lpos = pos, rpos = pos;
T.update(pos, len);
for (int i = 1; i <= len; i++) {
update(rpos, 1);
rpos = nlist[rpos].nxt;
}
nlist[rpos].pre = nlist[pos].pre;
for (int i = 1; i <= highbit(n); i++) {
if (!lpos) break;
lpos = nlist[lpos].pre;
update(lpos, 1);
}
if (nlist[pos].pre) nlist[nlist[pos].pre].nxt = rpos;
lpos = pos;
for (int i = 1; i <= highbit(n); i++) {
if (!lpos) break;
lpos = nlist[lpos].pre;
update(lpos, 0);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> bits;
bits = " " + bits;
for (int i = 1; i <= n; i++) nlist[i] = node{ i - 1,i + 1 };
nlist[n].nxt = 0;
for (int i = 1; i <= n; i++) update(i, 0);
for (int i = 1; i <= n; i++) {
if (!delq[i].size()) cout << -1 << ' ' << 0 << '\n';
else {
int top = delq[i].top();
cout << top - T.query(top) << ' ' << delq[i].size() << '\n';
erase(top, highbit(i));
}
}
return 0;
}
评分:\(\huge 5\)

浙公网安备 33010602011771号