Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Graph Algorithms

Builds on Flood Fill, which you haven’t finished yet.

P2MediumBuilt on Flood Fill~4 min speedrun

Bipartite Graph (Two-Coloring)

Built on
Flood FillEach node gets one of two sides, a neighbour gets the other side, and an edge inside one side means the answer is no.
The problem

Is Graph Bipartite? (LC 785)(opens leetcode.com)

You're given an undirected graph on n nodes (numbered 0 to n - 1) as an adjacency list graph, where graph[u] lists every node u shares an edge with. There are no self-loops, no duplicate edges, edges are listed symmetrically on both endpoints, and the graph doesn't need to be one connected piece.

Decide whether the nodes can be split into two groups A and B such that no edge ever connects two nodes in the same group — i.e., whether the graph is bipartite.

Return true if such a split exists, false otherwise.

Example 1
Input:graph = [[1,2,3],[0,2],[0,1,3],[0,2]]
Output:false
Nodes 0, 1 and 2 are all linked to each other in a triangle. Whichever way you split them into two groups, two of them land in the same group while sharing an edge.
0123

Bipartite Check

graph ➔ alternate side per edge ➔ valid split or conflict
Spot it when: Nodes (people, tasks, cells) must be split into two groups so that every edge or conflict pair runs between the groups, never inside one; or you must tell whether an undirected graph has an odd cycle.
Bipartite Check
Python
1from collections import deque
2from typing import List
3 
4def is_bipartite(graph: List[List[int]]) -> bool:
5 """Is Graph Bipartite?: two-color every component with BFS; a clash means an odd
6 cycle.
7 Time: O(V + E) | Space: O(V)
8 """
9 # color[node] is -1 (no side yet), 0 or 1 once a component's BFS reaches it
10 color = [-1] * len(graph)
11 # 1. Start A Search From Every Uncolored Node:
12 # The graph may be disconnected, so a node no earlier component's search reached
13 # still needs its own
14 # BFS to be colored.
15 for start in range(len(graph)):
16 if color[start] != -1:
17 continue
18 color[start] = 0 # either side works for the first node of a new component
19 queue = deque([start])
20 # 2. Two-Color The Component With BFS, Checking For A Clash:
21 # A neighbour with no side yet is forced onto the opposite side of the node that
22 # reached it; a
23 # neighbour already on the same side as node means the edge between them closes
24 # an odd-length
25 # cycle, which makes a valid two-way split impossible.
26 while queue:
27 node = queue.popleft()
28 for nxt in graph[node]:
29 if color[nxt] == -1:
30 color[nxt] = 1 - color[node]
31 queue.append(nxt)
32 elif color[nxt] == color[node]:
33 return False
34 return True
Invariant: A node's color is -1 until the search first reaches it and is never changed after, and every edge checked so far joins a side-0 node to a side-1 node.

In a two-group split, a node's side decides the side of each of its neighbours, so the first node of a component takes color[start] = 0 and every neighbour BFS reaches with no side yet takes color[nxt] = 1 - color[node]. Every color is then forced by that first choice and equals the parity of the node's distance from start, so the only way to fail is a neighbour that already has a side with color[nxt] == color[node]: the two paths from start plus that edge close a loop of odd length, and going around an odd loop the sides can't alternate and land back where they began, so return False is right; if no edge ever clashes, sides 0 and 1 are the two groups. The trap is coloring from node 0 only: the graph may be disconnected, so loop for start in range(len(graph)) and start a new search at every node still at color[start] == -1, or an odd cycle in a component that doesn't touch node 0 is never checked.

See it run

Is Graph Bipartite? with a two-side check, step by step

color [-1, -1, -1, -1, -1, -1, -1, -1]

color [0, -1, -1, -1, -1, -1, -1, -1]

color [0, 1, -1, 1, -1, -1, -1, -1]

color [0, 1, 0, 1, -1, -1, -1, -1]

color [0, 1, 0, 1, -1, -1, -1, -1]

color [0, 1, 0, 1, -1, -1, -1, -1]

color [0, 1, 0, 1, 0, -1, -1, -1]

color [0, 1, 0, 1, 0, 1, 1, -1]

color [0, 1, 0, 1, 0, 1, 1, -1]

color [0, 1, 0, 1, 0, 1, 1, -1] · answer False

Each circle is a node. The tag under it is its side. The row under the graph is queue: the left box leaves first.

  • pop the node just taken from queue
  • side 0 the node has side 0
  • side 1 the node has side 1
  • clash an edge whose two ends have the same side

The graph has 8 nodes. color starts as 8 copies of -1, so no node has a side yet.

Node 0 has color -1, so a new search starts there. It takes side 0 and goes into queue.

Pop 0, which has side 0. Neighbours 1 and 3 had no side, so they get side 1 and go into queue.

Pop 1, which has side 1. Neighbour 2 had no side, so it gets side 0 and goes into queue.

Pop 3, which has side 1. Every neighbour already has side 0, so nothing changes.

Pop 2, which has side 0. Every neighbour already has side 1, so nothing changes.

Nodes 1 to 3 already have sides, so the loop skips them. Node 4 has color -1, so a new search starts there. It takes side 0 and goes into queue.

Pop 4, which has side 0. Neighbours 5 and 6 had no side, so they get side 1 and go into queue.

Pop 5, which has side 1. Its neighbour 6 already has side 1 too. The edge 5-6 joins two nodes on the same side, so the function returns False.

The function returns False. The edge 5-6 and the paths that reached its two ends close a cycle of odd length, so no split into two sides exists.

01234567
queue
empty
1 / 10
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Starting the search from node 0 only

    Mistake
    for start in [0]:
    Fix
    for start in range(len(graph)):

    What happens: The graph may be disconnected. A search from node 0 colors only the nodes that node 0 can reach. An odd cycle in another component is then never checked. Looping over every start begins a new search at each node that still has color[start] == -1.

    Example: Is Graph Bipartite? (LC 785): graph = [[], [2, 3], [1, 3], [1, 2]]
    Mistake: returns TrueFix: returns False
  2. 2

    Gotcha 2: Filling color with 0 instead of -1

    Mistake
    color = [0] * len(graph)
    Fix
    color = [-1] * len(graph)

    What happens: -1 means "no side yet". With [0] * len(graph), every node already looks colored on side 0. So if color[start] != -1: continue skips every node. No search runs, and the function returns True for any graph.

    Example: graph = [[1, 2, 3], [0, 2], [0, 1, 3], [0, 2]]
    Mistake: returns TrueFix: returns False
  3. 3

    Gotcha 3: Giving every new neighbour side 1 instead of 1 - color[node]

    Mistake
    color[nxt] = 1
    Fix
    color[nxt] = 1 - color[node]

    What happens: The sides must alternate along every edge. With color[nxt] = 1, a neighbour of a side-1 node also gets side 1. The check then finds an edge with both ends on side 1. The function then returns False for a graph that can be split.

    Example: graph = [[1], [0, 2], [1]]
    Mistake: returns FalseFix: returns True
  4. 4

    Gotcha 4: Comparing with color[start] instead of color[node]

    Mistake
    elif color[nxt] == color[start]:
    Fix
    elif color[nxt] == color[node]:

    What happens: A clash is an edge whose two ends are on the same side. So the neighbour must be compared with the node on the other end, node. A node two edges from start is supposed to share its side. Comparing with start reports a clash there.

    Example: graph = [[1], [0, 2], [1]]
    Mistake: returns FalseFix: returns True
Not this one if…
  • Edges arrive one at a time and you need the first one that joins two nodes already connected, as in Redundant Connection (LC 684): the question is which group a node belongs to, not which side of an edge it sits on. See Union-Find
  • You only have to count or explore the connected groups, as in Number of Provinces (LC 547): no edge says its two ends must land in different groups. See Flood Fill
  • The edges have a direction and any cycle breaks the answer, as in Course Schedule (LC 207): two-coloring only catches odd cycles in an undirected graph. See Cycle Detection

Deep Dive

Why the first node decides every other node

A bipartite graph is a graph whose nodes can be split into two groups so that every edge joins one node of each group. No edge may have both ends in the same group. This card calls the groups side 0 and side 1.

Take one component, which is a set of nodes that connect to each other. Give its first node start side 0. Each neighbour of start shares an edge with it, so each neighbour must have side 1. Each neighbour of those nodes must have side 0, and so on. Every side in the component is forced by the first choice.

The forced sides follow a simple rule. A node has side 0 if its distance from start is even. It has side 1 if its distance is odd. BFS (breadth-first search) reaches each node by a shortest path, so color[nxt] = 1 - color[node] gives exactly this rule.

A clash happens when an edge joins two nodes that already have the same side. Both nodes were reached from start, and they have the same parity of distance. The two paths from start plus that one edge make a cycle with an odd number of edges. A cycle with an odd number of edges cannot be split, because the sides would have to alternate around the loop and end on the wrong side.

Example: a loop of four nodes [[1, 3], [0, 2], [1, 3], [0, 2]] gets sides [0, 1, 0, 1], and the answer is True. A triangle gets side 0 for the first node, side 1 for the other two, and then the edge between those two is a clash. The answer is False.

One trace: the clash is in the second component

The board uses 8 nodes in two components. Nodes 0 to 3 are the four-node loop from Is Graph Bipartite? (LC 785). Nodes 4 to 7 are a triangle 4, 5, 6 with node 7 attached to node 6.

The search from node 0 gives color = [0, 1, 0, 1, -1, -1, -1, -1]. It checks 8 neighbour entries and finds no clash. Then the for start loop skips nodes 1, 2 and 3, because they already have sides. Node 4 still has color[4] == -1, so a new search starts there.

Pop node 4, which has side 0. Its neighbours 5 and 6 get side 1. Then pop node 5, which has side 1. Its neighbour 4 has side 0, which is fine. Its neighbour 6 has side 1, the same as node 5. This is the clash, and the function returns False.

Node 7 is never reached. The function stops at the first clash, so it does not need to color the rest.

The trap: starting only from node 0

The loop for start in range(len(graph)) is needed. A graph can have several components, and a BFS from one node only reaches its own component.

On the board graph, a search that starts only at node 0 colors nodes 0 to 3 and returns True. The answer is wrong, because the triangle 4, 5, 6 has an odd cycle that nobody checked. The correct answer is False.

Another example is graph = [[], [2, 3], [1, 3], [1, 2]]. Node 0 has no edges. A search from node 0 reaches nothing else and returns True. The nodes 1, 2 and 3 form a triangle, so the correct answer is False.

A second trap is the starting value. color = [-1] * len(graph) uses -1 for no side. If you fill color with 0, every node already looks colored, if color[start] != -1 skips every node, and the function returns True for every graph. On [[1, 2, 3], [0, 2], [0, 1, 3], [0, 2]], the correct answer is False.

The cost, and which problems fit

Each node enters queue once, because a node gets a side the first time it is seen and is never changed. Each edge is looked at twice, once from each end. So the time is O(V + E), where V is the number of nodes and E is the number of edges. color and queue hold one entry per node, so the space is O(V).

Example: the four-node loop has 4 edges. The pops look at 8 neighbour entries in total, which is 2 * E. The count follows the formula.

The same check solves any question of the form "can these items be split into two groups?" Possible Bipartition (LC 886) gives pairs of people who dislike each other. Each pair is an edge, and the answer is True when the people can be split into two groups with no pair inside one group. Only the way you build graph changes.

DFS (depth-first search) works as well as BFS here. In a graph that can be split, both give the same sides when the first node of each component takes side 0. If the edges have directions, as in Course Schedule (LC 207), a different check is needed, because this card detects only odd cycles in an undirected graph.

60-Second Pitch

"The items must split into two groups, and every pair rule says two items go to different groups. So I build a graph with one edge per rule and two-color it. This is the question of whether the graph is bipartite. I loop over every node that has no side yet, because the graph may have several components. I give that node side 0 and run BFS. Each new neighbour gets the opposite side, 1 - color[node]. If a neighbour already has the same side as the node I am looking at, the edge closes a cycle with an odd number of edges, and I return False. If no edge clashes, the two sides are the two groups, and I return True. Each node is pushed once and each edge is looked at twice, so the time is O(V + E) and the space is O(V)."

Common Questions

Why does an odd cycle make a two-group split impossible?

Going around a cycle, the sides must alternate at every edge. After an even number of edges you are back on the starting side, which is correct. After an odd number of edges you would be back at the start node on the other side, which is a contradiction. So a cycle with an odd number of edges cannot be split.

Why is it enough to find one edge whose two ends have the same side?

Every side in a component was forced by the first node, so there was no other choice. If one edge joins two nodes with the same side, no other assignment can fix it. The two paths from start and that edge form a cycle with an odd number of edges.

Can you use DFS and not BFS?

Yes. DFS also gives each new neighbour the opposite side of the node that reached it, and it also compares an already colored neighbour with the current node. Use an explicit stack for a long chain, because a recursive DFS can pass Python's recursion limit.

What are the time and space complexity?

O(V + E) time and O(V) space. Each node enters queue once, and each edge is looked at twice, once from each end. color and queue hold one entry per node.

How is this different from union-find?

Union-find joins items that must be in the same group, and it answers which group an item belongs to. Two-coloring splits items into two groups, and the rule is that connected items go to different groups. For a version where edges arrive one at a time, see Redundant Connection (LC 684), which asks for the edge that closes a cycle of any length.

SWE Reasoning

Spot it

  • The items must be split into exactly two groups, teams or sets.
  • A pair rule says two items must be in different groups: they dislike each other, they conflict, or they cannot share a resource.
  • The question can also be phrased as "does the graph have a cycle with an odd number of edges?" The two questions have the same answer.
  • The graph is undirected. Each edge works in both directions.
  • The input can have several components, so the check must start from every node that has no side yet.

The invariant, step by step

  1. color starts as -1 for every node. A node with -1 has no side yet.
  2. The for start loop finds a node with color[start] == -1. It gives that node side 0 and puts it in queue.
  3. BFS pops node from the front of queue. For each neighbour nxt with color[nxt] == -1, it sets color[nxt] = 1 - color[node] and pushes nxt.
  4. A neighbour that already has a side is compared with node. If color[nxt] == color[node], the edge joins two nodes on the same side, and the function returns False.
  5. A side is never changed after it is set, so every edge checked so far joins side 0 and side 1.
  6. If the loop ends with no clash, every edge was checked from both ends. Side 0 and side 1 are the two groups, and the function returns True.

Traps

  • Searching only from node 0. An odd cycle in another component is never checked. Loop over every start and skip nodes that already have a side.
  • Filling color with 0. Every node then looks colored, if color[start] != -1 skips all of them, and the function returns True for any graph. Use -1.
  • Giving every new neighbour side 1. The sides must alternate along each edge. Use color[nxt] = 1 - color[node].
  • Comparing with color[start]. A clash is an edge whose two ends have the same side. Compare color[nxt] with color[node], the node on the other end of the edge.
  • Using it on a directed problem. Course Schedule (LC 207) asks about any cycle in a directed graph. Two-coloring detects only odd cycles in an undirected graph.

Practice

Canonical LeetCode:Is Graph Bipartite? (LC 785)Possible Bipartition (LC 886)
Theory Context•Graph Algorithms

Builds on Flood Fill, which you haven’t finished yet.

P2MediumBuilt on Flood Fill~4 min speedrun

Bipartite Graph (Two-Coloring)

Built on
Flood FillEach node gets one of two sides, a neighbour gets the other side, and an edge inside one side means the answer is no.
The problem

Is Graph Bipartite? (LC 785)(opens leetcode.com)

You're given an undirected graph on n nodes (numbered 0 to n - 1) as an adjacency list graph, where graph[u] lists every node u shares an edge with. There are no self-loops, no duplicate edges, edges are listed symmetrically on both endpoints, and the graph doesn't need to be one connected piece.

Decide whether the nodes can be split into two groups A and B such that no edge ever connects two nodes in the same group — i.e., whether the graph is bipartite.

Return true if such a split exists, false otherwise.

Example 1
Input:graph = [[1,2,3],[0,2],[0,1,3],[0,2]]
Output:false
Nodes 0, 1 and 2 are all linked to each other in a triangle. Whichever way you split them into two groups, two of them land in the same group while sharing an edge.
0123

Bipartite Check

graph ➔ alternate side per edge ➔ valid split or conflict
Spot it when: Nodes (people, tasks, cells) must be split into two groups so that every edge or conflict pair runs between the groups, never inside one; or you must tell whether an undirected graph has an odd cycle.
Bipartite Check
Python
1from collections import deque
2from typing import List
3 
4def is_bipartite(graph: List[List[int]]) -> bool:
5 """Is Graph Bipartite?: two-color every component with BFS; a clash means an odd
6 cycle.
7 Time: O(V + E) | Space: O(V)
8 """
9 # color[node] is -1 (no side yet), 0 or 1 once a component's BFS reaches it
10 color = [-1] * len(graph)
11 # 1. Start A Search From Every Uncolored Node:
12 # The graph may be disconnected, so a node no earlier component's search reached
13 # still needs its own
14 # BFS to be colored.
15 for start in range(len(graph)):
16 if color[start] != -1:
17 continue
18 color[start] = 0 # either side works for the first node of a new component
19 queue = deque([start])
20 # 2. Two-Color The Component With BFS, Checking For A Clash:
21 # A neighbour with no side yet is forced onto the opposite side of the node that
22 # reached it; a
23 # neighbour already on the same side as node means the edge between them closes
24 # an odd-length
25 # cycle, which makes a valid two-way split impossible.
26 while queue:
27 node = queue.popleft()
28 for nxt in graph[node]:
29 if color[nxt] == -1:
30 color[nxt] = 1 - color[node]
31 queue.append(nxt)
32 elif color[nxt] == color[node]:
33 return False
34 return True
Invariant: A node's color is -1 until the search first reaches it and is never changed after, and every edge checked so far joins a side-0 node to a side-1 node.

In a two-group split, a node's side decides the side of each of its neighbours, so the first node of a component takes color[start] = 0 and every neighbour BFS reaches with no side yet takes color[nxt] = 1 - color[node]. Every color is then forced by that first choice and equals the parity of the node's distance from start, so the only way to fail is a neighbour that already has a side with color[nxt] == color[node]: the two paths from start plus that edge close a loop of odd length, and going around an odd loop the sides can't alternate and land back where they began, so return False is right; if no edge ever clashes, sides 0 and 1 are the two groups. The trap is coloring from node 0 only: the graph may be disconnected, so loop for start in range(len(graph)) and start a new search at every node still at color[start] == -1, or an odd cycle in a component that doesn't touch node 0 is never checked.

See it run

Is Graph Bipartite? with a two-side check, step by step

color [-1, -1, -1, -1, -1, -1, -1, -1]

color [0, -1, -1, -1, -1, -1, -1, -1]

color [0, 1, -1, 1, -1, -1, -1, -1]

color [0, 1, 0, 1, -1, -1, -1, -1]

color [0, 1, 0, 1, -1, -1, -1, -1]

color [0, 1, 0, 1, -1, -1, -1, -1]

color [0, 1, 0, 1, 0, -1, -1, -1]

color [0, 1, 0, 1, 0, 1, 1, -1]

color [0, 1, 0, 1, 0, 1, 1, -1]

color [0, 1, 0, 1, 0, 1, 1, -1] · answer False

Each circle is a node. The tag under it is its side. The row under the graph is queue: the left box leaves first.

  • pop the node just taken from queue
  • side 0 the node has side 0
  • side 1 the node has side 1
  • clash an edge whose two ends have the same side

The graph has 8 nodes. color starts as 8 copies of -1, so no node has a side yet.

Node 0 has color -1, so a new search starts there. It takes side 0 and goes into queue.

Pop 0, which has side 0. Neighbours 1 and 3 had no side, so they get side 1 and go into queue.

Pop 1, which has side 1. Neighbour 2 had no side, so it gets side 0 and goes into queue.

Pop 3, which has side 1. Every neighbour already has side 0, so nothing changes.

Pop 2, which has side 0. Every neighbour already has side 1, so nothing changes.

Nodes 1 to 3 already have sides, so the loop skips them. Node 4 has color -1, so a new search starts there. It takes side 0 and goes into queue.

Pop 4, which has side 0. Neighbours 5 and 6 had no side, so they get side 1 and go into queue.

Pop 5, which has side 1. Its neighbour 6 already has side 1 too. The edge 5-6 joins two nodes on the same side, so the function returns False.

The function returns False. The edge 5-6 and the paths that reached its two ends close a cycle of odd length, so no split into two sides exists.

01234567
queue
empty
1 / 10
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Starting the search from node 0 only

    Mistake
    for start in [0]:
    Fix
    for start in range(len(graph)):

    What happens: The graph may be disconnected. A search from node 0 colors only the nodes that node 0 can reach. An odd cycle in another component is then never checked. Looping over every start begins a new search at each node that still has color[start] == -1.

    Example: Is Graph Bipartite? (LC 785): graph = [[], [2, 3], [1, 3], [1, 2]]
    Mistake: returns TrueFix: returns False
  2. 2

    Gotcha 2: Filling color with 0 instead of -1

    Mistake
    color = [0] * len(graph)
    Fix
    color = [-1] * len(graph)

    What happens: -1 means "no side yet". With [0] * len(graph), every node already looks colored on side 0. So if color[start] != -1: continue skips every node. No search runs, and the function returns True for any graph.

    Example: graph = [[1, 2, 3], [0, 2], [0, 1, 3], [0, 2]]
    Mistake: returns TrueFix: returns False
  3. 3

    Gotcha 3: Giving every new neighbour side 1 instead of 1 - color[node]

    Mistake
    color[nxt] = 1
    Fix
    color[nxt] = 1 - color[node]

    What happens: The sides must alternate along every edge. With color[nxt] = 1, a neighbour of a side-1 node also gets side 1. The check then finds an edge with both ends on side 1. The function then returns False for a graph that can be split.

    Example: graph = [[1], [0, 2], [1]]
    Mistake: returns FalseFix: returns True
  4. 4

    Gotcha 4: Comparing with color[start] instead of color[node]

    Mistake
    elif color[nxt] == color[start]:
    Fix
    elif color[nxt] == color[node]:

    What happens: A clash is an edge whose two ends are on the same side. So the neighbour must be compared with the node on the other end, node. A node two edges from start is supposed to share its side. Comparing with start reports a clash there.

    Example: graph = [[1], [0, 2], [1]]
    Mistake: returns FalseFix: returns True
Not this one if…
  • Edges arrive one at a time and you need the first one that joins two nodes already connected, as in Redundant Connection (LC 684): the question is which group a node belongs to, not which side of an edge it sits on. See Union-Find
  • You only have to count or explore the connected groups, as in Number of Provinces (LC 547): no edge says its two ends must land in different groups. See Flood Fill
  • The edges have a direction and any cycle breaks the answer, as in Course Schedule (LC 207): two-coloring only catches odd cycles in an undirected graph. See Cycle Detection

Deep Dive

Why the first node decides every other node

A bipartite graph is a graph whose nodes can be split into two groups so that every edge joins one node of each group. No edge may have both ends in the same group. This card calls the groups side 0 and side 1.

Take one component, which is a set of nodes that connect to each other. Give its first node start side 0. Each neighbour of start shares an edge with it, so each neighbour must have side 1. Each neighbour of those nodes must have side 0, and so on. Every side in the component is forced by the first choice.

The forced sides follow a simple rule. A node has side 0 if its distance from start is even. It has side 1 if its distance is odd. BFS (breadth-first search) reaches each node by a shortest path, so color[nxt] = 1 - color[node] gives exactly this rule.

A clash happens when an edge joins two nodes that already have the same side. Both nodes were reached from start, and they have the same parity of distance. The two paths from start plus that one edge make a cycle with an odd number of edges. A cycle with an odd number of edges cannot be split, because the sides would have to alternate around the loop and end on the wrong side.

Example: a loop of four nodes [[1, 3], [0, 2], [1, 3], [0, 2]] gets sides [0, 1, 0, 1], and the answer is True. A triangle gets side 0 for the first node, side 1 for the other two, and then the edge between those two is a clash. The answer is False.

One trace: the clash is in the second component

The board uses 8 nodes in two components. Nodes 0 to 3 are the four-node loop from Is Graph Bipartite? (LC 785). Nodes 4 to 7 are a triangle 4, 5, 6 with node 7 attached to node 6.

The search from node 0 gives color = [0, 1, 0, 1, -1, -1, -1, -1]. It checks 8 neighbour entries and finds no clash. Then the for start loop skips nodes 1, 2 and 3, because they already have sides. Node 4 still has color[4] == -1, so a new search starts there.

Pop node 4, which has side 0. Its neighbours 5 and 6 get side 1. Then pop node 5, which has side 1. Its neighbour 4 has side 0, which is fine. Its neighbour 6 has side 1, the same as node 5. This is the clash, and the function returns False.

Node 7 is never reached. The function stops at the first clash, so it does not need to color the rest.

The trap: starting only from node 0

The loop for start in range(len(graph)) is needed. A graph can have several components, and a BFS from one node only reaches its own component.

On the board graph, a search that starts only at node 0 colors nodes 0 to 3 and returns True. The answer is wrong, because the triangle 4, 5, 6 has an odd cycle that nobody checked. The correct answer is False.

Another example is graph = [[], [2, 3], [1, 3], [1, 2]]. Node 0 has no edges. A search from node 0 reaches nothing else and returns True. The nodes 1, 2 and 3 form a triangle, so the correct answer is False.

A second trap is the starting value. color = [-1] * len(graph) uses -1 for no side. If you fill color with 0, every node already looks colored, if color[start] != -1 skips every node, and the function returns True for every graph. On [[1, 2, 3], [0, 2], [0, 1, 3], [0, 2]], the correct answer is False.

The cost, and which problems fit

Each node enters queue once, because a node gets a side the first time it is seen and is never changed. Each edge is looked at twice, once from each end. So the time is O(V + E), where V is the number of nodes and E is the number of edges. color and queue hold one entry per node, so the space is O(V).

Example: the four-node loop has 4 edges. The pops look at 8 neighbour entries in total, which is 2 * E. The count follows the formula.

The same check solves any question of the form "can these items be split into two groups?" Possible Bipartition (LC 886) gives pairs of people who dislike each other. Each pair is an edge, and the answer is True when the people can be split into two groups with no pair inside one group. Only the way you build graph changes.

DFS (depth-first search) works as well as BFS here. In a graph that can be split, both give the same sides when the first node of each component takes side 0. If the edges have directions, as in Course Schedule (LC 207), a different check is needed, because this card detects only odd cycles in an undirected graph.

60-Second Pitch

"The items must split into two groups, and every pair rule says two items go to different groups. So I build a graph with one edge per rule and two-color it. This is the question of whether the graph is bipartite. I loop over every node that has no side yet, because the graph may have several components. I give that node side 0 and run BFS. Each new neighbour gets the opposite side, 1 - color[node]. If a neighbour already has the same side as the node I am looking at, the edge closes a cycle with an odd number of edges, and I return False. If no edge clashes, the two sides are the two groups, and I return True. Each node is pushed once and each edge is looked at twice, so the time is O(V + E) and the space is O(V)."

Common Questions

Why does an odd cycle make a two-group split impossible?

Going around a cycle, the sides must alternate at every edge. After an even number of edges you are back on the starting side, which is correct. After an odd number of edges you would be back at the start node on the other side, which is a contradiction. So a cycle with an odd number of edges cannot be split.

Why is it enough to find one edge whose two ends have the same side?

Every side in a component was forced by the first node, so there was no other choice. If one edge joins two nodes with the same side, no other assignment can fix it. The two paths from start and that edge form a cycle with an odd number of edges.

Can you use DFS and not BFS?

Yes. DFS also gives each new neighbour the opposite side of the node that reached it, and it also compares an already colored neighbour with the current node. Use an explicit stack for a long chain, because a recursive DFS can pass Python's recursion limit.

What are the time and space complexity?

O(V + E) time and O(V) space. Each node enters queue once, and each edge is looked at twice, once from each end. color and queue hold one entry per node.

How is this different from union-find?

Union-find joins items that must be in the same group, and it answers which group an item belongs to. Two-coloring splits items into two groups, and the rule is that connected items go to different groups. For a version where edges arrive one at a time, see Redundant Connection (LC 684), which asks for the edge that closes a cycle of any length.

SWE Reasoning

Spot it

  • The items must be split into exactly two groups, teams or sets.
  • A pair rule says two items must be in different groups: they dislike each other, they conflict, or they cannot share a resource.
  • The question can also be phrased as "does the graph have a cycle with an odd number of edges?" The two questions have the same answer.
  • The graph is undirected. Each edge works in both directions.
  • The input can have several components, so the check must start from every node that has no side yet.

The invariant, step by step

  1. color starts as -1 for every node. A node with -1 has no side yet.
  2. The for start loop finds a node with color[start] == -1. It gives that node side 0 and puts it in queue.
  3. BFS pops node from the front of queue. For each neighbour nxt with color[nxt] == -1, it sets color[nxt] = 1 - color[node] and pushes nxt.
  4. A neighbour that already has a side is compared with node. If color[nxt] == color[node], the edge joins two nodes on the same side, and the function returns False.
  5. A side is never changed after it is set, so every edge checked so far joins side 0 and side 1.
  6. If the loop ends with no clash, every edge was checked from both ends. Side 0 and side 1 are the two groups, and the function returns True.

Traps

  • Searching only from node 0. An odd cycle in another component is never checked. Loop over every start and skip nodes that already have a side.
  • Filling color with 0. Every node then looks colored, if color[start] != -1 skips all of them, and the function returns True for any graph. Use -1.
  • Giving every new neighbour side 1. The sides must alternate along each edge. Use color[nxt] = 1 - color[node].
  • Comparing with color[start]. A clash is an edge whose two ends have the same side. Compare color[nxt] with color[node], the node on the other end of the edge.
  • Using it on a directed problem. Course Schedule (LC 207) asks about any cycle in a directed graph. Two-coloring detects only odd cycles in an undirected graph.

Practice

Canonical LeetCode:Is Graph Bipartite? (LC 785)Possible Bipartition (LC 886)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•