A de Bruijn graph built from fixed-length k-mers uses each length-(k−1) prefix as an edge source and its length-(k−1) suffix as the destination. The k-mer itself is the directed edge. A unitig follows a maximal path whose internal vertices have exactly one incoming and one outgoing edge. Branches stop extension, preserving alternative routes instead of silently choosing one. This builder deduplicates input k-mers, rejects mixed lengths and k less than two, and retains explicit incoming and outgoing edge lists. After starting paths at branch or endpoint vertices, it processes any unused edges, which covers isolated one-in/one-out cycles. Each returned sequence spells its represented edges by taking the first k-mer and appending the last character of each successor. It does not estimate coverage, resolve repeats, or assemble an original genome.
De Bruijn graphs: compact non-branching k-mer routes into unitigs
Operational case
The k-mers ATG, TGA, GAC, ACT, CTG, TGC, and GCA form three displayed unitigs: ATG, TGACTG, and TGCA. Vertex TG branches toward GA and GC, so the compacted paths do not claim one unique continuation across it. The first k-mer of every path supplies the initial two characters; appending a successor's final character spells each later edge once. Duplicate input k-mers are collapsed to one edge. If the task needs multiplicity or read depth, a count field must be added instead of assuming the deduplicated graph retains it. An isolated directed cycle may be represented with an arbitrary sorted starting edge; its rotation is not a canonical biological result.
Working Python program
from collections import defaultdict
class KmerRouteIndex:
def __init__(self, kmers):
kmers = set(kmers)
if not kmers or len({len(kmer) for kmer in kmers}) != 1 or len(next(iter(kmers))) < 2:
raise ValueError("need equal-length k-mers with k at least two")
self.k = len(next(iter(kmers)))
outgoing = defaultdict(list)
incoming = defaultdict(list)
for kmer in sorted(kmers):
source, target = kmer[:-1], kmer[1:]
outgoing[source].append(kmer)
incoming[target].append(kmer)
self.vertices = set(outgoing) | set(incoming)
self.outgoing = dict(outgoing)
self.incoming = dict(incoming)
self.edges = kmers
def unitigs(self):
used = set()
paths = []
def extend(first):
sequence = first
used.add(first)
vertex = first[1:]
while len(self.incoming.get(vertex, [])) == 1 and len(self.outgoing.get(vertex, [])) == 1:
successor = self.outgoing[vertex][0]
if successor in used:
break
sequence += successor[-1]
used.add(successor)
vertex = successor[1:]
return sequence
for vertex in sorted(self.vertices):
if len(self.incoming.get(vertex, [])) == 1 and len(self.outgoing.get(vertex, [])) == 1:
continue
for edge in self.outgoing.get(vertex, []):
if edge not in used:
paths.append(extend(edge))
for edge in sorted(self.edges):
if edge not in used:
paths.append(extend(edge))
return paths
if __name__ == "__main__":
index = KmerRouteIndex(["ATG", "TGA", "GAC", "ACT", "CTG", "TGC", "GCA"])
print(index.unitigs())
print(len(index.edges), len(index.vertices))Output
['ATG', 'TGACTG', 'TGCA']
7 7Time, space, and tradeoff
Let E be the number of distinct k-mers and V the number of observed prefix/suffix vertices. Building edge lists takes O(Ek) time due to slicing k-character strings and O(Ek + V) Python storage. Sorting k-mers and vertices for stable output adds O(E log E + V log V) comparisons with string costs. The graph walk marks each edge once, but repeated Python string concatenation can make one long output path quadratic in its length; collecting characters and joining once would remove that avoidable copying. A CSR graph is a better frozen general-purpose adjacency layout, while this structure uses overlap semantics specific to equal-length strings.
Common Mistakes
- Do not extend a unitig through a vertex with two outgoing choices.
- Do not forget isolated one-in/one-out cycles after endpoint walks.
- Do not interpret deduplicated edges as read coverage counts.
- Do not claim that unitig compaction reconstructs one unique source sequence.
Connected lessons
- Graphs
- Data Structures
- CSR graphs: pack sparse neighbors for repeated scans
- Compressed suffix trees: locate patterns across a frozen text
- Suffix arrays: indexed substring search and adjacent LCP
- FM-index backward search: narrow a suffix interval by character
- Condensation DAGs: compress directed cycles before path queries
- Reachability bitsets: precompute directed paths for a fixed graph
- Projects
- Quizzes
Compare the operation boundary with Double-array tries: static incident-code transitions with BASE and CHECK, Hopscotch hashing: keep each shipment near its home bucket, Invertible Bloom tables: peel differences between replica ID sets, then complete the audit project and decision quiz.
