P6378 题解
题意简述
给出一张 $n(1\le n\le 10^6)$ 个点 $m(1\le m\le 10^6)$ 条边的无向图。现在对 $n$ 个点黑白染色,满足每条边连接的两点至少有一个是黑点;同时题目将这 $n$ 个点恰好分为 $k(1\le k\le n)$ 部分,要求每部分有且只有一个黑点。求是否有满足条件的染色方案。
题目分析
很明显是 2-SAT 问题。如一般 2-SAT 题相同,我们设 $x_0$ 为 $x$ 点是白色的状态,$x_1$ 为 $x$ 点是黑色的状态。
那么我们对每条边 $(x,y)$,如果 $x$ 是白点,那么 $y$ 一定是黑点。所以我们从 $x_0$ 向 $y_1$ 连边;同理从 $y_0$ 向 $x_1$ 连边。
对于每“部分”点内的条件转化,首先可以发现每个“部分”至少一个黑点这一条件是无用的。因为若该部分内有至少两个点之间有连边,那么就至少有一个黑点;否则任意两个点之间都没有连边,随便找一个点定为黑点就行。而每个“部分”内只有至多一个黑点一条件,直接处理是将“部分”中每个点 $x$ 对“部分”中其他所有点 $y$,从 $x_1$ 向 $y_0$ 连边。这样做边数是 $O(n^2)$ 级别的,空间、时间都会炸。
事实上,对于这种类似于对一个图中某点集内的点互相之间连边的操作,可以考虑前缀优化建图的方式解决。例如本题,以 $\{1,2,3,5,9\}$ 这一“部分”点中 $3_1$ 点向其他点连边为例,如图:
这样对一个点暴力建边的复杂度为 $O(n)$。注意到这些边的终点实际上是这一“部分”中首尾两段点,又考虑到 tarjan 算法本质上只需要维护两点之间的可达性。那么对“部分”中的开头一段,我们就可以采用“前缀”的方式维护可达性。具体来说,对每个 $x$ 点多建立一个 $x_2$ 点,然后从 $x_2$ 向 $x_0$ 连边,接着对每“部分”中的所有点,将 $x_2$ 连成一条链。如图所示:
然后我们发现只需要从 $3_1$ 向 $2_2$ 连一条边就可以实现从 $3_1$ 到 $1_0$ 与 $2_0$ 连边的效果:
对“部分”中结尾一段,也可以类似地采用后缀方式优化。
这样,我们就可以在 $4\times n$ 个点和至多 $8\times n$ 条边的图上跑 tarjan 解决问题即可。时间复杂度仍是 $O(n+m)$。
代码实现
#include<bits/stdc++.h>
using namespace std;
int n,m,k,w,x,y;
int tot,hd[4000010],v[8000010],nt[8000010];
int cnt,dfn[4000010],low[4000010],stk[4000010]/*栈*/,top,num,c[4000010]/*点所在 SCC 编号*/;
bool ins[4000010];//是否入栈
//用下标为 x 的点代表 x_0,x+n 代表 x_1,x+2*n 代表 x_2,x+3*n 代表 x_3
void rd(int &x)
{
x=0;
char c=getchar();
for(;c>'9'||c<'0';c=getchar());
for(;c<='9'&&c>='0';c=getchar())
x=(x<<3)+(x<<1)+c-'0';
}//10^6 的数据,用个快读
void add(int x,int y)
{
v[++tot]=y;
nt[tot]=hd[x];
hd[x]=tot;
}//建边
void tarjan(int x)//tarjan 求 SCC
{
dfn[x]=low[x]=++cnt;
stk[++top]=x;
ins[x]=1;
for(int i=hd[x];i;i=nt[i])
{
int y=v[i];
if(!dfn[y])
{
tarjan(y);
low[x]=min(low[x],low[y]);
}
else if(ins[y])
low[x]=min(low[x],dfn[y]);
}
if(dfn[x]==low[x])
{
int y;
num++;
do
{
y=stk[top--];
ins[y]=0;
c[y]=num;
}while(y!=x);
}
}
int main()
{
rd(n),rd(m),rd(k);
for(int i=1;i<=m;i++)
{
rd(x),rd(y);
add(x,y+n);
add(y,x+n);
}
for(int i=1;i<=k;i++)
{
rd(w);
y=0;//上一个点
for(int j=1;j<=w;j++)
{
rd(x);
add(x+2*n,x);
add(x+3*n,x);
if(y)
{
add(x+n,y+2*n);
add(y+n,x+3*n);
add(x+2*n,y+2*n);
add(y+3*n,x+3*n);
}
y=x;
}
}
for(int i=1;i<=n;i++)
if(!dfn[i])
tarjan(i);
for(int i=1;i<=n;i++)
if(c[i]==c[i+n])//如果 i_0 和 i_1 在同一个 SCC 里面,推出矛盾!
{
printf("NIE");
return 0;
}
printf("TAK");
return 0;
}