【题解】CF1000F One Occurrence(莫队、线段树、可持久化线段树)
CF1000F One Occurrence
一道挺有意思的数据结构
有若干解法
题面
给定长度为 \(n\) 的序列 \(a_1,a_2,...a_n\)。
\(q\) 次询问,每次给定 \(l,r\),求区间 \([l,r]\) 中恰好出现 \(1\) 次的数,输出任意一个即可(或判断无解)。
\(n,q,a_i\le 5\times 10^5\)。
题解一
感觉是莫队,考虑怎么维护恰好出现一次的数(快速添加、删除、查找)。
- 方法一:栈
可能会想到用 set,但是这东西带 log。并且实测卡不过去
我们需要什么样的数据结构呢?支持插入一个数、删除一个数、询问任意一个存在的数。这个东西比 set 简单,所以可以尝试优化。
用一个栈,添加时直接放到栈顶,同时记录这个数在栈中的位置。
对于删除,将栈顶元素移动到需要删除的数的位置,然后删掉栈顶。
查找时直接取出栈顶即可。
所有操作都是 \(O(1)\) 的,所以总时间复杂度为 \(O(n\sqrt{q})\)。
- 方法二:值域分块
值域分块,维护所有数的出现次数,可以通过维护块内出现次数为 \(1\) 的数量快速查找。
这样添加和查找都是 \(O(1)\) 的,查找是 \(O(\sqrt{a_i})\) 的。
因此,莫队指针移动的时间复杂度为 \(O(n\sqrt{q})\),询问答案为 \(O(q\sqrt{a_i})\),总时间复杂度为两者之和。
这里的值域分块达到了分块平衡的作用。
- 方法三:unordered_map?
欢迎卡常带师。我是没卡过去
题解二
考虑对每个位置 \(i\),预处理 \(pre[i]\) 表示 \(a_i\) 上一次出现的位置。
当询问 \([l,r]\) 时,数值 \(x\) 只出现一次等价于:对于 \([l,r]\) 中最靠右边的 \(x\) ,设其位置为 \(pos(x)\),有 \(pos(x)<l\)
需要注意,必须是区间中最右边的 \(x\) 满足此条件才行,其他的 \(x\) 满足均不等价。
- 方法一:线段树
有了这个性质,我们可以想到用线段树维护 \(pre[i]\)。
但是由于必须是区间中最右边的 \(x\),所以我们对询问的右端点排序,然后从小到大枚举右端点 \(r\),维护 \([1, r]\) 的 \(pre\) 值。
对于当前的 \(r\),如果 \(pre[r]\) 存在,那么在线段树上将点 \(pre[r]\) 的值赋为无穷大,就可以保证上述性质。
答案就是经过上述维护之后,线段树区间查询 \([l,r]\) 的最小值。
时间复杂度 \(O(n\log{n})\)。
- 方法二:可持久化线段树
在线段树的解法中,我们对询问的 \(r\) 排序,其原因是对于不同的 \(r\),有些 \(pre[i]\) 的值会变成正无穷。
那么就可以从 \(1\) 到 \(n\) 枚举 \(r\),然后用主席树记录所有 \(n\) 个版本的 \(pre\) 值。
这样就可以在线处理询问。对于询问 \([l, r]\),在版本 \(r\) 的线段树上查找区间最小值即可。
时间复杂度 \(O(n\log{n})\),常熟略大。
参考代码
- 莫队(值域分块,2152ms):https://codeforces.com/contest/1000/submission/155503579
- 线段树(842ms):https://codeforces.com/contest/1000/submission/155549327
- 可持久化线段树(1731ms):https://codeforces.com/contest/1000/submission/155562515