| Aspect | Details |
|---|---|
| Problem Type | 2-SAT (2-Satisfiability) |
| Core Technique | Implication Graph + SCC |
| Time Complexity | O(n + m) |
| Space Complexity | O(n + m) |
| Key Data Structure | Directed Graph |
| Difficulty | Hard |
By completing this problem, you will learn:
Uolevi wants to order a giant pizza with n toppings. There are m family members, each with exactly two wishes about the toppings. Each wish specifies whether a particular topping should be included (+) or excluded (-).
Goal: Determine if there exists a valid topping selection where every family member has at least one of their two wishes satisfied.
Input:
n m (n = number of toppings, m = number of family members)+ 1 - 3 means “include topping 1 OR exclude topping 3”Output:
Constraints:
Example:
Input:
3 4
+ 1 + 2
- 1 + 2
+ 1 - 2
- 1 - 2
Output:
- + -
2-SAT is a special case of the Boolean Satisfiability problem where each clause contains exactly 2 literals. A literal is either a variable (x) or its negation (NOT x).
General Form: (a OR b) AND (c OR d) AND …
The key insight: Every clause (a OR b) can be rewritten as two implications:
(a OR b) is equivalent to:
- (NOT a) -> b (if a is false, b must be true)
- (NOT b) -> a (if b is false, a must be true)
Why this works:
a is false and the clause must be true, then b must be trueb is false and the clause must be true, then a must be trueFor wish “+ 1 - 2” (topping 1 included OR topping 2 excluded):
Create 2n nodes for n variables:
2i: Variable xi is TRUE2i + 1: Variable xi is FALSEAlternative indexing (used in code):
i (0-indexed: i-1)n + i (or use XOR trick: idx ^ 1)For each clause (a OR b):
Implication Graph Construction:
Variables: x1, x2, x3 (3 toppings)
Clause: (x1 OR NOT x2)
Nodes: x1 NOT_x1 x2 NOT_x2 x3 NOT_x3
[0] [1] [2] [3] [4] [5]
Implications from (x1 OR NOT x2):
NOT x1 -> NOT x2: edge from [1] to [3]
x2 -> x1: edge from [2] to [0]
Theorem: A 2-SAT formula is satisfiable if and only if no variable x and its negation NOT x belong to the same Strongly Connected Component.
Why?
Satisfiability Check:
SCC Analysis:
+-------------------+ +-------------------+
| SCC 1 | | SCC 2 |
| x1, x3, NOT_x2 | | NOT_x1, NOT_x3, x2|
+-------------------+ +-------------------+
Check each variable:
x1 in SCC1, NOT_x1 in SCC2 -> Different SCCs (OK)
x2 in SCC2, NOT_x2 in SCC1 -> Different SCCs (OK)
x3 in SCC1, NOT_x3 in SCC2 -> Different SCCs (OK)
Result: SATISFIABLE
After finding SCCs using Kosaraju’s or Tarjan’s algorithm, SCCs are naturally in reverse topological order.
Assignment Rule: For each variable x:
Assignment Strategy:
SCCs in reverse topological order (Kosaraju output):
SCC[0]: {NOT_x1, x2, NOT_x3} <- processed first
SCC[1]: {x1, NOT_x2, x3} <- processed second
SCC indices:
scc[x1] = 1, scc[NOT_x1] = 0
scc[x2] = 0, scc[NOT_x2] = 1
scc[x3] = 1, scc[NOT_x3] = 0
Assignment (if scc[x] > scc[NOT_x], then x = TRUE):
x1: scc[x1]=1 > scc[NOT_x1]=0 -> x1 = TRUE (+)
x2: scc[x2]=0 < scc[NOT_x2]=1 -> x2 = FALSE (-)
x3: scc[x3]=1 > scc[NOT_x3]=0 -> x3 = TRUE (+)
Result: + - +
Example: n=3 toppings, m=4 wishes
Wishes:
1. + 1 + 2 -> (x1 OR x2)
2. - 1 + 2 -> (NOT_x1 OR x2)
3. + 1 - 2 -> (x1 OR NOT_x2)
4. - 1 - 2 -> (NOT_x1 OR NOT_x2)
Implication Graph:
+-------+ +-------+
| x1 |<-------------------|NOT_x2 |
+-------+ +-------+
| ^ ^ |
| | | |
v | | v
+-------+ +-------+
|NOT_x1 |------------------->| x2 |
+-------+ +-------+
\ /
\ /
v v
+------------------------+
| (other edges) |
+------------------------+
From clause (x1 OR x2):
NOT_x1 -> x2 (edge 1)
NOT_x2 -> x1 (edge 2)
From clause (NOT_x1 OR x2):
x1 -> x2 (edge 3)
NOT_x2 -> NOT_x1 (edge 4)
From clause (x1 OR NOT_x2):
NOT_x1 -> NOT_x2 (edge 5)
x2 -> x1 (edge 6)
From clause (NOT_x1 OR NOT_x2):
x1 -> NOT_x2 (edge 7)
x2 -> NOT_x1 (edge 8)
Input:
3 4
+ 1 + 2
- 1 + 2
+ 1 - 2
- 1 - 2
Step 1: Parse wishes into clauses
Wish 1: + 1 + 2 -> (x1 OR x2)
Wish 2: - 1 + 2 -> (NOT_x1 OR x2)
Wish 3: + 1 - 2 -> (x1 OR NOT_x2)
Wish 4: - 1 - 2 -> (NOT_x1 OR NOT_x2)
Step 2: Build implication graph (edges)
Graph with 6 nodes: x1(0), NOT_x1(1), x2(2), NOT_x2(3), x3(4), NOT_x3(5)
From (x1 OR x2): 1->2, 3->0
From (NOT_x1 OR x2): 0->2, 3->1
From (x1 OR NOT_x2): 1->3, 2->0
From (NOT_x1 OR NOT_x2): 0->3, 2->1
Adjacency list:
0 (x1): [2, 3]
1 (NOT_x1): [2, 3]
2 (x2): [0, 1]
3 (NOT_x2): [0, 1]
Step 3: Find SCCs (Kosaraju’s algorithm)
First DFS (finish order): [0, 2, 1, 3] (example order)
Transpose graph, DFS in reverse finish order:
SCC 0: {3, 1} = {NOT_x2, NOT_x1}
SCC 1: {2, 0} = {x2, x1}
Step 4: Check satisfiability
scc[x1]=1, scc[NOT_x1]=0 -> Different SCCs (OK)
scc[x2]=1, scc[NOT_x2]=0 -> Different SCCs (OK)
Formula is SATISFIABLE!
Step 5: Determine assignment
x1: scc[x1]=1 > scc[NOT_x1]=0 -> x1 = TRUE (+)
x2: scc[x2]=1 > scc[NOT_x2]=0 -> x2 = TRUE (+)
x3: not constrained, default to - (FALSE)
Output: - + -
import sys
from collections import defaultdict
sys.setrecursionlimit(300000)
def solve():
input_data = sys.stdin.read().split()
idx = 0
n, m = int(input_data[idx]), int(input_data[idx + 1])
idx += 2
# Build implication graph
# Node indexing: variable i (1-indexed) -> positive: i-1, negative: n+i-1
graph = defaultdict(list)
reverse_graph = defaultdict(list)
def pos(i):
return i - 1
def neg(i):
return n + i - 1
def get_node(sign, var):
return pos(var) if sign == '+' else neg(var)
def get_negation(node):
if node < n:
return node + n
return node - n
for _ in range(m):
sign1, var1 = input_data[idx], int(input_data[idx + 1])
sign2, var2 = input_data[idx + 2], int(input_data[idx + 3])
idx += 4
# Clause: (lit1 OR lit2)
# Implications: NOT lit1 -> lit2, NOT lit2 -> lit1
a = get_node(sign1, var1)
b = get_node(sign2, var2)
not_a = get_negation(a)
not_b = get_negation(b)
graph[not_a].append(b)
graph[not_b].append(a)
reverse_graph[b].append(not_a)
reverse_graph[a].append(not_b)
# Kosaraju's algorithm for SCC
visited = [False] * (2 * n)
order = []
def dfs1(node):
visited[node] = True
for neighbor in graph[node]:
if not visited[neighbor]:
dfs1(neighbor)
order.append(node)
for i in range(2 * n):
if not visited[i]:
dfs1(i)
visited = [False] * (2 * n)
scc_id = [-1] * (2 * n)
current_scc = 0
def dfs2(node, scc):
visited[node] = True
scc_id[node] = scc
for neighbor in reverse_graph[node]:
if not visited[neighbor]:
dfs2(neighbor, scc)
for node in reversed(order):
if not visited[node]:
dfs2(node, current_scc)
current_scc += 1
# Check satisfiability and build assignment
result = []
for i in range(n):
if scc_id[pos(i + 1)] == scc_id[neg(i + 1)]:
print("IMPOSSIBLE")
return
# If positive literal has larger SCC id, set to TRUE
if scc_id[pos(i + 1)] > scc_id[neg(i + 1)]:
result.append('+')
else:
result.append('-')
print(' '.join(result))
solve()
Wrong: For clause (a OR b), adding edges a -> b and b -> a Correct: Add edges NOT_a -> b and NOT_b -> a
Remember: (a OR b) means "if a is false, b must be true"
So the implication is: NOT_a -> b (not a -> b)
Wrong: Only adding one implication per clause Correct: Each clause (a OR b) produces TWO edges
Clause (a OR b) requires:
1. NOT_a -> b
2. NOT_b -> a <- Don't forget this one!
Wrong: Confusing positive and negative literal indices Correct: Use consistent indexing scheme throughout
Common scheme:
Variable i (1-indexed):
Positive literal: index i
Negative literal: index n + i
Or use XOR trick for 0-indexed:
Positive: 2*i
Negative: 2*i + 1
Negation: idx ^ 1
Wrong: Assigning TRUE when SCC index is smaller Correct: Assign TRUE when positive literal’s SCC index is LARGER
With Kosaraju's algorithm (outputs reverse topological order):
If scc[x] > scc[NOT_x]: set x = TRUE
If scc[x] < scc[NOT_x]: set x = FALSE
Wrong: Using recursive DFS without increasing stack limit Correct: Increase recursion limit or use iterative DFS
# Python: Add at the start
import sys
sys.setrecursionlimit(300000)
Time Complexity: O(n + m)
Space Complexity: O(n + m)