简介
2-SAT是一类适定性问题
适定性问题?
通俗的说就是确定是否可以满足所有的条件
k-SAT
有很多个集合,每个集合里面有若干元素,现给出一些取元素的规则,要你判断是否可行,可行则给出一个可行方案。如果所有集合中,元素个数最多的集合有k个,那么我们就说这是一个k-sat问题
3-SAT乃至k更大的情况已经被证明为是 NP完全问题了
2-SAT
在更多的情况下,2-SAT不仅仅是元素个数最多的集合含有2个元素,而是每个集合都含有2个元素,且这2个元素不允许同时取出
我们主要研究这一类问题
解决
例题---Poi2001 和平委员会
Description
根据宪法,Byteland民主共和国的公众和平委员会应该在国会中通过立法程序来创立。 不幸的是,由于某些党派代表之间的不和睦而使得这件事存在障碍。
此委员会必须满足下列条件:
每个党派都在委员会中恰有1个代表,
如果2个代表彼此厌恶,则他们不能都属于委员会。
每个党在议会中有2个代表。代表从1编号到2n。 编号为2i-1和2i的代表属于第I个党派。
任务
写一程序:
输入党派的数量和关系不友好的代表对,
计算决定建立和平委员会是否可能,若行,则列出委员会的成员表。
Input
第一个行有2非负整数n和m。 他们各自表示:党派的数量n,1<=n<=8000和不友好的代表对m,0 <=m <=20000。 在下面m行的每行为一对整数a,b,1<=a
Output
如果委员会不能创立,输出中应该包括单词NIE。若能够成立,输出中应该包括n个从区间1到2n选出的整数,按升序写出,每行一个,这些数字为委员会中代表的编号。 如果委员会能以多种方法形成,程序输出字典序最小的一个。
Sample Input
3 2
1 3
2 4
Sample Output
1
4
5
分析
这是典型的2-SAT题
可以去这里交(多组数据)
算法1
设党派A, B:A中两个人A1, A2;B中两个人B1, B2
如果A1, B2有矛盾,那么A1与B1连单向边,表示选了A1就只能选B1
要求字典序最小,从小到大枚举人,Dfs染色,有冲突时撤销
没错,就是这么暴力
# include <bits/stdc++.h>
# define RG register
# define IL inline
# define Fill(a, b) memset(a, b, sizeof(a))
using namespace std;
typedef long long ll;
const int _(2e4 + 5);
IL int Input(){
RG int x = 0, z = 1; RG char c = getchar();
for(; c < '0' || c > '9'; c = getchar()) z = c == '-' ? -1 : 1;
for(; c >= '0' && c <= '9'; c = getchar()) x = (x << 1) + (x << 3) + (c ^ 48);
return x * z;
}
int n, m, first[_], cnt, col[_], num, S[_];
struct Edge{
int to, next;
} edge[_ << 1];
IL void Add(RG int u, RG int v){
edge[cnt] = (Edge){v, first[u]}; first[u] = cnt++;
}
IL int Oth(RG int x){
return (x & 1) ? x + 1 : x - 1;
}
IL bool Dfs(RG int u){
S[++S[0]] = u;
if(col[u] == col[Oth(u)]) return 0;
for(RG int e = first[u]; e != -1; e = edge[e].next){
RG int v = edge[e].to;
if(!col[v]){
col[v] = col[u];
if(!Dfs(v)) return 0;
}
else if(col[v] != col[u]) return 0;
}
return 1;
}
int main(RG int argc, RG char* argv[]){
while(scanf("%d%d", &n, &m) != EOF){
Fill(first, -1), Fill(col, 0), num = cnt = 0;
for(RG int i = 1; i <= m; ++i){
RG int u = Input(), v = Input();
Add(u, Oth(v)), Add(v, Oth(u));
}
RG int tmp = n << 1;
for(RG int i = 1; i <= tmp; ++i){
if(col[i]) continue;
col[i] = 2, S[0] = 0;
if(Dfs(i)) continue;
for(RG int j = 1; j <= S[0]; ++j) col[S[j]] = 0;
}
for(RG int i = 1; i <= tmp; ++i) num += col[i] == 2;
if(num != n) puts("NIE");
else for(RG int i = 1; i <= tmp; ++i) if(col[i] == 2) printf("%d
", i);
}
return 0;
}
算法2
建立于不用字典序最小的情况下一个优美的优化
还是直接看别人的博客好
连边
拆点a,a',表示选或不选
a->b表示选a必须选b
- a、b不能同时选:选了a就要选b',选了b就要选a'
- a、b必须同时选:选了a就要选b,选了b就要选a,选了a'就要选b',选了b'就要选a'
- a、b必须选一个:选了a就要选b',选了b就要选a',选了a'就要选b,选了b'就要选a
- a必须选:a'->a。
流程
- 连边
- 跑tarjan
- 判可行性,即同一集合中的两个点是否同属一个强连通分量
- 缩点建新图,边反向
- 记录矛盾,一个集合的两个元素分布在了两个不同的块中,那么这两个块就是矛盾的,即不可能同时被选择,,我们要找到与一个块对立的块,并把它们保存下来。
- 拓扑排序,若当前点没有被访问过,则选择该点,不选择其另外的点
任意一组可行解拓扑排序求出
做法:
选A就必须选B等价于不选B就必须不选A
也就是说把边反向后,选了A,那么A'就不能选,染色就好了,注意A'不能选,则这个反边的图中A'相连的都不能选,要传递下去
详见Poj3863
问题就解决了
题目
POJ 3678
POJ 2723
POJ 2749
POJ 3863