一、定义
要知道2-sat是什么我们先要知道什么是适定性问题(Satisfiability)。适定性问题通俗的来说就是确定是否可以满足所有条件,或者说就是确定一个满足所有条件的方案。取英文的前三个字母,简称sat问题。
通俗的sat问题表述一般是这样的:有很多个集合,每个集合里面有若干元素,现给出一些取元素的规则,要你判断是否可行,可行则给出一个可行方案。如果所有集合中,元素个数最多的集合有k个,那么我们就说这是一个k-sat问题,2-sat问题就是k=2时的情况。
当 k > 2 时该问题为 NP完全问题。所以我们只研究 k = 2 的情况。
二、分析
我们先引入一个2-sat的经典问题:
题目大意:一国有n个党派,每个党派在议会中都有2个代表,现要组建和平委员会,要从每个党派在议会的代表中选出1人,一共n人组成和平委员会。已知有一些代表之间存在仇恨,也就是说他们不能同时被选为和平委员会的成员,现要你判断满足要求的和平委员会能否创立?如果能,请任意给出一种方案。
(HDU 1814)
很明显,就从上面的定义上来看我们就可以看成这题是给2-sat的问题(事实上大部分2-sat的问题都一眼可以看出)。
那么对于这到底,很容易想到的做法就是搜索,将所有的仇恨关系建图,然后在图上遍历, 这也是 2-sat的基本思想,至于如何建图和遍历我们往下看。
如何建图
上面就说了,将2-sat问题往图上靠,我们可以发现,对于2-sat问题的每个点,我们要么取,要么不取,即不是0就是1。因此,如果给你n个点,对于点ai,我们可以建两个点
2
i
−
1
2i-1
2i−1和
2
i
2i
2i来分别表示不取(0)和取(1)。
那么下一步我们便需要利用所给的仇恨关系来建边,我们定义一个有向边:x→y表示如果选择了x一定要选y。
那么对于2-sat问题的每个集合的两个元素a,b来说,一般有四种规则:
- a,b必须都选:这个就很简单,连边就是 a → b , b → a a→b,b→a a→b,b→a(关系应该是双向的);
- a,b不能同时选:即选了a就不能选b,选了b就不能选a,我们记 a ′ , b ′ a',b' a′,b′分别为不选a,不选b,那么建边关系就应该是 a → b ′ , b → a ′ a→b',b→a' a→b′,b→a′;
- a,b至少选一个:这我们可以反过来考虑,既然至少选一个,那么如果不选a就一定要选b,反之同理。建边关系应该是 b ′ → a , a ′ → b b'→a,a'→b b′→a,a′→b;
- a(b)必须选:建边应该是 a ′ → a a'→a a′→a;
建完图,下面就要考虑这么利用图来解决问题了。
三、解决方法
通常的解决方法就是利用TarjanSCC缩点的方法来做。(不了解Tarjan算法的,可以先去了解一下,Tarjan算法是用来求图的强连通分量和强连通性的一个十分快速的算法,与dfs类似)
我们建完边后跑一边Tarjan SCC来判断是否有一个集合中的两个元素出现在同一个SCC(强连通分量)中,若有则直接输出不可能。构造方案只需要把几个不矛盾的 SCC 拼起来就好了。因为 Tarjan
算法求强连通分量时使用了栈,所以 Tarjan 求得的 SCC 编号相当于反拓扑序。
算法的基本思想大致为:就是沿着图上一条路径,如果一个点被选择了,那么这条路径以后的点都将被选择,那么,出现不可行的情况就是,存在一个集合中两者都被选择了。
四、模板
代码中附带算法步骤的详细解释。
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<vector>
#define ll long long
#define inf 0x3f3f3f3f
using namespace std;
const int N=1100;
bool mark[2*N];
int stack[2*N];
int n,m;//n为人数,m为关系数
int top;
vector<int> g[2*N];//范围开为数据范围的两倍
void init()//初始化
{
memset(mark,false,sizeof(mark));
for(int i=0;i<2*N;i++)
{
g[i].clear();
}
}
void addedge(int x,int y)//建边函数,该函数随题目不同而变化
{
g[x].push_back(y^1);//选x必须选y'
g[y].push_back(x^1);//选y必须选x'
}
bool dfs(int x)
{
if(mark[x^1]) return false;//如果该点的对立面为真,该点必定为假
if(mark[x]) return true;//如果该点之前扫过,为真,那么直接返回
mark[x]=true;//如果这个点没讨论过,那么把该点赋为真
stack[++top]=x;
int len=g[x].size();
for(int i=0;i<len;i++)//遍历这条路径上的所有点
{
if(!dfs(g[x][i])) return false;//该点为真,那么和这个点相连的每个点全必须为真,否则返回false
}
return true;
}
bool twosat()
{
for(int i=0;i<2*n;i+=2)
{
if(!mark[i]&&!mark[i+1])//如果该点没讨论过
{
top=0;
if(!dfs(i))
{
while(top>0)
{
mark[stack[top--]]=false;//栈里元素全出栈,并赋值为假
}
if(!dfs(i+1)) return false;//如果!dfs(i+1),说明该事物的正反两面都在一个强连通分量里,不符
}
}
}
return true;
}
int main()
{
int x,y;
init();
scanf("%d%d",&n,&m);
for(int i=0;i<m;i++)
{
scanf("%d%d",&x,&y);
addedge(x,y);
}
//判断可行性
if(twosat()) printf("yes\n");
else printf("no\n");
/*输出最小字典序
if(twosat())
{
for(int i=0;i<2*n;i++)
{
if(mark[i]) printf("%d\n",i+1);
}
}
*/
return 0;
}