题意
传送门 Codeforces 1858 E1 Rollbacks (Easy Version) / E2 Rollbacks (Hard Version)
题解
E1 树
此版本允许离线,则根据查询构造出一棵树。这棵树满足性质:每一个查询都对应树上的一条从根节点自顶向下的路径。对于 ‘+’ 则以当前节点为父节点新建一个节点;对于 ‘-’ 则利用树上倍增,向根节点方向移动 k k k 个单位。对于撤销操作,维护一个栈即可。总时间复杂度 O ( q log q ) O(q\log q) O(qlogq)。
#include <bits/stdc++.h>
using namespace std;
struct Query {
char op;
int x;
};
struct Edge {
int to, x;
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int q;
cin >> q;
vector<Query> qs(q);
int n = 1;
for (int i = 0; i < q; ++i) {
auto &[op, x] = qs[i];
cin >> op;
if (op == '+' || op == '-') {
cin >> x;
if (op == '+') {
n += 1;
x -= 1;
}
}
}
vector<vector<Edge>> g(n);
const int lg = 20;
vector<vector<int>> par(lg, vector<int>(n, -1));
vector<int> stk;
stk.reserve(q);
int v_idx = 0;
auto add = [&](int p, int x) {
int v = v_idx;
v_idx += 1;
if (p != -1) {
g[p].push_back({v, x});
}
par[0][v] = p;
for (int i = 0; i + 1 < lg; ++i) {
if (par[i][v] == -1) {
break;
}
par[i + 1][v] = par[i][par[i][v]];
}
return v;
};
auto get = [&](int v, int k) {
for (int i = 0; i < lg; ++i) {
if (k >> i & 1) {
v = par[i][v];
}
}
return v;
};
int v = add(-1, -1);
vector<int> pos(q, -1);
for (int i = 0; i < q; ++i) {
auto &[op, x] = qs[i];
if (op == '+') {
stk.push_back(v);
int u = add(v, x);
v = u;
} else if (op == '-') {
stk.push_back(v);
int u = get(v, x);
v = u;
} else if (op == '!') {
int u = stk.back();
stk.pop_back();
v = u;
} else {
pos[i] = v;
}
}
const int mx_x = 1e6;
vector<int> cnt(mx_x);
int sum = 0;
vector<int> res(n);
auto update = [&](int x, int d) {
if (cnt[x] == 0) {
sum += 1;
}
cnt[x] += d;
if (cnt[x] == 0) {
sum -= 1;
}
};
function<void(int)> dfs = [&](int v) {
res[v] = sum;
for (auto [u, x] : g[v]) {
update(x, 1);
dfs(u);
update(x, -1);
}
};
dfs(0);
for (int i = 0; i < q; ++i) {
if (qs[i].op == '?') {
cout << res[pos[i]] << '\n';
}
}
return 0;
}
E2 前缀和
此版本强制在线。维护数组 c i c_i ci:若 a i a_i ai 的索引是对应数字在数组最小的,则 c i = 1 c_i=1 ci=1,反之为 0 0 0。 则 ‘-’ 及其撤销操作转化为不同前缀的 ∑ c i \sum c_i ∑ci 查询。一个直观的做法是,维护一个足够大的数组 A A A 的信息(各个数字出现的位置以及 c i c_i ci、前缀和),像维护栈一样,不断维护 A A A,使每次操作后的 a a a 都是 A A A 的一个前缀。对于 ‘+’ 及其撤销操作,只用修改一个数字的至多两个位置的信息。使用 std::set 维护数字位置,使用 BIT 维护前缀和。总时间复杂度 O ( q log q ) O(q\log q) O(qlogq)。
其实上不必动态地更新 A A A 中所有元素的信息,只需要维护前缀 a a a 的信息,然后 O ( 1 ) O(1) O(1) 撤销操作即可。时间复杂度 O ( q ) O(q) O(q)。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int q;
cin >> q;
const int max_x = 1e6;
vector<int> pos(max_x + 1, q);
vector<int> a(q), c(q + 1);
vector<int> sum(q + 1);
vector<array<int, 5>> stk;
int n = 0;
for (int i = 0; i < q; ++i) {
char op;
cin >> op;
if (op == '+') {
int x;
cin >> x;
stk.push_back({x, pos[x], a[n], pos[a[n]], sum[n + 1]});
if (pos[a[n]] == n) {
c[pos[a[n]]] -= 1;
pos[a[n]] = q;
c[pos[a[n]]] += 1;
}
if (pos[x] > n) {
c[pos[x]] -= 1;
pos[x] = n;
c[pos[x]] += 1;
}
a[n] = x;
sum[n + 1] = sum[n] + c[n];
n += 1;
} else if (op == '-') {
int k;
cin >> k;
n -= k;
stk.push_back({-1, k});
} else if (op == '!') {
auto [x, px, y, py, s] = stk.back();
stk.pop_back();
if (x == -1) {
n += px;
} else {
n -= 1;
a[n] = y;
sum[n + 1] = s;
if (px != pos[x]) {
c[pos[x]] -= 1;
pos[x] = px;
c[pos[x]] += 1;
}
if (py != pos[y]) {
c[pos[y]] -= 1;
pos[y] = py;
c[pos[y]] += 1;
}
}
} else {
cout << sum[n] << endl;
}
}
return 0;
}