前缀 mex_题解

【题解提供者】吴立强

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

我们假定 f ( k − 1 ) = t f(k-1)=t f(k1)=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;
}
  • 26
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值