Dfs và bfs khác nhau như thế nào
Nếu 1 người nào đó trong gia phả muốn tìm kiếm 1 người nào đó vẫn còn sống thì ta thấy DFS thường hoạt động nhanh hơn.Tuy nhiên, nếu người đó muốn tìm một người nào đó đã mất cách đây rất lâu thì BFS thường nhanh hơn DFS.
Nói tóm lại , cái nào tốt hơn cái nào thì còn tùy thuộc vào dữ liệu và những gì bạn đang tìm kiếm. Code: nếu bạn nào rành C# thì sẽ thấy code rất gần với mã giả vì trong C# đã hỗ trợ kiểu dữ liệu tree, stack, queue, list(có sẵn các hàm truy vấn dữ liệu luôn)… vì vậy công việc của bạn chỉ là gọi đúng hàm đúng việc là được. Ah còn câu thầy hỏi BFS và DFS là thuật toán hay giải thuật mình xin có vài ý kiến. Theo mình biết vẫn chưa có định nghĩa thuật toán nào được chấp nhận cả và ít thông dụng, nhưng còn giải thuật (đặc biệt là giải thuật heurictis) thì được sử dụng rộng rãi. Để đáp ứng những yêu cầu của thuật toán đòi hỏi thời gian chạy rất lớn, hoặc các điều kiện cho thuật toán khó đáp ứng(tính xác định và tính đúng đắn). Tuy nhiên ngày nay người ta đã mở rộng 2 tiêu chuẩn này.Chẳng hạn mở rộng tính xác định thể hiện qua các giải thuật đệ quy và ngẫu nhiên.Còn tính chính xác thì người ta đã chấp nhận các cách giải ít phức tạp và cho kết quả tốt(nhưng không phải lúc nào cũng tốt). Các cách giải chấp nhận được nhưng không hoàn toàn đáp ứng đủ các tiêu chuẩn của thuật toán được gọi là giải thuật. Vì vậy mình nghĩ BFS và DFS là thuật toán. Vì nó thỏa 2 yêu cầu của thuật toán.Dữ liệu là có giới hạn, chẳng hạn các trạng thái của 1 ván cờ vua tuy nhiều nhưng có giới hạn vì nó giới hạn trong phạm vi bàn cờ, giới hạn số quân và giới hạn cách đi của mỗi quân cờ. Vậy là thỏa tính xác định. Tính đúng đắn thì khỏi bàn cải, chỉ cần thứ mình tìm có tồn tại trong dữ liệu chắc chắn sẽ tìm thấy, vì nó duyệt qua hết “ngóc ngách” của dữ liệu. Trong bài này, class function bfs(s, e) { Breadth first search là thuật toán tìm kiếm đơn giản nhất trong graph và có thể được sử dụng nhiều trong các thuật toán khác: thuật toán của Prim tìm cây khung nhỏ nhất, thuật toán Dijkstra tìm đường đi ngắn nhất cũng sử dụng ý tưởng giống với BFS. Thuật toán BFS thường tỏ ra hiệu quả khi mình cần tìm đường đi ngắn nhất (shortest path) trong một đồ thị không trọng số (unweighted graph). Ý tưởng của thuật toán BFS là mình sẽ bắt đầu tại một node (thường gọi là source) của đồ thị sau đó “loang” ra các node kề (hoặc có cạnh nối) với node source, trước khi đi đến những node kề xa hơn. Để biết được những node nào sẽ được thăm, BFS sử dụng một queue (hàng đợi) để lưu thông tin các node kề với node đang được thăm. Đây cũng là một khác biệt quan trọng của BFS với DFS, mình có thể viết DFS dưới dạng đệ quy (gọi lại hàn DFS(), giống hình ảnh của Stack — ngăn xếp) nhưng khi implement BFS thì cần nhớ thuật toán này sử dụng Queue. Queue Q có thể được implement đơn giản bằng một mảng và hai thao tác enqueue, dequeue tương ứng với thao tác push và shift (để đảm bảo tính FIFO của queue) trong array. function Queue() { Khi nói đến độ phức tạp của thuật toán BFS này, trong giải thuật trên, chúng ta có vòng lặp đầu tiên để khởi tạo những giá trị ban đầu cho các đỉnh trong G.V trừ đỉnh source (gốc), có O(V) time complexity. Các thao tác gán giá trị cho đỉnh source sau đó mất O(1). Việc enqueue và dequeue cũng mất thời gian O(1). Vòng lặp khi queue Q khác rỗng thực chất là mình đang đi duyệt tất cả các cạnh trong danh sách kề với đỉnh u. Như vậy, khi vòng lặp kết thúc, tất cả các cạnh sẽ được duyệt nên mình có time complexity là O(E). Từ đó mình có thể kết luận độ phức tạp về mặt thời gian cho thuật toán này là O(V+E). Bidirectional searchBidirectional search được sử dụng để tìm đường đi ngắn nhất giữa 2 đỉnh source và destination. Thuật toán chạy hai hàm BFS, một bắt đầu từ đỉnh source và một bắt đầu từ đỉnh destination. Trong thuật toán BFS nguyên thủy, mình search k nodes kề với node gốc, mỗi node kề lại có thể có thêm k′ nodes kề với node kth. Nếu mình thực hiện d lần thì mình phải search qua O(k^d) nodes. Ví dụ trong hình trên là d=4. Bidirectional search sẽ giúp mình giảm xuống còn khoảng k^(d/2) nodes cho mỗi thuật toán chạy BFS từ đỉnh s và từ đỉnh t. Implement thuật toán BFSSau khi đã tìm hiểu ý tưởng của thuật toán BFS, mình sẽ implement thuật toán này với JavaScript. Yêu cầu của mình hiện tại là hàm function bfs(s, e) { Để thuận tiện, hàm function bfs(s, e) { function bfs(s, e) { function bfs(s, e) { function bfs(s, e) { function bfs(s, e) { function bfs(s, e) { function bfs(s, e) { Việc chứng minh đường đi (nếu có) từ s đến e, trong trường hợp test của mình là từ 0 đến 3 theo BFS cũng chính là đường đi ngắn nhất từ s đến e ( từ 0 đến 3) (theo định nghĩa đi qua ít cạnh nhất), các bạn có thể xem trong sách Introduction to Algorithms, CLRS (implement bằng mã giả) hoặc tài liệu của thầy Lê Minh Hoàng (implement bằng Pascal). Thử một đồ thị có nhiều đỉnh và cạnh hơn để xem thuật toán của mình chạy như thế nào: const graph = new Graph(13); Ngoài ra, cách cài đặt trên cho hàm function bfs(s, e) { Tương tự như thuật toán BFS, mình cũng sẽ tìm hiểu template của thuật toán DFS trước bằng mã giả sau đó implement chi tiết bằng JavaScript. Thuật toán DFS ưu tiên duyệt theo chiều “sâu”. Bắt đầu từ node source, DFS sẽ duyệt các node thuộc cùng một nhánh đến khi không thể đi xa hơn được nữa, mình mới chuyển sang duyệt nhánh tiếp theo (đệ quy backtrack). Thuật toán cứ tiếp tục đến khi tất cả các cạnh của đồ thị được duyệt quạ Như vậy, có thể thấy về mặt time complexity, DFS cũng giống như BFS là O(V+E). Trong các bài toán tìm đường đi ngắn nhất hoặc duyệt để kiểm tra có hay không đường đi giữa các đỉnh thì thuật toán BFS tỏ ra hiệu quả hơn dù phần cài đặt cần nhiều kỹ thuật một chút . DFS sẽ không có hiệu quả khi implement thuần túy, tuy nhiên ý tưởng của DFS sẽ rất hiệu quả trong các thuật toán quan trọng của đồ thị về tìm thành phần liên thông, liên thông mạnh, xác định khớp đồ thị, … Implement thuật toán DFSBây giờ mình sẽ implement thuật toán DFS bằng ngôn ngữ JavaScript. Để thuận tiện mình sẽ không đánh dấu hay tô màu các đỉnh dựa vào trạng thái của chúng trong quá trình duyệt như mã giả ở trên, thay vào đó mình dùng một mảng const graph = new Graph(13); const graph = new Graph(13); const graph = new Graph(13); Chương trình const graph = new Graph(13); const graph = new Graph(13); const graph = new Graph(13); Thủ tục đệ quy const graph = new Graph(13); const graph = new Graph(13); function dfs(s) { Để kiểm tra xem hàm const graph = new Graph(13); function bfs(s, e) { function dfs(s) { function dfs(s) { const graph = new Graph(13); Khi đó kết quả ở màn hình console mình thu được như sau: visit node: 0 Sau khi đối chiếu với hình minh họa của đồ thị này trong phần BFS ở trên, mình thấy kết quả duyệt theo DFS là đúng. Như đã nói, thuật toán DFS sẽ rất hữu dụng trong việc xác định các connected components của đồ thị nên mình sẽ thử áp dụng ý tưởng DFS trên cho bài toán này. Các ứng dụng khác của DFS sẽ được nói đến ở các bài sau. Connected ComponentConnected components có thể được định nghĩa là những vùng thuộc đồ thị, bao gồm các đỉnh sao cho 2 đỉnh x, y bất kỳ trong 1 component mình luôn có:
Các components này chia graph ban đầu thành những vùng khác nhau, mình có thể phân biệt các vùng này bằng cách đánh dấu các đỉnh thuộc cùng một component thành 1 màu. Việc tô màu các đỉnh tương đương với việc mình label các đỉnh trong component đó bằng cùng một giá trị nào đó, có thể là (0,1,2,…) giống như hình minh họa dưới đây: Giả sử ban đầu các đỉnh chưa được tô màu, mình sẽ tạo một đồ thị như hình trên và đánh số các đỉnh theo thứ tự từ 0…17. let newGraph = new Graph(18);newGraph.addEdge(0, 4); Thuật toán ở đây là mình cũng sẽ cần một mảng đánh dấu xem node thứ ith đã được duyệt qua hay chưa là const graph = new Graph(13); function dfs(s) { function dfs(s) { function dfs(s) { Hàm function dfs(s) { function dfs(s) { visit node: 0 function findComponents() { Kết quả ở màn hình console của mình: Find components: { Từ mảng visit node: 0 function dfs(s) { visit node: 0 Ngoài ra, mảng visit node: 0 function dfs(s) { Bằng cách sử dụng visit node: 0 visit node: 0 let map = new Map(); Thử chạy và kiểm tra kết quả trên màn hình console: Connected Components: Map { Trong chương trình in ra các connected components, mình tạo ra một visit node: 0 visit node: 0 let newGraph = new Graph(18);newGraph.addEdge(0, 4); let newGraph = new Graph(18);newGraph.addEdge(0, 4); visit node: 0 visit node: 0 let newGraph = new Graph(18);newGraph.addEdge(0, 4); Xác định các connected components bằng BFSTrong thuật toán tìm thành phần liên thông (Connected Components) ở trên, mình đã sử dụng ý tưởng duyệt DFS. Nếu quan sát kỹ đồ thị ở ví dụ này, mình vẫn có thể xác định các thành phần liên thông bằng thuật toán duyệt BFS. Cụ thể, thay vì duyệt từ đỉnh 0 (ví dụ) rồi đi đến một đỉnh nào đó có cạnh nối với 0, … thì mình sẽ duyệt theo BFS “loang” ra tất cả các đỉnh kề với đỉnh 0, những đỉnh này chắc chắn sẽ thuộc chung một connected components và sẽ được label chung một chỉ số. Chương trình dưới đây implement cho thuật toán tìm thành phần liên thông theo BFS. function bfs(s, e) { Thử chạy chương trình và so sánh visit node: 0 function dfs(s) { function bfs(s, e) { Từ kết quả trả về của hàm let newGraph = new Graph(18);newGraph.addEdge(0, 4); visit node: 0 |