数据结构杂题
简介
本博客主要用于记录一些神鸡数据结构题,并记录一些有趣的 trick。
“插入——标记——回收”算法解决函数复合问题
考虑这样一个函数复合问题:
给定一串函数 \(f_i\)(有可能是分段函数,即根据 \(x\) 的范围导致的 \(f\) 解析式不同),每次询问,给出 \(l, r, x\),求 \(f_l \circ f_{l + 1} \circ f_{l + 2} \circ \cdots \circ f_r \circ x\)。允许离线。
做法是:
- 将询问离线下来,考虑进行扫描线。
- 对于每一个询问,考虑将其两端点各拆成一个单点操作。
- 每个操作,就是将一个数 \(x\) 加入一个可重集(或删除出并获取答案)。
- 那么这样,就可以考虑如何全局维护一个集合的函数。
- 发现维护时有可能需要根据 \(x\) 的范围来确定函数种类/有无。
- 使用平衡树(FHQ Treap)来维护这件事情。
- 最终维护的总体时间复杂度为 \(\mathcal O(n \log{n})\)。
例题:QOJ #8672. 排队
题意:给定 \(f_i(x) = x + [x \in [l_i, r_i]]\),每次询问 \(f_l \circ f_{l + 1} \circ f_{l + 2} \circ \cdots \circ f_r \circ x\)。
将操作离线后,用“插入-标记-回收”算法处理,建一颗 FHQ Treap,然后每次对于一个函数,直接对平衡树内 \([l_i, r_i]\) 打上 \(+ 1\) 标签即可。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
mt19937 gen(145);
inline int read(){
int x = 0; char ch = getchar();
while(ch <'0' || ch > '9') ch = getchar();
while(ch >= '0' && ch <= '9') x = (x << 1) + (x << 3) + (ch ^ 48), ch = getchar();
return x;
}
int n, q;
int l[N], r[N];
vector<int> in[N], out[N];
struct FHQ{
int val[N], rnd[N], ls[N], rs[N], tag[N], fa[N], stk[N];
int idx = 0, rt = 0, tot = 0;
pair<int, int> tmp = {0, 0};
int newnode(int v){ val[++ idx] = v, fa[idx] = 0, tag[idx] = 0, ls[idx] = rs[idx] = 0, rnd[idx] = gen(); return idx; }
void down(int u, int x){ val[u] += x, tag[u] += x; }
void down(int u){ if(tag[u]) down(ls[u], tag[u]), down(rs[u], tag[u]), tag[u] = 0; }
pair<int, int> split(int u, int x){
if(!u) return make_pair(0, 0);
down(u);
if(val[u] <= x) {
tmp = split(rs[u], x);
rs[u] = tmp.first;
fa[rs[u]] = u;
fa[tmp.second] = 0;
return {u, tmp.second};
} else {
tmp = split(ls[u], x);
ls[u] = tmp.second;
fa[ls[u]] = u;
fa[tmp.first] = 0;
return {tmp.first, u};
}
}
int merge(int u, int v){
if(!u || !v) return u + v;
if(rnd[u] > rnd[v]) return down(u), rs[u] = merge(rs[u], v), fa[rs[u]] = u, u;
else return down(v), ls[v] = merge(u, ls[v]), fa[ls[v]] = v, v;
}
int getfirst(int u){ while(ls[u]) down(u), u = ls[u]; return val[u]; }
int join(int x, int y){
int ans = 0;
while(x){
tmp = split(x, getfirst(y));
ans = merge(ans, tmp.first), x = tmp.second;
swap(x, y);
}
return merge(ans, y);
}
void load(int u){
while(fa[u]) u = fa[u], stk[++ tot] = u;
while(tot) down(stk[tot --]);
}
} T;
int to[N], ans[N];
int main(){
n = read(), q = read();
for(int i = 1; i <= n; i ++) l[i] = read(), r[i] = read();
for(int i = 1, x, y; i <= q; i ++) x = read(), y = read(), in[x].emplace_back(i), out[y].emplace_back(i);
for(int i = 1; i <= n; i ++){
for(auto j : in[i]){
to[j] = T.newnode(0);
T.rt = T.join(T.rt, to[j]);
}
int x, y, z;
tie(x, y) = T.split(T.rt, l[i] - 1), tie(y, z) = T.split(y, r[i]);
T.down(y, 1);
T.rt = T.merge(x, T.join(y, z));
for(auto j : out[i]){
T.load(to[j]), ans[j] = T.val[to[j]];
}
}
for(int i = 1; i <= q; i ++) printf("%d\n", ans[i]);
return 0;
}
矩阵乘法优化线段树标记问题
D
维护一个长度为 \(T\) 的整数序列:
Q x y:求 \([x, y]\) 的当前最大值。A x y:求 \([x, y]\) 的历史最大值(包括现在)。P x y Z:将 \([x, y]\) 每个数增加 \(Z\)。C x y Z:将 \([x, y]\) 覆盖为 \(Z\)。
我们发现实际上我们并不好维护 \([x, y]\) 的历史最大值,无法很好地描述懒标记的下传。
矩阵乘法统一操作
我们使用矩阵乘法将所有操作统一为矩阵区间乘,避免懒标记的影响。
我们使用 \((max, +)\) 半环作为我们的矩阵。
初始矩阵:
考虑矩阵操作需要常数项,所以在此处我们添加一项常数,即加法单位元,作为我们的初始矩阵。
区间加 \(z\)
构造矩阵
区间覆盖 \(z\)
\(\begin{bmatrix}-\infty &-\infty &z \\ -\infty &0 &z \\ -\infty &-\infty &0\end{bmatrix}\)
使用完矩阵乘法优化后,我们发现,矩阵乘法的常数依旧很大,来到了 \(27\) 的级别。
考虑优化:
我们发现,矩阵中 \((1, 2), (2, 2), (3, 1), (3, 2), (3, 3)\) 是恒不变的,故我们考虑只维护剩下四个位置的情况,这样时间常数下降,来到了 \(4\)。
其他的东西就和普通的线段树差不多了,记录懒标记时,如果没有操作,则记 \(tag\) 为 \(单位矩阵 \; I\)。
上述做法就是矩阵优化线段树,主要用于懒标记维护多或者标记难以下传等问题,适配于查询历史信息等经典模型。
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e5 + 5;
const int INF = 1e18;
const int NINF = -1e18;
int n, m;
int a[N];
int add(int x, int y) { return (x == NINF || y == NINF) ? NINF : x + y; }
struct Mat {
int a11, a13, a21, a23;
Mat() : a11(0), a13(NINF), a21(NINF), a23(NINF) {}
Mat(int _11, int _13, int _21, int _23) : a11(_11), a13(_13), a21(_21), a23(_23) {}
Mat operator * (const Mat& b) const {
Mat c;
c.a11 = add(a11, b.a11);
c.a13 = max(add(a11, b.a13), a13);
c.a21 = max(add(a21, b.a11), b.a21);
c.a23 = max(max(add(a21, b.a13), b.a23), a23);
return c;
}
};
namespace segtree {
#define ls (u << 1)
#define rs (u << 1 | 1)
#define mid ((l + r) >> 1)
#define segroot int u = 1, int l = 1, int r = n
#define lson ls, l, mid
#define rson rs, mid + 1, r
struct Node { int mx, his; Mat tag; } t[N*4];
void up(int u) { t[u].mx = max(t[ls].mx, t[rs].mx), t[u].his = max(t[ls].his, t[rs].his); }
void down(int u, const Mat& M) {
int mx = t[u].mx, his = t[u].his;
t[u].mx = max(add(M.a11, mx), M.a13);
t[u].his = max(max(add(M.a21, mx), his), M.a23);
t[u].tag = M * t[u].tag;
}
void down(int u) { down(ls, t[u].tag), down(rs, t[u].tag), t[u].tag = Mat(); }
void bld(segroot) {
t[u].tag = Mat();
if(l == r) { return t[u].mx = t[u].his = a[l], void(); }
bld(lson), bld(rson), up(u);
}
void mdf(int ql, int qr, const Mat& M, segroot) {
if(qr < l || r < ql) return;
if(ql <= l && r <= qr) { down(u, M); return; }
down(u);
mdf(ql, qr, M, lson), mdf(ql, qr, M, rson);
up(u);
}
pair<int,int> query(int ql, int qr, segroot) {
if(ql <= l && r <= qr) return {t[u].mx, t[u].his};
down(u);
if(qr <= mid) return query(ql, qr, lson);
if(ql > mid) return query(ql, qr, rson);
auto L = query(ql, qr, lson), R = query(ql, qr, rson);
return {max(L.first, R.first), max(L.second, R.second)};
}
}
using namespace segtree;
signed main() {
scanf("%lld", &n);
for(int i = 1; i <= n; ++i) scanf("%lld", &a[i]);
bld();
scanf("%lld", &m);
char op; int x, y, z;
while(m--) {
scanf(" %c", &op);
if(op == 'Q') {
scanf("%lld%lld", &x, &y);
printf("%lld\n", query(x, y).first);
} else if(op == 'A') {
scanf("%lld%lld", &x, &y);
printf("%lld\n", query(x, y).second);
} else if(op == 'P') {
scanf("%lld%lld%lld", &x, &y, &z);
mdf(x, y, Mat(z, NINF, z, NINF));
} else if(op == 'C') {
scanf("%lld%lld%lld", &x, &y, &z);
mdf(x, y, Mat(NINF, z, NINF, z));
}
}
return 0;
}
P7838 「Wdoi-3」夜雀 treating - 洛谷
一个 \(2q + 1\) 的排列,每次操作取出最中间的数,并选择再一个数从排列中删去。求最后取出得到的序列最长连续段长度最长可以是多少。
同样地,此题也是:T696069 列表 - 洛谷。
引理1 一个子集 \(S\) 最后能够得到,对 \(\forall i \in Q\),初始序列中一段下标 \([N + 1 - i, N + 1 + i]\) 中的 \(2i + 1\) 个数中至少含有 \(i + 1\) 个数 \(\in S\)。
证明:首先,可以证明,第 \(i + 1\) 次操作加入的数的下标一定在 \([N + 1 - i, N + 1 + i]\) 内。所以说在这个范围内合法,一定至少要有 \(i + 1\) 个数。反过来,每次删除一个数,就将里中心最近的一个不在 \(S\) 里数删掉,一定最优。
引理2 一个连续段 \(A\) 想要满足 \(A \subseteq S\),当且仅当:\(\forall i \in Q\),不在序列初始中间下标 \([N - i + 1, N + i - 1]\) 中的数最多有 \(N - i\) 个数 \(\in A\)。
所以说,一个连续段 \(A\) 合法,当且仅当 \(\forall i, |A \cap [N - i + 1, N + i - 1]| \ge |A| - N + i\)。
由此,我们想到做法:枚举连续数字段然后判定是否合法,时间复杂度 \(O(N^2)\)。
我们容易发现,对于一个左端点 \(l\),都有一个最大的右端点,称为 \(f(l)\),且 \(f(l) \le f(l + 1)\),于是我们可以尝试双指针。
现在我们对于连续段 $A = [l, r] $ “合法”的判定方式是:
其中 \(m = r - l + 1\)。
转化一下:对于一个位置 \(p\),到中心的距离 \(d = |p - (N + 1)|\)。
这个位置 \(\in [N - i + 1, N + i + 1]\),当且仅当 \(i \ge d\)。
所以我们每加入或删除一个位置的数,就会对一个后缀产生 \(+1/-1\) 的影响,故我们使用线段树维护:
那么最后我们检查是否合法,就是检查:
动态全局记录一个 \(|A|\) 即可。
最终时间复杂度:\(\mathcal O(N \log N)\)。
#include<bits/stdc++.h>
using namespace std;
const int N = 4e5 + 5;
int n, m, Mid;
int pos[N];
struct segtree{
#define ls u << 1
#define rs u << 1 | 1
#define mid (l + r >> 1)
#define segroot int u = 1, int l = 0, int r = n
#define lson ls, l, mid
#define rson rs, mid + 1, r
int mn[N << 2], tag[N << 2];
void up(int u){ mn[u] = min(mn[ls], mn[rs]); }
void bld(segroot){
if(l == r) return mn[u] = -l, void();
bld(lson), bld(rson), up(u);
}
void down(int u, int x){ mn[u] += x, tag[u] += x; }
void down(int u){ down(ls, tag[u]), down(rs, tag[u]), tag[u] = 0; }
void mdf(int ql, int qr, int v, segroot){
if(l > qr || r < ql) return ;
if(ql <= l && r <= qr) return down(u, v);
down(u), mdf(ql, qr, v, lson), mdf(ql, qr, v, rson), up(u);
}
int qry(){ return mn[1]; }
} T;
inline int getd(int x){ return abs(pos[x] - Mid); }
int main() {
freopen("echo.in", "r", stdin);
freopen("echo.out", "w", stdout);
ios::sync_with_stdio(0), cin.tie(nullptr), cout.tie(nullptr);
cin>>n;
int m = 2 * n + 1;
for(int i = 1, x; i <= m; i ++) cin>>x, pos[x] = i;
Mid = n + 1;
T.bld();
int ans = 0;
for (int l = 1, r = 0; l <= m; l ++){
r = max(r, l - 1);
while (r < m) {
int nxt = r + 1;
T.mdf(getd(nxt), n, 1);
int new_m = r - l + 2;
if (T.qry() >= new_m - n) {
r = nxt;
ans = max(ans, new_m);
} else {
T.mdf(getd(nxt), n, -1);
break;
}
}
if (l <= r) {
T.mdf(getd(l), n, -1);
}
}
cout<<ans<<'\n';
return 0;
}
H - 不同的整数 --- H - Distinct Integers
每次询问一个区间 \([X_i, Y_i]\) 中有多少个子区间没有重复的数字。
设 \(pre_x\) 表示 \([1, x)\) 中最后一个与 \(x\) 位置颜色相同的位置。
一段合法的区间满足:\(\forall i \in [l, r], pre_i < l\),那我们对于每个 \(l\) 记录一个 \(Ans = \min\{pre_r > l\}\),不太好做,我们考虑换一个思路。
我们考虑固定 \(r\),记录 \(Ans = r - \max\limits_{j=X_i}^{r} \{pre_j\}\)。
那对于所有的 \(r\),\(Ans = \sum\limits_{r=X_i}^{Y_i}(r - \max\limits_{j=X_i}^{r} \{pre_j\}) = \sum\limits_{r=X_i}^{Y_i}r - \sum\limits_{r=X_i}^{Y_i}\max\limits_{j=X_i}^{r} \{pre_j\}\)。
我们现在要维护的就是前缀最大值的和。
类比楼房重建,我们维护最大值与前缀最大值之和。
对于左子树的和直接加起来,右子树直接递归类比线段树二分求解。
然后对于修改操作,我们需要支持插入或删除,使用平衡树或者开一个 set 维护 \(pre\) 即可。
最终时间复杂度为:\(\mathcal O(n \log^2n)\)。
P2839 国家集训队 middle - 洛谷
求左端点在 \([a, b]\) 右端点在 \([c, d]\) 的所有区间的最大中位数。
我们钦定一个中位数 \(k\),再来判断 \(k\) 是不是真的是中位数。
我们把小于 \(k\) 的看作 \(-1\),大于 \(k\) 的看作 \(1\),等于 \(k\) 的看作 \(0\)。
一个区间 \([l, r]\) 就可以分成三段,\([l, b]\)、\((b, c)\)、\([c, r]\)。
我们只需要判断这里的和是否 \(\ge 0\),如果是,那么中位数还可以更大,不是的话,中位数就需要往小了找。
那怎么建立这样的线段树呢?
我们使用主席树。
考虑当 \(k\) 从 \(k\) 变成 \(k+1\) 时,我们只需要对于有影响的点改动就可以修改成 \(k+1\) 的版本。
复杂度均摊下来就可以做到 \(\mathcal O(n \log n)\)。
#include<bits/stdc++.h>
using namespace std;
const int N = 20005;
const int M = N * 20;
int n, m, cnt;
int a[N], val[N];
int rt[N];
struct Node {
int s, l, r;
Node(int _s = 0, int _l = 0, int _r = 0) : s(_s), l(_l), r(_r) {}
};
inline Node merge(const Node &x, const Node &y) {
return Node(x.s + y.s, max(x.l, x.s + y.l), max(y.r, y.s + x.r));
}
struct PST {
#define mid ((l + r) >> 1)
#define segroot int &u, int l = 1, int r = n
#define lson ls[u], l, mid
#define rson rs[u], mid + 1, r
int ls[M], rs[M], sum[M], lmx[M], rmx[M], idx;
void up(int u) {
sum[u] = sum[ls[u]] + sum[rs[u]];
lmx[u] = max(lmx[ls[u]], sum[ls[u]] + lmx[rs[u]]);
rmx[u] = max(rmx[rs[u]], sum[rs[u]] + rmx[ls[u]]);
}
void bld(segroot) {
if(!u) u = ++ idx;
if (l == r) return sum[u] = lmx[u] = rmx[u] = 1, void();
bld(lson), bld(rson), up(u);
}
void mdf(int pos, int v, segroot) {
if(l > pos || r < pos) return ;
u = ++ idx;
ls[u] = ls[v], rs[u] = rs[v], sum[u] = sum[v]; lmx[u] = lmx[v], rmx[u] = rmx[v];
if (l == r) return sum[u] = lmx[u] = rmx[u] = -1, void();
mdf(pos, ls[v], lson), mdf(pos, rs[v], rson), up(u);
}
Node qry(int ql, int qr, int u, int l=1, int r=n) {
if (ql > qr) return Node(0, 0, 0);
if (ql <= l && r <= qr) return Node(sum[u], lmx[u], rmx[u]);
if (qr <= mid) return qry(ql, qr, lson);
if (ql > mid) return qry(ql, qr, rson);
return merge(qry(ql, qr, lson), qry(ql, qr, rson));
}
} T;
int main() {
scanf("%d", &n);
vector<int> vec;
for (int i = 1; i <= n; ++i) {
scanf("%d", &a[i]);
vec.push_back(a[i]);
}
sort(vec.begin(), vec.end());
vec.erase(unique(vec.begin(), vec.end()), vec.end());
cnt = vec.size();
for(int i = 1; i <= n; i ++) val[i] = lower_bound(vec.begin(), vec.end(), a[i]) - vec.begin() + 1;
vector<vector<int>> pos(cnt + 1);
for(int i = 1; i <= n; i ++) pos[val[i]].push_back(i);
T.bld(rt[1]);
int cur = rt[1];
for(int i = 2; i <= cnt; i ++){
for(int p : pos[i - 1]){
int new_rt = 0;
T.mdf(p, cur, new_rt);
cur = new_rt;
}
rt[i] = cur;
}
scanf("%d", &m);
int ans = 0;
while (m --) {
int q[4];
scanf("%d%d%d%d", &q[0], &q[1], &q[2], &q[3]);
for (int i = 0; i < 4; ++i) q[i] = (q[i] + ans) % n;
sort(q, q + 4);
int a = q[0] + 1, b = q[1] + 1, c = q[2] + 1, d = q[3] + 1;
int l = 1, r = cnt;
while(l <= r){
int md = (l + r) >> 1;
Node L = T.qry(a, b, rt[md]);
Node M = T.qry(b + 1, c - 1, rt[md]);
Node R = T.qry(c, d, rt[md]);
if (L.r + M.s + R.l >= 0) ans = vec[md - 1], l = md + 1;
else r = md - 1;
}
printf("%d\n", ans);
}
return 0;
}
P4735 最大异或和 - 洛谷
进行前缀异或和处理。
对于一个区间。 \(pre_N \oplus x\) 是一个定值 \(k\),我们现在就要求 \(pre_{p - 1} \oplus k\) 最大。
发现如果没有区间限制,就会是一个简单的 \(\text{Trie}\) 可做的事情。
对于区间询问, 我们考虑使用可持久化 \(\text{01Trie}\),用 \([1, r]\) 中的信息除去 \([1, l - 1]\) 中的信息即可。
#include<bits/stdc++.h>
using namespace std;
const int N = 3e5 + 5;
int n, m;
int nxt[N * 25][2], idx, cnt[N * 25];
int rt[N * 2], sum[N * 2];
void add(int u, int v, int w, int x){
if(w < 0) return ;
int c = (x >> w) & 1;
nxt[u][c ^ 1] = nxt[v][c ^ 1];
nxt[u][c] = ++ idx;
cnt[nxt[u][c]] = cnt[nxt[v][c]] + 1;
add(nxt[u][c], nxt[v][c], w - 1, x);
}
int qry(int u, int v, int x, int w){
if(w < 0) return 0;
int c = (x >> w) & 1;
if(cnt[nxt[v][c ^ 1]] > cnt[nxt[u][c ^ 1]]) return qry(nxt[u][c ^ 1], nxt[v][c ^ 1], x, w - 1) ^ (1 << w);
return qry(nxt[u][c], nxt[v][c], x, w - 1);
}
int main(){
scanf("%d%d", &n, &m);
add(rt[0] = ++ idx, 0, 24, 0);
for(int i = 1, x; i <= n; i ++){
scanf("%d", &x);
sum[i] = sum[i - 1] ^ x;
add(rt[i] = ++ idx, rt[i - 1], 24, sum[i]);
}
while(m --){
char op; int x, y, z; scanf("\n%c%d", &op, &x);
if(op == 'A'){
n ++;
sum[n] = sum[n - 1] ^ x;
add(rt[n] = ++ idx, rt[n - 1], 24, sum[n]);
} else {
scanf("%d%d", &y, &z);
x --, y --;
if(x == 0) printf("%d\n", qry(0, rt[y], sum[n] ^ z, 24));
else printf("%d\n", qry(rt[x - 1], rt[y], sum[n] ^ z, 24));
}
}
return 0;
}

浙公网安备 33010602011771号