• 【转】2SAT问题及其算法


    原文地址:http://www.cppblog.com/MatoNo1/archive/2011/07/13/150766.aspx

    为了便于查看,只有转载下来了,免得找不到了

    【2-SAT问题】
    现有一个由N个布尔值组成的序列A,给出一些限制关系,比如A[x] AND A[y]=0、A[x] OR A[y] OR A[z]=1等,要确定A[0..N-1]的值,使得其满足所有限制关系。这个称为SAT问题,特别的,若每种限制关系中最多只对两个元素进行限制,则称为2-SAT问题。

    由于在2-SAT问题中,最多只对两个元素进行限制,所以可能的限制关系共有11种:
    A[x]
    NOT A[x]
    A[x] AND A[y]
    A[x] AND NOT A[y]
    A[x] OR A[y]
    A[x] OR NOT A[y]
    NOT (A[x] AND A[y])
    NOT (A[x] OR A[y])
    A[x] XOR A[y]
    NOT (A[x] XOR A[y])
    A[x] XOR NOT A[y]
    进一步,A[x] AND A[y]相当于(A[x]) AND (A[y])(也就是可以拆分成A[x]与A[y]两个限制关系),NOT(A[x] OR A[y])相当于NOT A[x] AND NOT A[y](也就是可以拆分成NOT A[x]与NOT A[y]两个限制关系)。因此,可能的限制关系最多只有9种。

    在实际问题中,2-SAT问题在大多数时候表现成以下形式:有N对物品,每对物品中必须选取一个,也只能选取一个,并且它们之间存在某些限制关系(如某两个物品不能都选,某两个物品不能都不选,某两个物品必须且只能选一个,某个物品必选)等,这时,可以将每对物品当成一个布尔值(选取第一个物品相当于0,选取第二个相当于1),如果所有的限制关系最多只对两个物品进行限制,则它们都可以转化成9种基本限制关系,从而转化为2-SAT模型。

    【建模】
    其实2-SAT问题的建模是和实际问题非常相似的。
    建立一个2N阶的有向图,其中的点分为N对,每对点表示布尔序列A的一个元素的0、1取值(以下将代表A[i]的0取值的点称为i,代表A[i]的1取值的点称为i')。显然每对点必须且只能选取一个。然后,图中的边具有特定含义。若图中存在边<i, j>,则表示若选了i必须选j。可以发现,上面的9种限制关系中,后7种二元限制关系都可以用连边实现,比如NOT(A[x] AND A[y])需要连两条边<x, y'>和<y, x'>,A[x] OR A[y]需要连两条边<x', y>和<y', x>。而前两种一元关系,对于A[x](即x必选),可以通过连边<x', x>来实现,而NOT A[x](即x不能选),可以通过连边<x, x'>来实现。

    【O(NM)算法:求字典序最小的解】
    根据2-SAT建成的图中边的定义可以发现,若图中i到j有路径,则若i选,则j也要选;或者说,若j不选,则i也不能选;
    因此得到一个很直观的算法:
    (1)给每个点设置一个状态V,V=0表示未确定,V=1表示确定选取,V=2表示确定不选取。称一个点是已确定的当且仅当其V值非0。设立两个队列Q1和Q2,分别存放本次尝试选取的点的编号和尝试不选的点的编号。
    (2)若图中所有的点均已确定,则找到一组解,结束,否则,将Q1、Q2清空,并任选一个未确定的点i,将i加入队列Q1,将i'加入队列Q2;
    (3)找到i的所有后继。对于后继j,若j未确定,则将j加入队列Q1;若j'(这里的j'是指与j在同一对的另一个点)未确定,则将j'加入队列Q2;
    (4)遍历Q2中的每个点,找到该点的所有前趋(这里需要先建一个补图),若该前趋未确定,则将其加入队列Q2;
    (5)在(3)(4)步操作中,出现以下情况之一,则本次尝试失败,否则本次尝试成功:
    <1>某个已被加入队列Q1的点被加入队列Q2;
    <2>某个已被加入队列Q2的点被加入队列Q1;
    <3>某个j的状态为2;
    <4>某个i'或j'的状态为1或某个i'或j'的前趋的状态为1;
    (6)若本次尝试成功,则将Q1中的所有点的状态改为1,将Q2中所有点的状态改为2,转(2),否则尝试点i',若仍失败则问题无解。
    该算法的时间复杂度为O(NM)(最坏情况下要尝试所有的点,每次尝试要遍历所有的边),但是在多数情况下,远远达不到这个上界。
    具体实现时,可以用一个数组vst来表示队列Q1和Q2。设立两个标志变量i1和i2(要求对于不同的i,i1和i2均不同,这样可以避免每次尝试都要初始化一次,节省时间),若vst[i]=i1则表示i已被加入Q1,若vst[i]=i2则表示i已被加入Q2。不过Q1和Q2仍然是要设立的,因为遍历(BFS)的时候需要队列,为了防止重复遍历,加入Q1(或Q2)中的点的vst值必然不等于i1(或i2)。中间一旦发生矛盾,立即中止尝试,宣告失败。

    该算法虽然在多数情况下时间复杂度到不了O(NM),但是综合性能仍然不如下面的O(M)算法。不过,该算法有一个很重要的用处:求字典序最小的解!
    如果原图中的同一对点编号都是连续的(01、23、45……)则可以依次尝试第0对、第1对……点,每对点中先尝试编号小的,若失败再尝试编号大的。这样一定能求出字典序最小的解(如果有解的话),因为一个点一旦被确定,则不可更改
    如果原图中的同一对点编号不连续(比如03、25、14……)则按照该对点中编号小的点的编号递增顺序将每对点排序,然后依次扫描排序后的每对点,先尝试其编号小的点,若成功则将这个点选上,否则尝试编号大的点,若成功则选上,否则(都失败)无解。

    【具体题目】HDU1814(求字典序最小的解)

    #include <iostream>
    #include
    <stdio.h>
    using namespace std;
    #define re(i, n) for (int i=0; i<n; i++)
    const int MAXN = 20000, MAXM = 100000, INF = ~0U >> 2;
    struct node {
    int a, b, pre, next;
    } E[MAXM], E2[MAXM];
    int _n, n, m, V[MAXN], ST[MAXN][2], Q[MAXN], Q2[MAXN], vst[MAXN];
    bool res_ex;
    void init_d()
    {
    re(i, n) E[i].a
    = E[i].pre = E[i].next = E2[i].a = E2[i].pre = E2[i].next = i;
    m
    = n;
    }
    void add_edge(int a, int b)
    {
    E[m].a
    = a; E[m].b = b; E[m].pre = E[a].pre; E[m].next = a; E[a].pre = m; E[E[m].pre].next = m;
    E2[m].a
    = b; E2[m].b = a; E2[m].pre = E2[b].pre; E2[m].next = b; E2[b].pre = m; E2[E2[m].pre].next = m++;
    }
    void solve()
    {
    re(i, n) {V[i]
    = 0; vst[i] = 0;} res_ex = 1;
    int i, i1, i2, j, k, len, front, rear, front2, rear2;
    bool ff;
    re(_i, _n) {
    if (V[_i << 1] == 1 || V[(_i << 1) + 1] == 1) continue;
    i
    = _i << 1; len = 0;
    if (!V[i]) {
    ST[len][
    0] = i; ST[len++][1] = 1; if (!V[i ^ 1]) {ST[len][0] = i ^ 1; ST[len++][1] = 2;}
    Q[front
    = rear = 0] = i; vst[i] = i1 = n + i; Q2[front2 = rear2 = 0] = i ^ 1; vst[i ^ 1] = i2 = (n << 1) + i; ff = 1;
    for (; front<=rear; front++) {
    j
    = Q[front];
    for (int p = E[j].next; p != j; p=E[p].next) {
    k
    = E[p].b;
    if (V[k] == 2 || vst[k] == i2 || V[k ^ 1] == 1 || vst[k ^ 1] == i1) {ff = 0; break;}
    if (vst[k] != i1) {
    Q[
    ++rear] = k; vst[k] = i1;
    if (!V[k]) {ST[len][0] = k; ST[len++][1] = 1;}
    }
    if (vst[k ^ 1] != i2) {
    Q2[
    ++rear2] = k ^ 1; vst[k ^ 1] = i2;
    if (!V[k]) {ST[len][0] = k ^ 1; ST[len++][1] = 2;}
    }
    }
    if (!ff) break;
    }
    if (ff) {
    for (; front2<=rear2; front2++) {
    j
    = Q2[front2];
    for (int p = E2[j].next; p != j; p=E2[p].next) {
    k
    = E2[p].b;
    if (V[k] == 1 || vst[k] == i1) {ff = 0; break;}
    if (vst[k] != i2) {
    vst[k]
    = i2; Q2[++rear] = k;
    if (!V[k]) {ST[len][0] = k; ST[len++][1] = 2;}
    }
    }
    if (!ff) break;
    }
    if (ff) {
    re(j, len) V[ST[j][
    0]] = ST[j][1];
    continue;
    }
    }
    }
    i
    = (_i << 1) + 1; len = 0;
    if (!V[i]) {
    ST[len][
    0] = i; ST[len++][1] = 1; if (!V[i ^ 1]) {ST[len][0] = i ^ 1; ST[len++][1] = 2;}
    Q[front
    = rear = 0] = i; vst[i] = i1 = n + i; Q2[front2 = rear2 = 0] = i ^ 1; vst[i ^ 1] = i2 = (n << 1) + i; ff = 1;
    for (; front<=rear; front++) {
    j
    = Q[front];
    for (int p = E[j].next; p != j; p=E[p].next) {
    k
    = E[p].b;
    if (V[k] == 2 || vst[k] == i2 || V[k ^ 1] == 1 || vst[k ^ 1] == i1) {ff = 0; break;}
    if (vst[k] != i1) {
    Q[
    ++rear] = k; vst[k] = i1;
    if (!V[k]) {ST[len][0] = k; ST[len++][1] = 1;}
    }
    if (vst[k ^ 1] != i2) {
    Q2[
    ++rear2] = k ^ 1; vst[k ^ 1] = i2;
    if (!V[k]) {ST[len][0] = k ^ 1; ST[len++][1] = 2;}
    }
    }
    if (!ff) break;
    }
    if (ff) {
    for (; front2<=rear2; front2++) {
    j
    = Q2[front2];
    for (int p = E2[j].next; p != j; p=E2[p].next) {
    k
    = E2[p].b;
    if (V[k] == 1 || vst[k] == i1) {ff = 0; break;}
    if (vst[k] != i2) {
    vst[k]
    = i2; Q2[++rear] = k;
    if (!V[k]) {ST[len][0] = k; ST[len++][1] = 2;}
    }
    }
    if (!ff) break;
    }
    if (ff) {
    re(j, len) V[ST[j][
    0]] = ST[j][1];
    continue;
    }
    }
    }
    if (V[_i << 1] + V[(_i << 1) + 1] != 3) {res_ex = 0; break;}
    }
    }
    int main()
    {
    int _m, a, b;
    while (scanf("%d%d", &_n, &_m) != EOF) {
    n
    = _n << 1; init_d();
    re(i, _m) {
    scanf(
    "%d%d", &a, &b); a--; b--;
    if (a != (b ^ 1)) {add_edge(a, b ^ 1); add_edge(b, a ^ 1);}
    }
    solve();
    if (res_ex) {re(i, n) if (V[i] == 1) printf("%d\n", i + 1);} else puts("NIE");
    }
    return 0;
    }

    【O(M)算法】
    根据图的对称性,可以将图中所有的强连通分支全部缩成一个点(因为强连通分支中的点要么都选,要么都不选),然后按照拓扑逆序(每次找出度为0的点,具体实现时,在建分支邻接图时将所有边取反)遍历分支邻接图,将这个点(表示的连通分支)选上,并将其所有对立点(注意,连通分支的对立连通分支可能有多个,若对于两个连通分支S1和S2,点i在S1中,点i'在S2中,则S1和S2对立)及这些对立点的前趋全部标记为不选,直到所有点均标记为止。这一过程中必然不会出现矛盾(详细证明过程省略,论文里有)。
    无解判定:若求出所有强分支后,存在点i和i'处于同一个分支,则无解,否则必定有解。
    时间复杂度:求强分支时间复杂度为O(M),拓扑排序的时间复杂度O(M),总时间复杂度为O(M)。

    该算法的时间复杂度低,但是只能求出任意一组解,不能保证求出解的字典序最小。当然,如果原题不需要求出具体的解,只需要判定是否有解(有的题是二分 + 2-SAT判有解的),当然应该采用这种算法,只要求强连通分支(Kosaraju、Tarjan均可,推荐后者)即可。

    【具体题目】PKU3648(本题的特殊情况非常多,具体见Discuss)

    #include <iostream>
    #include
    <stdio.h>
    using namespace std;
    #define re(i, n) for (int i=0; i<n; i++)
    #define re2(i, l, r) for (int i=l; i<r; i++)
    #define re3(i, l, r) for (int i=l; i<=r; i++)
    const int MAXN = 1000, MAXM = 10000, INF = ~0U >> 2;
    struct edge {
    int a, b, pre, next;
    } E[MAXM], E2[MAXM];
    int n, n2, m, m2, stk[MAXN], stk0[MAXN], V[MAXN], w[MAXN], st[MAXN], dfn[MAXN], low[MAXN], sw[MAXN], L[MAXN], R[MAXN];
    int V2[MAXN], Q[MAXN], de[MAXN], Q0[MAXN];
    bool vst[MAXN], res[MAXN], res_ex;
    void init_d()
    {
    re(i, n) E[i].a
    = E[i].pre = E[i].next = i;
    m
    = n;
    }
    void init_d2()
    {
    re(i, n2) {E2[i].a
    = E2[i].pre = E2[i].next = i; de[i] = 0;}
    m2
    = n2;
    }
    void add_edge(int a, int b)
    {
    E[m].a
    = a; E[m].b = b; E[m].pre = E[a].pre; E[m].next = a; E[a].pre = m; E[E[m].pre].next = m++;
    de[b]
    ++;
    }
    void add_edge2(int a, int b)
    {
    E2[m2].a
    = a; E2[m2].b = b; E2[m2].pre = E2[a].pre; E2[m2].next = a; E2[a].pre = m2; E[E[m2].pre].next = m2++;
    }
    void solve()
    {
    int tp, tp0, x0, x, y, ord = 0, ord0 = 0; n2 = 0;
    bool fd;
    res_ex
    = 1; re(i, n) V[i] = 0;
    re(i, n)
    if (!V[i]) {
    stk[tp
    = 0] = stk0[tp0 = 0] = i; V[i] = 1; dfn[i] = low[i] = ++ord; st[i] = E[i].next;
    while (tp0 >= 0) {
    x
    = stk0[tp0]; fd = 0;
    for (int p=st[x]; p != x; p = E[p].next) {
    y
    = E[p].b;
    if (!V[y]) {
    stk[
    ++tp] = stk0[++tp0] = y; V[y] = 1; dfn[y] = low[y] = ++ord; st[y] = E[y].next; st[x] = E[p].next; fd = 1; break;
    }
    else if (V[y] == 1 && dfn[y] < low[x]) low[x] = dfn[y];
    }
    if (!fd) {
    V[x]
    = 2;
    if (low[x] == dfn[x]) {
    L[n2]
    = ord0;
    while ((y = stk[tp]) != x) {w[y] = n2; sw[ord0++] = y; tp--;}
    w[stk[tp
    --]] = n2; sw[ord0] = x; R[n2++] = ord0++;
    };
    if (tp0) {x0 = stk0[tp0 - 1]; if (low[x] < low[x0]) low[x0] = low[x];} tp0--;
    }
    }
    }
    re(i, n)
    if (w[i] == w[i ^ 1]) {res_ex = 0; return;}
    init_d2();
    re2(i, n, m) {
    x
    = w[E[i].a]; y = w[E[i].b];
    if (x != y) add_edge2(y, x);
    }
    int front = 0, rear = -1, front0, rear0; re(i, n2) {if (!de[i]) Q[++rear] = i; V2[i] = 0;}
    for (; front<=rear; front++) {
    int i = Q[front], j, j0;
    if (!V2[i]) {
    V2[i]
    = 1; front0 = 0; rear0 = -1;
    re(k, n2) vst[k]
    = 0;
    re3(x, L[i], R[i]) {
    j
    = w[sw[x] ^ 1]; Q0[++rear0] = j; vst[j] = 1;
    }
    for (; front0<=rear0; front0++) {
    j
    = Q0[front0]; V2[j] = 2;
    for (int p=E2[j].next; p != j; p=E2[p].next) {
    j0
    = E2[p].b;
    if (!vst[j0]) {Q0[++rear0] = j0; vst[j0] = 1;}
    }
    }
    }
    for (int p=E[i].next; p != i; p=E[p].next) {
    j
    = E[p].b; de[j]--;
    if (!de[j]) Q[++rear] = j;
    }
    }
    re(i, n) res[i]
    = 0;
    re(i, n2)
    if (V2[i] == 1) re3(j, L[i], R[i]) res[sw[j]] = 1;
    }
    int main()
    {
    int n0, m0, x1, x2, N1, N2;
    char c1, c2;
    while (1) {
    scanf(
    "%d%d", &n0, &m0); if (!n0 && !m0) break; else {n = n0 << 1; init_d();}
    re(i, m0) {
    scanf(
    "%d%c%d%c", &x1, &c1, &x2, &c2);
    if (c1 == 'h') N1 = x1 << 1; else N1 = (x1 << 1) + 1;
    if (c2 == 'h') N2 = x2 << 1; else N2 = (x2 << 1) + 1;
    add_edge(N1
    ^ 1, N2); add_edge(N2 ^ 1, N1);
    }
    add_edge(
    0, 1);
    solve();
    if (res_ex) {
    bool spc = 0;
    re(i, n)
    if (i != 1 && res[i]) {
    if (spc) putchar(' '); else spc = 1;
    printf(
    "%d%c", i >> 1, i & 1 ? 'w' : 'h');
    }
    puts(
    "");
    }
    else puts("bad luck");
    }
    return 0;
    }
  • 相关阅读:
    第七章 路由 68 路由-前端路由和后端路由的概念
    第六章 组件 67 使用ref获取DOM元素和组件引用
    第六章 组件 65-66 组件案例-发表评论功能的实现
    第六章 组件 63 组件传值-父组件向子组件传值和data与props的区别
    第六章 组件 62 组件-组件定义方式的复习
    第六章 组件 61 动画-小球动画flag标识符的作用分析
    第六章 组件 60 组件切换-应用切换动画和mode方式
    第六章 组件 59 组件切换-使用Vue提供的component元素实现组件切换
    vscode代码格式化 空格的配置
    vim配置C++开发环境 win10
  • 原文地址:https://www.cnblogs.com/kuangbin/p/2145536.html
Copyright © 2020-2023  润新知