前缀 mex_题解

【题解提供者】吴立强

解法【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=i1,实际上 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(k1)f(k)

我们假定 f(k−1)=tf(k-1)=tf(k1)=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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值