Thuật toán cài đặt đồ thị

8 3.2K 72
Thuật toán cài đặt đồ thị

Đang tải... (xem toàn văn)

Thông tin tài liệu

Thuật toán cài đặt đồ thị

Một cách cài đặt đồ thị bằng phương pháp lập trình hướng đối tượngTrần Minh QuangĐồ thị (Graph) có thể nói là một trong những cấu trúc rời rạc thường được sử dụng nhất trong ngành khoa học máy tính. Mỗi khi cần mô hình hóa một tập đối tượng cùng với các mối quan hệ giữa chúng, chúng ta luôn có thể trừa tượng hóa (abstract) chúng bằng đồ thị. Một mạng giao thông, mạng máy tính, quan hệ quen biết giữa một nhóm người, quan hệ về sở thích (A thích ăn cam, B thích ăn quýt)…v…v. tất cả đều có thể đưa về mô hình của một đồ thị. Mục đích của bài viết tuy nhiên không phải là giới thiệu lý thuyết về đồ thị cũng như phương pháp lập trình hướng đối tượng mà muốn trình bày một cách cài đặt đồ thị tương đối hiệu quả bằng phương pháp lập trình hướng đối tượng bằng ngôn ngữ lập trình Java. Vì vậy, tác giả xem như người đọc đã biết lý thuyết về đồ thị cũng như lập trình hướng đối tượng trên Java. I. Nhắc lại khái niệm của đồ thị Đồ thị G được cấu thành từ một tập hợp các đỉnh (Vertices) và các cạnh (Edges) ký hiệu là G = (V, E). Đồ thị G gọi là đồ thị vô hướng nếu các cạnh của G không có thứ tự, tức là chúng không phân biệt cạnh AB với BA (tương tự đường 2 chiều). Đồ thị G gọi là đồ thị vô hướng nếu các cạnh của G có thứ tự, tức là cạnh AB khác với cạnh BA (tương tự đường 1 chiều). Đồ thị có trọng số là đồ thị trong đó mỗi cạnh của đồ thị được gán bằng trọng số (khoảng cách, chi phí v v). II. Phân tích & Thiết kế Như thường lệ, khi bắt đầu thiết kế một chương trình hướng đối tượng chúng ta thường phải đặt ra các mục tiêu sau đây: a. Mô tả (interface) của đồ thị phải độc lập với các biến thể cài đặt chúng Vì đồ thị là một khái niệm chung chung, có rất nhiều cách đề cài đặt nó. Người này có thể cài đặt bằng phương pháp danh sách cạnh kề (Adjacency List), người kia lại thích bằng ma trận (matrix). Kiến trúc (Architecture) của chương trình chúng ta phải đảm bảo tách rời interface một đồ thị với các biến thể cài đặt chúng.b. Khả năng mở rộng (scalable) Là khả năng mở rộng chương trình mà kô phải tốn nhiều công sức thiết kế lại kiến trúc của nó. Chẳng hạn: đồ thị chúng ta sẽ cài đặt ở đây là đồ thị không trọng số, giả sử sau này ta muốn mở rộng sang loại đồ thị có trọng số thì toàn bộ kiến trúc của chương trình không được phép sụp đổ và phải thiết kế lại. Đây là một yêu cầu rất quan trọng trong lĩnh vực phát triển phần mềm vì khách hàng sau khi sử dụng phần mêm một thời gian thường có nhu cầu bổ sung các chức năng mới hoặc thay đổi một số chức năng của chương trình (change requests)c. Khả năng sử dụng lại (resuable)Đó là khi những đoạn mã chúng ta viết một lần và luôn có thể được sử dụng lại cho các ứng dụng tiếp theo. Ví dụ như: ta bỏ công ra để cài đặt đồ thị này, sau này có một lúc nào đấy ta nghiên cứu về mạng Neuron của lĩnh vực trí tuệ nhân tạo (AI). Vì mạng Neuron thực chất cũng là một đồ thị, ta có thể sử dụng lại các lớp (class) mà ta viết hôm này với khả năng kế thừa của OOP rồi thay đổi một số thuộc tính (Attribute) hoặc phương thức (Method) cho phù hợp với yêu cầu bài toán mới, ta có thể giải quyết vấn đề mới trong thời gian ngắn hơn nhiều là khi ta phải làm mọi thứ từ đầu. Đấy âu cũng là một nét đẹp của OOP vậy! Một đồ thị có nghĩa vụ cần cung cấp những giao tiếp (Methods) cơ bản sau đây: - lấy số đỉnh của đồ thị: v() - lấy số cạnh của đồ thi: e() - thêm một cạnh vào đồ thị: ađ(int u, int v) - xóa một cạnh khỏi đồ thị: remove(int u, int v) - kiểm tra một cạnh có thuộc đồ thị hay không: contains(int u, int v) - lấy ra tất cả các đỉnh kề với một đỉnh cho trước: adj(int u) - hiển thị đồ thị ra màn hình: displayGraph() Với Java ta có thể mô tả “định nghĩa” một đồ họa như trên bằng interface như sau: public interface Graph { boolean ađ(int u, int v); boolean remove(int u, int v); List adj(int u); boolean contains(int u, int v); int v(); int e(); void displayGraph(); } Có một điều cần lưu ý là chúng ta cần hết sức thận trọng khi thiết kế một interface!. Khi một lớp (class) cài đặt một interface thì nó phải cài đặt tất cả các phương thức của interface ấy, chẳng hạn: ta cài đặt đồ họa bằng ma trận vào gọi lớp này là: GraphMatrix. Ta sẽ có khai báo là: GraphMatrix implements Graph. Khi đấy trong phần cài đặt của lớp GraphMatrix nhất thiết phải chứa đầy đủ 7 phương thức của interface Graph từ ađ(int u, int v) cho đến displayGraph(). Lại giả sử rằng ta viết một thư viện cài đặt đồ thị trong đó interface Graph của chúng ta chứa 7 phương thức như ở trên. Một công ty phần mềm mua thư viện này về và khi dẫn xuất (extends) interface Graph của chúng ta họ sẽ phải cài đặt đủ 7 phương thức này. Một thời gian sau, chúng ta thấy cảm thấy không ưng ý về interface Graph ở trên và muốn thêm vào một vài phương thức mới ví dụ như: gradeSequence() đưa ra danh sách các bậc của các đỉnh của đồ thị chẳng hạn…v…v. Sau khi sửa đổi lại thư viện, công ty nọ cũng nhận được một bản cập nhật như theo thỏa thuận với interface Graph chúng ta vừa sửa đổi này. Và tại họa đã xảy ra….toàn bộ những chương trình mà công ty đã viết sử dụng interface Graph đều không họat động nữa vì các lớp cài đặt interface Graph đều không cài đặt các phuơng phức mới thêm vào interface sau này (như gradeSequence()). Bài học rút ra ở đây là: việc thiết kế interface đòi hỏi chúng ta phải nhìn trước được tất cả các phương thức mà interface cung cấp. Điều này tất nhiên là không hề đơn giản chút nào!! Quay trở lại với interface Graph. Nếu suy nghĩ kỹ ta sẽ nhận thấy một trong những thao tác quan trọng đối với một đồ thị là duyệt đồ thị theo một thứ tự nào đấy (chẳng hạn theo chiều rộng BFS, hoặc chiều sâu DFS). Với Java điều này tương đương với “Graph is iterable”. Java cung cấp interface “Iterable”, theo Java SDK Documentation: public interface Iterable { Iterator iterator() Returns an iterator over a set of elements of type T. } “Graph is iterable” chuyển thể sang ngôn ngữ Java: public interface Graph extends Iterable với khai báo này, interface Graph sẽ có thêm một phương thức mới kế thừa từ interface Iterable là Iterator iterator (). Phương thức này trả về một đối tượng “Iterator” dùng để duyệt đồ thị của chúng ta. III. Các phương pháp cài đặt đồ thị Ở trên chúng ta mới chỉ mô tả “thế nào là một đồ thị” theo nghĩa một interface mà không hề đưa ra bất cứ quy định về cài đặt nào. Có rất nhiều cách đề cài đặt một đồ thị, 3 phương pháp phổ biến nhất là: a) Bằng ma trận (Matrix) Đồ thị N đỉnh được lưu trữ bởi một ma trận N x N trong đó a[u][v] = 1 nếu tồn tại cạnh giữa u và v b) Bằng danh sách kề (Adjacency list) Đồ thị N đỉnh. Một danh sách gồm N phần tử, mỗi phần tử i của danh sách này lại là một danh sách chưa các đỉnh liền kề với đỉnh i.c) Danh sách cạnh (Edges list) Một danh sách chứa tất cả các cạnh của đồ thị Dưới đây là mã nguồn chương trình cài đặt đồ thị đơn giản tức là: đồ thị vô hướng, không cho phép cạnh nối một đỉnh với chính nó, giữa một cặp đỉnh tồn tại tối đa một cạnh liên kết chúng bằng ma trận: import java.util.*; public class GraphMatrix implements Graph { private int v, e; private boolean connected[][]; //Contructure public GraphMatrix(int v) { //Only allow positive number of vertices if (v > 0) { this.e = 0; this.v = v; //Connected array is automatically initialized with “false” connected = new boolean[v][v]; } } public boolean ađ(int u, int v) { if (!isValidNode(u) || !isValidNode(v) || (u == v) || contains(u, v)) return false; connected[u][v] = true; connected[v][u] = true; this.e++; return true; } public boolean remove(int u, int v) { if (!isValidNode(u) || !isValidNode(v) || (u == v) || !contains(u, v)) return false; connected[u][v] = false; connected[v][u] = false; this.e--; return true; } public List<Integer> adj(int u) { if (!isValidNode(u)) return null; List<Integer> adjList = new LinkedList<Integer>(); for (int i = 0; i < this.v; i++) if (connected[u][i]) adjList.ađ(i); //The returned adj list can not be modified from outside, important!!!return (List<Integer>)Collections.unmodifiableList(adjList); } public boolean contains(int u, int v) { if (!isValidNode(u) || !isValidNode(v)) return false; else return connected[u][v]; } public int v() { return v; } public int e() { return e; } public Iterator iterator() { //not implemented yet return null; } public void printGraph() { System.out.println("****GRAPH*****"); System.out.println("Number of vertices: " + this.v); System.out.println("Number of edges: " + this.e); System.out.println("Connection - Matrix"); for (int i = 0; i < this.v; i++) { for (int j = 0; j < this.v; j++) System.out.print(connected[i][j] ? "1 " : "0 "); System.out.println(); } System.out.println("***************"); } private boolean isValidNode(int u) { return (u >= 0) && (u <= this.v-1); }}Mảng connected dùng để lưu thông tin của đồ thị. Hai biến v, e tương ứng là số đỉnh và số cạnh. Vì chúng ta chưa đi vào nghiên cứu cách thức duyệt đồ thị, tạm thời phương thức Iterator iterator() chỉ trả về giá trị null. Chương trình chính main() dùng để test: public static void main(String[] args) { Graph g = new GraphMatrix(8); g.ađ(0, 1); g.ađ(0, 2); g.ađ(0, 5); g.ađ(0, 6); g.ađ(0, 7); g.ađ(1, 7); g.ađ(2, 7); g.ađ(3, 4);g.ađ(3, 5); g.ađ(4, 5);g.ađ(4, 6); g.ađ(4, 7); //Display graph on the console g.printGraph(); //Print the list of adjacent nodes of node 0 System.out.println(g.adj(0)); } Nếu bây giờ chúng ta quyết định cài đặt đồ thị bằng danh sách kề thay bằng ma trận như ở trên và viết một lớp mới tên là: GraphAdj. Thì trong phương thức main() ở trên ta chỉ cần thay dòng Graph g = new GraphMatrix(8) bằng Graph g = new GraphAdj(8). Phần còn lại không cần phải thay đổi gì cả. Trong lớp GraphAdj ta được tự do tổ chức chương trình theo ý muốn. Đấy là một nguyên tắc quan trọng để thiết kế những chương trình có tính linh hoạt (dynamic) cao. Như đã đề cập ở trên: “Hãy tách rời phần interface với phần cài đặt”. Cài đặt đồ thị bằng danh sách kề và danh sách cạnh coi như bài tập để bạn đọc nghiên cứu. Một gợi ý nhỏ là, với danh sách kề các bạn có thể dùng một mảng danh sách List[] trong đó mỗi phần tử của mảng là một danh sách liên kết. Danh sách thứ i chứa các cạnh kề với đỉnh i trong đồ thị. . trúc của nó. Chẳng hạn: đồ thị chúng ta sẽ cài đặt ở đây là đồ thị không trọng số, giả sử sau này ta muốn mở rộng sang loại đồ thị có trọng số thì toàn. “Iterator” dùng để duyệt đồ thị của chúng ta. III. Các phương pháp cài đặt đồ thị Ở trên chúng ta mới chỉ mô tả “thế nào là một đồ thị theo nghĩa một interface

Ngày đăng: 11/09/2012, 14:59

Từ khóa liên quan

Tài liệu cùng người dùng

Tài liệu liên quan