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;
}
posted @ 2021-03-04 22:45  hachuochuo  阅读(177)  评论(0编辑  收藏  举报