Menu
CoddyTech

Tìm kiếm theo chiều sâu (DFS)

Cập nhật lần cuối

Tìm kiếm theo chiều sâu (depth-first search) duyệt đồ thị bằng cách đi sâu hết mức có thể theo từng nhánh trước khi quay lui. Bắt đầu từ một đỉnh nguồn, thuật toán thăm một đỉnh kề, rồi đỉnh kề của đỉnh đó, cứ thế cho đến khi gặp ngõ cụt (một đỉnh không còn đỉnh kề nào chưa thăm), lúc đó nó quay lui và thử nhánh tiếp theo. Nhấn Phát ở trên để xem thuật toán đi xuống một đường, chạm đáy rồi lùi ra.

DFS được diễn đạt tự nhiên bằng ngăn xếp (stack): hoặc một ngăn xếp tường minh (như trong hoạt ảnh này), hoặc ngăn xếp lời gọi hàm thông qua đệ quy. Thuật toán thăm mỗi đỉnh và mỗi cạnh một lần, nên chạy trong thời gian O(V + E) với đồ thị có V đỉnh và E cạnh.

Độ phức tạp thời gian và không gian

Tiêu chíĐộ phức tạpGhi chú
Thời gianO(V + E)Mỗi đỉnh và mỗi cạnh được thăm một lần
Không gianO(V)Ngăn xếp + tập đã thăm, xấu nhất chứa mọi đỉnh
Cách duyệtSâu nhất trướcĐi hết một nhánh rồi mới sang nhánh khác
Cấu trúc dữ liệuNgăn xếp (hoặc đệ quy)Thứ tự LIFO tạo ra cách duyệt theo chiều sâu

Từng bước thực hiện

BướcĐiều gì xảy ra
1Đẩy đỉnh nguồn vào ngăn xếp.
2Lấy một đỉnh ra; nếu đã thăm thì bỏ qua.
3Đánh dấu đã thăm và ghi lại cạnh dẫn đến nó.
4Đẩy tất cả các đỉnh kề chưa thăm của nó vào ngăn xếp.
5Lặp lại cho đến khi ngăn xếp rỗng.

Ví dụ minh họa

DFS dạng vòng lặp từ A trên đồ thị A → [B, C], B → [D], C → [E], đẩy các đỉnh kề theo thứ tự liệt kê (ngăn xếp LIFO lấy ra phần tử được đẩy vào sau cùng trước):

BướcNgăn xếp (đỉnh ở bên phải)Đã thămThao tác
1[A]{}Đẩy đỉnh nguồn A.
2[B, C]{A}Lấy A ra, đánh dấu đã thăm, đẩy các đỉnh kề B rồi C.
3[B, E]{A, C}Lấy C ra, đánh dấu đã thăm, đẩy đỉnh kề E.
4[B]{A, C, E}Lấy E ra, đánh dấu đã thăm, không có đỉnh kề.
5[D]{A, C, E, B}Lấy B ra, đánh dấu đã thăm, đẩy đỉnh kề D.
6[]{A, C, E, B, D}Lấy D ra, đánh dấu đã thăm, ngăn xếp rỗng: xong.

Khi nào nên dùng DFS

Nên dùng khiTránh dùng khi
Bạn cần phát hiện chu trình, sắp xếp topo hoặc tìm các thành phần liên thông.Bạn cần đường đi ngắn nhất trên đồ thị không trọng số: BFS làm được, DFS thì không.
Đồ thị rộng và nông, nên ngăn xếp tốn ít bộ nhớ hơn nhiều so với một hàng đợi chứa nhiều đỉnh biên.Đồ thị rất sâu và bạn dùng đệ quy: ngăn xếp lời gọi hàm có thể bị tràn.
Bạn muốn duyệt cạn mọi đường đi, như khi giải mê cung hoặc tìm kiếm quay lui.Bạn cần phát hiện các đỉnh theo thứ tự khoảng cách từ đỉnh nguồn.
Bạn đang kiểm tra khả năng đến được hoặc có tồn tại đường đi giữa hai đỉnh hay không.Bạn phải bảo đảm đường đi tìm được có số cạnh ít nhất.

Một cách cài đặt Depth-First Search gọn gàng, chạy được ngay bằng Python, JavaScript, Java, C++, C. Chọn một ngôn ngữ, sao chép code hoặc mở sẵn trong Coddy Playground.

Code Depth-First Search bằng Python

Python
1def dfs(graph, node, visited):2    visited.add(node)3    order = [node]4    for neighbor in graph[node]:5        if neighbor not in visited:6            order.extend(dfs(graph, neighbor, visited))7    return order8
9
10graph = {11    "A": ["B", "C"],12    "B": ["D", "E"],13    "C": ["F"],14    "D": [],15    "E": ["F"],16    "F": [],17}18
19order = dfs(graph, "A", set())20print("DFS order:", " -> ".join(order))
Chạy code này trong Playground Python

Câu hỏi thường gặp về tìm kiếm theo chiều sâu

Độ phức tạp thời gian của DFS là bao nhiêu?
DFS chạy trong thời gian O(V + E), với V là số đỉnh và E là số cạnh, vì thuật toán thăm mỗi đỉnh một lần và xét mỗi cạnh một lần. DFS dùng O(V) bộ nhớ cho ngăn xếp và tập đã thăm.
DFS và BFS khác nhau như thế nào?
DFS dùng ngăn xếp và đi sâu theo một nhánh trước khi quay lui, còn BFS dùng hàng đợi và duyệt theo từng tầng, đỉnh gần nhất trước. BFS tìm được đường đi ngắn nhất trên đồ thị không trọng số; DFS thì không, nhưng tốn ít bộ nhớ hơn trên đồ thị rộng và phù hợp tự nhiên với các bài toán như phát hiện chu trình và sắp xếp topo.
DFS dùng đệ quy hay vòng lặp?

Cả hai đều được. Phiên bản đệ quy dùng ngầm ngăn xếp lời gọi hàm của chương trình và rất ngắn gọn; phiên bản vòng lặp dùng một ngăn xếp tường minh (như hoạt ảnh ở đây) và tránh tràn ngăn xếp do đệ quy quá sâu trên đồ thị lớn.

Khi nào nên dùng DFS thay cho BFS?

Dùng DFS khi bạn cần duyệt trọn các đường đi: phát hiện chu trình, sắp xếp topo, tìm thành phần liên thông hoặc tìm kiếm quay lui như giải mê cung. DFS cũng tốn ít bộ nhớ hơn trên đồ thị rộng, vì ngăn xếp mỗi lúc chỉ giữ biên của một nhánh. Hãy chọn BFS khi bạn cần đường đi ngắn nhất trên đồ thị không trọng số hoặc cần các đỉnh theo thứ tự khoảng cách từ đỉnh nguồn.

DFS có tìm được đường đi ngắn nhất không?
Không đáng tin cậy. DFS trả về đường đi đầu tiên mà nó tình cờ tìm thấy, thường không phải đường có ít cạnh nhất, vì nó ưu tiên đi sâu thay vì lan rộng đều ra xung quanh. Để tìm đường đi ngắn nhất trên đồ thị không trọng số, hãy dùng BFS, còn với đồ thị có trọng số, hãy dùng thuật toán Dijkstra.
Vì sao DFS cần tập đã thăm?
Không có tập đã thăm, DFS sẽ thăm lại các đỉnh có thể đến được bằng nhiều đường và, trên đồ thị có chu trình, sẽ lặp vô hạn. Đánh dấu một đỉnh đã thăm khi lấy nó ra (và bỏ qua những lần lấy ra đỉnh đã thăm) bảo đảm mỗi đỉnh chỉ được xử lý một lần và giữ thời gian chạy ở O(V + E). Một sai lầm phổ biến là chỉ đánh dấu khi lấy ra nhưng vẫn đẩy các bản trùng vào, cách này vẫn đúng nhưng có thể để lại các mục cũ trên ngăn xếp cần phải bỏ qua.
Hình minh họa các ngôn ngữ lập trình của Coddy

Thành thạo thuật toán cùng Coddy

Bắt đầu