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\n";
            return 0;
        }
    }

    // 打印路径
    cout << "POSSIBLE\n";
    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;
}

3.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);  // i[0]和j[0]矛盾,那么i[0],j[1] 或 i[1],j[0]
            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\n";
            return 0;
        }
    }

    // 打印路径
    cout << "YES\n";
    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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值