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|\),另一种情况交换即可。有两种方法。
- 遍历 \(A\) 里面的每一个元素,若在 \(B\) 里面皆存在,那么 \(A\subseteq B\)。只要保证 \(B\) 有序(\(O(n\log n)\)),就可以用
std::lower_bound
来求解。这样的复杂度是 \(O(|A|\log |B|)\),适合处理 \(|A|\) 小 \(|B|\) 大的极端情况。 - 先保证 \(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;
}