【题解提供者】吴立强
解法【1】
思路
对于每个 f(k)f(k)f(k) 单独计算,那么等价于:给定 kkk 个数,求其 mex 值。
解法【1.1】
对于一个包含 kkk 个数的数组,求其 mex 可以从 0 至 ∞ 判断每个数是否存在,找到第一个不存在的数,即是答案。
代码展示
#include <iostream>
using namespace std;
const int N = 200009;
int a[N], n;
int ask(int k) { /// 求 a 数组中前 k 个元素组成子数组的 mex
for(int i = 0; ; i ++) { /// 从小到大枚举 i
bool get = false;
for(int j = 1; j <= k; j ++) { /// 判断 i 是否存在于前 k 个数中
if(a[j] == i) get = true;
}
if(get == false) /// 找不到 i 这个数,那它就是答案
return i;
}
}
int main() {
cin >> n;
for(int i = 1; i <= n; i ++) {
cin >> a[i];
cout << ask(i) << ' ';
}
return 0;
}
算法分析
不难发现,上述程序的循环内所需运行次数为 12+22+32+...+n21^2+2^2+3^2+...+n^212+22+32+...+n2,其级别为 O(n3)O(n^3)O(n3) 会 TLE。
解法【1.2】
可以优化上述算法中判断一个数是否存在的代码逻辑。利用一个 bool 类型标记数组 vis,初始时其中每个位置都赋值为 false 代表该位置元素不存在,每次将新出现的元素加入进去即可。
可以发现,由于 vis 数组需要预分配空间,而出现的数最大可能到 10910^9109,上述算法貌似会出现空间不足(MLE)的问题。
再进一步分析,可以得出结论,数组只需要开到 nnn 的级别即可。根据鸽笼原理,假设这 nnn 个元素中存在某个元素大于等于 nnn,那么必然存在至少一个小于 nnn 的位置不存在元素,根据 mex 的定义,答案必然是所有不存在的位置中最小的那个,即答案不可能超过 nnn,那么对于所有大于等于 nnn 的元素我们可以不用纪录,如此一来空间复杂度即为 O(n)O(n)O(n) 级别,不会 MLE。
代码展示
#include <iostream>
using namespace std;
const int N = 200009;
int a[N], n;
bool vis[N]; /// bool 类型,全局变量初始时默认为 false(0)
int ask(int k) {
for(int i = 0; ; i ++) {
if(vis[i] == false)
return i;
}
}
int main() {
cin >> n;
for(int i = 1; i <= n; i ++) {
cin >> a[i];
if(a[i] < n) vis[a[i]] = true; /// 只存储小于 n 的元素是否存在的信息
cout << ask(i) << ' ';
}
return 0;
}
算法分析
上述优化将单次判断的时间复杂度从 O(k)O(k)O(k) 优化到了 O(1)O(1)O(1)(特定的几次运算即可)。
注意到算法所需运行次数为 ∑k=1nf(k)\sum_{k=1}^nf(k)∑k=1nf(k),那么在极限数据下(Ai=i−1A_i = i-1Ai=i−1,实际上 OJ 中也存在这一组数据),有 f(k)=kf(k) = kf(k)=k 时间复杂度即为 1+2+3+...+n1+2+3+...+n1+2+3+...+n,其级别为 O(n2)O(n^2)O(n2) 在本题数据下仍旧会超时。
解法【2】
思路
由于需要被求解的子数组都是一个前缀部分,即集合中存在的元素在后续集合中也一定存在,那么可以证明对于任意大于 2 的 kkk,必定有 f(k−1)≤f(k)f(k-1)\le f(k)f(k−1)≤f(k)。
我们假定 f(k−1)=tf(k-1)=tf(k−1)=t,那么在求解 f(k)f(k)f(k) 时只需要从 ttt 开始枚举,判断其是否存在即可。
代码展示
#include <iostream>
using namespace std;
const int N = 200009;
bool vis[N];
int main() {
int n; cin >> n;
for(int i = 1, ans = 0; i <= n; i ++) { /// ans 初值为 0
int x; cin >> x; /// 每个元素不需要再次访问,可以用临时变量存储,降低算法空间复杂度
if(x < n) vis[x] = true;
while(vis[ans] == true) ans ++; /// 从 ans 开始判断后续元素是否出现过
cout << ans << ' ';
}
return 0;
}
算法分析
可以发现除 12 行外,程序时间复杂度为 O(n)O(n)O(n)。
单独分析第 12 行将被执行的次数,由于 ansansans 变量只增不减,那么可以认为其运行次数大概在 maxk=1n{f(k)}max_{k=1}^n\{f(k)\}maxk=1n{f(k)} 即 f(n)f(n)f(n),根据鸽笼原理,可以确定必然有 f(n)≤nf(n)\le nf(n)≤n 存在。
故整个算法的时间复杂度即为 O(n)O(n)O(n),可以通过本题。
拓展
解法【2】中的 vis 数组可以通过 C++ 标准模板库(STL)中所存在容器 set 完成。
代码展示
#include <iostream>
#include <set> /// 引入 set 所在库文件
using namespace std;
int main() {
int n; cin >> n;
set<int> se; /// 创建一个存储 int 型变量的 set 容器
/// set 底层以红黑树实现,其空间复杂度为存储元素个数*单个元素空间
for(int i = 1, ans = 0; i <= n; i ++) {
int x; cin >> x;
se.insert(x); /// 红黑树中单次插入时间复杂度为 log(k),k 为存储元素个数
while(se.find(ans) != se.end()) ans ++; /// 红黑树中单次查找时间复杂度为 log(k)
/// 查找某个元素,返回值是这个元素所在的迭代器,如果不存在返回 end() 迭代器
cout << ans << ' ';
}
return 0;
}