【题解提供者】吴立强
解法【1】
思路
对于每个 f ( k ) f(k) f(k) 单独计算,那么等价于:给定 k k k 个数,求其 mex 值。
解法【1.1】
对于一个包含 k k k 个数的数组,求其 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;
}
算法分析
不难发现,上述程序的循环内所需运行次数为 1 2 + 2 2 + 3 2 + . . . + n 2 1^2+2^2+3^2+...+n^2 12+22+32+...+n2,其级别为 O ( n 3 ) O(n^3) O(n3) 会 TLE。
解法【1.2】
可以优化上述算法中判断一个数是否存在的代码逻辑。利用一个 bool 类型标记数组 vis,初始时其中每个位置都赋值为 false 代表该位置元素不存在,每次将新出现的元素加入进去即可。
可以发现,由于 vis 数组需要预分配空间,而出现的数最大可能到 1 0 9 10^9 109,上述算法貌似会出现空间不足(MLE)的问题。
再进一步分析,可以得出结论,数组只需要开到 n n n 的级别即可。根据鸽笼原理,假设这 n n n 个元素中存在某个元素大于等于 n n n,那么必然存在至少一个小于 n n n 的位置不存在元素,根据 mex 的定义,答案必然是所有不存在的位置中最小的那个,即答案不可能超过 n n n,那么对于所有大于等于 n n n 的元素我们可以不用纪录,如此一来空间复杂度即为 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 = 1 n f ( k ) \sum_{k=1}^nf(k) ∑k=1nf(k),那么在极限数据下( A i = i − 1 A_i = i-1 Ai=i−1,实际上 OJ 中也存在这一组数据),有 f ( k ) = k f(k) = k f(k)=k 时间复杂度即为 1 + 2 + 3 + . . . + n 1+2+3+...+n 1+2+3+...+n,其级别为 O ( n 2 ) O(n^2) O(n2) 在本题数据下仍旧会超时。
解法【2】
思路
由于需要被求解的子数组都是一个前缀部分,即集合中存在的元素在后续集合中也一定存在,那么可以证明对于任意大于 2 的 k k k,必定有 f ( k − 1 ) ≤ f ( k ) f(k-1)\le f(k) f(k−1)≤f(k)。
我们假定 f ( k − 1 ) = t f(k-1)=t f(k−1)=t,那么在求解 f ( k ) f(k) f(k) 时只需要从 t t t 开始枚举,判断其是否存在即可。
代码展示
#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 行将被执行的次数,由于 a n s ans ans 变量只增不减,那么可以认为其运行次数大概在 m a x k = 1 n { f ( k ) } max_{k=1}^n\{f(k)\} maxk=1n{f(k)} 即 f ( n ) f(n) f(n),根据鸽笼原理,可以确定必然有 f ( n ) ≤ n f(n)\le n f(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;
}