2-SAT 习题

2-SAT 习题

1. Codeforces - 468B Two Sets

1.1 题意

给定 n ( 1 ≤ n ≤ 1 0 5 ) n(1 \le n \le 10^5) n(1n105) 个互不相同的数 p i ( 1 ≤ p i ≤ 1 0 9 ) p_i(1 \le p_i \le 10^9) pi(1pi109),现在要将它们分成两个集合 A A A B B B,并满足以下两个条件:

  1. 如果 x ∈ A x \in A xA,则 a − x ∈ A a-x \in A axA

  2. 如果 x ∈ B x \in B xB,则 b − x ∈ B b-x \in B bxB

给定 a , b ( 1 ≤ a , b ≤ 1 0 9 ) a,b(1 \le a,b \le 10^9) a,b(1a,b109),将序列 p p p 进行划分,或者输出无解。

1.2 解题过程

x 0 x_0 x0 表示 x ∈ A x \in A xA x 1 x_1 x1 表示 x ∈ B x \in B xB

则根据题意,我们进行如下建边:

  1. < x 0 , ( a − x ) 0 > <x_0, (a-x)_0> <x0,(ax)0> (条件 1)
  2. < ( a − x ) 1 , x 1 > <(a-x)_1, x_1> <(ax)1,x1>(条件 1 的逆否命题)这条很不显然,但可以这么想:如果 a − x ∈ B a-x \in B axB,那么 x x x 必然不可能属于 A A A,否则就与题意矛盾了。
  3. < x 1 , ( b − x ) 1 > <x_1,(b-x)_1> <x1,(bx)1> (条件 2)
  4. < ( b − x ) 0 , x 0 > <(b-x)_0,x_0> <(bx)0,x0> (条件 2 的逆否命题)

需要注意的是,进行上述建边的条件是 x x x a − x a-x ax 或者 x x x b − x b-x bx 同时在集合 p p p 中。

特别地,若对于 x ∈ p x \in p xp,在 p p p 中不存在 a − x a-x ax,则建边 < x 0 , x 1 > <x_0,x_1> <x0,x1>,表示 x x x 必然不可能属于 A A A,根据必须分割的原则, x x x 必须属于 B B B x x x b − x b-x bx 同理。

之后跑一遍 2-SAT 即可。

时间复杂度: O ( 2 n ) O(2n) O(2n)

1.3 错误点

  1. 逆否命题在大部分情况下都需要考虑,不要根据自己不深刻的理解断言不需要/不可以建立逆否命题。

1.4 代码

int head[maxn], n, m, cnt, tot, top, num;
int dfn[maxn], low[maxn], st[maxn], col[maxn];
bool instack[maxn];
struct edge
{
    int v, nxt;
} Edge[8 * maxn];
void init()
{
    for(int i = 0; i <= 2 * n; i++) head[i] = -1;
    cnt = tot = top = num = 0;
}
void addedge(int u, int v)
{
    Edge[cnt].v = v;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}
void tarjan(int id)
{
    dfn[id] = low[id] = ++tot;
    st[++top] = id;
    instack[id] = true;
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        if(!dfn[v])
        {
            tarjan(v);
            low[id] = min(low[id], low[v]);
        }
        else if(instack[v])
            low[id] = min(low[id], dfn[v]);
    }
    if(dfn[id] == low[id])
    {
        int v;
        num++;
        do
        {
            v = st[top--];
            instack[v] = false;
            col[v] = num;
        } while(v != id);
    }
}
int p[maxn], ans[maxn], opp[maxn];
unordered_map<int, int> mp;
int main()
{
    int a, b;
    scanf("%d%d%d", &n, &a, &b);
    init();
    for (int i = 1; i <= n; i++) {
        scanf("%d", &p[i]);
        mp[p[i]] = i;
    }
    for (int u = 1; u <= n; u++) {
        int x = p[u];
        int y = a - x;
        int mark = 0;
        if (mp.count(y)) {
            int v = mp[y];
            addedge(u, v);
            addedge(v + n, u + n);
        } else {
            addedge(u, u + n);
        }
        y = b - x;
        if (mp.count(y)) {
            int v = mp[y];
            addedge(u + n, v + n);
            addedge(v, u);
        } else {
            addedge(u + n, u);
        }
    }
    for (int i = 1; i <= 2 * n; i++) {
        if (!dfn[i]) tarjan(i);
    }
    for (int i = 1; i <= n; i++) {
        if (col[i] == col[i + n]) return 0 * puts("NO");
        opp[i] = i + n;
        opp[i + n] = i;
    }
    puts("YES");
    for (int i = 1; i <= 2 * n; i++) {
        ans[i] = (col[i] > col[opp[i]]);
    }
    for (int i = 1; i <= n; i++) {
        if (ans[i] == 1 || ans[opp[i]] == 0) printf("1 ");
        else printf("0 ");
    }
    return 0;
}

2. Codeforces - 776D - The Door Problem

2.1 题意

给你 n ( 2 ≤ n ≤ 1 0 5 ) n(2 \le n \le 10^5) n(2n105) 扇门和 m ( 2 ≤ m ≤ 1 0 5 ) m(2 \le m \le 10^5) m(2m105) 个开关,每扇门恰好由两个开关控制,每个开关控制着若干个门。

对于门 i i i,若 r i = 0 r_i=0 ri=0 表明初始状态下门是锁着的,若 r i = 1 r_i=1 ri=1 表明初始状态下门是打开的。

触动开关会使其控制的每一扇门的状态取反

问是否可以通过某种策略(即选择某些开关将其打开),使得所有的门都是打开的。

2.2 解题过程

本题为 2-SAT \text{2-SAT} 2-SAT 问题。

设点 j ( 1 ≤ j ≤ m ) j(1 \le j \le m) j(1jm) 表示开关 j j j 处于关闭(初始)状态,设点 j + m j+m j+m 表示开关 j j j 处于开启状态。

考虑每扇门 i i i,有以下两种情况

( 1 ) (1) (1) r i = 0 r_i=0 ri=0,则控制这扇门的两个开关必须且只能开启其中一个

因此建立有向边 < i + m , j > <i+m, j> <i+m,j> < j , i + m > <j, i+m> <j,i+m> 以及它们的逆否命题 < j + m , i > <j+m, i> <j+m,i> < i , j + m > <i, j + m> <i,j+m>

( 2 ) (2) (2) r i = 1 r_i=1 ri=1,则控制这扇门的两个开关要么全部打开,要么全部关闭

因此建立有向边 < i + m , j + m > <i+m, j+m> <i+m,j+m> < i , j > <i, j> <i,j> 以及它们的逆否命题 < j , i > <j, i> <j,i> < j + m , i + m > <j+m, i+m> <j+m,i+m>

图建完之后,跑一遍 2-SAT \text{2-SAT} 2-SAT 即可。

时间复杂度: O ( 2 m ) O(2m) O(2m)

2.3 代码

int head[maxn], n, m, cnt, tot, top, num;
int dfn[maxn], low[maxn], st[maxn], col[maxn];
bool instack[maxn];

int r[maxn];

struct edge
{
    int v, nxt;
} Edge[8 * maxn];

void init()
{
    for(int i = 0; i <= 2 * m; i++) head[i] = -1;
    cnt = tot = top = num = 0;
}

void addedge(int u, int v)
{
    Edge[cnt].v = v;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}

void tarjan(int id)
{
    dfn[id] = low[id] = ++tot;
    st[++top] = id;
    instack[id] = true;
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        if(!dfn[v])
        {
            tarjan(v);
            low[id] = min(low[id], low[v]);
        }
        else if(instack[v])
            low[id] = min(low[id], dfn[v]);
    }
    if(dfn[id] == low[id])
    {
        int v;
        num++;
        do
        {
            v = st[top--];
            instack[v] = false;
            col[v] = num;
        } while(v != id);
    }
}

vector<int> ve[maxn];

int main()
{
    int num, v;
    scanf("%d%d", &n, &m);
    init();

    for(int i = 1; i <= n; i++) scanf("%d", &r[i]);

    for(int i = 1; i <= m; i++)
    {
        scanf("%d", &num);
        for(int j = 1; j <= num; j++)
        {
            scanf("%d", &v);
            ve[v].pb(i);
        }
    }

    for(int i = 1; i <= n; i++)
    {
        int a = ve[i][0], b = ve[i][1];
        if(r[i])
        {
            addedge(a + m, b + m);
            addedge(a, b);
            addedge(b + m, a + m);
            addedge(b, a);
        }
        else
        {
            addedge(a + m, b);
            addedge(a, b + m);
            addedge(b, a + m);
            addedge(b + m, a);
        }
    }

    for(int i = 1; i <= 2 * m; i++)
    {
        if(!dfn[i]) tarjan(i);
    }

    bool isok = true;
    for(int i = 1; i <= m; i++)
    {
        if(col[i] == col[i + m])
        {
            isok = false;
            break;
        }
    }

    if(isok)    printf("YES\n");
    else printf("NO\n");
    return 0;
}

3. Codeforces - 228E - The Road to Berland is Paved With Good Intentions

3.1 题意

现在有一个有 n ( 1 ≤ n ≤ 100 ) n(1 \le n \le 100) n(1n100) 个节点, m ( 1 ≤ m ≤ n ( n − 1 ) 2 ) m(1 \le m \le \frac{n(n-1)}{2}) m(1m2n(n1)) 条边的无向图。

每条边都有边权 c i ( c i ∈ { 0 , 1 } ) c_i(c_i \in \left\{0,1 \right\}) ci(ci{0,1})

现在可以进行若干次(可以为零)如下操作:选择一个点,将其所有出边的权值全部取反

问是否可以通过若干次操作,使得所有边的权值为 1 1 1

若可以,输出所要操作的节点编号。

题目保证两点之间最多有一条路径。

3.2 解题思路

本题为 2-SAT \text{2-SAT} 2-SAT 问题。

对于每一条边 e d g e i edge_i edgei,若其权值为 1 1 1,则连接它的两点要么全部操作,要么全部不操作;

若其权值为 0 0 0,则连接它的两点需要且只能操作其中一个。

求解思路与 776D \text{776D} 776D 相同,这里不再赘述。

3.3 代码

int head[maxn], n, m, cnt, tot, top, num;
int dfn[maxn], low[maxn], st[maxn], col[maxn], var[maxn];
bool instack[maxn];
struct edge
{
    int v, nxt;
} Edge[8 * maxn * maxn];
void init()
{
    for(int i = 0; i <= 2 * n; i++) head[i] = -1;
    cnt = tot = top = num = 0;
}
void addedge(int u, int v)
{
    Edge[cnt].v = v;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}
void tarjan(int id)
{
    dfn[id] = low[id] = ++tot;
    st[++top] = id;
    instack[id] = true;
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        if(!dfn[v])
        {
            tarjan(v);
            low[id] = min(low[id], low[v]);
        }
        else if(instack[v])
            low[id] = min(low[id], dfn[v]);
    }
    if(dfn[id] == low[id])
    {
        int v;
        num++;
        do
        {
            v = st[top--];
            instack[v] = false;
            col[v] = num;
        } while(v != id);
    }
}
vector<int> ans;
int main()
{
    int u, v, w;
    scanf("%d%d", &n, &m);
    init();
    for(int i = 1; i <= m; i++)
    {
        scanf("%d%d%d", &u, &v, &w);
        if(w)
        {
            addedge(u + n, v + n);
            addedge(v, u);
            addedge(v + n, u + n);
            addedge(u, v);
        }
        else
        {
            addedge(u + n, v);
            addedge(v + n, u);
            addedge(u, v + n);
            addedge(v, u + n);
        }
    }
    for(int i = 1; i <= 2 * n; i++)
    {
        if(!dfn[i]) tarjan(i);
    }
    bool isok = true;
    for(int i = 1; i <= n; i++)
    {
        if(col[i] == col[i + n])
        {
            isok = false;
            break;
        }
    }
    if(!isok)
    {
        printf("Impossible\n");
        return 0;
    }
    for(int i = 1; i <= n; i++)
    {
        var[i] = col[i] > col[i + n];
        if(var[i])  ans.pb(i);
    }
    printf("%d\n", (int)ans.size());
    for(int i = 0; i < ans.size(); i++)
    {
        if(i > 0)   printf(" ");
        printf("%d", ans[i]);
    }
    return 0;
}

4. Codeforces - 780D - Innokenty and a Football League

4.1 题意

现在有 n ( 1 ≤ n ≤ 1 0 3 ) n(1 \le n \le 10^3) n(1n103) 支球队,每支球队 i i i 有两个名字 n a m e i , 0 name_{i,0} namei,0 n a m e i , 1 name_{i,1} namei,1。每个球队必须在两个名字中选择其中一个,并且满足如下条件:

( 1 ) (1) (1) 不能有任意两支球队的名字相同。

( 2 ) (2) (2) 对于球队 i i i,若其选择了 n a m e i , 1 name_{i, 1} namei,1,则对于任意的 j ( 1 ≤ j ≤ n , i ≠ j ) j(1 \le j \le n,i \ne j) j(1jn,i=j),若满足 n a m e i , 0 ≠ n a m e j , 0 name_{i,0} \ne name_{j,0} namei,0=namej,0,则不可选择 n a m e j , 0 name_{j,0} namej,0

问每支球队的取名方案。

4.2 解题思路

本题为 2-SAT \text{2-SAT} 2-SAT 问题,需要建立一个 2 n 2n 2n 个点的有向图

i , j ∈ [ 1 , n ] , i ≠ j i,j \in [1,n], i \ne j i,j[1,n],i=j

则设点 i , j i,j i,j 分别表示 i i i 选择了 n a m e i , 0 name_{i,0} namei,0 j j j 选择了 n a m e j , 0 name_{j,0} namej,0

设点 i + n , j + n i + n,j + n i+n,j+n 分别表示 i i i 选择了 n a m e i , 1 name_{i,1} namei,1 j j j 选择了 n a m e j , 1 name_{j,1} namej,1

设有向边 < i , j > <i,j> <i,j> 表示 i i i 选择了 n a m e i , 0 name_{i,0} namei,0 j j j 必须选择 n a m e j , 0 name_{j,0} namej,0

其他情况同理

我们可以在 [ 1 , n ] [1,n] [1,n] 范围内遍历 i i i j ( i ≠ j ) j(i \ne j) j(i=j),根据题中条件有以下三种情况

( 1 ) (1) (1) n a m e i , 0 = n a m e j , 0 name_{i,0}=name_{j,0} namei,0=namej,0,则添加如下边:

< i , j + n > <i,j+n> <i,j+n> 及其逆否命题 < j , i + n > <j, i+n> <j,i+n>(条件 ( 1 ) (1) (1)

< i + n , j + n > <i+n, j+n> <i+n,j+n> 及其逆否命题 < j , i > <j, i> <j,i>(条件 ( 2 ) (2) (2)

(考虑条件 2 的逆否命题:若 j j j 选择了 n a m e j , 0 name_{j,0} namej,0 n a m e i , 0 = n a m e j , 0 name_{i,0}=name_{j,0} namei,0=namej,0,则 i i i 选择了 n a m e i , 0 name_{i,0} namei,0

( 2 ) (2) (2) n a m e i , 1 = n a m e j , 1 name_{i,1}=name_{j,1} namei,1=namej,1,则添加如下边:

< i + n , j > <i+n, j> <i+n,j> 及其逆否命题 < j + n , i > <j+n, i> <j+n,i>(条件 ( 1 ) (1) (1)

( 3 ) (3) (3) n a m e i , 0 = n a m e j , 1 name_{i,0}=name_{j,1} namei,0=namej,1,则添加如下边:

< i , j > <i, j> <i,j> 及其逆否命题 < j + n , i + n > <j+n, i+n> <j+n,i+n>(条件 ( 1 ) (1) (1)

( 4 ) (4) (4)若 $name_{i,0} \ne name_{j,0} $,则添加如下边:

< i + n , j + n > <i+n, j+n> <i+n,j+n> 及其逆否命题 < j , i > <j, i> <j,i>(条件 ( 2 ) (2) (2)

建边之后,在整张图上面跑 Tarjan,求出所有的强连通分量

之后,判断每个 i ( 1 ≤ i ≤ n ) i(1 \le i \le n) i(1in) 是否满足 i i i i + n i + n i+n 在同一个强连通分量中,若满足,则表明若选择 n a m e i , 0 name_{i,0} namei,0,则必须选择 n a m e i , 1 name_{i,1} namei,1,这与题意矛盾,因此答案不存在。

否则,输出一种合法的答案。

时间复杂度: O ( 2 n ) O(2n) O(2n)

4.3 代码

int head[maxn], n, m, cnt, tot, top, num;
int dfn[maxn], low[maxn], st[maxn], col[maxn], rev[maxn], var[maxn];
bool instack[maxn];

struct edge
{
    int v, nxt;
} Edge[8 * maxn * maxn];

void init()
{
    for(int i = 0; i <= 2 * n; i++) head[i] = -1;
    cnt = tot = top = num = 0;
}

void addedge(int u, int v)
{
    Edge[cnt].v = v;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}

void tarjan(int id)
{
    dfn[id] = low[id] = ++tot;
    st[++top] = id;
    instack[id] = true;
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        if(!dfn[v])
        {
            tarjan(v);
            low[id] = min(low[id], low[v]);
        }
        else if(instack[v])
            low[id] = min(low[id], dfn[v]);
    }
    if(dfn[id] == low[id])
    {
        int v;
        num++;
        do
        {
            v = st[top--];
            instack[v] = false;
            col[v] = num;
        } while(v != id);
    }
}

string name[maxn][2];

int main()
{
    string stra, strb;
    scanf("%d", &n);
    init();

    for(int i = 1; i <= n; i++)
    {
        cin >> stra >> strb;
        name[i][0] += stra[0];
        name[i][0] += stra[1];
        name[i][0] += stra[2];
        name[i][1] += stra[0];
        name[i][1] += stra[1];
        name[i][1] += strb[0];
    }

    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j <= n; j++)
        {
            if(i == j)  continue;
            if(name[i][0] == name[j][0])
            {
                addedge(i, j + n);
                addedge(j, i + n);
                addedge(i + n, j + n);
                addedge(j, i);
            }
            if(name[i][1] == name[j][1])
            {
                addedge(i + n, j);
                addedge(j + n, i);
            }
            if(name[i][0] == name[j][1])
            {
                addedge(i, j);
                addedge(j + n, i + n);
            }
        }
    }

    for(int i = 1; i <= 2 * n; i++)
    {
        if(!dfn[i]) tarjan(i);
    }

    bool isok = true;
    for(int i = 1; i <= n; i++)
    {
        if(col[i] == col[i + n])
        {
            isok = false;
            break;
        }
        rev[i] = i + n;
        rev[i + n] = i;
    }

    if(!isok)
    {
        printf("NO\n");
        return 0;
    }

    for(int i = 1; i <= 2 * n; i++)
    {
        var[i] = col[i] > col[rev[i]];
    }

    printf("YES\n");
    for(int i = 1; i <= n; i++)
    {
        cout << name[i][var[i]] << endl;
    }
    return 0;
}

5. Codeforces - 875C - National Property

题目链接:https://codeforces.com/problemset/problem/875/C

5.1 题意

给你 n ( 2 ≤ n ≤ 1 0 5 ) n(2 \le n \le 10^5) n(2n105) 个字符串(由 [ 1 , m ] ( 1 ≤ m ≤ 1 0 5 ) [1,m](1 \le m \le 10^5) [1,m](1m105) 之间的小写字母组成)。

现在可以把某些字母变成大写,例如 2 2 2 变成 2 ′ 2' 2

规定大写字母的字典序小于小写字母,如果同是大写字母或同是小写字母,则值小的字典序小。

问是否存在一种小写转大写的方案,使得字符串按照字典序不减的顺序排列。

题目保证字符串的总长不超过 1 0 5 10^5 105

5.2 解题过程

比较显然的 2-SAT 问题。

x 0 x_0 x0 表示 x x x 字母保持小写状态, x 1 x_1 x1 表示 x x x 字母转化为大写。

则我们可以遍历所有相邻的字符串,之后内层循环枚举字符串中相同位置的字母,分别设为 u u u v v v

则存在以下三种情况:

  1. u = v u=v u=v,此时无需进行任何操作;
  2. u > v u > v u>v,此时只有一种方案,那就是 u u u 强制转化为大写, v v v 强制保持为小写。因此建有向边 < u 0 , u 1 > <u_0,u_1> <u0,u1> 以及 < v 1 , v 0 > <v_1,v_0> <v1,v0>
  3. u < v u < v u<v,此时若 u u u 保持小写,则 v v v 也必须保持小写,因此建立有向边 < u 0 , v 0 > <u_0,v_0> <u0,v0> 以及其逆否命题 < v 1 , u 1 > <v_1,u_1> <v1,u1>;若 u u u 转化为大写,则 v v v 的状态随意,即没有任何限制,无需建边。

之后一遍 2-SAT 即可解决问题。

注意要特判前面字符串是后面字符串前缀,且前面字符串的长度更小的情况,此时无解。

时间复杂度: O ( n m + 2 m ) O(nm+2m) O(nm+2m)

5.3 错误点

  1. 没有特判前面字符串是后面字符串前缀,且前面字符串的长度更小的情况。

5.4 代码

int head[maxn], n, m, cnt, tot, top, num;
int dfn[maxn], low[maxn], st[maxn], col[maxn];
bool instack[maxn];
struct edge
{
    int v, nxt;
} Edge[8 * maxn];
void init()
{
    for(int i = 0; i <= 2 * m; i++) head[i] = -1;
    cnt = tot = top = num = 0;
}
void addedge(int u, int v)
{
    Edge[cnt].v = v;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}
void tarjan(int id)
{
    dfn[id] = low[id] = ++tot;
    st[++top] = id;
    instack[id] = true;
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        if(!dfn[v])
        {
            tarjan(v);
            low[id] = min(low[id], low[v]);
        }
        else if(instack[v])
            low[id] = min(low[id], dfn[v]);
    }
    if(dfn[id] == low[id])
    {
        int v;
        num++;
        do
        {
            v = st[top--];
            instack[v] = false;
            col[v] = num;
        } while(v != id);
    }
}
vector<int> words[maxn];
int ans[maxn];
vector<int> ve;
int main()
{
    scanf("%d%d", &n, &m);
    init();
    for (int i = 1; i <= n; i++) {
        int k;
        scanf("%d", &k);
        for (int j = 1; j <= k; j++) {
            int num;
            scanf("%d", &num);
            words[i].emplace_back(num);
        }
    }
    for (int i = 1; i < n; i++) {
        int len = min(words[i].size(), words[i + 1].size());
        for (int j = 0; j < len; j++) {
            int u = words[i][j];
            int v = words[i + 1][j];
            if (u > v) {
                addedge(u, u + m);
                addedge(v + m, v);
                break;
            } else if (u < v) {
                addedge(u, v);
                addedge(v + m, u + m);
                break;
            }
            if (j == len - 1 && j < (int)words[i].size() - 1) {
                return 0 * puts("No");
            }
        }
    }
    for (int i = 1; i <= 2 * m; i++) {
        if (!dfn[i]) tarjan(i);
    }
    for (int i = 1; i <= m; i++) {
        if (col[i] == col[i + m]) return 0 * puts("No");
    }
    for (int i = 1; i <= m; i++) {
        ans[i] = col[i] > col[i + m];
        if (ans[i]) ve.emplace_back(i);
    }
    puts("Yes");
    printf("%d\n", (int)ve.size());
    for (auto x: ve) {
        printf("%d ", x);
    }
    return 0;
}

6. Codeforces - 1218I - The Light Square

题目链接:https://codeforces.com/problemset/problem/1218/I

6.1 题意

给你一个 n × n ( 1 ≤ n ≤ 2000 ) n \times n(1 \le n \le 2000) n×n(1n2000) 0 − 1 0-1 01 矩阵 A A A,然后给你一个长度为 n n n 的串 s s s

每次你可将某行(从左往右看)或某列(从上往下看)异或上 s s s

问是否存在一种方案,可以使 A A A 转化为 B B B,或者说明方案不存在。

6.2 解题过程

首先,我们需要清楚, 0 0 0 异或上一个数会使这个数保持原样, 1 1 1 异或上一个数会使这个数字取反。

之后,设 l i n e [ i ] [ 0 ] line[i][0] line[i][0] 表示第 i i i 行不进行任何操作, l i n e [ i ] [ 1 ] line[i][1] line[i][1] 表示将第 i i i 行异或上 s s s c o l [ j ] [ 0 ] col[j][0] col[j][0] 表示第 j j j 列不进行任何操作, c o l [ j ] [ 1 ] col[j][1] col[j][1] 表示将第 j j j 列异或上 s s s

遍历矩阵 A A A,设当前遍历到的元素为 A [ i ] [ j ] A[i][j] A[i][j],则分两种情况讨论:

(一)若 A [ i ] [ j ] ≠ B [ i ] [ j ] A[i][j] \ne B[i][j] A[i][j]=B[i][j]

  1. 如果 s [ i ] = 0 s[i]=0 s[i]=0 s [ j ] = 0 s[j]=0 s[j]=0,表明 A [ i ] [ j ] A[i][j] A[i][j] 的值已经无法改变,此时无解。
  2. 如果 s [ i ] = 1 s[i]=1 s[i]=1 s [ j ] = 1 s[j]=1 s[j]=1,表明我们必须对第 i i i 行和第 j j j 列中的恰好一个进行操作,因此建边 < l i n e [ i ] [ 0 ] , c o l [ j ] [ 1 ] > <line[i][0], col[j][1]> <line[i][0],col[j][1]> 及其逆否命题 < c o l [ j ] [ 0 ] , l i n e [ i ] [ 1 ] > <col[j][0], line[i][1]> <col[j][0],line[i][1]>,以及 < l i n e [ i ] [ 1 ] , c o l [ j ] [ 0 ] > <line[i][1], col[j][0]> <line[i][1],col[j][0]> 及其逆否命题 < c o l [ j ] [ 1 ] , l i n e [ i ] [ 0 ] > <col[j][1], line[i][0]> <col[j][1],line[i][0]>
  3. 如果 s [ i ] = 1 s[i]=1 s[i]=1 s [ j ] = 0 s[j]=0 s[j]=0,表明第 i i i 行必须操作,因此建边 < l i n e [ i ] [ 0 ] , l i n e [ i ] [ 1 ] > <line[i][0], line[i][1]> <line[i][0],line[i][1]>
  4. 如果 s [ i ] = 0 s[i]=0 s[i]=0 s [ j ] = 1 s[j]=1 s[j]=1,表明第 j j j 列必须操作,因此建边 < c o l [ j ] [ 0 ] , c o l [ j ] [ 1 ] > <col[j][0], col[j][1]> <col[j][0],col[j][1]>

(二)若 A [ i ] [ j ] = B [ i ] [ j ] A[i][j] = B[i][j] A[i][j]=B[i][j]

  1. 如果 s [ i ] = 0 s[i]=0 s[i]=0 s [ j ] = 0 s[j]=0 s[j]=0,表明第 i i i 行或第 j j j 列的异或与否,没有任何影响,因此无需进行任何操作。
  2. 如果 s [ i ] = 1 s[i]=1 s[i]=1 s [ j ] = 1 s[j]=1 s[j]=1,表明我们对第 i i i 行和第 j j j 列要么同时进行操作,要么都不进行操作,因此建边 < l i n e [ i ] [ 0 ] , c o l [ j ] [ 0 ] > <line[i][0], col[j][0]> <line[i][0],col[j][0]> 及其逆否命题 < c o l [ j ] [ 1 ] , l i n e [ i ] [ 1 ] > <col[j][1], line[i][1]> <col[j][1],line[i][1]>,以及 < l i n e [ i ] [ 1 ] , c o l [ j ] [ 1 ] > <line[i][1], col[j][1]> <line[i][1],col[j][1]> 及其逆否命题 < c o l [ j ] [ 0 ] , l i n e [ i ] [ 0 ] > <col[j][0], line[i][0]> <col[j][0],line[i][0]>
  3. 如果 s [ i ] = 1 s[i]=1 s[i]=1 s [ j ] = 0 s[j]=0 s[j]=0,表明第 i i i 行不可以操作,因此建边 < l i n e [ i ] [ 1 ] , l i n e [ i ] [ 0 ] > <line[i][1], line[i][0]> <line[i][1],line[i][0]>
  4. 如果 s [ i ] = 0 s[i]=0 s[i]=0 s [ j ] = 1 s[j]=1 s[j]=1,表明第 j j j 列不可以操作,因此建边 < c o l [ j ] [ 1 ] , c o l [ j ] [ 0 ] > <col[j][1], col[j][0]> <col[j][1],col[j][0]>

建边之后,跑一遍 2-SAT,之后输出方案即可。

时间复杂度: O ( n 2 + 4 n ) O(n^2 + 4n) O(n2+4n)

6.3 代码

int head[maxn], n, m, cnt, tot, top, num;
int dfn[maxn], low[maxn], st[maxn], col[maxn];
bool instack[maxn];
struct edge {
    int v, nxt;
} Edge[2 * maxn];
void init() {
    for (int i = 0; i <= 4 * n; i++) {
        head[i] = -1;
    }
    cnt = tot = top = num = 0;
}
void addedge(int u, int v) {
    Edge[cnt].v = v;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}
void tarjan(int id)
{
    dfn[id] = low[id] = ++tot;
    st[++top] = id;
    instack[id] = true;
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        if(!dfn[v])
        {
            tarjan(v);
            low[id] = min(low[id], low[v]);
        }
        else if(instack[v])
            low[id] = min(low[id], dfn[v]);
    }
    if(dfn[id] == low[id])
    {
        int v;
        num++;
        do
        {
            v = st[top--];
            instack[v] = false;
            col[v] = num;
        } while(v != id);
    }
}
char S[2005][2005], T[2005][2005];
char bar[2005];
int ans[4005];
vector<pii> ve;
int main()
{
    scanf("%d", &n);
    init();
    for (int i = 1; i <= n; i++) {
        scanf("%s", S[i] + 1);
        for (int j = 1; j <= n; j++) {
            S[i][j] = (S[i][j] == '1');
        }
    }
    for (int i = 1; i <= n; i++) {
        scanf("%s", T[i] + 1);
        for (int j = 1; j <= n; j++) {
            T[i][j] = (T[i][j] == '1');
        }
    }
    scanf("%s", bar + 1);
    for (int i = 1; i <= n; i++) {
        bar[i] = (bar[i] == '1');
    }
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            if (S[i][j] != T[i][j]) {
                if (bar[i] == 0 && bar[j] == 0) {
                    return 0 * puts("-1");
                }
                if (bar[i] && bar[j]) {
                    addedge(i, j + 2 * n + n);
                    addedge(j + 2 * n, i + n);
                    addedge(i + n, j + 2 * n);
                    addedge(j + 2 * n + n, i);
                } else if (bar[j]) {
                    addedge(i, i + n);
                } else if (bar[i]) {
                    addedge(j + 2 * n, j + 2 * n + n);
                }
            } else {
                if (bar[i] && bar[j]) {
                    addedge(i, j + 2 * n);
                    addedge(j + 2 * n + n, i + n);
                    addedge(i + n, j + 2 * n + n);
                    addedge(j + 2 * n, i);
                } else if (bar[j]) {
                    addedge(i + n, i);
                } else if (bar[i]) {
                    addedge(j + 2 * n + n, j + 2 * n);
                }
            }
        }
    }
    for (int i = 1; i <= 4 * n; i++) {
        if (!dfn[i]) {
            tarjan(i);
        }
    }
    for (int i = 1; i <= n; i++) {
        if (col[i] == col[i + n]) return 0 * puts("-1");
        ans[i] = (col[i] > col[i + n]);
    }
    for (int i = 1 + 2 * n; i <= 4 * n; i++) {
        if (col[i] == col[i + n]) return 0 * puts("-1");
        ans[i] = (col[i] > col[i + n]);
    }
    for (int i = 1; i <= n; i++) {
        if (ans[i]) ve.pb({0, i - 1});
    }
    for (int i = 1 + 2 * n; i <= 3 * n; i++) {
        if (ans[i]) ve.pb({1, i - 2 * n - 1});
    }
    printf("%d\n", (int)ve.size());
    for (auto element: ve) {
        if (!element.first) printf("row %d\n", element.second);
        else printf("col %d\n", element.second);
    }
    return 0;
}

7. Codeforces - 27D - Ring Road 2

题目链接:https://codeforces.com/problemset/problem/27/D

7.1 题意

现在有一个 n ( 4 ≤ n ≤ 100 ) n(4 \le n \le 100) n(4n100) 元环和 m ( 1 ≤ m ≤ 100 ) m(1 \le m \le 100) m(1m100) 条边,第 i i i 条边连接 a i a_i ai b i b_i bi 两个点。

每条边可以被安排到在环内或者环外。

现在需要你输出一种安排边的方案,使得所有的边不存在在非端点处相交的情况,或者说明答案不存在。

7.2 解题过程

如果把每条边都看作一段线段的话,会发现边在非端点处相交等价于线段在非端点处相交。

x 0 x_0 x0 表示 x x x 在环内, x 1 x_1 x1 表示 x x x 在环外。

对于两条边 i i i j j j

  1. i i i j j j 不在非端点处相交,则可以任意安排,无任何限制。
  2. i i i j j j 在非端点处相交,表明 i i i j j j 不可以同时在环内或者环外,因此建边 < i 0 , j 1 > <i_0, j_1> <i0,j1> 及其逆否命题 < j 0 , i 1 > <j_0, i_1> <j0,i1>,以及 < i 1 , j 0 > <i_1, j_0> <i1,j0> 及其逆否命题 < j 1 , i 0 > <j_1, i_0> <j1,i0>

之后一遍 2-SAT 之后输出方案即可。

时间复杂度: O ( m 2 + 2 ⋅ m ) O(m^2 + 2 \cdot m) O(m2+2m)

7.3 代码

int head[maxn], n, m, cnt, tot, top, num;
int dfn[maxn], low[maxn], st[maxn], col[maxn];
bool instack[maxn];
struct edge {
    int v, nxt;
} Edge[2 * maxn];
void init() {
    for (int i = 0; i <= 2 * m; i++) {
        head[i] = -1;
    }
    cnt = tot = top = num = 0;
}
void addedge(int u, int v) {
    Edge[cnt].v = v;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}
void tarjan(int id)
{
    dfn[id] = low[id] = ++tot;
    st[++top] = id;
    instack[id] = true;
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        if(!dfn[v])
        {
            tarjan(v);
            low[id] = min(low[id], low[v]);
        }
        else if(instack[v])
            low[id] = min(low[id], dfn[v]);
    }
    if(dfn[id] == low[id])
    {
        int v;
        num++;
        do
        {
            v = st[top--];
            instack[v] = false;
            col[v] = num;
        } while(v != id);
    }
}
bool check(pii a, pii b) {
    if (a > b) swap(a, b);
    if (b.first > a.first && b.first < a.second && b.second > a.second) return false;
    return true;
}
pii point[maxn];
int ans[maxn];
int main()
{
    int u, v;
    scanf("%d%d", &n, &m);
    init();
    for (int i = 1; i <= m; i++) {
        scanf("%d%d", &u, &v);
        point[i] = {min(u, v), max(u, v)};
    }
    for (int i = 1; i <= m; i++) {
        for (int j = i + 1; j <= m; j++) {
            if (!check(point[i], point[j])) {
                addedge(i, j + m);
                addedge(j, i + m);
                addedge(i + m, j);
                addedge(j + m, i);
            }
        }
    }
    for (int i = 1; i <= 2 * m; i++) {
        if (!dfn[i]) {
            tarjan(i);
        }
    }
    for (int i = 1; i <= m; i++) {
        if (col[i] == col[i + m]) return 0 * puts("Impossible");
        ans[i] = (col[i] > col[i + m]);
    }
    for (int i = 1; i <= m; i++) {
        if (ans[i]) printf("o");
        else printf("i");
    }
    return 0;
}

8. Gym - 100112K - Kindergarten

题目链接:https://codeforces.com/gym/100112

题目来源:2012 Nordic Collegiate Programming Contest (NCPC)

8.1 题意

有三个班级(编号为 { 0 , 1 , 2 } \left\{ 0,1,2\right\} {0,1,2})和 n ( n ≤ 200 ) n(n \le 200) n(n200) 名学生,每个学生最初都有一个班级。

现在每个学生都要转入一个新的班级,且每个学生都有一个对其他 n − 1 n-1 n1 个学生的排序表。

现在要找到最小的 T T T,使得满足每个学生新班级中的学生都来自其排序表中的前 T T T 项。

8.2 解题过程

首先,我们可以发现,若一个 T 0 T_0 T0 可行,则所有的 T > T 0 T > T_0 T>T0 均可行,因为条件被放宽了。

因此我们可以二分这个 T T T,之后只需进行可行性判断。

每个学生新班级中的学生都来自其排序表中的前 T T T 项,等价于排序表中的后面项都不可以出现在学生的新班级中。

考虑 2-SAT,设 x 0 x_0 x0 表示学生 x x x 转入了第一个可以转入的班级, x 1 x_1 x1 表示学生 x x x 转入了第二个可以转入的班级。( 0 0 0 班可转入 { 1 , 2 } \left\{ 1,2\right\} {1,2} 班, 1 1 1 班可转入 { 0 , 2 } \left\{ 0,2\right\} {0,2} 班, 2 2 2 班可转入 { 0 , 1 } \left\{ 0,1\right\} {0,1} 班)。

c l a s s [ x ] class[x] class[x] 表示学生 x x x 所属班级, s t a t e [ y ] [ 0 ] state[y][0] state[y][0] 表示原班级为 x x x 的学生可转入的第一个班级, s t a t e [ y ] [ 1 ] state[y][1] state[y][1] 表示原班级为 x x x 的学生可转入的第二个班级。

之后对于每个学生 x x x,枚举其在前 T T T 个意向学生之外的学生 y y y,以及 x x x 转入的班级顺序号 k ( k ∈ [ 0 , 1 ] ) k(k \in [0,1]) k(k[0,1]),然后进行分类讨论:

  1. s t a t e [ c l a s s [ x ] ] [ k ] ≠ s t a t e [ c l a s s [ y ] ] [ 0 ] state[class[x]][k] \ne state[class[y]][0] state[class[x]][k]=state[class[y]][0] 并且 s t a t e [ c l a s s [ x ] ] [ k ] ≠ s t a t e [ c l a s s [ y ] ] [ 1 ] state[class[x]][k] \ne state[class[y]][1] state[class[x]][k]=state[class[y]][1],表明若 x x x 转入第 k k k 个可以转入的班级, y y y 一定和 x x x 不同班,因此无需进行任何操作;

  2. s t a t e [ c l a s s [ x ] ] [ k ] ≠ s t a t e [ c l a s s [ y ] ] [ 0 ] state[class[x]][k] \ne state[class[y]][0] state[class[x]][k]=state[class[y]][0] 并且 s t a t e [ c l a s s [ x ] ] [ k ] = s t a t e [ c l a s s [ y ] ] [ 1 ] state[class[x]][k] = state[class[y]][1] state[class[x]][k]=state[class[y]][1],表明若 x x x 转入第 k k k 个可以转入的班级, y y y 若想和 x x x 不同班,就必须转入他的第一个可转入的班级。因此建边 < x k , y 0 > <x_k, y_0> <xk,y0> 及其逆否命题 < y 1 , x 1 − k > <y_1, x_{1-k}> <y1,x1k>

  3. s t a t e [ c l a s s [ x ] ] [ k ] = s t a t e [ c l a s s [ y ] ] [ 0 ] state[class[x]][k] = state[class[y]][0] state[class[x]][k]=state[class[y]][0] 并且 s t a t e [ c l a s s [ x ] ] [ k ] ≠ s t a t e [ c l a s s [ y ] ] [ 1 ] state[class[x]][k] \ne state[class[y]][1] state[class[x]][k]=state[class[y]][1],表明若 x x x 转入第 k k k 个可以转入的班级, y y y 若想和 x x x 不同班,就必须转入他的第二个可转入的班级。因此建边 < x k , y 1 > <x_k, y_1> <xk,y1> 及其逆否命题 < y 0 , x 1 − k > <y_0, x_{1-k}> <y0,x1k>

之后通过 2-SAT 判断合法性即可。

时间复杂度: O ( n 2 log ⁡ n ) O(n^2 \log n) O(n2logn)

8.3 代码

int head[maxn], n, m, cnt, tot, top, num;
int dfn[maxn], low[maxn], st[maxn], col[maxn];
bool instack[maxn];
struct edge
{
    int v, nxt;
} Edge[8 * maxn];
void init()
{
    for(int i = 0; i <= 2 * n; i++) {
        head[i] = -1;
        dfn[i] = 0;
        low[i] = 0;
        st[i] = 0;
        col[i] = 0;
        instack[i] = false;
    }
    cnt = tot = top = num = 0;
}
void addedge(int u, int v)
{
    Edge[cnt].v = v;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}
void tarjan(int id)
{
    dfn[id] = low[id] = ++tot;
    st[++top] = id;
    instack[id] = true;
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        if(!dfn[v])
        {
            tarjan(v);
            low[id] = min(low[id], low[v]);
        }
        else if(instack[v])
            low[id] = min(low[id], dfn[v]);
    }
    if(dfn[id] == low[id])
    {
        int v;
        num++;
        do
        {
            v = st[top--];
            instack[v] = false;
            col[v] = num;
        } while(v != id);
    }
}
int teacher[maxn], matrix[205][205];
int state[3][2] = {1, 2, 0, 2, 0, 1};
int id(int x, int type) {
    if (type == 1) return x + n;
    return x;
}
bool check(int x) {
    init();
    for (int i = 1; i <= n; i++) {
        for (int j = n - 1; j > x; j--) {
            for (int k = 0; k < 2; k++) {
                int v = matrix[i][j];
                if (state[teacher[i]][k] != state[teacher[v]][0] && state[teacher[i]][k] != state[teacher[v]][1]) {
                    continue;
                } else if (state[teacher[i]][k] != state[teacher[v]][0]) {
                    addedge(id(i, k), v);
                    //addedge(v, id(i, k));
                    //addedge(id(i, 1 - k), v + n);
                    addedge(v + n, id(i, 1 - k));
                } else if (state[teacher[i]][k] != state[teacher[v]][1]) {
                    addedge(id(i, k), v + n);
                    //addedge(v + n, id(i, k));
                    //addedge(id(i, 1 - k), v);
                    addedge(v, id(i, 1 - k));
                }
            }
        }
    }
    for (int i = 1; i <= 2 * n; i++) {
        if (!dfn[i]) tarjan(i);
    }
    for (int i = 1; i <= n; i++) {
        if (col[i] == col[i + n]) return false;
    }
    return true;
}
int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) {
        scanf("%d", &teacher[i]);
        for (int j = 1; j < n; j++) {
            scanf("%d", &matrix[i][j]);
        }
    }
    int ans = 0x3f3f3f3f;
    int left = 0, right = n - 1;
    while (left <= right) {
        int mid = (left + right) / 2;
        if (check(mid)) {
            ans = min(ans, mid);
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    printf("%d\n", ans);
    return 0;
}

9. Gym - 101987K - TV Show Game

题目链接:https://codeforces.com/gym/101987

题目来源:2018-2019 ACM-ICPC, Asia Seoul Regional Contest

9.1 题意

k ( 3 < k ≤ 5000 ) k(3 < k \le 5000) k(3<k5000) 盏灯,每个灯可以亮红色和蓝色其中之一的颜色,最初不知道每个灯的颜色。

现在有 n ( 1 ≤ n ≤ 10000 ) n(1 \le n \le 10000) n(1n10000) 名竞猜者,每个人可以随意挑选三盏灯并猜测它们的颜色。

猜中至少两个灯的竞猜者获胜。

现在需要设计一种灯泡颜色的方案,使得每个人都可以获胜,或者说明无解。

9.2 解题过程

考虑反面情况,如果一个选手猜测的一个灯猜错了,则他猜测的其他两盏灯必须是正确的。

基于此,可以通过 2-SAT 来进行求解。

时间复杂度: O ( n + k ) O(n + k) O(n+k)

9.3 代码

int head[maxn], n, m, cnt, tot, top, num;
int dfn[maxn], low[maxn], st[maxn], col[maxn];
bool instack[maxn];
struct edge
{
    int v, nxt;
} Edge[8 * maxn];
void init()
{
    for(int i = 0; i <= 2 * n; i++) head[i] = -1;
    cnt = tot = top = num = 0;
}
void addedge(int u, int v)
{
    Edge[cnt].v = v;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}
void tarjan(int id)
{
    dfn[id] = low[id] = ++tot;
    st[++top] = id;
    instack[id] = true;
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        if(!dfn[v])
        {
            tarjan(v);
            low[id] = min(low[id], low[v]);
        }
        else if(instack[v])
            low[id] = min(low[id], dfn[v]);
    }
    if(dfn[id] == low[id])
    {
        int v;
        num++;
        do
        {
            v = st[top--];
            instack[v] = false;
            col[v] = num;
        } while(v != id);
    }
}
struct node {
    int a, b;
} data[5];
int state[3][2] = {2, 3, 1, 3, 1, 2};
int id(int x, int y) {
    if (y) return x + n;
    else return x;
}
int ans[maxn];
int main()
{
    scanf("%d%d", &n, &m);
    init();
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= 3; j++) {
            char str[2];
            scanf("%d", &data[j].a);
            scanf("%s", str);
            data[j].b = (str[0] == 'B');
        }
        for (int j = 1; j <= 3; j++) {
            int x = state[j - 1][0];
            int y = state[j - 1][1];
            addedge(id(data[j].a, 1 - data[j].b), id(data[x].a, data[x].b));
            addedge(id(data[j].a, 1 - data[j].b), id(data[y].a, data[y].b));
        }
    }
    for (int i = 1; i <= 2 * n; i++) {
        if (!dfn[i]) tarjan(i);
    }
    for (int i = 1; i <= n; i++) {
        if (col[i] == col[i + n]) return 0 * puts("-1");
        ans[i] = col[i] > col[i + n];
    }
    for (int i = 1; i <= n; i++) {
        if (ans[i]) printf("B");
        else printf("R");
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值