zoukankan      html  css  js  c++  java
  • 2-sat

    2-sat

    1.算法分析

        有 n 个变量x[1...n],每个变量的可能取值为1或0(或称i和~i必取到其中1个)。
        给定 m 个约束条件,每个约束条件形如:

    若 x[i] 取 i(或者~i),则 x[j] 必取 j(或者~j)
    

        判定是否存在对每个变量的合法赋值,使所有约束都被满足

    判定方法:

    1. 建立 2N 个点有向图,i 和 ~i 一般设为 i 和 i+N
    2. 对于每个约束条件连2条有向边(原命题以及其逆否命题),例如 i->j,则同时连 (~j) -> (~i)
    3. Tarjan算法求出有向图的scc
          对于存在某个变量x[i],i 和 ~i 属于同一个scc(即 i 和 ~i 可以相互导出)则必然无解,否则有解。
    4. 如果要求出路径,那么把每个点的scc[i]和scc[opp(i)]进行比较,如果scc[i] < scc[opp(i)], 那么输出1; 否则,输出0

    本质:
        2-sat的本质就是判断一个xi的属性,因为xi只能是0或1,因此如果xi既是0也是1那么就是非法状态(scc[xi] == scc[~xi])。2-sat和扩展域并查集的本质相同,不同的在于适用条件不同,2-sat只需要一个条件能够推导出2个命题即可(原命题、逆否命题),而扩展域并查集则需要一个条件能够推导出4个命题(原命题、逆否命题、否命题、逆命题)。异或能够导出4个命题,与/或能导出2个命题。2-sat建立的是有向边,所以使用tarjan算法处理;扩展域并查集使用无向边,所以使用并查集处理。

    建边技巧:
        建边的原则就是建边时一定要考虑原命题和它的逆否命题,建边分两种情况:

    1. 第一种:已经告诉i和j有连边,且已知他们之间的关系,&、|、 ^ , 然后按照给定的关系进行建边,&建2条边,|建2条边,^建4条边。
    2. 没有告诉i和j连边,那么只能N^2去枚举i和j,判断它两之间是否能够建边,然后根据判断的结果建边。比如i和j之间不能建边,那就说明i->~j, j->~i。同时,可能存在特殊情况,需要去枚举M^2,那么一般来说M都必须规约到N的数量级才行。注意枚举的时候要判断i==j->continue

    2. 版子

    // 属于已知边关系建边
    #include<bits/stdc++.h>
    
    using namespace std;
    
    int const N = 2e6 + 10, M = 2e6 + 10;
    // dfn记录每个点的时间戳,low记录每个点的回溯值,scc[i]=x表示i在标号为x的强连通分量里,stk维护一个栈,sccnum记录强连通分量的个数
    int dfn[N], low[N], scc[N], stk[N], sccnum, top, timestamp;  
    int h[N], e[M], ne[M], idx;
    int n, m;
    
    // a->b有一条边
    void add(int a, int b)
    {
        e[idx] = b, ne[idx] = h[a], h[a] = idx++;
    }
    
    // tarjan算法求强连通分量
    void tarjan(int root)
    {
        if (dfn[root]) return;  // 时间戳不为0,返回
        dfn[root] = low[root] = ++timestamp;  // 记录当前点的时间戳和回溯值,初始化二者相同,而后dfn[root]>=low[root]
        stk[++top] = root;  // 把根放入栈内
        for (int i = h[root]; i != -1; i = ne[i])  // 遍历每一个与根节点相邻的点
        {
            int j = e[i];  // 与i相邻的点为j
            if (!dfn[j])  // j点没有访问过
            {
                tarjan(j);  // 继续dfs,得到所有以j点为根的子树内所有的low和dfn
                low[root] = min(low[root], low[j]);  // 根的low是其子树中low最小的那个
            }
            else if (!scc[j])  // 如果j这个点还在栈内(在栈内的话不属于任何一个scc),同时一个栈内的点在一个scc内
            {
                low[root] = min(low[root], dfn[j]);  // low代表所能到达的最小的时间戳
            }
        }
        
        // 如果root的后代不能找到更浅的节点(更小的时间戳)
        if (low[root] == dfn[root])  // 只有某个强连通分量的根节点的low和dfn才会相同
        {
            sccnum++;
            while (1)  // 出栈直到等于root
            {
                int x = stk[top--];
                scc[x] = sccnum;
                if (x == root) break;
            }
        }
    }
    
    int opp(int x) {
        if (x > n) return x - n;
        else return x + n;
    }
    
    int main()
    {
        cin >> n >> m;
        memset(h, -1, sizeof h);
        for (int i = 1, a, b, c, d; i <= m; ++i) {
            scanf("%d %d %d %d", &a, &b, &c, &d);
            if (!b) a = opp(a);
            if (!d) c = opp(c);
    
            // a|c=1 => ~a->c, ~c->a 
            add(opp(a), c), add(opp(c), a);
        }
    
        // tarjan求scc
        for (int i = 1; i <= n * 2; ++i)
            if (!dfn[i]) tarjan(i);
            
        // 判断是否满足条件
        for (int i = 1; i <= n; ++i) {
            if (scc[i] == scc[opp(i)]) {
                cout << "IMPOSSIBLE
    ";
                return 0;
            }
        }
    
        // 打印路径
        cout << "POSSIBLE
    ";
        for (int i = 1; i <= n; ++i) {
            if (scc[i] < scc[opp(i)]) cout << "1 ";
            else cout << "0 ";
        }
        return 0;
    }
    

    3. 典型例题

    3.1 已知点与点关系

    acwing370卡图难题
    有N个变量X0~XN−1,每个变量的可能取值为0或1。
    给定M个算式,每个算式形如 Xa op Xb=c,其中 a,b 是变量编号,c 是数字0或1,op 是 and,or,xor 三个位运算之一。求是否存在对每个变量的合法赋值,使所有算式都成立。

    /* 本题的建边需要好好牢记,&、|能够推出2个命题,^能够推出4个命题 */
    #include<bits/stdc++.h>
    
    using namespace std;
    
    int const N = 1e3 + 10, M = 4e6 + 10;
    // dfn记录每个点的时间戳,low记录每个点的回溯值,scc[i]=x表示i在标号为x的强连通分量里,stk维护一个栈,sccnum记录强连通分量的个数。该题属于已知边关系建边。
    int dfn[N], low[N], scc[N], stk[N], sccnum, top, timestamp;  
    int h[N], e[M], ne[M], idx;
    int n, m;
    char op[10];
    
    // a->b有一条边
    void add(int a, int b)
    {
        e[idx] = b, ne[idx] = h[a], h[a] = idx++;
    }
    
    // tarjan算法求强连通分量
    void tarjan(int root)
    {
        if (dfn[root]) return;  // 时间戳不为0,返回
        dfn[root] = low[root] = ++timestamp;  // 记录当前点的时间戳和回溯值,初始化二者相同,而后dfn[root]>=low[root]
        stk[++top] = root;  // 把根放入栈内
        for (int i = h[root]; i != -1; i = ne[i])  // 遍历每一个与根节点相邻的点
        {
            int j = e[i];  // 与i相邻的点为j
            if (!dfn[j])  // j点没有访问过
            {
                tarjan(j);  // 继续dfs,得到所有以j点为根的子树内所有的low和dfn
                low[root] = min(low[root], low[j]);  // 根的low是其子树中low最小的那个
            }
            else if (!scc[j])  // 如果j这个点还在栈内(在栈内的话不属于任何一个scc),同时一个栈内的点在一个scc内
            {
                low[root] = min(low[root], dfn[j]);  // low代表所能到达的最小的时间戳
            }
        }
        
        // 如果root的后代不能找到更浅的节点(更小的时间戳)
        if (low[root] == dfn[root])  // 只有某个强连通分量的根节点的low和dfn才会相同
        {
            sccnum++;
            while (1)  // 出栈直到等于root
            {
                int x = stk[top--];
                scc[x] = sccnum;
                if (x == root) break;
            }
        }
    }
    
    int main()
    {
        cin >> n >> m;
        memset(h, -1, sizeof h);
        for (int i = 1, a, b, c; i <= m; ++i)
        {
            scanf("%d%d%d%s", &a, &b, &c, op);
            getchar();
            if(op[0]=='A' && c==0){//a&b = 0, a->~b, b->~a
                add(a, b + n);
                add(b, a + n);
            }
            if(op[0]=='A' && c==1){//a&b = 1, ~a->a, -b->b
                add(a + n, a);
                add(b + n, b);
            }
    
            if(op[0]=='O' && c==0){//a|b = 0, a->~a, b->~b
                add(a, a + n);
                add(b, b + n);
            }
            if(op[0]=='O' && c==1){//a|b = 1, ~a->b, ~b->a
                add(a + n, b);
                add(b + n, a);
            }
    
            if(op[0]=='X' && c==0){//a^b = 0, a->b, ~a->~b, b->a, ~b->-a
                add(a, b);
                add(a + n, b + n);
                add(b, a);
                add(b + n, a + n);
            }
    
            if(op[0]=='X' && c==1){//a^b = 1, a->~b, ~a->b, b->~a, ~b->a
                add(a, b + n);
                add(a + n, b);
                add(b, a + n);
                add(b + n, a);
            }
        }
    
        // tarjan求scc
        for (int i = 1; i <= n * 2; ++i)
            if (!dfn[i]) tarjan(i);
            
        // 判断是否满足条件
        for (int i = 1; i <= n; ++i) {
            if (scc[i] == scc[i + n]) {
                cout << "NO";
                return 0;
            }
        }
        cout << "YES";
        return 0;
    }
    

    2.2 未知点与点关系

    acwing371牧师约翰最忙碌的一天
    9月1日这天牧师需要忙碌婚礼的事情,有 N 对情侣在这天准备结婚,每对情侣都预先计划好了婚礼举办的时间,其中第 i 对情侣的婚礼从时刻 Si 开始,到时刻 Ti 结束。第 i 对情侣需要 Di 分钟完成这个仪式,即必须选择 Si~Si+Di 或 Ti−Di~Ti 两个时间段之一。现在给定时间可选时间段,求出是否存在合法方案,并打印路径

    // 本题没有给出边的关系,但点的数目很少,直接枚举建边
    // 属于边未知关系,枚举建边
    #include<bits/stdc++.h>
    
    using namespace std;
    
    int const N = 2e3 + 10, M = N * N;
    
    // dfn记录每个点的时间戳,low记录每个点的回溯值,scc[i]=x表示i在标号为x的强连通分量里,stk维护一个栈,sccnum记录强连通分量的个数
    int dfn[N], low[N], scc[N], stk[N], sccnum, top, timestamp;  
    int h[N], e[M], ne[M], idx, d[N], n, m, t[N][2];
    
    // a->b有一条边
    void add(int a, int b)
    {
        e[idx] = b, ne[idx] = h[a], h[a] = idx++;
    }
    
    // tarjan算法求强连通分量
    void tarjan(int root)
    {
        if (dfn[root]) return;  // 时间戳不为0,返回
        dfn[root] = low[root] = ++timestamp;  // 记录当前点的时间戳和回溯值,初始化二者相同,而后dfn[root]>=low[root]
        stk[++top] = root;  // 把根放入栈内
        for (int i = h[root]; i != -1; i = ne[i])  // 遍历每一个与根节点相邻的点
        {
            int j = e[i];  // 与i相邻的点为j
            if (!dfn[j])  // j点没有访问过
            {
                tarjan(j);  // 继续dfs,得到所有以j点为根的子树内所有的low和dfn
                low[root] = min(low[root], low[j]);  // 根的low是其子树中low最小的那个
            }
            else if (!scc[j])  // 如果j这个点还在栈内(在栈内的话不属于任何一个scc),同时一个栈内的点在一个scc内
            {
                low[root] = min(low[root], dfn[j]);  // low代表所能到达的最小的时间戳
            }
        }
        
        // 如果root的后代不能找到更浅的节点(更小的时间戳)
        if (low[root] == dfn[root])  // 只有某个强连通分量的根节点的low和dfn才会相同
        {
            sccnum++;
            while (1)  // 出栈直到等于root
            {
                int x = stk[top--];
                scc[x] = sccnum;
                if (x == root) break;
            }
        }
    }
    
    // 判断是否矛盾
    bool isx(int i, int fi, int j, int fj){
        if(t[i][fi] >= t[j][fj]+d[j]) return 0;
        if(t[i][fi]+d[i] <= t[j][fj]) return 0;
        return 1;
    }
    
    int str2int(string s){
        int x = 0;
        x = (s[0]-'0')*10 + (s[1]-'0');
        x *= 60;
        x += (s[3]-'0')*10 + (s[4]-'0');
        return x;
    }
    
    string int2str(int x){
        string s = "00:00";
        int h = x/60, m = x%60;
        s[0] = '0' + h/10; s[1] = '0' + h%10;
        s[3] = '0' + m/10; s[4] = '0' + m%10;
        return s;
    }
    
    int main()
    {
        cin >> n;
        memset(h, -1, sizeof h);
        
        // 读入并进行时间转换
        for (int i = 1; i <= n; ++i) {
            string start, end;
            cin >> start >> end >> d[i];
            t[i][0] = str2int(start);
            t[i][1] = str2int(end) - d[i];
        }
        
        // 枚举建边
        for (int i = 1; i <= n; ++i) {
            for (int j = 1; j <= n; ++j) {
                if (i == j) continue;
                
                // isx函数判断是否矛盾
                if (isx(i, 0, j, 0)) add(i, j + n), add(j, i + n);
                if (isx(i, 1, j, 0)) add(i + n, j + n), add(j, i);
                if (isx(i, 0, j, 1)) add(i, j), add(j + n, i + n);
                if (isx(i, 1, j, 1)) add(i + n, j), add(j + n, i);
            }
        }
    
        // tarjan求scc
        for (int i = 1; i <= n * 2; ++i)
            if (!dfn[i]) tarjan(i);
            
        // 判断是否满足条件
        for (int i = 1; i <= n; ++i) {
            if (scc[i] == scc[i + n]) {
                cout << "NO
    ";
                return 0;
            }
        }
    
        // 打印路径
        cout << "YES
    ";
        for (int i = 1; i <= n; ++i) {
            if (scc[i] < scc[i + n]) cout << int2str(t[i][0]) << " " << int2str(t[i][0] + d[i]) << endl;
            else cout << int2str(t[i][1]) << " " << int2str(t[i][1] + d[i]) << endl;
        }
        return 0;
    }
    
  • 相关阅读:
    MiniUI表单验证实践
    MiniUI官方表单验证示例
    MiniUI表单验证总结
    Js-事件分发与DOM事件流
    Windows远程桌面连接的利器-mRemote
    Git 以分支的方式同时管理多个项目
    GIT 如何合并另一个远程Git仓库的文件到本地仓库里某个指定子文件夹并不丢失远程提交记录?
    如何导入另一个 Git库到现有的Git库并保留提交记录
    Total Commander如何设置自定义快捷键在当前目录打开ConEmu
    PHP ECSHOP中 诡异的问题:expects parameter 1 to be double
  • 原文地址:https://www.cnblogs.com/spciay/p/13135069.html
Copyright © 2011-2022 走看看