2.3. Cách tiếp cận giải quyết bài toán
Để giải quyết bài toán này, bước đầu tiên chúng ta phải đi tìm một cấu trúc dữ liệu để biểu diễn đồ thị trong máy tính. Cấu trúc dữ liệu phải đáp ứng mục đích cao nhất của bài toán là trả lại các kết quả truy vấn tìm khoảng cách ngắn nhất giữa hai đỉnh trong đồ thị nhanh nhất có thể cùng với ràng buộc về tài nguyên hệ thống. Điều đó đồng nghĩa với các phương thức thêm cạnh, xóa cạnh cũng phải thực hiện rất nhanh. Sau khi tìm được cấu trúc dữ liệu phù hợp, chúng ta phải tìm các phương pháp để ba phép toán tìm khoảng cách ngắn nhất, thêm cạnh, xóa cạnh chạy nhanh nhất có thể. Cuối cùng, dựa vào công nghệ đa luồng dựa trên máy tính có nhiều chíp xử lý, chúng ta có thể tận dụng để song song hóa quá trình tìm đường đi ngắn nhất của các truy vấn liên tiếp nhau. Chi tiết về phương pháp giải quyết bài toán được trình bày trong các phần tiếp theo.
2.4. Cấu trúc dữ liệu phù hợp
Hiện nay, việc xử lý lệnh bên trong một CPU nhanh hơn rất nhiều khi so sánh với việc lấy dữ liệu từ trong bộ nhớ chính. Trong cấu trúc của một bộ nhớ đệm, dữ liệu có thể được truy xuất ngay lập tức khi ở trong bộ nhớ đệm. Điều đó có nghĩa là, một khối bộ nhớ đệm có thể bao gồm 64/4 = 8 nốt. Dựa trên kiến trúc bộ nhớ đệm của CPU, khi một chương trình xử lý dữ liệu lớn, dữ liệu được tổ chức liên tiếp dường như là cách tốt nhất để tăng tỉ lệ nằm trong bộ nhớ (cache hit). Sự trễ này được miêu tả chi tiết trong Bảng 2.1.
Bảng 2.1: Độ trễ trong bộ nhớ 2016
Thời gian (ns)
Ghi chú
L1 cache 0.5 >2 ALU instruction latency
Branch mispredict 3
L2 cache reference 4 8 x L1 cache
Mutex lock/unlock 17
Main memory reference 100 25x L2 cache, 200x L1 cache
Trong đồ thị, cách đơn giản và thuận tiện nhất để biểu diễn các đỉnh của đồ thị là định nghĩa nó dưới dạng một số nguyên dương. Do vậy, nếu đồ thị có |V| đỉnh thì các đỉnh của nó sẽ có định danh từ 0 đến |V| - 1. Để biểu diễn các đỉnh trong đồ thị trên máy tính, như đã trình bày ở mục 1.1.3, có 4 phương pháp chính
sau: danh sách cạnh, danh sách kề, ma trận liên thuộc và ma trận kề. Bởi vì quy mô đồ thị của chúng ta rất lớn, số lượng cạnh và đỉnh lên tới vài triệu, cùng với ràng buộc về tài nguyên hệ thống nên cách biểu diễn bởi ma trận liên thuộc hay ma trận kề không còn phù hợp (không gian lưu trữ cần thiết là Θ(|V| x |E|) và Θ(|V|2) tương ướng). Biểu diễn bằng danh sách cạnh tốn ít không gian lưu trữ nhất Θ(|E|) và đơn giản nhất, tuy nhiên, với cách biểu diễn quá trình tìm đường đi ngắn nhất cần duyệt tất cả các cạnh liền kề của một đỉnh sẽ mất thời gian khá lớn (O(m)). Do vậy, danh sách kề chính là cách thích hợp nhất để biểu diễn đồ thị quy mô lớn có nhiều thay đổi bởi vì không gian lưu trữ tương đối nhỏ (Θ(|V| + |E|) và thuận tiện để thêm hoặc xóa đỉnh, cạnh.
Trong đơn đồ thị có hướng, không trọng số, đồ thị có thể được biểu diễn theo hai danh sách kề như sau:
Danh sách kề các đỉnh vào của đồ thị
Đồ thị sẽ được biểu diễn bởi một danh sách các đỉnh vào của các nốt liên tiếp nhau (incoming_edges) và một mảng chỉ số vào (incoming_index) để có thể lấy được danh sách các đỉnh vào của một nốt trong truy vấn tìm đường ngắn nhất. Vị trí đỉnh vào đầu tiên của nốt N sẽ được lưu trữ tại vị trí N trong mảng chỉ số vào (inconming_index[N]). Thêm vào đó, giá trị số lượng đỉnh vào của một đỉnh cũng được lưu trữ để thuận tiện cho việc duyệt sau này. Để tăng tỉ lệ cache hit, giá trị số lượng đỉnh vào này cần được lưu trữ liền kề với vị trí đỉnh vào đầu tiên của một nốt. Cụ thể, một cặp (vị trí đỉnh vào đầu tiên, tổng số đỉnh vào) được lưu trữ trong mảng chỉ số vào incoming_index. Khi đó, chúng ta có thể lấy giá trị đỉnh vào đầu tiên và số lượng đỉnh vào của đỉnh N tại vị trí incoming_index[Nx2] và incoming_index[Nx2 + 1] tương ứng.
Ví dụ: đồ thị ở Hình 2.1 được biểu diễn theo danh sách kề các đỉnh vào như sau:
Incoming_edges: [3, 4, 1, 2, 2]
Incoming_index: [(0,0), (0,2), (2,1), (3,1), (4,1)]
Danh sách kề các đỉnh ra của đồ thị
Tương tự như danh sách kề các đỉnh vào của đồ thị, đồ thị sẽ được biểu diễn bởi một danh sách các đỉnh ra của các nốt liên tiếp nhau (outgoing_edges) và một mảng chỉ số ra (outgoing_index) để có thể lấy được danh sách các đỉnh ra của một nốt trong truy vấn tìm đường đi ngắn nhất. Vị trí đỉnh ra đầu tiên của nốt N sẽ được lưu trữ tại vị trí N trong mảng chỉ số ra (outgoing_index[N]). Thêm vào đó, giá trị số lượng đỉnh ra của một đỉnh cũng được lưu trữ để thuận tiện cho việc
duyệt sau này. Để tăng tỉ lệ cache hit, giá trị số lượng đỉnh ra này cần được lưu trữ liền kề với vị trí đỉnh ra đầu tiên của một nốt. Cụ thể, một cặp (vị trí đỉnh ra đầu tiên, tổng số đỉnh ra) được lưu trữ trong mảng chỉ số outgoing_index. Khi đó, chúng ta có thể lấy giá trị đỉnh ra đầu tiên và số lượng đỉnh ra của đỉnh N tại vị trí outgoing_index[Nx2] và outgoing_index[Nx2 + 1] tương ứng.
Ví dụ: đồ thị ở Hình 2.1 được biểu diễn theo danh sách kề các đỉnh ra như sau:
Outgoing_edges: [2, 3, 4, 1, 1]
Outgoing_index: [(0,0), (0,1), (1,2), (3,1), (4,1)]
Từ những ý tưởng nêu trên, đồ thị được mô hình hóa dữ liệu dưới dạng theo cấu trúc danh sách kề, được miêu tả cụ thể như sau:
- Mỗi đỉnh được biểu diễn bởi một số nguyên dương.
- Tất cả các đỉnh vào và đỉnh ra được tổ chức dưới dạng danh sách kề. - Một mảng lưu vị trí bắt đầu và số lượng đỉnh vào/đỉnh ra của mỗi nốt.