【套路数据结构】CF1858E2 Rollbacks (Hard Version)

· · 题解

题意

维护一个初始为空的序列 a,支持以下操作:

## 解法 **本题解含有 $O(q\log q)$ 做法与 $O(q)$ 做法。** 经典地,我们用 set 维护所有数字的出现的所有位置,更新时只需要查询其首位即可。 这样我们可以轻松实现 $\texttt +$ 操作。 为了实现 $\texttt -$ 操作,我们引入序列 $A$,使得序列 $A$ 的前 $l$ 位恰好为序列 $a$($l$ 为序列 $a$ 此时的长度)。这样一来,我们更新时直接将 $l$ 更改为 $l-k$ 即可。 这样做同样是为了方便进行 $\texttt !$ 操作。因为我们实际上保留了 $1\sim l_{\max}$ 的所有信息,足以进行回溯。 对于 $\texttt !$ 操作,我们使用 stack 存储所有操作,并尝试逆向地进行还原,具体情况与上面的 $\texttt +/\texttt -$ 操作思路一致。 **特别地,对于 $\texttt +$ 操作,我们需要存储原来的 $A_{l'}$ 而不是 $x$。这是容易理解的。** 至于答案的更新,我们在所有元素**第一次出现的位置**标记为 $1$,这个是已经维护过的。 那么 $\texttt ?$ 操作只需要输出该数组在 $[1,l]$ 上的区间和即可。 至此,我们需要一个可以支持单点修改、查询区间和的数据结构。拉一个树状数组即可。 时间复杂度 $O(q\log q)$。 ## 代码 ```cpp #include <cstdio> #include <algorithm> #include <set> #include <vector> #include <cstring> #define N 1000010 #define M 1000000 #define ll long long using namespace std; inline ll rd(){ char c; bool flag = 0; while ((c = getchar()) < '0' || c > '9') if (c == '-') flag = 1; ll res = c - '0'; while ((c = getchar()) >= '0' && c <= '9') res = (res << 3) + (res << 1) + c - '0'; return flag ? -res : res; } int q, l = 0; int a[N], c[N]; set <int> s[N]; void add(int x, int y){ for( ; x <= M ; x += x & -x) c[x] += y; } int query(int x){ int ans = 0; for( ; x ; x -= x & -x) ans += c[x]; return ans; } struct lst{ int st, x; }; lst init(char op, int x){ lst t; t.st = (op == '+'), t.x = x; return t; } vector <lst> oper; int main(){ memset(a, -1, sizeof(a)); q = rd(); while(q--){ char op; int x; scanf("\n%c", &op); if(op == '+'){ x = rd(); ++l; if(a[l] != -1){ if(s[a[l]].size()){ add(*s[a[l]].begin(), -1); s[a[l]].erase(l); } if(s[a[l]].size()) add(*s[a[l]].begin(), 1); } oper.push_back(init(op, a[l])); a[l] = x; if(x != -1){ if(s[x].size()) add(*s[x].begin(), -1); s[x].insert(l); add(*s[x].begin(), 1); } } else if(op == '-'){ x = rd(); l -= x; oper.push_back(init(op, x)); } else if(op == '?'){ printf("%d\n", query(l)); fflush(stdout); } else{ lst last_alt = oper.back(); oper.pop_back(); if(last_alt.st){ int t = last_alt.x; if(a[l] != -1){ if(s[a[l]].size()){ add(*s[a[l]].begin(), -1); s[a[l]].erase(l); } if(s[a[l]].size()) add(*s[a[l]].begin(), 1); } a[l] = t; if(t != -1){ if(s[t].size()) add(*s[t].begin(), -1); s[t].insert(l); if(s[t].size()) add(*s[t].begin(), 1); } --l; } else l += last_alt.x; } } return 0; } ``` ## 优化 虽然上述代码已经可以通过,但是考虑优化之。 逐个解决瓶颈: - **树状数组**:可以用前缀和替代。容易发现是可行的; - **维护各个元素出现的位置**:我们发现改变一个元素第一次出现的位置,必然伴随着一个当前已知位置该元素的更新或该元素的彻底删除。据此可以只用一个数组 $pos$ 代替上述题解中提出的 set 进行维护。 据此,我们得到时间复杂度 $O(q)$ 的做法。代码从略。 ___ **2023.08.17** 修正一处笔误。