k-SAT 问题
(SAT)是适定性(Satisfiability)问题的简称。一般形式为 (k-)适定性问题,简称(k-SAT)。而当(k>2)时该问题为NP完全的。所以我们只研究(k=2)的情况。
(2-SAT),简单的说就是给出(n)个集合,每个集合有两个元素,已知若干个(<a,b>),表示(a)与(b)矛盾(其中(a)与(b)属于不同的集合)。然后从每个集合选择一个元素,判断能否一共选(n)个两两不矛盾的元素。显然可能有多种选择方案,一般题中只需要求出一种即可。
Example 1
现有三个物品(a,b,c),给出制约关系——
1、(a,b)必须同时选
2、(b,c)不能同时选
由这两个条件构成的问题即为(2-SAT)问题,它的每个制约关系只针对两个元素
该例子可行性显然(选(a,b)或者选(c))
Example 2
现有三个物品(a,b,c),给出制约关系——
1、(a,b)必须同时选
2、(b,c)必须同时选
3、(a,c)不能同时选
明显,该例子不存在可行解
2-SAT 在图中的表示
在描述中有提及,(2-SAT)问题的每个元素仅存在取或者不取这两种状态
这里我们将取视作(1),不取视作(0)
所以对于每个元素(x),可以将其拆成两个点(x_0,x_1)
对于这两个点,在存在可行解的情况下一定会选到其中一个点
如果答案中取的是(x_0),则表示答案不取元素(x)
反之,如果答案中取的是(x_1),则表示答案取了元素(x)
接下来就是将两点之间的“关系”转化成图形来表示——边
对于两个元素(x,y)之间的关系,可分为以下几种
- (x,y)必须同时选中:表示要么都选,要么都不选,所以连边情况为(x_1 ightarrow y_1,y_1 ightarrow x_1)
- (x,y)不能同时选中:选了(x)就不能选(y),反之亦然,所以连边情况为(x_1 ightarrow y_0,y_1 ightarrow x_0)
- (x,y)至少选一个:三种情况,(x/y/x&y),即如果确定其中一个不选,则另一个就必须选,所以连边情况为(x_0 ightarrow y_1,y_0 ightarrow x_1)
- (x,y)必选且只能选一个:相互限制,确定选中另一个就不选,确定不选另一个就选中,所以连边情况为(x_0 ightarrow y_1,x_1 ightarrow y_0,y_0 ightarrow x_1,y_1 ightarrow x_0)
- (x)必须选:直接连接(x_0 ightarrow x_1),强制选择
然后对图进行处理,即可开始求解问题
2-SAT 的 SCC 解法
主要靠建边后寻找强连通分量,缩点后确定可行解的方式求解
如果对于某个元素(x),其(x_0)与(x_1)若在同一强连通分量内(可互相到达),则会造成条件冲突,显然无解
否则,缩点后图将会变成一张DAG,那就根据拓扑序来构造答案状态(即判断(x_0)与(x_1)的SCC编号大小)
如果(x_0)编号在(x_1)前((scc[x_0]<scc[x_1])),则取(x=x_0),否则(x=x_1)
时间复杂度为(O(n+m))
例题 1 (可行性判断)
题意
每对夫妻只能选择一人参加聚会,但由于某些人之间存在矛盾,不能同时让他们参加聚会,问是否存在一种方案使得聚会能够有(n)个人参加(即每对夫妻都能有一人参加)
解
显然,两夫妻如果其中一个不参加,另外一个就会参加(假定先不考虑冲突)
所以可以将一对夫妻看作是一个元素的(x_0)与(x_1)这两种情况,合法解下为二选一状态,正好也符合了题意
所以直接根据(2-SAT)的建图法进行建图,将有矛盾的两人(两点(x,y))对应连边,符合“不能同时选中”的情况,所以建图方式为(x_1 ightarrow y_0,y_1 ightarrow x_0)
跑SCC缩点,最后判断是否存在一对({x_0,x_1})是在同一个SCC内的即可,如果存在则无解,不存在则有解
#include<bits/stdc++.h>
using namespace std;
const int maxn=2050,maxm=4000050,maxk=10050;
struct Edge
{
int to,next;
}eg[maxm];
int pre[maxn],lowlink[maxn],sccno[maxn],head[maxn];
int STACK[maxk],STACKTop;
int dfs_clock,scc_cnt,tot;
bool ins[maxn];
inline void SPush(int a)
{
STACK[STACKTop++]=a;
}
inline void SPop()
{
STACKTop--;
}
inline bool SEmpty()
{
return STACKTop==0;
}
inline int STop()
{
return STACK[STACKTop-1];
}
void init(int nn)
{
dfs_clock=scc_cnt=STACKTop=tot=0;
memset(head,-1,(nn<<2)+5);
memset(sccno,0,(nn<<2)+5);
memset(pre,0,(nn<<2)+5);
memset(ins,false,(nn<<2)+5);
}
void addedge(int u,int v)
{
eg[tot].to=v;
eg[tot].next=head[u];
head[u]=tot++;
}
void tarjan(int in)
{
pre[in]=lowlink[in]=++dfs_clock;
SPush(in);
ins[in]=true;
for(int i=head[in];i!=-1;i=eg[i].next)
{
int &v=eg[i].to;
if(!pre[v])
{
tarjan(v);
lowlink[in]=min(lowlink[in],lowlink[v]);
}
else if(ins[v])
lowlink[in]=min(lowlink[in],pre[v]);
}
if(lowlink[in]==pre[in])
{
scc_cnt++;
while(1)
{
int x=STop();
SPop();
sccno[x]=scc_cnt;
ins[x]=false;
if(x==in)
break;
}
}
}
int n;
void solve()
{
int m,a1,a2,c1,c2;
scanf("%d",&m);
init(n<<1);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d%d",&a1,&a2,&c1,&c2);
a1=a1<<1|c1;
a2=a2<<1|c2;
addedge(a1,a2^1);
addedge(a2,a1^1);
}
for(int i=0;i<(n<<1);i++)
{
if(!pre[i])
tarjan(i);
}
for(int i=0;i<n;i++)
{
if(sccno[i<<1]==sccno[i<<1|1])
{
puts("NO");
return;
}
}
puts("YES");
}
int main()
{
while(scanf("%d",&n)!=EOF)
solve();
return 0;
}
例题 2 (可行方案求解)
2018-2019 ACM-ICPC, Asia Seoul Regional Contest K - TV Show Game
题意
现有(k(k>3))盏灯,仅有两种可能的颜色(R/B)
每个人都会选择(3)盏灯猜,猜中两盏及以上即可获奖
试确定一种灯的涂色方案,使得所有人都能获奖,不存在输出(-1)
解
根据“猜中两盏及以上即可获奖”这一条件可以得知
最多只能猜错一盏灯
假定三盏灯为(x,y,z),假如猜错了(x),那么必须让(y,z)都猜对
所以可以得到灯之间的关系为
表示如果猜错一盏,则必须让另外两盏灯都对
转化到题目中来,虽然我们能在“对/错”的基础上得出连边关系
但刚开始还是不知道指定的灯具体是什么颜色的,也就没办法判断对错
那么我们就可以假定所有的(R)色为对,(B)色为错,便能进行建边
其余套路不变,跑完SCC后判断是否同一位置两种状态在同一SCC内,是则无解
否则,按照sccno的大小来选择答案即可
#include<bits/stdc++.h>
using namespace std;
const int maxn=10050,maxm=60050,maxk=10050;
struct Edge
{
int to,next;
}eg[maxm];
int pre[maxn],lowlink[maxn],sccno[maxn],head[maxn];
int STACK[maxk],STACKTop;
int dfs_clock,scc_cnt,tot;
bool ins[maxn];
inline void SPush(int a)
{
STACK[STACKTop++]=a;
}
inline void SPop()
{
STACKTop--;
}
inline bool SEmpty()
{
return STACKTop==0;
}
inline int STop()
{
return STACK[STACKTop-1];
}
void init(int nn)
{
dfs_clock=scc_cnt=STACKTop=tot=0;
memset(head,-1,(nn<<2)+5);
memset(sccno,0,(nn<<2)+5);
memset(pre,0,(nn<<2)+5);
memset(ins,false,(nn<<2)+5);
}
void addedge(int u,int v)
{
eg[tot].to=v;
eg[tot].next=head[u];
head[u]=tot++;
}
void tarjan(int in)
{
pre[in]=lowlink[in]=++dfs_clock;
SPush(in);
ins[in]=true;
for(int i=head[in];i!=-1;i=eg[i].next)
{
int &v=eg[i].to;
if(!pre[v])
{
tarjan(v);
lowlink[in]=min(lowlink[in],lowlink[v]);
}
else if(ins[v])
lowlink[in]=min(lowlink[in],pre[v]);
}
if(lowlink[in]==pre[in])
{
scc_cnt++;
while(1)
{
int x=STop();
SPop();
sccno[x]=scc_cnt;
ins[x]=false;
if(x==in)
break;
}
}
}
int a[4];
char s[4][2];
bool vis[10050];
char ans[10050];
int main()
{
int k,n;
scanf("%d%d",&k,&n);
init(k<<1|1);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=3;j++)
scanf("%d%s",&a[j],s[j]);
addedge(a[1]<<1|(s[1][0]=='B'),a[2]<<1|(s[2][0]=='R'));
addedge(a[1]<<1|(s[1][0]=='B'),a[3]<<1|(s[3][0]=='R'));
addedge(a[2]<<1|(s[2][0]=='B'),a[1]<<1|(s[1][0]=='R'));
addedge(a[2]<<1|(s[2][0]=='B'),a[3]<<1|(s[3][0]=='R'));
addedge(a[3]<<1|(s[3][0]=='B'),a[1]<<1|(s[1][0]=='R'));
addedge(a[3]<<1|(s[3][0]=='B'),a[2]<<1|(s[2][0]=='R'));
}
for(int i=2;i<=(k<<1|1);i++)
if(!pre[i])
tarjan(i);
for(int i=2;i<=(k<<1);i+=2)
if(sccno[i]==sccno[i|1])
{
puts("-1");
return 0;
}
for(int i=2;i<=(k<<1);i+=2)
{
int x=sccno[i],y=sccno[i|1];
if(vis[x])
{
ans[i>>1]='B';
continue;
}
if(vis[y])
{
ans[i>>1]='R';
continue;
}
if(x<y)
{
vis[x]=true;
ans[i>>1]='B';
}
else
{
vis[y]=true;
ans[i>>1]='R';
}
}
ans[k+1]=' ';
puts(ans+1);
return 0;
}
2-SAT 的 DFS 解法
DFS解法如果加上一堆优化后是可以做到(O(n+m))的时间复杂度(不大会)
一般不大会用这种写法
但DFS能够直接求出字典序最小的解
所以也不失为一种好的暴力方法
例题 (字典序最小可行方案求解)
HDU 1814 - Peaceful Commission
题意
(n)个党派,每个党派有两个代表
每个党派需要派出一个代表参加聚会
但是可能有些代表之间关系不好,所以不能同时派出
问是否存在派出的方案,使得每个党派都能派出一人,且派出的人之间关系不会不好
存在则输出字典序最小解,不存在输出NIE
解
与上面的例题1差不多,也是道模板题
关键在于字典序最小的限制
所以直接跑2-SAT的DFS的板子就行
#include<bits/stdc++.h>
using namespace std;
const int maxn=8050,maxm=20050;
struct Edge
{
int to,next;
}eg[maxm<<1];
bool vis[maxn<<1];
int head[maxn<<1],tot,sk[maxn<<1],skp;
bool dfs(int p)
{
if(vis[p^1]) //另外一人已经派出,则直接返回false
return false;
if(vis[p]) //否则如果自己已经派出,直接返回true
return true;
vis[p]=true;
sk[skp++]=p;
for(int i=head[p];i!=-1;i=eg[i].next)
if(!dfs(eg[i].to))
return false;
return true;
}
void init(int n)
{
n<<=1;
for(int i=0;i<n;i++)
{
vis[i]=false;
head[i]=-1;
}
skp=tot=0;
}
void addedge(int x,int y)
{
eg[tot].next=head[x];
eg[tot].to=y^1;
head[x]=tot++;
eg[tot].next=head[y];
eg[tot].to=x^1;
head[y]=tot++;
}
bool solve(int n)
{
for(int i=0;i<(n<<1);i+=2)
{
if(!vis[i]&&!vis[i|1])
{
skp=0;
if(!dfs(i))
{
while(skp)
vis[sk[--skp]]=false;
if(!dfs(i|1))
return false;
}
}
}
return true;
}
int main()
{
int n,m,a,b;
while(scanf("%d%d",&n,&m)!=EOF)
{
init(n);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&a,&b);
a--;b--;
addedge(a,b);
}
if(solve(n))
{
for(int i=0;i<(n<<1);i++)
if(vis[i])
printf("%d
",i+1);
}
else
puts("NIE");
}
return 0;
}