⚙️AlgorithmIntermediate

2-SAT

Key Points

  • 2-SAT solves Boolean formulas where every clause has exactly two literals, and it is solvable in linear time relative to the size of the implication graph.
  • Convert each clause (a ∨ b) into two implications (¬) and (¬) and analyze reachability on this directed graph.
  • The formula is satisfiable if and only if no variable x and its negation ¬x end up in the same strongly connected component (SCC).
  • A satisfying assignment is obtained by processing SCCs in reverse topological order and assigning each variable according to which of x or ¬x appears later.
  • The implication graph has 2n nodes for n variables and at most 2m edges for m clauses, so SCC can be found in O(n + m) time.
  • 2-SAT is ideal for modeling pairwise choices, implications, mutual exclusion, equivalence, and XOR between variables.
  • Common pitfalls include incorrect literal indexing and forgetting to add both implications for every clause.
  • While many scheduling and selection problems reduce to 2-SAT, clauses with three or more literals generally cannot be expressed in pure 2-CNF.

Prerequisites

  • Graph representations (adjacency lists)The implication graph is built and traversed using adjacency lists for efficiency.
  • Depth-First Search (DFS)Both Kosaraju’s and Tarjan’s algorithms rely on DFS to compute SCCs.
  • Strongly Connected Components (SCC)Satisfiability in 2-SAT is characterized by SCCs of the implication graph.
  • Topological ordering / DAGsAssignments are derived from processing SCCs in reverse topological order.
  • Boolean logic (CNF, literals, clauses)Understanding how to translate logic clauses into implications is essential.
  • Implication and equivalence in logic2-SAT modeling frequently uses a⇒b, a⇔b, and XOR relations.
  • Asymptotic analysisTo reason about the linear-time complexity O(n + m) of the algorithm.
  • Bitwise operations (XOR)Common id mapping uses XOR with 1 for fast literal complementation.
  • Stack vs recursion limits in C++Large instances may require iterative DFS or increased stack size.
  • Problem modelingTranslating real-world constraints into 2-CNF clauses is the key skill for using 2-SAT.

Detailed Explanation

Tap terms for definitions

01Overview

Imagine you have many yes/no decisions to make, but the rules relating them are all small and local: each rule says either at least one of two facts must be true, or that if one fact is true then another must be too. This is the world of 2-SAT (2-satisfiability). In 2-SAT, every rule (clause) is simply an OR of two literals, such as (x ∨ ¬y). The beautiful part is that—unlike general SAT—2-SAT can be solved in linear time using graph algorithms. The key trick is to view each literal as a node in a graph and interpret each 2-literal clause as two directed edges (implications). Then a deep statement becomes astonishingly simple to check: the formula is satisfiable if and only if no variable and its negation belong to the same strongly connected component (SCC). Hook: You can solve what looks like a logic puzzle by running two depth-first searches! Concept: Build the implication graph, find SCCs, and derive an assignment. Example: The clause (x ∨ y) is turned into (¬x → y) and (¬y → x). If there is a cycle forcing x to imply ¬x and ¬x to imply x, x is impossible and the whole formula is unsatisfiable.

02Intuition & Analogies

Think of each literal (x and ¬x) as positions on a directed road map. A clause (a ∨ b) says, "if you are not at a, you must be at b" and also "if you are not at b, you must be at a." That’s why we draw two one-way roads: (¬a → b) and (¬b → a). If you start driving from some place and can come back to the same place via directed roads, you are inside a strongly connected component (SCC). Now consider a contradiction loop: suppose you can drive from x to ¬x and also from ¬x back to x. That means x and ¬x are inseparable—choosing x immediately forces ¬x and vice versa—making the formula impossible. Conversely, if every variable and its negation live in different SCCs, then we can choose one side of each pair consistently. How do we choose? Collapse each SCC into a single node to get a DAG. If a literal’s SCC appears after its negation’s SCC in the DAG’s reverse topological order, set that literal to true. Intuitively, later SCCs are more “constrained by” earlier ones; assigning the later literal to true ensures its negation doesn’t force a contradiction earlier in the chain. This is like deciding obligations in the order of consequences: resolve tightly constrained items last so earlier commitments don’t paint you into a corner.

03Formal Definition

A 2-CNF formula over Boolean variables ,, is a conjunction of clauses, each clause being a disjunction of exactly two literals. A literal is either or . Formally, a 2-CNF is F = ( ). The implication graph G has a node for every literal and two edges per clause: for (a b), add ( a b) and ( b a). Let SCC(v) denote the strongly connected component containing literal v. The classical theorem states: F is satisfiable if and only if i, () ( ). Proof sketch: If x and ¬x lie in the same SCC, G contains paths x x and x x, hence x must be simultaneously true and false under any assignment respecting implications—impossible. Conversely, if no variable clashes with its negation, consider the condensation DAG of SCCs. It is acyclic, so take a reverse topological order and assign each variable to true if SCC() appears after SCC( ); otherwise set . This assignment satisfies all implications and therefore all clauses. The entire procedure uses standard SCC algorithms (Kosaraju or Tarjan) in linear time.

04When to Use

Use 2-SAT whenever your constraints can be expressed as implications and pairwise choices. Typical patterns include: (1) Mutual exclusion between two options: "not both" (\neg a \lor \neg b). (2) At least one of two options: (a \lor b). (3) Implication: a \Rightarrow b encoded as (\neg a \lor b). (4) Equivalence: a \Leftrightarrow b via (a \Rightarrow b) \wedge (b \Rightarrow a). (5) XOR between two bits: a \oplus b via (a \lor b) \wedge (\neg a \lor \neg b). Practical cases: course/exam scheduling with two possible times per course; graph orientation choices with pairwise constraints; choosing exactly one of two variants per module; enforcing that selecting an item forces prerequisites; and puzzle constraints that link pairs of booleans. Competitive programming tip: 2-SAT shines when each decision is binary and each rule touches at most two literals. Limitations: clauses involving three or more literals, or “at least one among many,” are not directly expressible as pure 2-CNF. For sets with more than two alternatives you can sometimes refactor the problem into multiple binary decisions (e.g., pick left/right among two options repeatedly), but general k-ary OR clauses require SAT beyond 2-SAT.

⚠️Common Mistakes

  1. Forgetting both implications: For (a \lor b), you must add (\neg a \Rightarrow b) and (\neg b \Rightarrow a). Missing one changes the logic. 2) Wrong literal indexing: A standard scheme is id(x_{i}, true) = 2i and id(x_{i}, false) = 2i + 1 with complement given by id \oplus 1. Mixing indices causes subtle bugs. 3) Misreading SCC order: The conventional assignment is x_{i} = (\text{comp}[x_{i}] > \text{comp}[\neg x_{i}]) when components are numbered in the second pass of Kosaraju’s algorithm following the reverse postorder of the first pass. If you change numbering, adjust the comparison accordingly. 4) Assuming 2-SAT encodes arbitrary k-ary clauses: It cannot represent an arbitrary (x_{1} \lor \cdots \lor x_{k}) with k > 2 in pure 2-CNF. 5) O(k^{2}) blowup for "at most one of k": Pairwise constraints (\neg x_{i} \lor \neg x_{j}) for all i < j are correct but quadratic; if k is large, rethink modeling or problem structure. 6) Recursion depth/stack limits: DFS recursion may overflow on large instances in C++; consider iterative DFS. 7) Forgetting to clear graphs or reinitialize arrays between test cases. 8) Off-by-one / 0-based vs 1-based variable IDs when mapping input to internal indices.

Key Formulas

Clause to implications

Explanation: Each 2-literal clause becomes two directed implications in the graph. This is the key reduction from logic to graph reachability.

Implication as disjunction

Explanation: An implication is logically equivalent to an OR clause. This lets us move between implication rules and 2-CNF clauses.

Satisfiability criterion

Explanation: A 2-CNF formula is satisfiable if and only if no variable collides with its negation in the same SCC. Such a collision forces a contradiction.

Graph size

Explanation: The implication graph for n variables and m clauses has two nodes per variable and at most two edges per clause. This bounds the graph size.

Time complexity

Explanation: Both Kosaraju’s and Tarjan’s SCC algorithms run in linear time in the number of nodes and edges. Building the graph is also linear.

Literal indexing

Explanation: A common mapping in code uses adjacent indices for a literal and its negation, enabling fast complement via XOR with 1.

Assignment by component order

Explanation: After computing SCCs, assign each variable based on which literal’s component appears later in reverse topological order. Later components are set to true.

Pairwise at-most-one size

Explanation: Encoding "at most one of k" via 2-SAT requires a clause ( ) for each pair , which is quadratic in k.

Equivalence encoding

Explanation: To force x and y to be equal, add implications in both directions. This fits perfectly into 2-SAT.

XOR encoding

Explanation: Exactly one of x or y is true can be expressed as the conjunction of one positive OR and one mutual exclusion clause.

Complexity Analysis

Let n be the number of variables and m the number of 2-literal clauses. The implication graph contains exactly 2n nodes (one for each literal and ¬) and at most 2m directed edges because each clause (a ∨ b) produces two implications (¬) and (¬). Constructing the graph is O(n + m), assuming O(1) time to map literals to indices. Both standard SCC algorithms—Kosaraju’s and Tarjan’s—run in linear time with respect to the size of the graph. Specifically, they perform a constant amount of work per node and per edge, resulting in O( + ) = O(n + m) time. Kosaraju uses two DFS passes, one over the original graph (to compute a finishing order) and one over the reversed graph (to assign components). Tarjan performs a single DFS with a stack, maintaining discovery indices and lowlink values to identify component roots. Space complexity is also linear. We store adjacency lists for both the graph and (if using Kosaraju) its reverse, requiring O( + ) memory. Auxiliary arrays such as visited flags, component IDs, order lists, and the final assignment vector each use O() = O(n) space. Thus total space is O(n + m). The assignment extraction phase iterates over the 2n literals and performs constant-time comparisons per variable to set truth values, costing O(n) time. The dominant costs are graph construction and SCC computation, so the overall time remains O(n + m) and space O(n + m).

Code Examples

Minimal 2-SAT solver (Kosaraju) with demonstration
1#include <bits/stdc++.h>
2using namespace std;
3
4struct TwoSAT {
5 int n; // number of variables
6 vector<vector<int>> g, gr; // implication graph and its reverse
7 vector<int> comp; // component id per node
8 vector<int> order; // finish order in first pass
9 vector<int> vis; // visited flag
10 vector<int> assignment; // resulting assignment for variables
11
12 explicit TwoSAT(int n_vars) : n(n_vars) {
13 g.assign(2*n, {});
14 gr.assign(2*n, {});
15 }
16
17 // Map (var, value) -> node id. value=true means literal x_var; value=false means literal ¬x_var
18 static int id(int var, bool value) { return (var<<1) ^ (value ? 0 : 1); }
19 static int neg(int x) { return x ^ 1; } // complement literal index
20
21 // Add implication u -> v
22 void add_implication(int u, int v) {
23 g[u].push_back(v);
24 gr[v].push_back(u);
25 }
26
27 // Add clause (x_var=valueX) OR (y_var=valueY)
28 void add_or(int x_var, bool valueX, int y_var, bool valueY) {
29 int a = id(x_var, valueX);
30 int b = id(y_var, valueY);
31 add_implication(neg(a), b); // ¬a -> b
32 add_implication(neg(b), a); // ¬b -> a
33 }
34
35 // Force literal (v=value) to be true
36 void add_true(int v, bool value) {
37 int a = id(v, value);
38 add_implication(neg(a), a); // (a) ≡ (¬a -> a)
39 }
40
41 // Add implication (x=valueX) -> (y=valueY)
42 void add_implies(int x_var, bool valueX, int y_var, bool valueY) {
43 int a = id(x_var, valueX);
44 int b = id(y_var, valueY);
45 add_implication(a, b);
46 }
47
48 // Add equivalence (x == y)
49 void add_equiv(int x, bool vx, int y, bool vy) {
50 add_implies(x, vx, y, vy);
51 add_implies(y, vy, x, vx);
52 }
53
54 // Add XOR: exactly one of (x=vx) or (y=vy)
55 void add_xor(int x, bool vx, int y, bool vy) {
56 // (x=vx OR y=vy) AND (¬(x=vx) OR ¬(y=vy))
57 add_or(x, vx, y, vy);
58 add_or(x, !vx, y, !vy);
59 }
60
61 // Add at-most-one among provided literals (pairwise O(k^2) clauses)
62 void add_at_most_one(const vector<pair<int,bool>>& lits) {
63 int k = (int)lits.size();
64 for (int i = 0; i < k; ++i) for (int j = i+1; j < k; ++j) {
65 int a = id(lits[i].first, lits[i].second);
66 int b = id(lits[j].first, lits[j].second);
67 // not both true: (¬a OR ¬b)
68 add_or(a>>1, !( (a&1)==0 ), b>>1, !( (b&1)==0 ));
69 // The above expands to add_implication(a, neg(b)) and add_implication(b, neg(a))
70 }
71 }
72
73 // First pass DFS to compute finishing order (recursive for clarity)
74 void dfs1(int v) {
75 vis[v] = 1;
76 for (int to : g[v]) if (!vis[to]) dfs1(to);
77 order.push_back(v);
78 }
79
80 // Second pass DFS on reversed graph to assign components
81 void dfs2(int v, int cid) {
82 comp[v] = cid;
83 for (int to : gr[v]) if (comp[to] == -1) dfs2(to, cid);
84 }
85
86 bool solve() {
87 int N = 2*n;
88 vis.assign(N, 0);
89 order.clear(); order.reserve(N);
90 for (int v = 0; v < N; ++v) if (!vis[v]) dfs1(v);
91 comp.assign(N, -1);
92 int cid = 0;
93 for (int i = N-1; i >= 0; --i) {
94 int v = order[i];
95 if (comp[v] == -1) {
96 dfs2(v, cid++);
97 }
98 }
99 assignment.assign(n, 0);
100 for (int i = 0; i < n; ++i) {
101 if (comp[2*i] == comp[2*i^1]) return false; // x and ¬x in same SCC -> UNSAT
102 assignment[i] = (comp[2*i] > comp[2*i^1]); // later component set to true
103 }
104 return true;
105 }
106};
107
108int main() {
109 ios::sync_with_stdio(false);
110 cin.tie(nullptr);
111
112 // Example: variables x0, x1, x2
113 // Constraints:
114 // (x0 OR x1), (¬x0 OR x2), (¬x1 OR ¬x2), and x2 -> x0
115 TwoSAT sat(3);
116 sat.add_or(0, true, 1, true); // x0 ∨ x1
117 sat.add_or(0, false, 2, true); // ¬x0 ∨ x2
118 sat.add_or(1, false, 2, false); // ¬x1 ∨ ¬x2
119 sat.add_implies(2, true, 0, true); // x2 -> x0
120
121 bool ok = sat.solve();
122 if (!ok) {
123 cout << "UNSATISFIABLE\n";
124 } else {
125 cout << "SATISFIABLE\nAssignment:" << '\n';
126 for (int i = 0; i < 3; ++i) cout << "x" << i << " = " << (sat.assignment[i] ? "true" : "false") << '\n';
127 }
128 return 0;
129}
130

This program implements a classic 2-SAT solver using Kosaraju’s algorithm. It maps each variable and its negation to two adjacent indices, converts each clause to two implications, and finds SCCs. If a variable and its negation share a component, the instance is unsatisfiable. Otherwise, the assignment is decided by the component order. The main function demonstrates building a small formula and prints a satisfying assignment if it exists.

Time: O(n + m)Space: O(n + m)
Scheduling with two options per task: encode conflicts and exact-one
1#include <bits/stdc++.h>
2using namespace std;
3
4// Reuse the TwoSAT structure from the previous example (implemented inline here for a standalone file)
5struct TwoSAT {
6 int n; vector<vector<int>> g, gr; vector<int> comp, order, vis; vector<int> assignment;
7 explicit TwoSAT(int n_vars): n(n_vars) { g.assign(2*n, {}); gr.assign(2*n, {}); }
8 static int id(int var, bool val){ return (var<<1) ^ (val?0:1);} static int neg(int x){return x^1;}
9 void add_implication(int u,int v){ g[u].push_back(v); gr[v].push_back(u);}
10 void add_or(int x,bool vx,int y,bool vy){ int a=id(x,vx), b=id(y,vy); add_implication(neg(a),b); add_implication(neg(b),a);}
11 void add_implies(int x,bool vx,int y,bool vy){ add_implication(id(x,vx), id(y,vy)); }
12 void add_true(int x,bool vx){ int a=id(x,vx); add_implication(neg(a), a);}
13 void add_exactly_one(int x,int y){ // exactly one of x,y is true
14 add_or(x,true, y,true); // at least one
15 add_or(x,false,y,false); // at most one
16 }
17 void dfs1(int v){ vis[v]=1; for(int to:g[v]) if(!vis[to]) dfs1(to); order.push_back(v);}
18 void dfs2(int v,int cid){ comp[v]=cid; for(int to:gr[v]) if(comp[to]==-1) dfs2(to,cid);}
19 bool solve(){ int N=2*n; vis.assign(N,0); order.clear(); order.reserve(N); for(int v=0;v<N;++v) if(!vis[v]) dfs1(v); comp.assign(N,-1); int cid=0; for(int i=N-1;i>=0;--i){ int v=order[i]; if(comp[v]==-1) dfs2(v,cid++);} assignment.assign(n,0); for(int i=0;i<n;++i){ if(comp[2*i]==comp[2*i^1]) return false; assignment[i]=(comp[2*i]>comp[2*i^1]); } return true; }
20};
21
22// Problem: We have tasks, each with two possible time slots. Some slots conflict with others.
23// Model: For task i, variable i means "choose slot A"; then ¬i means "choose slot B".
24// We want exactly one slot per task, and we ban conflicting pairs of chosen slots.
25int main(){
26 ios::sync_with_stdio(false); cin.tie(nullptr);
27
28 int tasks = 4; // tasks 0..3
29 TwoSAT sat(tasks);
30
31 // Exactly one of two slots for each task i (A or B)
32 for(int i=0;i<tasks;++i){
33 // At least one: (slotA or slotB) = (i or ¬i) is always true; but we must enforce exactly one.
34 // Exactly one for two choices reduces to XOR of (i) and (¬i): that is just setting a variable! Simpler:
35 // Force that the variable takes some boolean value; no explicit clause needed.
36 // However, to illustrate, we add (i ∨ ¬i) and (¬i ∨ i) which are tautologies; the real constraint is at most one:
37 sat.add_or(i, true, i, false); // tautology, kept for symmetry
38 sat.add_or(i, false, i, true); // tautology
39 // At most one between slotA and slotB is (¬i ∨ ¬¬i) = (¬i ∨ i), also a tautology. For two options, "exactly one" is inherent:
40 // choosing slot A corresponds to setting variable i=true; B means i=false. So there is exactly one by construction.
41 }
42
43 // Now encode conflicts between concrete slots:
44 // Let's say task 0's slot A conflicts with task 1's slot A: (x0=true) and (x1=true) cannot both hold -> (¬x0 ∨ ¬x1)
45 sat.add_or(0,false, 1,false); // which encodes "not both (0=true) and (1=true)" as (¬x0 ∨ ¬x1)
46
47 // task 1's slot B conflicts with task 2's slot A
48 // That means not( x1=false AND x2=true ) -> (x1=true OR ¬x2) i.e., (x1 ∨ ¬x2)
49 sat.add_or(1,true, 2,false);
50
51 // task 2's slot B conflicts with task 3's slot B
52 // not( x2=false AND x3=false ) -> (x2=true OR x3=true)
53 sat.add_or(2,true, 3,true);
54
55 // Additionally, suppose choosing task 3's slot A implies task 0 must take slot B: (x3=true) -> (x0=false)
56 sat.add_implies(3,true, 0,false);
57
58 bool ok = sat.solve();
59 if(!ok){ cout << "UNSATISFIABLE\n"; return 0; }
60 cout << "SATISFIABLE\n";
61 for(int i=0;i<tasks;++i){
62 cout << "Task " << i << ": choose slot " << (sat.assignment[i] ? 'A' : 'B') << "\n";
63 }
64 return 0;
65}
66

We encode each task with two possible time slots as a single Boolean variable: true = slot A, false = slot B. Conflicts are translated into pairwise 2-SAT clauses disallowing certain simultaneous choices. Because each task inherently picks exactly one of its two slots by the variable’s truth value, we do not need additional clauses to enforce exact-one within a task. The program prints a feasible schedule if one exists.

Time: O(n + m)Space: O(n + m)
2-SAT using Tarjan's SCC; modeling implication, equivalence, and XOR; UNSAT detection demo
1#include <bits/stdc++.h>
2using namespace std;
3
4struct TwoSATTarjan {
5 int n, N; // n variables, N = 2*n nodes
6 vector<vector<int>> g; // implication graph
7 vector<int> index_, low, comp, st; // Tarjan arrays
8 vector<char> onstack; // on-stack flags
9 int timer = 0, comps = 0; // DFS index and component count
10 vector<int> assignment; // result
11
12 explicit TwoSATTarjan(int n_vars): n(n_vars), N(2*n_vars) {
13 g.assign(N, {});
14 }
15
16 static int id(int var, bool val){ return (var<<1) ^ (val?0:1);}
17 static int neg(int x){ return x ^ 1; }
18
19 void add_implication(int u, int v){ g[u].push_back(v); }
20
21 // (x=vx) OR (y=vy)
22 void add_or(int x,bool vx,int y,bool vy){ int a=id(x,vx), b=id(y,vy); add_implication(neg(a), b); add_implication(neg(b), a);}
23
24 // implication (x=vx) -> (y=vy)
25 void add_implies(int x,bool vx,int y,bool vy){ add_implication(id(x,vx), id(y,vy)); }
26
27 // equivalence (x == y)
28 void add_equiv(int x,bool vx,int y,bool vy){ add_implies(x,vx,y,vy); add_implies(y,vy,x,vx); }
29
30 // XOR exactly one of two literals true
31 void add_xor(int x,bool vx,int y,bool vy){ add_or(x,vx,y,vy); add_or(x,!vx,y,!vy);}
32
33 void tarjan(int v){
34 index_[v] = low[v] = ++timer;
35 st.push_back(v); onstack[v] = 1;
36 for(int to: g[v]){
37 if(index_[to] == 0){ tarjan(to); low[v] = min(low[v], low[to]); }
38 else if(onstack[to]) low[v] = min(low[v], index_[to]);
39 }
40 if(low[v] == index_[v]){
41 // root of an SCC
42 while(true){
43 int w = st.back(); st.pop_back(); onstack[w] = 0;
44 comp[w] = comps;
45 if(w == v) break;
46 }
47 ++comps;
48 }
49 }
50
51 bool solve(){
52 index_.assign(N, 0); low.assign(N, 0); comp.assign(N, -1); onstack.assign(N, 0); st.clear(); timer = 0; comps = 0;
53 for(int v=0; v<N; ++v) if(index_[v]==0) tarjan(v);
54 assignment.assign(n, 0);
55 for(int i=0;i<n;++i){ if(comp[2*i] == comp[2*i^1]) return false; assignment[i] = (comp[2*i] > comp[2*i^1]); }
56 return true;
57 }
58};
59
60int main(){
61 ios::sync_with_stdio(false); cin.tie(nullptr);
62
63 // Build two scenarios: one SAT, one UNSAT
64 {
65 cout << "Scenario 1 (SAT)\n";
66 TwoSATTarjan sat(3);
67 // x0 -> x1 ; x1 <-> x2 ; x0 XOR x2
68 sat.add_implies(0,true, 1,true); // x0 -> x1
69 sat.add_equiv(1,true, 2,true); // x1 == x2
70 sat.add_xor(0,true, 2,true); // x0 XOR x2
71 bool ok = sat.solve();
72 if(!ok){ cout << "UNSATISFIABLE\n"; }
73 else{ cout << "SATISFIABLE\n"; for(int i=0;i<3;++i) cout << "x"<<i<<"="<<(sat.assignment[i]?"T":"F")<<" "; cout << "\n"; }
74 }
75
76 {
77 cout << "\nScenario 2 (UNSAT)\n";
78 TwoSATTarjan sat(2);
79 // Force contradiction on x0: x0 -> ¬x0 and ¬x0 -> x0
80 sat.add_implies(0,true, 0,false);
81 sat.add_implies(0,false, 0,true);
82 // Also add unrelated clauses with x1
83 sat.add_or(1,true, 1,false); // tautology
84 bool ok = sat.solve();
85 if(!ok) cout << "UNSATISFIABLE (x0 and ¬x0 in same SCC)\n";
86 else{ cout << "SATISFIABLE (unexpected)\n"; }
87 }
88 return 0;
89}
90

This version uses Tarjan’s single-pass SCC algorithm. It demonstrates helpers for implication, equivalence, and XOR, solves a satisfiable instance, and then constructs an explicit contradiction that forces a variable and its negation into the same SCC, producing UNSAT.

Time: O(n + m)Space: O(n + m)