思维挺高一题,挺好的
\(ps\):首先声明,思路来源于这篇博客
另:\(The~problem~is\) \(here\)
\(first\)
作为一道\(2-SAT\)的类模板题,那当然要有\(2-SAT\)的常规操作了。设\(0\)为后勤,\(1\)为同谋者,则显然当\(x\)与\(y\)是熟人且\(x_1\)时,肯定会造成\(y_0\);同理,当\(x,y\)不是熟人且\(x_0\)时,肯定会造成\(y_1\).
按上述方法连边,然后跑一遍\(2-SAT\)基础操作就差不多完成了(如果还不知道\(2-SAT\)的话请左转)。
\(second\)
首先,通过每个数\(2-SAT\)缩点后的\(belong\)值,即在缩点后的点所在强连通分量的逆拓扑序。
为了得到一组可行的解,只需要当\(x_0\)所在的强连通分量的拓扑序在\(x_1\)所在的强连通分量的拓扑序之后取为真(即后勤)就可以了,又由于是逆拓扑序,故将\(belong_{x_0}<belong_{x_1}\)的所有\(x\)加入后勤,其余加入同谋者,这样的解一定是一组可行解。
注意:\(belong\)间的小于号不能改变,否则会使后续操作出锅(别问我是怎么知道的)
\(third\)
最后最难的一步,也正是本题的重点:计算方案数。
这里为了方便得出答案,定义一个非常重要的量——冲突点。关于冲突点我们进行如下定义:当点\(x\)在后勤时,其熟人\(y\)在同谋的话,\(y\)为\(x\)的冲突点;或者,当\(x\)在同谋时,其陌生人\(y\)在后勤的话,\(y\)同样为\(x\)的冲突点。
另外我们还需要想到一个特殊的性质,在一个正解的基础上,若要得到一个新的方案最多只能将一个人替换阵营或将两个不同阵营的人替换位置。
这个还算显然吧,因为后勤任两人互相认识,所以不能换两个到同谋,同理同谋也不能同时换两个到后勤。
冲突点就是为了方便枚举替换方案数的。
\(forth\)
重温一下定义就知道,冲突点是可能可以替换的点。
若我们在给定一个数组\(contradition_i\)来保存\(i\)的冲突点个数,易得有且仅有当\(contradition_i<=1\)时可能可以进行将\(i\)移动至对方阵营的新解拓展。
注意,是可能。
下面分情况讨论:
当\(contradition_i=1\)时,当且仅当其冲突点\(j\)满足\(contradition_j=0\)时可让\(i\)与\(j\)互换,否则\(i\)绝无可能到达对方阵营。这个的正确性也是显然。
当\(contradition_i=0\)时,可以直接放到对方阵营(前提条件是该阵营人数大于等于二,即有放人的富余),也可以与对方阵营中任意个同样无冲突点的点互换(这个直接用一个\(tmp_1\)和\(tmp_2\)统计一下两边阵营的无冲突点数,最后乘一下就好了),至于仅以\(i\)为冲突点的情况已在上一种情况中考虑过了,不需要重复算,可以证明,其他点无法与之交换。
于是这题被秒了
\(fifth\)
一些细节:注意当最开始的解满足条件时加上最开始的解,当\(2-SAT\)跑完后\(belong\)冲突时直接输出\(0\).
另外的……应该没什么了吧……
时间复杂度\(O(n^2)\),都花在后面的计算了。
\(sixth\)
贴代码(有略注):
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
#define in read()
#define re register
#define fur(i,a,b) for(re int i=a;i<=b;++i)
#define fdr(i,a,b) for(re int i=a;i>=b;--i)
#define cl(a,b) memset(a,b,sizeof(a))
#define jinitaimei signed
inline int read() //读优
{
int x=0;
char ch=getchar();
for(;!isalnum(ch);ch=getchar());
for(;isalnum(ch);ch=getchar()) x=x*10+ch-'0';
return x;
}
const int xx=1e4+101,yy=5e3+10;
bool know[yy][yy]; //是否认识
int bel[xx],all=0; //缩点必备
int sta[xx],top=0;
int id[xx],low[xx],num=0;
bool vis[xx],con[yy]; //con[]存储是否为同谋成员
int logis[yy],cnt_log=0;//logistics 存储后勤成员与数量
int consp[yy],cnt_con=0;//conspirator 存储同谋成员与数量
struct edge{int to,nxt;}e[yy*yy]; //邻接表
int contradition[yy],match[yy]; //前者为冲突点数量,后面是冲突点编号
int head[xx],cnt_edge=0;
inline void add(int x,int y) //连边
{
e[++cnt_edge]=(edge){y,head[x]};
head[x]=cnt_edge;
}
inline void Tarjan(int g) //缩点
{
low[g]=id[g]=++num;
vis[g]=true;
sta[++top]=g;
for(int i=head[g];i;i=e[i].nxt)
{
int j=e[i].to;
if(!id[j])
{
Tarjan(j);
low[g]=min(low[g],low[j]);
}
else if(vis[j]) low[g]=min(low[g],id[j]);
}
if(id[g]!=low[g]) return;
++all;
do
{
int tp=sta[top];
bel[tp]=all;
vis[tp]=false;
}
while(sta[top--]!=g);
}
jinitaimei main()
{
int n=in;
fur(i,1,n)
{
int k=in,x;
while(k--) x=in,know[i][x]=true;
}
fur(i,1,n) fur(j,1,n) if(i!=j)
{
if(know[i][j]) add(i+n,j);
else add(i,j+n);
}
fur(i,1,2*n) if(!id[i]) Tarjan(i);
fur(i,1,n) //分阵营
{
if(bel[i]==bel[i+n]) puts("0"),exit(0);
if(bel[i]<bel[i+n]) logis[++cnt_log]=i;
else consp[++cnt_con]=i,con[i]=true;
}
int ans=(cnt_log&&cnt_con),tmp1=0,tmp2=0;//若两阵营人数不为零,加上初始解
fur(i,1,cnt_log) fur(j,1,cnt_con) if(know[logis[i]][consp[j]]) ++contradition[logis[i]],match[logis[i]]=consp[j];
fur(i,1,cnt_con) fur(j,1,cnt_log) if(!know[consp[i]][logis[j]]) ++contradition[consp[i]],match[consp[i]]=logis[j];//冲突点初始化
fur(i,1,n) if(contradition[i]==1) if(!contradition[match[i]]) ++ans;//当冲突点唯一,且对方无冲突点,则可交换二者,答案加一
fur(i,1,n) if(!contradition[i])//无冲突点时
{
if((con[i]&&cnt_con>1)||(!con[i]&&cnt_log>1)) ++ans;//若己方阵营人数大于一,可将该点放入对方阵营,答案加一
if(con[i]) ++tmp2; //统计同谋者无冲突点的点数
else ++tmp1; //统计后勤无冲突点的点数
}
ans+=tmp1*tmp2; //统计两阵营无冲突点可直接交换的方案数
printf("%d\n",ans);//额……输出答案
return 0;
}
完结撒花,\(QwQ\).