2-SAT算法+方案输出
定义
给出 \(n\) 个集合,每个集合都有两个元素,已知若干对 \(<a,b>\)(\(a,b\)属于不同的集合)不能同时出现,从每个合集都选一个元素,判断能否在 \(n\) 个集合里选出 \(n\) 个元素。
解决方法:
- 建有向图。对于某一个条件如 \(a\vee b\) 为真,我们可以增加两条边 \(!a\rightarrow b\) , \(!b\rightarrow a\) 。
- 假设有 \(a1,a2\) 和 \(b1,b2\) 两对,已知 \(a1\) 和 \(b2\) 间有矛盾,于是为了方案自洽,由于两者中必须选一个,所以我们就要拉两条条有向边 \(a1,b1\)、 \(b2,a2\) 表示选了 \(a1\) 则必须选 \(b1\),选了 \(b2\) 则必须选 \(a2\) 才能够自洽。
- 在建完图以后,我们就可以通过 \(tarjan\) 算法求强连通分量(\(scc\))。
- 如果某一个 \(scc\) 内出现相同集合中的元素, 那么答案就是 \(impossible\)。
- 对于其他情况就是有方案的。
- 对于一个集合中该选哪个,通过对于其属于 \(scc\) 的编号决定,每次都选 \(scc\) 编号更小的即可。
代码:
以louguP4782为例
#include<bits/stdc++.h>
#define MAXN 4000400
using namespace std;
stack<int> stk;
// instk:是否在栈中 scc:每个点所属的强连通分量编号 cscc:强连通分量的数量
int dfsn[MAXN], low[MAXN], instk[MAXN], scc[MAXN], cnt, cscc;
int ans[MAXN]; //记录答案
vector<int>edges[MAXN];
void tarjan(int p)
{
low[p] = dfsn[p] = ++cnt;
instk[p] = 1;
stk.push(p); // 进栈
for (auto q : edges[p])
{
if (!dfsn[q]) // 未访问过
{
tarjan(q); // 递归地搜索
low[p] = min(low[p], low[q]);
}
else if (instk[q]) // 访问过,且q可达p
low[p] = min(low[p], dfsn[q]);
}
if (low[p] == dfsn[p]) // 发现强连通分量的根
{
int top;
cscc++;
do
{
top = stk.top();
stk.pop();
instk[top] = 0;
scc[top] = cscc; // 记录所属的强连通分量
} while (top != p); // 直到弹出p才停止
}
}
int main()
{
int n,m;
scanf("%d %d",&n,&m);
for(int i=1;i<=m;i++){
int x,a,y,b;
scanf("%d%d%d%d",&x,&a,&y,&b);
int xx1,yy1,xx2,yy2;
if(a==0){
xx1=2*x;
xx2=2*x-1;
}else{
xx1=2*x-1;
xx2=2*x;
}
if(b==0){
yy1=2*y-1;
yy2=2*y;
}else{
yy1=2*y;
yy2=2*y-1;
}
edges[xx1].push_back(yy1);
edges[yy2].push_back(xx2);
}
for(int i=1;i<=2*n;i++){
if(!dfsn[i]){
tarjan(i);
}
}
for(int i=1;i<=n;i++){
if(scc[2*i-1]==scc[2*i]){
cout<<"IMPOSSIBLE"<<endl;
return 0;
}else{
if(scc[2*i-1]<scc[2*i]){ //选择tarjan后,所属连通分量编号小的。
ans[i]=0;
}else{
ans[i]=1;
}
}
}
cout<<"POSSIBLE"<<endl;
for(int i=1;i<=n;i++){
printf("%d%c",ans[i]," \n"[i==n]);
}
return 0;
}
越自律,越自由