2-SAT讲解+模板

一、定义

要知道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 2i1 2 i 2i 2i来分别表示不取(0)和取(1)。
那么下一步我们便需要利用所给的仇恨关系来建边,我们定义一个有向边x→y表示如果选择了x一定要选y。

那么对于2-sat问题的每个集合的两个元素a,b来说,一般有四种规则:

  1. a,b必须都选:这个就很简单,连边就是 a → b , b → a a→b,b→a abba(关系应该是双向的);
  2. a,b不能同时选:即选了a就不能选b,选了b就不能选a,我们记 a ′ , b ′ a',b' a,b分别为不选a,不选b,那么建边关系就应该是 a → b ′ , b → a ′ a→b',b→a' abba;
  3. a,b至少选一个:这我们可以反过来考虑,既然至少选一个,那么如果不选a就一定要选b,反之同理。建边关系应该是 b ′ → a , a ′ → b b'→a,a'→b baab
  4. a(b)必须选:建边应该是 a ′ → a a'→a aa

建完图,下面就要考虑这么利用图来解决问题了。


三、解决方法

通常的解决方法就是利用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;
}


五、经典例题


HDU 1814
HDU 3062
HDU 1814和3062题解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值