NeFut Logo NeFut
Admin Login

In-Depth Analysis of 2-SAT Problem: Efficient Solutions Based on Topological Sorting and Strongly Connected Components

Published at: 2026-05-27 09:17 Last updated: 2026-06-11 09:49
#C++ #Tutorial

Core Logic and Mathematical Principles

Topological sorting plays a crucial role in the algebraic construction for solving the construction problem of 2-SAT (2-Satisfiability).

The goal of the 2-SAT problem is: given $N$ Boolean variables $x_1, x_2, \dots, x_n$, and $M$ constraints, each of which is in the form "either $x_i$ is true or $x_j$ is true" ($x_i \lor x_j$). We need to assign truth values (0 or 1) to each variable such that all conditions are satisfied simultaneously.

1. Algebraic Isomorphism from Propositional Logic to Directed Graphs (Implication Graphs)

According to the equivalent transformations of logical algebra, a disjunctive condition $A \lor B$ is mathematically equivalent to two implications:

$$\neg A \implies B \quad \text{and} \quad \neg B \implies A$$

2. Legitimacy Criterion for Strongly Connected Components (SCC)

Utilizing Tarjan's algorithm or Kosaraju's algorithm to compress the strongly connected components of the implication graph:

3. Greedy Consistent Assignment Driven by Reverse Topological Order

Once a solution is determined, how do we construct a specific valid solution?


Algorithm Derivation and State Design

1. Implication Graph State Design

For variable $i \in [1, n]$:

Common constraint transformations:

  1. $x_i \lor x_j$ (at least one is true): $\neg x_i \implies x_j$ and $\neg x_j \implies x_i$. Edges: add_edge(i + n, j), add_edge(j + n, i).
  2. $x_i \implies x_j$ (if $i$ is true, then $j$ must be true): Edges: add_edge(i, j), add_edge(j + n, i + n).
  3. $x_i$ must be true: equivalent to $x_i \lor x_i$. Edge: add_edge(i + n, i).

C++ Standard Source Code (High-Precision Template for 2-SAT Construction)

#include <iostream>
#include <vector>
#include <algorithm>
#include <stack>

const int MAXN = 200005; // 2 * N space base

std::vector<int> adj[MAXN * 2];
int dfn[MAXN * 2], low[MAXN * 2], scc[MAXN * 2];
bool in_stack[MAXN * 2];
std::stack<int> stk;
int timer = 0, scc_cnt = 0;

void tarjan(int u) {
    dfn[u] = low[u] = ++timer;
    stk.push(u);
    in_stack[u] = true;

    for (size_t i = 0; i < adj[u].size(); ++i) {
        int v = adj[u][i];
        if (!dfn[v]) {
            tarjan(v);
            low[u] = std::min(low[u], low[v]);
        } else if (in_stack[v]) {
            low[u] = std::min(low[u], dfn[v]);
        }
    }

    if (low[u] == dfn[u]) {
        scc_cnt++;
        while (true) {
            int v = stk.top();
            stk.pop();
            in_stack[v] = false;
            scc[v] = scc_cnt; // Tarjan marks the scc number naturally forms reverse topological order
            if (u == v) break;
        }
    }
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    int n, m;
    if (!(std::cin >> n >> m)) return 0;

    for (int k = 0; k < m; ++k) {
        int i, a, j, b;
        // Input format: variable i takes value a(0/1) or variable j takes value b(0/1)
        std::cin >> i >> a >> j >> b;

        // State mapping: a == 0 represents false(i+n), a == 1 represents true(i)
        int u_true = i, u_false = i + n;
        int v_true = j, v_false = j + n;

        // Determine the actual state based on 0/1 input
        int pos_u = (a == 1) ? u_true : u_false;
        int neg_u = (a == 1) ? u_false : u_true;
        int pos_v = (b == 1) ? v_true : v_false;
        int neg_v = (b == 1) ? v_false : v_true;

        // Core condition transformation: pos_u V pos_v  <=>  (neg_u -> pos_v) AND (neg_v -> pos_u)
        adj[neg_u].push_back(pos_v);
        adj[neg_v].push_back(pos_u);
    }

    // 1. Run Tarjan to partition the strongly connected topological space
    for (int i = 1; i <= 2 * n; ++i) {
        if (!dfn[i]) {
            tarjan(i);
        }
    }

    // 2. Completeness compliance check
    for (int i = 1; i <= n; ++i) {
        if (scc[i] == scc[i + n]) {
            std::cout << "IMPOSSIBLE\n"; // Positive and negative states belong to the same SCC, logic collapse
            return 0;
        }
    }

    std::cout << "POSSIBLE\n";
    // 3. Use the reverse topological property of the strongly connected component numbers to construct a greedy consistent solution
    for (int i = 1; i <= n; ++i) {
        // The smaller the scc number, the closer it is to the topological order.
        // If scc[i] < scc[i+n], it means the positive state i is behind, so choose the positive state (assign as 1)
        if (scc[i] < scc[i + n]) {
            std::cout << 1 << (i == n ? "" : " ");
        } else {
            std::cout << 0 << (i == n ? "" : " ");
        }
    }
    std::cout << "\n";

    return 0;
}

NOIP 实战避坑指南

  1. 强连通编号与拓扑序方向记反: 这是2-SAT输出方案时最高频的爆零点。Tarjan算法生成的 scc 缩点编号,在本质上是“逆拓扑序”(即最先被切断弹出的叶子子树其 scc 编号为1)。因此,scc[i] < scc[i+n] 意味着节点 $i$ 在有向图拓扑网络中处于更加靠后的位置。为了避免逻辑污染,我们要选靠后的点,所以此时应该选 $i$,即方案输出 1。如果选手误记为“编号小的拓扑靠前”,将输出逻辑反置,就会导致满盘皆输。
  2. 数组空间未开 2 倍(或 4 倍): 2-SAT伴随着变量拆点,点的总数直接翻倍变为 $2N$。而由于每个析取约束会对应两条蕴含边,边的总数也会翻倍变为 $2M$。选手的 adj 邻接表基座、Tarjan相关的 dfnlowscc 数组必须雷打不动地开辟2倍(点数)和4倍(边数)空间,否则在建边和递归松弛时会引发严重的越界。

经典 NOIP/洛谷 真题

1. 洛谷 P4782 【模板】2-SAT 问题

2. 洛谷 P3201 [HNOI2009] 梦幻布丁


[h] Back to Home