DSA sheet · Graphs · 2-colouring pattern

Is Graph Bipartite?

Can we split the nodes of a graph into two teams so that every edge joins two different teams? The teacher explains the real definition (two groups), then switches to the version everyone uses in practice: give each node one of two colours, and give every neighbour the opposite colour. If a neighbour already has your colour, the graph is not bipartite. She stores the colours as 1 and −1 in one array that also acts as the visited array (0 = not visited), solves it with DFS, fixes a bug in her main loop on screen, and then shows the BFS version. The graph may be split into several pieces, so every piece must be checked.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Graph basics this page needs

Nodes and edges

A graph is a set of nodes (points, here numbered 0 to n−1) joined by edges (lines). Two nodes joined by an edge are neighbours (the teacher often calls them "children" of the node we're standing on).

Directed, undirected, weighted

kindmeaningdrawn as
undirectedThe edge works both ways: from a you can go to b, and from b to a. This problem is undirected.(0)—(1)
directedOne-way edge.(0)→(1)
weightedEach edge has a number on it. No weights here.(0)—5—(1)

Three ways the input can arrive, and the adjacency list

formlooks like (square 0-1-2-3-0)neighbours of u
edge list[[0,1],[1,2],[2,3],[3,0]]scan all edges, so convert first
adjacency matrix4 × 4 grid of 0/1every v with M[u][v] == 1
adjacency list[[1,3],[0,2],[1,3],[0,2]]graph[u]

In this problem we are given the adjacency list directly: graph[u] is the list of u's neighbours. So, as the teacher points out, there is nothing to build. If you ever get an edge list instead, this is how you'd convert it:

edge list → adjacency list (undirected)
n = 4
edges = [[0, 1], [0, 3], [1, 2], [2, 3]]
graph = [[] for _ in range(n)]
for u, v in edges:
    graph[u].append(v)
    graph[v].append(u)          # both directions
print(graph)                    # [[1, 3], [0, 2], [1, 3], [0, 2]]

The visited array, and why we can't skip it

The teacher repeats this in every video: around 90% of graph problems need a visited record. In an undirected graph, 0 lists 1 and 1 lists 0 back, so without a record we would bounce between them forever. You can keep a separate True/False array, or, when possible, reuse an array you already need by giving it special values. That's what we'll do here: one array holds 0 (not visited) or a colour (visited).

Connected components

A connected component is a group of nodes that can all reach each other. The problem warns that the graph may not be connected (recall Number of Provinces, where we counted these groups). So a single traversal from node 0 might not see everything. The fix is the usual outer loop: for every node, if it's not visited, start a new traversal from it.

(0)—(1)      (4)—(5)
 |    |         \ /
(3)—(2)         (6)
two components: {0,1,2,3} and {4,5,6}

BFS and DFS

DFS: Depth First SearchBFS: Breadth First Search
ideaGo deep through one neighbour, then back up and try the next.Spread level by level: the start, then all nodes 1 edge away, then 2 edges away…
toolrecursion (the call stack)a queue: collections.deque
plain DFS and BFS over all components
from collections import deque

graph = [[1, 3], [0, 2], [1, 3], [0, 2], [5, 6], [4, 6], [4, 5]]
n = len(graph)

seen = [False] * n
order = []
def dfs(u):
    seen[u] = True
    order.append(u)
    for v in graph[u]:
        if not seen[v]:
            dfs(v)
for i in range(n):
    if not seen[i]:
        dfs(i)
print(order)                    # [0, 1, 2, 3, 4, 5, 6]

seen = [False] * n
order = []
for i in range(n):
    if not seen[i]:
        seen[i] = True
        q = deque([i])
        while q:
            u = q.popleft()
            order.append(u)
            for v in graph[u]:
                if not seen[v]:
                    seen[v] = True
                    q.append(v)
print(order)                    # [0, 1, 3, 2, 4, 5, 6]

New idea for this page: 2-colouring

Colour the start node with colour A. Every neighbour gets colour B, their neighbours get A again, and so on: the colour flips at every edge. If we ever find an edge whose two ends have the same colour, the flip rule has been broken, and no 2-team split is possible. That is the whole pattern.

Part A · 2-colouring with DFS

LeetCode 785 · Is Graph Bipartite?

1The question in simple words

You get an undirected graph with n nodes (0 to n−1) as an adjacency list graph. The graph is bipartite if you can split all nodes into two groups, A and B, so that every edge has one end in A and the other end in B. In other words, no edge joins two members of the same group. Return True or False.

Example 1: graph = [[1,2,3],[0,2],[0,1,3],[0,2]]
(0)———(1)
 | \    |
 |   \  |
 |     \|
(3)———(2)
edges: 0-1 0-2 0-3 1-2 2-3
Example 2: graph = [[1,3],[0,2],[1,3],[0,2]]
(0)———(1)
 |      |
 |      |
(3)———(2)
edges: 0-1 0-3 1-2 2-3

Reading the input: graph[0] = [1,2,3] means node 0 has edges to 1, 2 and 3. graph[1] = [0,2] means 1 is joined to 0 and 2, and so on. Since the graph is undirected, every edge shows up in both lists.

2What the constraints tell us

3Intuition: alternate the teams

The teacher tries Example 1 by hand: put 0 in A, 1 in B, 2 in A, 3 in B. The idea behind "alternating": a node and its neighbour must be in different groups, so as you walk along edges the group must keep switching A, B, A, B…

  A        B
(0)———(1)
 | \    |
 |   \  |          edge 0-2: A and A  ✗
 |     \|
(3)———(2)
  B        A

Edges 0-1, 1-2, 2-3, 3-0 are all fine (A–B). But edge 0-2 joins two A's. And there was no other choice: once 0 is A, its neighbour 1 is forced to be B, and then 1's neighbour 2 is forced to be A. So Example 1 is not bipartite. In Example 2 the same alternation works on every edge, so it's bipartite.

Groups = coloursThe real definition talks about two groups. To make it simpler, everyone says colours instead: colour each node red or yellow, and a neighbour must get the other colour. It's the same thing with a friendlier name.

4Building the logic step by step

Which values to store?

We could store the strings "red" and "yellow", but we only need two values, so numbers are simpler. The teacher picks 1 for red and −1 for yellow, and keeps 0 for "not visited yet".

value in color[u]meaning
0not visited yet
1visited, colour red (group A)
−1visited, colour yellow (group B)
Doubt 1: why 1 and −1, and not 0 and 1?
→ Two reasons. (a) We also need a "not visited" state. If 0 and 1 were the two colours, a fresh 0 would look like a coloured node. So the two colours must both be different from the "unvisited" value. (b) With 1 and −1, the opposite colour is just -color: −(1) = −1 and −(−1) = 1. One minus sign flips it.
Doubt 2: why not a separate visited array plus a colour array?
→ That works, but it's an extra array for nothing. "Not zero" already means "visited", whatever the colour. One array does both jobs.
Doubt 3: the teacher first marks visited nodes with just 1, then changes her mind. Why?
→ While dry-running she reaches a node whose neighbour is already visited and needs to ask "is the neighbour's colour the same as mine?" If every visited node just holds 1, that question can't be answered: the colour was never stored. So she stores the colour itself as the visited mark: 1 or −1, both non-zero.

What happens at one node

We arrive at node with a colour. First mark it: color[node] = c. Then look at each neighbour (child):

If the loop over children finishes without any problem, return True: this node and everything explored below it is fine.

Doubt 4: why do we need the final return True after the loop?
→ The caller checks if not dfs(...). If the function fell off the end, Python would return None, which counts as False, and every graph would be reported "not bipartite".

The main function: check every component

Because the graph may be in pieces, loop i from 0 to n−1. Whenever color[i] == 0 (a node no earlier search reached, so a new component), start dfs(i, 1). If any component returns False, the whole answer is False. If every component passes, return True.

(0)—(1)      (4)—(5)
 |    |         \ /
(3)—(2)         (6)
square: bipartite ✓      triangle: 4=A, 5=B, 6 must differ from both ✗
answer: False (one bad piece is enough)
Doubt 5: when could we skip the outer loop?
→ The teacher's point: if a problem said "start from node 0" (meaning everything is reachable from 0), there is only one piece, and a single dfs(0, 1) would decide the answer. Here the problem says the graph may be disconnected, so we must try every unvisited node.

The bug she fixed on screen

Her first version of the main loop called dfs(i, 1) for every i, without checking whether i was already coloured. The fix: only call it when color[i] == 0. Why it matters, on the tiny graph (0)—(1):

  1. i = 0: dfs(0, 1) colours 0 = 1, then 1 = −1. Fine, True.
  2. i = 1 (buggy): dfs(1, 1) overwrites node 1 with colour 1. Its neighbour 0 also has colour 1 → "same colour" → False. But a single edge is obviously bipartite!

With the check, i = 1 is skipped because it's already coloured, and the answer is True. (Even when the buggy version gets lucky, it re-walks whole components again and again.)

5Approach steps

  1. n = len(graph); color = [0] * n.
  2. For each node i with color[i] == 0: if dfs(i, 1) is False → return False.
  3. dfs(node, c): set color[node] = c. For each child: if 0 → recurse with -c (pass False up); else if color[child] == c → return False.
  4. dfs returns True after its loop; the main function returns True after its loop.

6Code (Python)

Part A · 2-colouring with DFS (the teacher's approach)
class Solution:
    def isBipartite(self, graph):
        n = len(graph)
        color = [0] * n                     # 0 = not visited, 1 / -1 = the two colours

        def dfs(node, c):
            color[node] = c
            for child in graph[node]:
                if color[child] == 0:
                    if not dfs(child, -c):  # child gets the opposite colour
                        return False
                elif color[child] == c:     # same colour on both ends of an edge
                    return False
            return True

        for i in range(n):
            if color[i] == 0:               # a new component
                if not dfs(i, 1):
                    return False
        return True

7Code line by line

linewhat it means
n = len(graph)The number of nodes is the length of the adjacency list.
color = [0] * nEveryone starts unvisited. This one array is both "visited" and "colour".
color[node] = cFirst thing on arrival: mark the node visited by giving it its colour.
for child in graph[node]:Walk this node's neighbours from its adjacency list.
if color[child] == 0:Not visited yet → it must take the other colour.
if not dfs(child, -c): return FalseGo deeper with the flipped colour. If anything below found a clash, stop and report False all the way up.
elif color[child] == c: return FalseAlready visited and coloured the same as us: an edge inside one group. Not bipartite.
return TrueNo clash around this node or below it.
if color[i] == 0:The fix from the video: only start from nodes no earlier search coloured.
if not dfs(i, 1): return FalseCheck this whole component. One bad component makes the answer False.
return TrueEvery component passed.

8Dry run

Example 2 (square): expected True

stepnode visited (colour)neighbours checkedcall stack aftercolor after [0,1,2,3]answer so far
1i = 0 → dfs(0, 1)-dfs(0)[1, 0, 0, 0]-
20 (1)1: unvisited → dfs(1, −1)dfs(0) dfs(1)[1, −1, 0, 0]-
31 (−1)0: colour 1 ≠ −1 ✓ · 2: unvisited → dfs(2, 1)dfs(0) dfs(1) dfs(2)[1, −1, 1, 0]-
42 (1)1: −1 ≠ 1 ✓ · 3: unvisited → dfs(3, −1)dfs(0) dfs(1) dfs(2) dfs(3)[1, −1, 1, −1]-
53 (−1)0: 1 ≠ −1 ✓ · 2: 1 ≠ −1 ✓ → returns Truedfs(0) dfs(1) dfs(2)same-
6back in 2, then 1no more children → True, Truedfs(0)same-
7back in 03: −1 ≠ 1 ✓ → True(empty)same-
8i = 1, 2, 3already coloured → skip(empty)sameTrue
deepest stack (step 4)
dfs(0, 1)dfs(1, −1)dfs(2, 1)dfs(3, −1)
final colours (R = 1, Y = −1)
(0 R)———(1 Y)
  |        |
(3 Y)———(2 R)
every edge R–Y ✓

Example 1: expected False

stepnode visited (colour)neighbours checkedcall stack aftercolor afteranswer so far
1dfs(0, 1)1: unvisited → dfs(1, −1)dfs(0) dfs(1)[1, −1, 0, 0]-
21 (−1)0: 1 ≠ −1 ✓ · 2: unvisited → dfs(2, 1)dfs(0) dfs(1) dfs(2)[1, −1, 1, 0]-
32 (1)0: colour 1 == 1 → return Falsedfs(0) dfs(1)[1, −1, 1, 0]False found
4back in 1not dfs(2) is True → return Falsedfs(0)same-
5back in 0return False(empty)same-
6main loopnot dfs(0, 1) → return False--False
stack at the clash (step 3)
dfs(0, 1)dfs(1, −1)dfs(2, 1) → False
the clash
(0 R)———(1 Y)
    \       |
      \     |
       (2 R)       edge 0-2: R and R ✗

Node 3 was never even reached: the first clash ends everything.

9Complexity & remember

Remember Part A0 = unvisited, 1 / −1 = colours. Unvisited child → dfs(child, -c). Visited child with my colour → False. Loop over every node with color[i] == 0, because the graph can be in pieces.
Extra (not from the video): when exactly does the clash happen?
→ Only when there is a cycle with an odd number of edges. Going round a cycle, the colour flips at every edge; after an odd number of flips you're back at the start with the wrong colour. Example 1 has the triangle 0-1-2 (3 edges). The square in Example 2 has 4 edges, so it's fine. A tree (no cycles at all) is always bipartite.

Part B · 2-colouring with BFS

The teacher shows that the same check works level by level with a queue. Steps ①–④ are the same as Part A: same colour array, same three cases for a neighbour, same loop over every component. What changes:

Doubt: does BFS colour nodes differently from DFS?
→ It may visit them in a different order, but in a connected component the colours are forced anyway: once the start is 1, every node at an even number of steps from it must be 1 and every node at an odd number of steps must be −1. BFS makes this very visible: each BFS level has one colour, and levels alternate.

5Approach steps

  1. For every node i with color[i] == 0: colour it 1 and push it.
  2. While the queue isn't empty: pop node; for each child:
  3. child 0 → color[child] = -color[node], push it.
  4. child has the same colour as node → return False.
  5. After all components → return True.

6Code (Python)

Part B · 2-colouring with BFS
from collections import deque

class Solution:
    def isBipartite(self, graph):
        n = len(graph)
        color = [0] * n

        for i in range(n):
            if color[i] != 0:
                continue                         # already checked with its component
            color[i] = 1
            q = deque([i])
            while q:
                node = q.popleft()
                for child in graph[node]:
                    if color[child] == 0:
                        color[child] = -color[node]   # opposite colour
                        q.append(child)
                    elif color[child] == color[node]:
                        return False             # ends the whole function
        return True

7Code line by line

linewhat it means
if color[i] != 0: continueSame fix as Part A: a coloured node was already handled with its component.
color[i] = 1 q = deque([i])New component: colour its first node red and put it in the queue.
node = q.popleft()Take the oldest waiting node; its colour is already set.
color[child] = -color[node] q.append(child)Colour the child the opposite way when pushing, so it can't be pushed twice.
elif color[child] == color[node]: return FalseAn edge inside one group. Return straight away: no recursion to unwind.
return TrueEvery component was coloured without a clash.

8Dry run

Example 2 (square): expected True

stepnode poppedneighbours checkedqueue aftercolor after [0,1,2,3]answer so far
0start i = 0-[0][1, 0, 0, 0]-
10 (1)1 → −1, push · 3 → −1, push[1, 3][1, −1, 0, −1]-
21 (−1)0: 1 ≠ −1 ✓ · 2 → 1, push[3, 2][1, −1, 1, −1]-
33 (−1)0 ✓ · 2: 1 ≠ −1 ✓[2]same-
42 (1)1 ✓ · 3 ✓[ ]same-
5i = 1, 2, 3 coloured → skip-[ ]sameTrue
level 00 Rcolour 1
level 11 Y3 Ycolour −1
level 22 Rcolour 1 again

Example 1: expected False

stepnode poppedneighbours checkedqueue aftercolor afteranswer so far
0start i = 0-[0][1, 0, 0, 0]-
10 (1)1 → −1 · 2 → −1 · 3 → −1, all pushed[1, 2, 3][1, −1, −1, −1]-
21 (−1)0: 1 ≠ −1 ✓ · 2: −1 == −1[2, 3]sameFalse

Here the clash shows up on edge 1-2 (both are neighbours of 0, so both are on level 1 with the same colour). DFS found it on edge 0-2. Different edge, same verdict: the triangle 0-1-2 can't be 2-coloured.

9Complexity & remember

Remember Part BColour when you push, with -color[node]. Same colour on a neighbour → return False right away. Outer loop over all uncoloured nodes.

Part C · Revision page

DFS (recursion)BFS (queue)
colour storecolor = [0]*n: 0 unvisited, 1 / −1 the two groups
start of a componentdfs(i, 1) if color[i] == 0color[i] = 1, push i
unvisited childdfs(child, -c), pass False upcolor[child] = -color[node], push
visited child, same colourreturn False
visited child, other colourfine, ignore
a False reaches the answer byreturning through every callone return (we're in the main function)
time / spaceO(n + E) / O(n)O(n + E) / O(n)
If you remember only 5 lines 1. Bipartite = two groups, every edge crosses between them.
2. Think colours: a neighbour always gets the opposite colour.
3. One array: 0 = unvisited, 1 / −1 = colour; flip with a minus sign.
4. Neighbour already has my colour → False.
5. Loop over all nodes and start only from uncoloured ones: the graph can be in pieces.
Mistakes to avoid ✗ starting only from node 0 (misses other components)
✗ calling dfs on every i without checking color[i] == 0 (re-colours nodes, wrong False)
✗ using 0 as a colour while 0 also means "unvisited"
✗ storing just "visited = 1" and losing the colour
✗ forgetting return True at the end of dfs (None counts as False)
✗ ignoring the result of the recursive call (a deep clash gets lost)
test it yourself (paste under any of the solutions above)
s = Solution()
print(s.isBipartite([[1, 2, 3], [0, 2], [0, 1, 3], [0, 2]]))   # False
print(s.isBipartite([[1, 3], [0, 2], [1, 3], [0, 2]]))         # True
print(s.isBipartite([[1], [0]]))                               # True
print(s.isBipartite([[1, 3], [0, 2], [1, 3], [0, 2], [5, 6], [4, 6], [4, 5]]))   # False
print(s.isBipartite([[]]))                                     # True

Based on this video: Is Graph Bipartite? | Graph | Pattern-wise DSA sheet