2-sat

http://www.javashuo.com/article/p-dfbzigjb-eb.htmlphp

概述

2-sat是k-sat問題中k==2時的一種狀況,,(廢話qaq,,html

當k大於等於3時是npc問題,,因此通常都是問的2-sat,,ios

這種題的大概形式是: 對於給定的n對的點,要求每一對都只能選擇一個,而且其中還有一些限制條件,好比說選了u就不能選擇v等等,,c++

而後問你有沒有可行解,,,算法

解決這類問題通常是用 染色法(求字典序最小的解)強連通份量法(拓撲排序只能獲得任意解),,spa

算法分析

  • 首先要明白一個道理:對於 u->v(選擇u就不能選擇v)這樣的限制條件能夠用它的逆否命題來轉換爲:u->v'(選擇u就必須選v')以及 v->u'(選擇v就必須選u')
  • 最後的建出的圖是對稱的,,
  • 具體的數學證實和算法推導看這裏kuangbin的博客,,多看幾遍,,跟着敲一遍代碼後再看看就差很少懂了

染色法(求字典序最小的解)

這個算法的大體思路就是遍歷每一對點的兩種狀況:選p或者選p',,,code

而後一直從p的下一個嘗試下去,,中間如果碰到不能避免的不知足題意的選擇時,證實這條路下來的嘗試時不行的,,從新選擇,,一直下去。。。也就是一個深搜的過程,,時間複雜度大概是 \(O(nm)\),,htm

能夠看看這篇博客,,blog

強連通份量法(拓撲排序只能獲得任意解)

這個算法的流程爲:排序

  • 建圖
  • 求極大聯通份量(子圖)
  • 縮點,轉化成DAG(有向無環圖)
  • 判斷有無解
  • 新圖拓撲排序
  • 自底向上選擇、刪除
  • 輸出

時間複雜度大概爲 \(O(m)\),,就是難寫,,並且不能輸出字典序小的解,,,

例題和模板

這道模板題,,讓輸出的書字典序小的解,,,只能用第一種方法了,,,

題意和上面那個百度文庫的例題同樣,,,

//#include <bits/stdc++.h>
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <string.h>
#include <vector>
#include <queue>
#include <functional>
#define aaa cout<<233<<endl;
#define endl '\n'
#define pb push_back
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int inf = 0x3f3f3f3f;//1061109567
const ll linf = 0x3f3f3f3f3f3f3f;
const double eps = 1e-6;
const double pi = 3.14159265358979;
const int maxn = 1e5 + 5;
const int maxm = 2e5 + 5;
const int mod = 1e9 + 7;
inline ll read() {
    char c = getchar(); int x = 0, f = 1;
    while(c < '0' || c > '9') {if(c == '-') f = -1; c = getchar();}
    while(c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
    return x * f;
}

//2sat_kuangbin
struct edge
{
    int to, next;
}edge[maxn];
int head[maxn], tot;
void init()
{
    tot = 0;
    memset(head, -1, sizeof head);
}
void addedge(int u, int v)
{
    edge[tot].to = v; edge[tot].next = head[u]; head[u] = tot++;
}
bool vis[maxn];
int s[maxn], top;
bool dfs(int u)
{
    if(vis[u^1])return false;       //若是這個點p的對立面p'選了,那麼這個點就不選
    if(vis[u])  return true;        //若是這個點已經選了,就不從這個點繼續向下找了
    vis[u] = true;                  //這個點p沒選而且對立面p'沒選的狀況下,選擇這個點,而且嘗試從這個點尋找可能的解法
    s[top++] = u;                   //把這個可能的一種狀況壓棧,保存
    for(int i = head[u]; ~i; i = edge[i].next)
        if(!dfs(edge[i].to))
            return false;           //嘗試全部與點u相連的點v,若是從點v出發的嘗試不可行時不選
    return true;
}
bool two_sat(int n)
{
    memset(vis, false, sizeof vis); //vis[i]標記那些點要選
    for(int i = 0; i < n; i += 2)
    {
        if(vis[i] || vis[i^1])continue;//若是這一對點有一個選過就嘗試下一對的點
        top = 0;
        if(!dfs(i))                 //若是從點i出發的嘗試不行,就將棧中全部這條可能的路徑上的點標記爲未選
        {
            while(top)vis[s[--top]] = false;
            if(!dfs(i^1))return false;//若是點i的對立面i'都不行的話,證實沒法找到這樣一條可行解,使得每一對點僅選擇一個而且知足對應的限制
        }
    }
    return true;
}
int main()
{
//    freopen("233.in" , "r" , stdin);
//    freopen("233.out" , "w" , stdout);
//    ios_base::sync_with_stdio(0);
//    cin.tie(0);cout.tie(0);
    int n, m, u, v;
    while(scanf("%d%d", &n, &m) != EOF)
    {
        init();
        for(int i = 1; i <= m; ++i)
        {
            scanf("%d%d", &u, &v);
            --u;--v;        //點的編號從0開始,方便使用p^1來表示p的對立面
            addedge(u, v^1);//建圖,限制條件u->v(選擇u就不能選擇v)等價於u->v' && v->u' (選擇u必須選額v' 和 選擇v就必須選擇u')
            addedge(v, u^1);
        }
        if(two_sat(2 * n))  //存在解時
        {
            for(int i = 0; i < 2 * n; ++i)
                if(vis[i])  //將最後字典序最小的可行解輸出
                    printf("%d\n", i + 1);
        }
        else
            printf("NIE\n");
    }
    return 0;
}

強連通份量的方法明天,啊不白天再說吧,,,溜了溜了

(loading)

本站公眾號
   歡迎關注本站公眾號,獲取更多信息