Light Bulbs (Hard Version) 题解
提供一个非常另类的解法,没有异或哈希,没有建图,没有缩点和强连通分量,而是使用了并查集和线段树的算法。
由于每个颜色恰好有两种,我们考虑把两个颜色的位置 \(i,j\) 变成一段区间 \([i,j]\)(\(i<j\)),然后每个颜色就能用一段区间 \([l,r]\) 表示。
根据题意,如果我们激活了一个区间 \([l,r]\),那么对于所有 \(l'\in (l,r)\wedge r'>r\) 的,或者 \(r'\in (l,r)\wedge l'<l\) 的区间 \([l',r']\),它们都会被自动激活。我们的目标就是激活最少的区间去覆盖 \([1,2n]\) 这个大的区间。
考虑使用并查集,把相交但不包含的任意两个区间合并到一个集合里面,即若 \([l_i,r_i]∩[l_j,r_j]\not= \varnothing\) 且 \([l_i,r_i],[l_j,r_j]\) 之间不存在包含关系,则把下标 \(i,j\) 合并。合并了所有 \((i,j)\) 之后,我们发现对于一个集合 \(S\),当我们激活了 \(S\) 中的任意一个区间 \([l,r]\),那么 \(S\) 的所有区间也必然都会被一起激活。也就是说,设 \(S\) 中所有区间的并区间为 \([L,R]\),我们覆盖 \([L,R]\) 就只需要随意激活 \(S\) 的任意一个区间。
由于题目保证了所有区间的并一定是 \([1,2n]\),要保证激活区间最少,我们只需要在不同的集合中各选择一个区间。但是有一种特殊情况,对于两个集合的区间并 \([L,R],[L',R']\),如果有 \(L<L'<R'<R\),那么我们就只需要激活 \([L,R]\),因为这样就已经覆盖了 \([L',R']\) 这个子区间,我们就没有必要去专门激活 \([L',R']\)。
而要处理这个特殊情况也很简单,只需要对每个集合求出它的区间并 \([L_i,R_i]\),然后判断哪些区间是被其余区间包含了的,如果被包含了,那就可以不用被激活。最后再求出所有必须被激活的区间的个数就行了。
不过题目还要询问方案数,这个问题也很简单,对于一个集合 \(S\),它含有 \(|S|\) 个区间,每个区间的两个端点只要被激活一个,那么整个区间就会被激活,因此我们对于每一个需要被激活的集合,求出它们的 \(2|S|\) 之积就行了。
大致思路就是这样,接下来讨论一些实现方法。
-
如果判断一个区间是否被另一个区间包含?首先把这些区间按照 \(l\) 从小到大排序,然后维护前缀的所有区间的 \(r\) 最大值,若 \(r_i<\max_{1\leq j<i}\{r_j\}\),则 \(i\) 就会被 \(j\) 这个区间包含。
-
如何快速把有交集且不包含的两个区间合并起来?我们可以对于每一个区间,按照 \(l\) 从小到大排序,用线段树维护前 \(i-1\) 个区间的右端点,当我们枚举到第 \(i\) 个区间时,我们就查询 \([l_i,r_i]\) 范围内的右端点,然后把 \(i\) 与这些右端点对应的区间编号合并。但是暴力对每一个区间合并是 \(O(n^2)\) 的。
我们可以在线段树维护的过程中,每次给 \(r_j\) 的位置赋一个值,这个值就是 \(r_j\) 这个右端点所对应的区间下标。每次要合并的时候,我们就查询一次 \([l_i,r_i]\) 中的最大值,只将 \(i\) 和这个最大值合并。统计完之后,我们再将所有区间按照 \(r\) 从大到小排序,用线段树维护区间 \(l\) 的区间下标最大值,然后每次合并 \(i\) 与 \([l_i,r_i]\) 中的最大值。这样下来就能完全地合并每一个 \((i,j)\)。正确性大家可以尝试去证明一下,这里就不赘述了。
#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
const int MAXN=4e5+5;
const int MOD=998244353;
int n;
int a[MAXN];
int dp[MAXN];
struct node
{
int l,r,id;
bool operator<(const node &f)const{ return l<f.l; }
}l[MAXN],b[MAXN];
bool cmp(node x,node y){ return x.r<y.r; }
int pre[MAXN],cnt,tot;
int fa[MAXN],siz[MAXN],L[MAXN],R[MAXN];
int find(int x)
{
if(fa[x]==x) return x;
return fa[x]=find(fa[x]);
}
void merge(int x,int y)
{
x=find(x),y=find(y);
if(x!=y) fa[x]=y,siz[y]+=siz[x],L[y]=min(L[y],L[x]),R[y]=max(R[y],R[x]);
}
struct Segment_Tree
{
int maxx;
}T[MAXN<<2];
void pushup(int x){ T[x].maxx=max(T[x<<1].maxx,T[x<<1|1].maxx); }
void change_tree(int x,int l,int r,int k,int v)
{
if(l==r) return (void)(T[x].maxx=v);
int mid=(l+r)/2;
if(k<=mid) change_tree(x<<1,l,mid,k,v);
else change_tree(x<<1|1,mid+1,r,k,v);
pushup(x);
}
int query(int x,int l,int r,int L,int R)
{
if(L<=l&&r<=R) return T[x].maxx;
int mid=(l+r)/2,res=0;
if(L<=mid) res=max(res,query(x<<1,l,mid,L,R));
if(R>mid) res=max(res,query(x<<1|1,mid+1,r,L,R));
return res;
}
void solve()
{
cnt=0,tot=0;
for(int i=1;i<=n;i++) pre[i]=0;
for(int i=1;i<=(n<<1);i++)
{
if(pre[a[i]]) l[++cnt]=(node){pre[a[i]],i,0},l[cnt].id=cnt;
pre[a[i]]=i;
}
for(int i=1;i<=cnt;i++) fa[i]=i,siz[i]=2,L[i]=l[i].l,R[i]=l[i].r;
sort(l+1,l+cnt+1);
for(int i=1;i<=cnt;i++)
{
int now=query(1,1,n*2,l[i].l,l[i].r);
if(now) merge(l[i].id,now);
change_tree(1,1,n*2,l[i].r,l[i].id);
}
for(int i=1;i<=cnt;i++) change_tree(1,1,n*2,l[i].r,0);
sort(l+1,l+cnt+1,cmp);
for(int i=cnt;i>=1;i--)
{
int now=query(1,1,n*2,l[i].l,l[i].r);
if(now) merge(l[i].id,now);
change_tree(1,1,n*2,l[i].l,l[i].id);
}
for(int i=1;i<=cnt;i++) change_tree(1,1,n*2,l[i].l,0);
int num=0,res=1;
for(int i=1;i<=cnt;i++)
{
if(fa[i]==i) b[++tot]=(node){L[i],R[i],i};
}
sort(b+1,b+tot+1);
int Max=0;
for(int i=1;i<=tot;i++)
{
if(Max<b[i].r) num++,res=1ll*res*siz[b[i].id]%MOD;
Max=max(Max,b[i].r);
}
cout<<num<<" "<<res<<'\n';
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
int T;
cin>>T;
while(T--)
{
cin>>n;
for(int i=1;i<=(n<<1);i++) cin>>a[i];
solve();
}
return 0;
}