【题解】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})\),常熟略大。

参考代码

posted @ 2022-05-02 19:58  hzy1  阅读(111)  评论(3编辑  收藏  举报