LuoguP8252 [NOI Online 2022 提高组] 讨论

题意简述

给定 \(n\) 个集合 \(\{S\}\) ,求是否存在一对集合 \((S_i,S_j)(i\neq j)\) 满足 \(S_i\not\subseteq S_j \wedge S_j\not\subseteq S_i \wedge S_i\cap S_j \neq \varnothing\)。存在则输出 YES,并输出 \(i\)\(j\),不存在则输出 NO。带多测。

对于所有测试点:令一组数据中 \(m=\sum |S_i|\),则 \(1\le T\le 5\)\(1\le \sum n\le {10}^6\)\(1\le \sum m\le 2\times {10}^6\),每个集合的大小大于等于 \(0\) 而小于等于 \(n\)。且满足:所有集合都是 \(\{1,2,3,\dots,n\}\) 的子集

解题思路

前置方法

如何判断两个集合 \(A,B\) 是否存在包含与被包含关系?不妨设 \(|A|\leq |B|\),另一种情况交换即可。有两种方法。

  1. 遍历 \(A\) 里面的每一个元素,若在 \(B\) 里面皆存在,那么 \(A\subseteq B\)。只要保证 \(B\) 有序(\(O(n\log n)\)),就可以用 std::lower_bound 来求解。这样的复杂度是 \(O(|A|\log |B|)\),适合处理 \(|A|\)\(|B|\) 大的极端情况。
  2. 先保证 \(A,B\) 皆有序。遍历 \(B\),并记一个指向 \(A\) 开头的指针 \(j\)。如果在 \(B\) 中遇到 \(B_i=A_j\),就令 \(j=j+1\)。如果发现 \(B_i>A_j\) 的话,一定不包含。如果在遍历中或结束时,\(A\) 已经被 \(j\) 遍历完了,那么就包含。其他情况都是不包含。这种方法的复杂度是 \(O(|B|)\),适合处理 \(|A|,|B|\) 差距不大的情况。

暴力

发现两个集合要满足的条件是两类:①交集非空②非包含关系。因此我们把两类条件分开来考虑。

发现要满足条件一很容易。我们可以开一个桶 \(ton\),定义 \(ton_i\) 储存包含 \(i\) 的集合的下标。那么,对于 \(ton_i\) 里面储存的任意两个元素,都能满足条件①;而虽然 \(\forall a\in ton_i,b\in ton_j,s.t. i\neq j\)\(S_a\)\(S_b\) 也有可能满足条件①,但无需担心,因为他们必定在同一个 \(ton_i\) 里出现过。

那么我们就能想出暴力的方法:遍历 \(ton\),在 \(ton_i\) 中任选两个数 \(x,y\) 进行比较,看是否满足 \(S_x\subseteq S_y\)\(S_y\subseteq S_x\)

这样做的复杂度比较高,是 \(O(n^4)\)。只能过第一个点。

优化 1

由于题目中只让我们找到一个不满足条件的二元组,我们可以考虑以下优化。

\(ton_i\) 按集合的大小排序。我们遍历 \(ton_i\) 时,发现如果要全部不满足条件,必须是: \(S_{ton_{i,0}}\subseteq S_{ton_{i,1}}\subseteq\dots \subseteq S_{ton_{i,{s-1}}}\),其中 \(s\) 指的是 \(ton_i\) 的大小。

一个细节是:这个排序应当是稳定的。否则效率会变低。这里不再赘述,请读者思考。

因此我们只需考虑 \(ton_{i,j}\) 是否包含 \(ton_{i,j-1}\) 了。

这样做,复杂度是 \(O(nm)\) 的。至少可以过前三个点。

优化 2

我们发现需要比较的次数太多了。如何减少呢?

我们考虑把每个人看做一个点。当两个点进行比较时,就在两点间连一条有向边。最后我们遍历图即可。

但是实际上比较次数还是一样多的。我们发现这个图和可持久化 trie 的结构一样:从每个点出发,能到达的点与经过的边是一棵树。于是可以考虑 dfs。

假设当前的点是 \(u\),从 \(u\) 出发能够到达的点构成的序列是 \(e_u\)。我们要对每一对 \((u,e_{u,i})\) 进行配对,这就慢下来了。其实我们可以把 \({S_{e_u}}\) 求一个并集,再判断是否有包含关系。这样做是没有任何问题的,并且可以快速找到 \(S_u\) 不存在的那个值 \(error\)。这样做不能优化复杂度,但的确大幅减少了比较次数,因为重复的值就不会比较了。

当然,如果你想优化求并集,这当然是可以的。采用分治即可。

还有一个小优化:每个点都很有可能大量重复到另外一个点,因此要 std::unique 一下。当然用桶排处理也可以。

复杂度证明

每个点只会比较一次。因此是 \(O(m)\) 的。

关键是求并集。它的复杂度好像很假,不是吗?

最劣的构造是这样的:一个点 \(u\) 的入度 \(in_u\) 特别大。那么它就要求 \(in_u\) 次并集。复杂度似乎是 \(O(\sum {{in_u}^2})\) 的。

但是我们发现,它的入度至多是 \(|S_u|\),并且每一个指向它的点的大小都大于 \(|S_u|\)(因为边的定义如此)。因此至多只有 \(O(\frac {m} {|S_u|})\) 个点指向它。因而计算所有点的复杂度是 \(O(m)\) 的。(这里还省去了许多细节,就请感性理解一下了。)

用分治求并集,复杂度会玄学一些,我不会证,但上限似乎还是 \(O(m)\) 的。

总的复杂度应该是 \(O(n\log n+m)\),瓶颈是排序,用桶排序可以优化这个复杂度,但没必要。

当然,你还可以做许多剪枝来优化这个过程,这里不再赘述。

程序实现

我的这份代码有很多提到的优化没有用,而且常数比较大。如果把我提到的优化写完并且常数小一些,控制在 200ms 应该是没问题的。需要 C++ 11 及以上标准。

#include<cstring>
#include<algorithm>
#include<iostream>
#include<vector>
#include<utility>
#include<set>
#define PAI pair<int,int>
#define val first
#define pos second
#define mkp make_pair
#define VI vector<int>::iterator
#define SI set<int>::iterator
#define R myio::read_int()
using namespace std;
namespace myio{
	int read_int(){
		int x=0;char ch,f=1;
		while(!isdigit(ch=getchar())) if(ch=='-') f=0;
		do x=(x<<1)+(x<<3)+ch-'0';
		while(isdigit(ch=getchar()));
		return (f==1?x:-x);
	}void PRINT(int x){
		if(x<=9) putchar(x+'0');
		else PRINT(x/10),putchar(x%10+'0');
	}void print_int(int x){
		if(x<0) putchar('-'),PRINT(-x);
		else PRINT(x);
		putchar(' ');
	}
}

const int N=1e6+6,P=998244353;
int T,n,tp,X,Y,ys[N],error;
vector<int> stu[N];
vector<PAI> ton[N];
vector<int> e[N];
void Clear() {
	for(int i=1;i<=n;i++) ton[i].clear();
	for(int i=1;i<=n;i++) stu[i].clear();
	for(int i=1;i<=n;i++) e[i].clear();
}
bool contain(set<int> &x,int y) { //stu[y] contains x
	SI j=x.begin();
	for(auto i:stu[y]) {
		if(i==*j) j++;
		if(j==x.end()) return true;
		if(i>*j) {error=*j;return false;}
	}error=*j;
	return false;
}
void build() {
	for(int i=1;i<=n;i++) if((int)ton[i].size()>=2) {
		sort(ton[i].begin(),ton[i].end());
		for(int j=1,sz=ton[i].size();j<sz;j++) 
			e[ton[i][j].pos].push_back(ton[i][j-1].pos);
	}for(int i=1;i<=n;i++) sort(e[i].begin(),e[i].end());
	for(int i=1;i<=n;i++) e[i].erase(unique(e[i].begin(),e[i].end()),e[i].end());
}
bool cson(int u) {
	set<int> bson;
	for(int i:e[u]) for(int j:stu[i]) bson.insert(j);
	if(contain(bson,u)) return true;
	return false;
}
int dfs(int u) {
	if(!cson(u)) return u;
	int err;
	for(int to:e[u]) {
		if(!e[to].empty()) {
			err=dfs(to);
			if(err!=0) return err;
		}
	} while(!e[u].empty()) e[u].pop_back();
	return 0;
}
int Check() {
	for(int i=1;i<=tp;i++) {
		if(e[i].empty()) continue;
		int err=dfs(i);
		if(err!=0) return err;
	} return 0;
}
int Find(int u) {
	for(auto to:e[u]) {
		VI it=lower_bound(stu[to].begin(),stu[to].end(),error);
		if(it!=stu[to].end()&&*it==error) return to;
	} return -1;
}
signed main(){
	T=R;
	while(T--) {
		n=R;Clear();tp=0;
		for(int i=1;i<=n;i++) {
			int k=R;
			if(k==0) continue;
			if(k==1) {k=R;continue;}
			tp++;ys[tp]=i;
			for(int j=1,x;j<=k;j++) {
				ton[x=R].push_back(mkp(k,tp));
				stu[tp].push_back(x);
			}sort(stu[tp].begin(),stu[tp].end());
		}build();
		int err=Check();
		if(err!=0) {
			puts("YES");
			X=ys[err],Y=ys[Find(err)];
			if(X>Y) swap(X,Y);
			myio::print_int(X);
			myio::print_int(Y);
			putchar('\n');
		}else puts("NO");
	}
	return 0;
}
posted @ 2023-03-06 16:49  robinyqc  阅读(16)  评论(0编辑  收藏  举报