Forberedelse til kodeinterviews · Lektion

BFS: korteste sti og niveaugennemløb

Brug BFS til at finde den korteste sti i en uvægtet graf, løs word-ladder niveau for niveau, og klon en graf med et hash map.

Lektion 2 af 413 trin

BFS: korteste sti og niveaugennemløb er en gratis Forberedelse til kodeinterviews-lektion på CoddyKit. Dette er lektion 2 af 4. Du kan læse hele lektionen gratis nedenfor — og derefter øve dig praktisk i browseren med en indbygget kodeeditor og en AI-vejleder, der er tilgængelig døgnet rundt. Den er en del af læringsforløbet i Forberedelse til kodeinterviews, og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. Forberedelse til kodeinterviews-kurset indeholder 4 lektioner i alt.

BFS og korteste vej i uvægtede grafer

BFS finder den korteste vej (færrest kanter) i en uvægtet graf, fordi den gennemgår knuder i rækkefølge efter stigende afstand fra kilden. Første gang en knude nås under BFS, sker det ad den kortest mulige vej. Denne egenskab gælder ikke for DFS. Til vægtede grafer med ikke-negative vægte skal du i stedet bruge Dijkstras algoritme — BFS antager implicit, at alle kanter har vægten 1.

from collections import deque, defaultdict

def shortest_path(graph, start, end):
    if start == end:
        return 0
    visited = {start}
    queue = deque([(start, 0)])  # (node, distance)
    while queue:
        node, dist = queue.popleft()
        for neighbour in graph[node]:
            if neighbour == end:
                return dist + 1
            if neighbour not in visited:
                visited.add(neighbour)
                queue.append((neighbour, dist + 1))
    return -1  # no path found

graph = defaultdict(list)
for u, v in [(0,1),(1,2),(2,3),(0,3),(1,4)]:
    graph[u].append(v); graph[v].append(u)
print(shortest_path(graph, 0, 3))  # 1 (direct edge)
print(shortest_path(graph, 0, 4))  # 2 (0->1->4)

Sporing af den faktiske korteste vej

Hvis du vil genskabe den faktiske vej (ikke kun dens længde), skal du vedligeholde en forældreordbog, der registrerer, hvordan hver knude blev nået. Når du når destinationen, følger du forældrekortet baglæns fra slutning til begyndelse og vender resultatet om. Det tilføjer O(V) plads til forældrekortet, men giver hele vejen i O(path_length)-tid, efter BFS er færdig.

from collections import deque, defaultdict

def shortest_path_with_route(graph, start, end):
    parent = {start: None}
    queue = deque([start])
    while queue:
        node = queue.popleft()
        if node == end:
            break
        for nb in graph[node]:
            if nb not in parent:
                parent[nb] = node
                queue.append(nb)
    if end not in parent:
        return []  # no path
    # Reconstruct path by tracing back
    path = []
    node = end
    while node is not None:
        path.append(node)
        node = parent[node]
    return path[::-1]  # reverse

graph = defaultdict(list)
for u, v in [(0,1),(1,2),(2,3),(0,4),(4,3)]:
    graph[u].append(v); graph[v].append(u)
print(shortest_path_with_route(graph, 0, 3))  # [0, 4, 3] or [0, 1, 2, 3]

Ordstige: BFS på en implicit graf

Ordstige (LeetCode #127) spørger, hvor mange ændringer af et enkelt tegn der mindst kræves for at omdanne et startord til et slutord, hvor hvert mellemled skal findes i en ordbog. Dette er en BFS på en implicit graf, hvor knuder er ord, og kanter forbinder ord, der adskiller sig med ét bogstav. Generér alle ændringer af ét bogstav, og kontrollér, om de findes i ordmængden. BFS garanterer den korteste omdannelsessekvens.

from collections import deque

def word_ladder(begin_word, end_word, word_list):
    word_set = set(word_list)
    if end_word not in word_set:
        return 0
    queue = deque([(begin_word, 1)])
    visited = {begin_word}
    while queue:
        word, steps = queue.popleft()
        for i in range(len(word)):
            for c in 'abcdefghijklmnopqrstuvwxyz':
                new_word = word[:i] + c + word[i+1:]
                if new_word == end_word:
                    return steps + 1
                if new_word in word_set and new_word not in visited:
                    visited.add(new_word)
                    queue.append((new_word, steps + 1))
    return 0

print(word_ladder('hit', 'cog', ['hot','dot','dog','lot','log','cog']))  # 5

Niveaugennemløb: sporing af afstand

Niveaugennemløb grupperer knuder efter deres afstand fra kilden, hvilket er direkte nyttigt i opgaver, der kræver behandling pr. niveau. Spor afstanden enten ved at gemme den i køelementet som en tupel (node, dist) eller ved at bruge teknikken baseret på køens størrelse (registrer køens størrelse før hvert niveau, behandl præcis så mange knuder, og øg derefter niveautælleren). Begge tilgange giver identiske resultater.

from collections import deque, defaultdict

def bfs_levels(graph, start):
    levels = {}
    visited = {start}
    queue = deque([start])
    dist = 0
    while queue:
        # Process all nodes at current distance
        for _ in range(len(queue)):
            node = queue.popleft()
            levels[node] = dist
            for nb in graph[node]:
                if nb not in visited:
                    visited.add(nb)
                    queue.append(nb)
        dist += 1
    return levels

graph = defaultdict(list)
for u, v in [(0,1),(0,2),(1,3),(2,3),(3,4)]:
    graph[u].append(v); graph[v].append(u)
print(bfs_levels(graph, 0))  # {0:0, 1:1, 2:1, 3:2, 4:3}

Kloning af graf

Kloning af graf (LeetCode #133) opretter en dyb kopi af en sammenhængende ikke-rettet graf. Brug BFS og en hash-tabel, der knytter oprindelige knuder til deres kloner. Når du besøger en knude første gang, opretter du dens klon og føjer den til tabellen. Når du behandler naboer, slår du deres kloner op eller opretter dem og forbinder kanterne. Hash-tabellen har to formål: at holde styr på besøgte knuder og at knytte originaler til kopier.

from collections import deque

class Node:
    def __init__(self, val=0, neighbors=None):
        self.val = val
        self.neighbors = neighbors if neighbors is not None else []

def clone_graph(node):
    if not node:
        return None
    old_to_new = {node: Node(node.val)}
    queue = deque([node])
    while queue:
        curr = queue.popleft()
        for nb in curr.neighbors:
            if nb not in old_to_new:
                old_to_new[nb] = Node(nb.val)
                queue.append(nb)
            old_to_new[curr].neighbors.append(old_to_new[nb])
    return old_to_new[node]

# Build a simple graph: 1 -- 2 -- 3 -- 4 -- 1
n1 = Node(1); n2 = Node(2); n3 = Node(3); n4 = Node(4)
n1.neighbors = [n2, n4]; n2.neighbors = [n1, n3]
n3.neighbors = [n2, n4]; n4.neighbors = [n3, n1]
cloned = clone_graph(n1)
print(cloned.val, [n.val for n in cloned.neighbors])  # 1 [2, 4]

Tovejs-BFS

Tovejs-BFS starter BFS fra både kilden og destinationen samtidig og udvider én niveau ad gangen fra hver ende. Når de to søgefronter mødes, har du fundet den korteste vej. I store grafer reducerer dette søgeområdet fra O(b^d) til O(2 * b^(d/2)), hvor b er forgreningsfaktoren, og d er vejens længde — en markant forbedring for tæt forbundne grafer som ordstiger med store ordbøger.

from collections import defaultdict

def word_ladder_bidir(begin, end, word_list):
    word_set = set(word_list)
    if end not in word_set:
        return 0
    front, back = {begin}, {end}
    visited = {begin, end}
    steps = 1
    while front and back:
        # Always expand the smaller frontier
        if len(front) > len(back):
            front, back = back, front
        next_front = set()
        for word in front:
            for i in range(len(word)):
                for c in 'abcdefghijklmnopqrstuvwxyz':
                    nw = word[:i] + c + word[i+1:]
                    if nw in back:  # frontiers met!
                        return steps + 1
                    if nw in word_set and nw not in visited:
                        visited.add(nw)
                        next_front.add(nw)
        front = next_front
        steps += 1
    return 0

print(word_ladder_bidir('hit','cog',['hot','dot','dog','lot','log','cog']))  # 5

0-1-BFS til vægtede grafer

0-1-BFS håndterer grafer, hvor kantvægtene kun er 0 eller 1. I stedet for en almindelig kø bruger du en dobbeltkø: føj elementer til bagenden for kanter med vægten 1 (næste niveau) og til forenden for kanter med vægten 0 (samme niveau). Det giver en beregning af korteste veje i O(V + E) — hurtigere end Dijkstras O((V+E) log V), når vægtene er binære. Det er almindeligt i gitteropgaver, hvor nogle træk er gratis, mens andre koster 1.

from collections import deque

def zero_one_bfs(graph, start, n):
    # graph: list of (neighbour, weight) where weight is 0 or 1
    dist = [float('inf')] * n
    dist[start] = 0
    dq = deque([start])
    while dq:
        node = dq.popleft()
        for nb, w in graph[node]:
            if dist[node] + w < dist[nb]:
                dist[nb] = dist[node] + w
                if w == 0:
                    dq.appendleft(nb)   # same level
                else:
                    dq.append(nb)       # next level
    return dist

# Simple test:
graph = [[(1, 0), (2, 1)],   # node 0: free to 1, cost 1 to 2
         [(3, 1)],            # node 1: cost 1 to 3
         [(3, 0)],            # node 2: free to 3
         []]
print(zero_one_bfs(graph, 0, 4))  # [0, 0, 1, 1]

Mure og porte (BFS med flere kilder)

Mure og porte udfylder hvert tomt rum med afstanden til den nærmeste port. Brug BFS med flere kilder: initialisér samtidig køen med alle porte (værdi 0), og udvid søgningen udad. Hver celles værdi sættes til det niveau, hvor cellen nås første gang. Denne løsning i O(mn) er mere effektiv end at køre BFS separat fra hvert tomt rum, hvilket ville give O(m²n²).

from collections import deque

def walls_and_gates(rooms):
    if not rooms:
        return
    rows, cols = len(rooms), len(rooms[0])
    INF = float('inf')
    queue = deque()
    # Multi-source: all gates at distance 0
    for r in range(rows):
        for c in range(cols):
            if rooms[r][c] == 0:  # gate
                queue.append((r, c))
    dirs = [(0,1),(0,-1),(1,0),(-1,0)]
    while queue:
        r, c = queue.popleft()
        for dr, dc in dirs:
            nr, nc = r+dr, c+dc
            if 0<=nr<rows and 0<=nc<cols and rooms[nr][nc]==INF:
                rooms[nr][nc] = rooms[r][c] + 1
                queue.append((nr, nc))

rooms = [[float('inf'),-1,0,float('inf')],
         [float('inf'),float('inf'),float('inf'),-1],
         [float('inf'),-1,float('inf'),-1],
         [0,-1,float('inf'),float('inf')]]
walls_and_gates(rooms)
print(rooms[0][0], rooms[1][1])  # 3, 2

BFS til slanger og stiger

Slanger og stiger (LeetCode #909) er en BFS-opgave om korteste vej på et talgitter. Modellér brættet som en uvægtet graf, hvor du kan kaste 1-6 fra en vilkårlig rude og eventuelt lande på en slange eller stige, der teleporterer dig. BFS finder det minimale antal terningekast. Den vigtigste udfordring er at konvertere mellem en endimensional position og todimensionale brætkoordinater, mens der tages højde for det boustrofedoniske layout (skiftende retning på rækkerne).

from collections import deque

def snakes_and_ladders(board):
    n = len(board)
    def get_board(pos):
        r, c = divmod(pos - 1, n)
        if r % 2 == 1: c = n - 1 - c  # alternating direction
        return board[n - 1 - r][c]

    visited = {1}
    queue = deque([(1, 0)])
    while queue:
        pos, moves = queue.popleft()
        for dice in range(1, 7):
            next_pos = pos + dice
            if next_pos > n * n:
                break
            val = get_board(next_pos)
            if val != -1:
                next_pos = val  # snake or ladder
            if next_pos == n * n:
                return moves + 1
            if next_pos not in visited:
                visited.add(next_pos)
                queue.append((next_pos, moves + 1))
    return -1

print('BFS models game as an unweighted shortest-path problem')

BFS-kompleksitet og optimeringer

BFS' tidskompleksitet er O(V + E), fordi hver knude indsættes i køen én gang, og hver kant undersøges et konstant antal gange. Pladskompleksiteten er O(V) for mængden af besøgte knuder og køen. For gittergrafer er V = m*n og E = 4*m*n (hver celle har 4 naboer), så BFS på et gitter er O(mn). En vigtig optimering er at bruge et sæt til besøgte knuder (O(1)-opslag), ikke en liste (O(n)-opslag). Markér en knude som besøgt, når du indsætter den i køen, ikke når du tager den ud.

# BFS on a graph with V vertices and E edges:
# Time:  O(V + E) -- each vertex and edge visited once
# Space: O(V)     -- visited set + queue

# BFS on an m x n grid:
# V = m*n cells
# E <= 4*m*n edges (4 directions, max)
# Time:  O(m*n)
# Space: O(m*n)

# Common pitfalls:
# 1. Marking visited on dequeue (not enqueue) -> same node queued multiple times
# 2. Using a list for visited -> O(n) membership check -> O(V*E) total
# 3. Not handling disconnected graph -> BFS from single source misses components
print('O(V+E) time, O(V) space -- mark visited on enqueue')

Nærmeste 0 i en binær matrix

01-matricen (LeetCode #542) finder afstanden fra hver celle til den nærmeste 0. BFS med flere kilder fra alle 0'er samtidig giver den optimale løsning i O(mn). Initialisér køen med alle 0-celler i afstand 0 og alle 1-celler med uendelig afstand. BFS spreder afstandene ud fra 0'erne og sætter afstanden for hver 1-celle første gang, den nås (hvilket garanterer den korteste afstand).

from collections import deque

def update_matrix(mat):
    rows, cols = len(mat), len(mat[0])
    dist = [[float('inf')] * cols for _ in range(rows)]
    queue = deque()
    for r in range(rows):
        for c in range(cols):
            if mat[r][c] == 0:
                dist[r][c] = 0
                queue.append((r, c))
    dirs = [(0,1),(0,-1),(1,0),(-1,0)]
    while queue:
        r, c = queue.popleft()
        for dr, dc in dirs:
            nr, nc = r+dr, c+dc
            if 0<=nr<rows and 0<=nc<cols:
                if dist[r][c] + 1 < dist[nr][nc]:
                    dist[nr][nc] = dist[r][c] + 1
                    queue.append((nr, nc))
    return dist

mat = [[0,0,0],[0,1,0],[1,1,1]]
result = update_matrix(mat)
for row in result: print(row)  # [[0,0,0],[0,1,0],[1,2,1]]

Hurtigt tjek

Test din forståelse af begreberne Data Structures & Algorithms — Coding Interview Prep fra denne lektion.

Opsummering af lektionen

I denne lektion lærte du om: BFS til korteste veje i uvægtede grafer med sporing af forældre til genskabelse af ruter, ordstige som en klassisk BFS på en implicit graf, tovejs-BFS til store grafer og BFS med flere kilder til opgaver med flere startpunkter. Næste gang bruger vi DFS til sammenhængende komponenter og flood fill.

Gratis at komme i gang

Lær Forberedelse til kodeinterviews med en AI-underviser — gratis

Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.

Kurser
90
Lektioner
360

Ofte stillede spørgsmål

Er lektionen “BFS: korteste sti og niveaugennemløb” gratis?

Ja — hele teksten til “BFS: korteste sti og niveaugennemløb” kan læses gratis her på nettet. Hvis du vil øve dig interaktivt med en indbygget kodeeditor og en AI-vejleder døgnet rundt og få adgang til resten af Forberedelse til kodeinterviews-kurset, skal du opgradere til CoddyKit PRO. Forberedelse til kodeinterviews-kurset indeholder 4 lektioner i alt.

Hvad lærer jeg i “BFS: korteste sti og niveaugennemløb”?

Brug BFS til at finde den korteste sti i en uvægtet graf, løs word-ladder niveau for niveau, og klon en graf med et hash map. Du øver dig i Forberedelse til kodeinterviews med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.

Skal jeg have erfaring for at begynde på Forberedelse til kodeinterviews?

Der kræves ingen tidligere erfaring. Forberedelse til kodeinterviews på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 2 af 4.

Hvor lang tid tager lektionen “BFS: korteste sti og niveaugennemløb”?

De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.

Kan jeg skrive og køre kode i denne Forberedelse til kodeinterviews-lektion?

Ja. Alle Forberedelse til kodeinterviews-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.

Alle lektioner i dette kursus

  1. Grafrepræsentationer og opsætning af gennemløb
  2. BFS: korteste sti og niveaugennemløb
  3. DFS: sammenhængende komponenter og flood fill
  4. Cyklusdetektion i rettede og ikke-rettede grafer
← Tilbage til Forberedelse til kodeinterviews