skip to content

Why does a two-coloring check seeded only at one start vertex wrongly report bipartite?

level: middleimportance: should knowfreq 40%

answer

  1. what did the sweep never visit?
  2. one seed reaches one component
  3. disconnected conflict lists are the norm
  4. wrap the sweep in an outer loop
  5. seed again from any uncolored vertex

basics

~20 s

Because bipartiteness is a whole-graph property checked per component. A sweep seeded at one vertex reaches only that component, so an odd cycle in an unvisited component is never examined. Restart the coloring from every still-uncolored vertex.

solid answer

~50 s

A single-seed sweep answers a narrower question than it claims: it decides whether the seed's connected component is two-colorable, then returns that verdict for the whole graph. Real conflict lists are routinely disconnected — one cluster of feuding vendors here, an unrelated cluster there — so a graph whose second cluster holds a five-vertex conflict ring passes the check and the seating still fails in the room. The fix is a loop over all vertices: whenever a vertex is still uncolored, seed a fresh sweep from it and continue with the **same** shared color state, so no vertex is ever processed twice. Return false the moment any component clashes; return true only after the loop ends. Total cost stays `O(V+E)`, since each vertex is seeded at most once. Note the two colorings of different components are independent — flipping one component's sides is still valid.

code

pseudocode · 13 lines
pseudocode
color = array of size n, every entry = -1
color[0] = 0
Q = empty queue
enqueue(Q, 0)
while Q is not empty:
    u = dequeue(Q)
    for v in adj[u]:
        if color[v] == -1:
            color[v] = 1 - color[u]
            enqueue(Q, v)
        else if color[v] == color[u]:
            return NOT_BIPARTITE
return BIPARTITE

go deeper

for a junior

Know that a graph can come in several disconnected pieces and that a traversal only reaches the piece it starts in. Being able to say that much already catches the bug.

for a middle

Explain the fix precisely: an outer loop that seeds a sweep from every still-uncolored vertex, sharing one color state, with the cost still linear in vertices plus edges.

for a senior

Point at the test gap — a suite of connected inputs passes this bug forever — and require a disconnected case, an isolated vertex, and an empty graph in the fixtures.

for a principal

Frame it as a class of defect rather than one bug: any global graph property computed by local traversal needs the outer seeding loop, so make that a review checkpoint rather than a fix.

## The bug in one line The sweep is correct; the **driver** is not. A traversal seeded once explores exactly the connected component of its seed, and a graph can have many components. Bipartiteness must hold for all of them, so a verdict computed from one component is a verdict about one component. ## Why this is not a contrived input Conflict data is disconnected by nature. A list of vendor pairs who must not share a table describes several independent feuds, not one giant web; a list of people who must be kept apart in a rota has clusters that never touch. It is entirely normal for a graph built from such a list to have dozens of components, plus a long tail of isolated vertices — entities that appear in the roster but in no conflict at all. A single-seed check on that input inspects one feud and blesses the rest sight unseen. The failure is silent and one-directional, which is what makes it dangerous: the check never reports a false 'not bipartite', only a false 'bipartite'. It fails in the direction where nothing complains until the real-world assignment blows up. ## The fix Wrap the sweep in an outer loop: 1. Keep one color array for the whole graph, all slots unset. 2. For each vertex `s` from `0` to `n-1`: if `color[s]` is still unset, seed a new sweep at `s` with color 0. 3. Any clash found in any sweep ends the whole check with 'not bipartite'. 4. Only after the outer loop completes without a clash do you report 'bipartite'. Two details matter. First, **do not reset the color array between components** — the shared state is what stops already-finished vertices being re-explored, and clearing it turns a linear check into repeated work or an endless loop. Second, `color[s]` being unset is itself the test for 'this vertex belongs to a component nobody has touched'; you do not need a separate visited marker, because 'colored' and 'visited' are the same fact here. ## Cost Still `O(V+E)`. The outer loop scans all `V` vertices, but it only *seeds* a sweep at a vertex that is still unset, and a seeded sweep colors every vertex it reaches. So each vertex is enqueued once across the entire run, and each edge is examined from each of its endpoints once. Extra space is `O(V)` for colors plus the frontier or recursion stack. ## Independence of components Each component is colored on its own. If a graph has `c` components and is bipartite, there are `2^c` valid colorings, because each component's assignment can be flipped independently without breaking any edge — no edge crosses between components, so nothing constrains their relative orientation. A caller who needs one global 'side A / side B' split may pick any of them. This is worth stating out loud in an interview: candidates sometimes 'fix' the per-component bug and then invent a second, unnecessary rule that all components must start with the same color. Edge cases fall out of the same reasoning. A graph with no edges is bipartite (every vertex is isolated; put them all on one side, or split them arbitrarily). A graph with a single vertex is bipartite. An empty graph is vacuously bipartite. None of these should be special-cased; the loop handles them. ## The same bug wearing other clothes The defect is in the seeding, not the traversal, so switching from a queue-driven sweep to a recursive one changes nothing — a depth-first version seeded once has exactly the same hole. The same shape of bug appears in any whole-graph property computed by traversal: counting components, detecting cycles in an undirected graph, labeling reachability. Whenever a property must hold globally but a traversal only sees locally, the outer 'for every unvisited vertex' loop is the part that turns a component algorithm into a graph algorithm. ## How to catch it The cheapest test case is two disjoint blocks: a well-behaved four-vertex ring plus a five-vertex ring, with no edge between them. Any correct implementation must say 'not bipartite' regardless of which block the vertex numbering puts first. A test suite with only connected inputs will pass a broken implementation forever, which is the more general lesson: graph test data must include a disconnected case, an isolated vertex, and an empty graph, or the driver logic is untested.

  • Does looping over every component change the overall complexity?
    No, it stays `O(V+E)`. The outer loop scans all vertices but only seeds a sweep where the color is still unset, and each sweep colors everything it reaches. So every vertex is enqueued once and every edge inspected from each endpoint once, with `O(V)` extra space for the colors.
  • Must the two sides be consistent across components?
    No. No edge crosses between components, so nothing constrains their relative orientation — each component can be flipped independently. A bipartite graph with `c` components has `2^c` valid colorings, and a caller needing one global split may take any of them.
  • Would a recursive depth-first version have the same bug?
    Yes. The defect lives in the seeding, not the traversal order: a single recursive call from one vertex explores exactly that component too. Swapping the queue for recursion changes only the discovery order and the space profile, never which vertices are reached.

saying these in an interview costs you the question

  • Assumes the input graph is always connected
  • Says a disconnected graph cannot be bipartite
  • Clears the color state between components and reprocesses vertices
  • Reports bipartite as soon as one component colors cleanly
  • Insists all components must start from the same color
  • Adds a separate visited marker alongside the color state

context