算法基础 - 2-sat问题

2-SAT问题

这个问题其实我们平时早就遇到过,只是从来没有认真的去思考这个问题的解法。这个在我们理解的时候,一般都是按照一个点开始,可以就继续,不可以就停止。

例子问题

例如我们有10个球,每个球都是编号好的,从:1 - 10这10个球,我们要从中拿出5个球,有以下要求:
并且选了1就不能选择3号球,选择了2号就不能选择4号球,选择了5号就不能选择6号。
但是这里面有个规则:{1, 2}不能同时存在。同理{3, 4}, {5 , 6}也不能存在。

这个问题很明了了,怎么解决呢?

最简单的就是枚举,首先选择一个球,看是否有规则,没有就选择下一个,有规则就按照规则选,不可以了,就回溯,一直到发现一个可以的情况,或者所有都不可以的时候,就有答案了。

可是这个方法是在太耗费时间了。更简单的办法,也就是利用图论的只是来解决。

下面是思想:

首先我们看第一个规则,1 3不能共存,那么这个时候可以得知,因为3 4必须选择一个,那么假如我们选择了1,就必须选择4了,这样我们就知道了一个边,就是1 -> 4这个边的意义就是,选择了1就必须选择4。那么假如我们选择的是3呢?那么另外一个就必须选择2。这个边就是3 -> 2了。依次类推,我们可以得到下面的图:

有向图

这样我们就得到了一个有向图,这个有向图的每条边都代表了选择了一个点,那么同时也要选择它指向的点。如果这个点是孤立的点,例如图中的7 8,那么就随便选择。那么怎么才算无解呢?

两个不相容的点,处在同一个环内,或者更通俗的说是,强连通图内。,就是无解的。

解法

上面通过例子已经讲解了如何解决这个问题:

  1. 构造有向图,这个有向图是一个表示了选择了一个点,一定要选择它能够到达的点的图。
  2. 对这个图求强连通图。
  3. 假如有两个点处在同一个强连通图内,这两个点是必须只能选择一个的一组内的点,就不可以。
  4. 否则可以

求强连通图的办法是Tarjan算法:传送门: Tarjan算法这是我之前博客里写的。

代码实现

下面这个代码是解决一个问题:

现在有一堆开关,1 2 3 4 5 ... n这么多,每个开关只有开关两个状态,现在要求,某些开关,不能同时开,或者不能同时关。

求解是否有这么一个状态,满足所有要求。

代码:

#include <iostream>
#include <cstring>
#include <string>
#include <vector>
#include <stack>


using namespace std;

#ifndef MAXNODE
#define MAXNODE 20005
#endif

int flag[MAXNODE] = {0};
int dfn[MAXNODE] = {0};
int low[MAXNODE] = {0};
int tpArr[MAXNODE] = {0};
int tpNum = 0;
int _index = 0;

vector<int> graph[MAXNODE];

stack<int> st;


void tarjan(int n){
    st.push(n);
    flag[n] = 1;
    dfn[n] = ++_index;
    low[n] = _index;
    for (int i = 0; i < graph[n].size(); i++) {
        int cur = graph[n][i];
        if (dfn[cur] == 0) {
            tarjan(cur);
            low[n] = min(low[n], low[cur]);
        }else{
            if (flag[cur] == 1) {
                low[n] = min(low[n], dfn[cur]);
            }
        }
    }
    if (dfn[n] == low[n]) {
        tpNum++;
        do{
            n = st.top();
            st.pop();
            flag[n] = 0;
            tpArr[n] = tpNum;
        }while(dfn[n] != low[n]);
    }
}

void addNode(int n1, int n2){
    if ((n1&1) > 0) {
        graph[n1].push_back(n2+1);
        graph[n2].push_back(n1+1);
    }else{
        graph[n1].push_back(n2-1);
        graph[n2].push_back(n1-1);
    }
}

void transAddNode(int a, int b, int l){
    if (l == 1) {
        addNode(a*2, b*2);
    }else{
        addNode(a*2-1, b*2-1);
    }
}

bool judge(int N){
    for (int i = 1; i <= N; i++) {
        if (tpArr[2*i-1] == tpArr[2*i]) {
            return false;
        }
    }
    return true;
}

int main(){
    int M;
    cin>>M; //测试组数
    while (M--) {
        int N,T;
        cin>>N>>T; //N是开关个数,T是规则个数
        int a,b,c;
        for (int i = 0; i < T; i++) {
            cin>>a>>b>>c; //输入a b c分别代表开关a,开关b,已经开关状态c只有(0/1),表示开关a,b不能同时处于状态c
            transAddNode(a, b, c);
        }
        for (int i = 1; i <= 2*N; i++) {
            if (dfn[i] == 0) {
                tarjan(i);
            }
        }
        if(judge(N)){
            cout<<"Yes"<<endl;
        }else{
            cout<<"No"<<endl;
        }
        for (int i = 1; i <= 2*N;i++) {
            flag[i] = 0;
            dfn[i] = 0;
            low[i] = 0;
            graph[i].clear();
        }
        _index = 0;
    }
    return 0;
}
  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值