2-SAT

简介

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\n", 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

posted @ 2018-02-22 21:31  Cyhlnj  阅读(975)  评论(1编辑  收藏  举报