10 dist[] với kích thước là số đỉnh của đồ thị và set giá trị của mảng bằng vô cực, trừ giá trị dist[src] với src là đỉnh nguồn của đồ thị + bước 2: tính khoảng cách ngắn nhất tới cách đ
Trang 1BỘ GIÁO DỤC VÀ ĐÀO TẠO TRƯỜNG ĐẠI HỌC CÔNG NGHỆ ĐÔNG Á KHOA: CÔNG NGHỆ THÔNG TIN
BÀI TẬP LỚN HỌC PHẦN: TOÁN RỜI RẠC
Hà Nội, năm 2023
Trang 2BỘ GIÁO DỤC VÀ ĐÀO TẠO TRƯỜNG ĐẠI HỌC CÔNG NGHỆ ĐÔNG Á KHOA: CÔNG NGHỆ THÔNG TIN
BÀI TẬP LỚN HỌC PHẦN: TOÁN RỜI RẠC
4 Nguyễn Xuân Sơn 20221558
5 Ngô Huy Hoàng 20221627
…
CÁN BỘ CHẤM 1 CÁN BỘ CHẤM 2
(Ký và ghi rõ họ tên) (Ký và ghi rõ họ tên)
Trang 31
Trang 40
MỤC LỤC
Phần 1: Cơ sở lí thuyết
1 Khái niệm đồ thị Trang 1
2 Biểu diễn đồ thị trên máy tính Trang 4 a) Ma trận kề
b) Danh sách kề (adjency list)
c) Danh sách cạnh (cung)
3 Duyệt đồ thị (DFS, BFS) Trang 6 a) Tìm kiếm theo chiều sâu DFS (depth – first – search)
b) Tìm kiếm theo chiều rộng BFS (breath – first – search)
4 Thuật toán tìm đường đi ngắn nhất Trang 11 a) Giới thiệu
b) Thuật toán Bellman - Ford
c) Thuật toán Dijkstra
d) Thuật toán Floyd - Warshall
5 Thuật toán tìm cây khung nhỏ nhất Trang 16 a) Khái niệm cây
b) Bài toán cây khung nhỏ nhất Trang 17 c) Cấu trúc dữ liệu disjoint set union Trang 18 d) Thuật toán Kruskal, Prim tìm cây khung nhỏ nhất Trang 19
Trang 5Một đồ thị gồm hai thành phần chính là các đỉnh (vertex hoặc node)
và các cạnh (edge) Đỉnh đại diện cho các đối tượng và các cạnh đại diện cho các tương tác giữa chúng Các đỉnh có thể được đánh số hoặc được gán những tên để dễ dàng nhận diện Tương tự, các cạnh cũng
có thể được đánh số hoặc được gán những tên để dễ dàng nhận diện.Biểu diễn: Đỉnh: các điểm node
III Giả đồ thị (pseudo – graph) G = (V, E)
E: cho phép lặp tại các đỉnh (gọi là khuyên)
IV Đồ thị có hướng G = (V, E) V: tập không rỗng của các đỉnh E: tập các cặp đỉnh (cạnh) có thứ tự
Cạnh nối 2 đỉnh được gọi là 1 cung (arc)
V Bậc của đỉnh
Cho G = (E, V) là đồ thị vô hướng và e = (u, v) E -
u và v gọi là 2 đỉnh liền kề
- e gọi là cạnh nối (cạnh kề) của u và v
- u, v là điểm cuối của e
Trang 62
bậc của đỉnh là số cạnh nối với nó Kí
hiệu: deg(e) =
Cho G = (E.V) là đồ thị có hướng và e = (u, v) E ∈
- u gọi là nối tới v, v gọi là được nối từ u - u gọi
là đỉnh đầu, v gọi là đỉnh cuối khi đó: deg (u):
VII Đường đi
- Đường đi có độ dài n từ đỉnh u đến đỉnh v, trong đó n là số nguyên dương, trên đồ thị vô hướng G = (E, V) là dãy:
Trang 7IX Đồ thị liên thông
- Đồ thị vô hướng G = (V, E) được gọi là liên thông nếu luôn tìm được đường đi giữa 2 đỉnh bất kì của nó
- Đồ thị con của của đồ thị G = (E, V) là đồ thị H = (W, F), trong đó W V ⊆
và F E gọi là thành phần liên thông của đồ thị ⊆
- Đỉnh v gọi là đỉnh rẽ nhánh nếu việc loại bỏ v cùng với các cạnh liên thuộc với nó khỏi đồ thị làm tăng số thành phần liên thông của đồ thị Cạnh e được gọi là cầu nếu việc loại bỏ nó khỏi đồ thị làm tăng thành phần liên thông của đồ thị
- Đồ thị có hướng G = (V, A) được gọi là liên thông mạnh nếu luôn tìm được đường đi giữa 2 đỉnh của nó
- Đồ thị có hướng G = (V, A) được gọi là liên thông yếu nếu đồ thị vô hướng tương ứng với nó là đồ thị vô hướng liên thông
Trang 84
- Định lí: đồ thị vô hướng liên thông là định hướng được khi và chỉ khi mỗi
cạnh của nó nằm ít nhất trên một chu trình
X Định lí
Định lí 1 (kuratovski): đồ thị là phẳng khi và chỉ khi nó không chứa đồ
thị con đồng cấu với k hoặc k33 5
Định lí 2(euler): giả sử G là đồ thị phẳng liên thông với n đỉnh, m cạnh
Gọi r là số miền của mặt phẳng bị chia bởi biểu diễn phẳng của G khi đó:
r = m – n + 2
2 Biểu diễn đồ thị trên máy tính:
Cho đồ thị sau:
a) Ma trận kề:
Ma trận kề: với đồ thị vô hướng ma trận kề của đồ thị có n đỉnh là ma trận vuông
cỡ n x n có các phần tử là 0 hoặc 1 A = {a , a = 1 nếu cạnh (i, j) là một cạnh của ịj ij
đồ thị, a = 0 nếu cạnh (i, j) không phải là cạnh của đồ thị}ij
Ưu điểm: dễ cài đặt, đơn giản, dễ dàng kiểm tra 2 đỉnh có kề nhau hay không trong
O (1) bằng cách kiểm tra giá trị của A [i, j]
Nhược điểm: tốn bộ nhớ, không biểu diễn được đồ thị với số lượng node lớn Ví dụ: ma trận kề của đồ thị G trên như sau:
b) Danh sách kề (adjency list):
- Đối với mỗi đỉnh u của đồ thị, ta lưu trữ danh sách đỉnh kề với u, dùng vector Khi đó để lưu trữ toàn bộ danh sách kề của các đỉnh ta dùng một mảng các vector:
Trang 95
Vd: vector<int> adj [1001]; Hoặc dùng vct
con: vector<vector<int>> adj
Ưu điểm: dễ dàng duyệt các đỉnh kề của 1 đỉnh, dễ dàng duyệt các cạnh
của đồ thị trong danh sách kề, tối ưu về phương pháp biểu diễn Nhược:
ông nào lập trình kém thì hơi khoai :D Ví dụ: danh sách kề của đồ thị
G trên như sau:
- Trong trường hợp đồ thị có trọng số, mỗi cạnh sẽ có thêm trọng số đi kèm đỉnh đầu và đỉnh cuối Trong trường hợp danh sách cạnh không có trọng
số có thể dùng pair<int, int> để biểu diễn thông tin một cạnh, với cạnh có trọng số có thể dùng std::tuple hoặc 1 struct để lưu thông tin cạnh như sau:
struct edge { int
dau, cuoi, w;
};
- Trong trường hợp đồ thị có hướng, chú ý hướng của cạnh Đồ thị có trọng
số ta làm tương tự với đồ thị vô hướng có trọng số
Ưu điểm: tiết kiệm được bộ nhớ nếu đồ thị thưa, thuận lợi cho các bài
toán chỉ liên quan đến cạnh của đồ thị
Nhược điểm: khi cần duyệt các đỉnh kề với đỉnh nào đó, bắt buộc phải
duyệt tất cả các cạnh dẫn tới chi phí tính toán lớn
Ví dụ: danh sách cạnh của đồ thị G trên như sau:
Trang 10vi-1
Pseudo code (c++):
//bắt đầu từ đỉnh u DFS(u)
{
<tham dinh u>;
Visited[u] = true; //đánh dấu là u đã được thăm
Biểu diễn bằng danh sách cạnh: O (V*E)
Biểu diễn bằng danh sách kề: O (V+E)
Mô phỏng thuật toán: ví dụ: duyệt
DFS cho đồ thị dưới đây
Kiểm thuật toán bắt đầu từ đỉnh 1 Trong quá trình duyệt ta quy ước mở rộng đỉnh có số thứ tự nhỏ hơn trước DFS (1) = 1, 2, 4, 5, 6, 7, 9, 8, 3
Trang 117
Khởi tạo một mảng visited với toàn bộ giá trị là false, khi xét từ một đỉnh, ta duyệt danh sách kề của đỉnh đó, nếu chưa được thăm ta push đỉnh đó vào trong stack và tiếp tục gọi hàm đệ quy tới đỉnh đó Nếu đỉnh kề của đỉnh ta xét đã được thăm, ta pop đỉnh đó khỏi stack và cập nhật đỉnh đó vào kết quả (theo nguyên lí LIFO) Đỉnh đã được thăm ta cập nhật giá trị của visited = true
Ta duyệt đỉnh kề với 1, trong trường hợp này là đỉnh 2 Ta
Trang 128
//step 1: khởi tạo hàng đợi Queue = ; // tạo
hàng đợi rỗng push (queue, u); //đẩy u vào
hàng đợi visited[u] = true; //đánh dấu u đã
được thăm
//lặp khi hàng đợi còn phần tử While
(queue != ){
v = queue.front(); //lấy ra đỉnh ở đầu hàng đợi
queue.pop(); //xóa khỏi đỉnh hàng đợi
Trang 13Có 3 thuật toán cơ bản tìm đường đi ngắn nhất:
+) thuật toán Bellman – Ford +)
thuật toán Dijkstra
+) thuật toán Floyd - Warshall
b) Thuật toán Bellman – Ford:
Ý tưởng thuật toán: thuật toán tính toán các đường đi ngắn nhất theo cách từ
dưới lên Đầu tiên, nó tính toán khoảng cách ngắn nhất có nhiều nhất một cạnh trên đường đi Sau đó, nó tính toán các đường đi ngắn nhất với tối đa 2 cạnh, v.v Sau lần lặp thứ i của vòng ngoài, các đường đi ngắn nhất có nhiều nhất i cạnh được tính toán Có thể có tối đa |V| – 1 cạnh trong bất kỳ đường đi đơn giản nào,
đó là lý do tại sao vòng ngoài chạy |v| - 1 lần Ý tưởng là, giả sử rằng không có chu kỳ trọng số âm nếu chúng ta đã tính toán các đường đi ngắn nhất với nhiều nhất là i cạnh, thì phép lặp trên tất cả các cạnh đảm bảo đưa ra đường đi ngắn nhất với nhiều nhất (i+1) cạnh các bước thực hiện:
+) bước 1: đặt đỉnh gốc ta xét = 0, khoảng cách từ đỉnh nguồn đến các đỉnh
khác là vô cực, trừ khoảng cách của đỉnh nguồn tới chính nó Khởi tạo 1 mảng
Trang 1410
dist[] với kích thước là số đỉnh của đồ thị và set giá trị của mảng bằng vô cực, trừ giá trị dist[src] với src là đỉnh nguồn của đồ thị
+) bước 2: tính khoảng cách ngắn nhất tới cách đỉnh kề đỉnh nguồn lặp n –
1 lần với n là số đỉnh của đồ thị, thực hiện như sau:
Nếu khoảng cách từ đỉnh v tới đỉnh nguồn > khoảng cách của đỉnh u tới nguồn + trọng số của cạnh uv, cập nhật khoảng cách từ v tới đỉnh nguồn = khoảng cách từ u tới nguồn + trọng số: dist[v] = dist[u] + trọng số
+) bước 3: Bước này báo lại rằng nếu có một chu kỳ trọng số âm trong biểu
đồ Đi qua từng cạnh một lần nữa và thực hiện theo cho từng cạnh u-v
……Nếu dist[v] > dist[u] trọng số của cạnh uv, thì “Đồ thị chứa chu kỳ trọng
số âm” Ý tưởng của bước 3 là, bước 2 đảm bảo khoảng cách ngắn nhất nếu biểu đồ không chứa chu kỳ trọng số âm Nếu chúng ta lặp qua tất cả các cạnh một lần nữa và nhận được một đường đi ngắn hơn cho bất kỳ đỉnh nào, thì sẽ
dist[i] = max, parent[i] = null, dist[src] = 0;
//bước 2: relaxation tất cả các đỉnh n – 1 lần, quãng đường đi ngắn nhất từ đỉnh nguồn đến các đỉnh khác có thể có tối đa n – 1 cạnh
for (int i = 1; i <= n;i++){
if (dist[u] != max && dist[u] + weight <
dist[v]){ đồ thị chứa trọng số âm
Trang 1511
Ví dụ: cho đồ thị trọng số G sau:
Tìm đường đi ngắn nhất của đồ thị trên từ đỉnh 1 đến đỉnh 6
Áp dụng thuật toán Bellman ta có đường đi ngắn nhất của đồ thị G: 1, 2, 4, 6 Với tổng chiều dài = 20
c) Thuật toán Dijkstra:
• Mô tả: Thuật toán Dijkstra cho phép chúng ta tìm đường đi ngắn nhất giữa
hai đỉnh bất kỳ của đồ thị Nó khác với cây khung nhỏ nhất vì khoảng cách ngắn nhất giữa hai đỉnh có thể không bao gồm tất cả các đỉnh của đồ thị
• Phương thức hoạt động: Thuật toán Dijkstra hoạt động trên cơ sở rằng mọi
đường con B -> D của đường đi ngắn nhất A -> D giữa các đỉnh A và D cũng
là đường đi ngắn nhất giữa các đỉnh B và D Thuật toán Dijkstra có sử dụng
cả thuật toán tham lam để tìm đường đi, dưới đây là các bước thực hiện thuật toán
Bước 1: khởi tạo mảng dist[] với giá trị vô cùng là khoảng cách ban đầu
của các đỉnh khác nguồn, mảng parent[] = chứa đỉnh cha của đỉnh ta xét Nếu đỉnh v khác với đỉnh nguồn, ta push(v) vào hàng đợi ưu tiên
và gán dist[s] = 0
Bước 2: lặp với điều kiện hàng đợi không rỗng, ta gọi u là giá trị nhỏ
nhất của đỉnh hiện tại bằng tổng của trọng số của cạnh nối đỉnh đó với đỉnh kề và khoảng cách từ đỉnh nguồn tới đỉnh ta xét
Bước 3: Từ tập hợp các đỉnh chưa được thăm, tùy ý đặt một đỉnh làm
đỉnh hiện tại mới, miễn là tồn tại một cạnh đối với nó sao cho nó là cạnh nhỏ nhất trong số tất cả các cạnh từ một đỉnh trong tập hợp các đỉnh đã
Trang 1612
thăm tới một đỉnh trong tập hợp các đỉnh chưa được thăm Để lặp lại: đỉnh hiện tại phải chưa được thăm và có cạnh trọng số tối thiểu từ đỉnh được thăm đến nó Điều này có thể được thực hiện đơn giản bằng cách lặp qua tất cả các đỉnh đã thăm và tất cả các đỉnh chưa thăm liền kề với
các đỉnh đã thăm đó, giữ đỉnh có cạnh trọng số nhỏ nhất nối nó Bước
4: lặp lại bước trên đến khi tất cả các đỉnh được đánh dấu là đã thăm
Trang 17Pseudo code:
Dijkstra(s){
//khởi tạo mảng lưu khoảng cách đường đi
vector<long long> d(n+1, vô cùng);
d[s] = 0; piority_queue Q; //dùng pair lưu khoảng cách, đỉnh push(0, s); while (!Q.empty()){
//chọn ra đỉnh có khoảng cách từ s nhỏ nhất
top = Q.top(); Q.pop();
u = top.second; khoang_cach = top.first;
if khoang_cach > d[u] continue;
//relaxation : cập nhật khoảng cách từ s cho tới mọi đỉnh kề với u for (it in adj[u]){ v = it.first; w = it.second;
if d[v] > d[u] + w : d[u] = d[v] + w;
Q.push({d[v], v});
d) Thuật toán Floyd – Warshall:
• Mô tả: Thuật toán Floyd-Warshall là một thuật toán được sử dụng để tìm
đường đi ngắn nhất giữa tất cả các cặp đỉnh trong đồ thị có hướng hoặc vô hướng và có thể có cạnh có trọng số âm Thuật toán này dựa trên một kỹ thuật gọi là "lập bảng"
• Các bước thực hiện của thuật toán Floyd-Warshall như sau:
• Khởi tạo một ma trận đường đi ngắn nhất ban đầu Với mỗi cặp đỉnh (u, v) trong đồ thị, gán giá trị đường đi ngắn nhất từ u đến v là trọng số của cạnh nối từ u đến v (nếu có) và gán giá trị là vô cùng nếu không có cạnh nối từ u đến v
• Duyệt tất cả các cặp đỉnh (i, j) trong đồ thị Nếu có một đường đi từ i đến j thông qua đỉnh k ngắn hơn đường đi trực tiếp từ i đến j, thì cập nhật giá trị đường đi ngắn nhất từ i đến j thành đường đi từ i đến k cộng với đường đi từ
k đến j
Trang 18phỏng thuật toán:
Bước 1: khởi tạo ma trận kề, mỗi ô a[i][j] là khoảng cách từ đỉnh i tới đỉnh j
nếu không có đường đi giữa 2 đỉnh i và j, để ô đó thành
Bước 2: khởi tạo một ma trận A1, phần tử cột đầu và dòng đầu tương tự như
trên, các ô trong ma trận được điền như sau: Gọi k là đỉnh trung gian trên đường đi ngắn nhất từ nguồn đến đích Trong bước này, k là đỉnh đầu tiên A[i][j] được điền như sau:
(a[i][j] + a[k][j]) if a[i][j] > a[i][k] + a[k][j]
Nếu như khoảng cách từ đỉnh nguồn đến đường đi > đường đi qua đỉnh k, ô a[i][j] được điền với a[i][k] + a[k][j]
Bước 3: duyệt toàn bộ ma trận và thực hiện bước 2, đưa ra đường đi ngắn nhất
cho mỗi cặp đỉnh Pseudo code:
5 Thuật toán tìm cây khung nhỏ nhất:
a) Khái niệm cây:
Cây khung (Spanning Tree) của một đồ thị là một đồ thị con có thể thu được bằng cách loại bỏ một số cạnh của đồ thị ban đầu, sao cho cây khung vẫn giữ nguyên tất cả các đỉnh và liên thông Nói cách khác,
Trang 19Bài toán tìm cây khung nhỏ nhất:
Bài toán tìm cây khung nhỏ nhất là một bài toán quan trọng trong lý thuyết đồ thị và có rất nhiều ứng dụng thực tế Mục tiêu của bài toán
là tìm một cây khung (subgraph) của đồ thị ban đầu, sao cho tổng trọng số của các cạnh trong cây khung là nhỏ nhất
b) Cấu trúc dữ liệu disjoint set union:
DSU – hay gọi là disjoint set union là một cấu trúc dữ liệu hỗ trợ các thao tác sau:
+) Tìm xem x thuộc tập nào
+) Gộp 2 tập A, B lại làm một
Ta xem mỗi tập hợp như một cây, nhu vậy DSU là một rừng gồm nhiều cây Để đơn giản thì ban đầu mỗi tập chỉ có 1 phần từ, quy ước nếu x là gốc của cây thì x.cha = x Pseudo code: void init(){ For i in range n { Parent[i] = I; d[i] = i;
Trang 2016
Ứng dụng: DSU là một cấu trúc dữ liệu rất hữu dụng, sử dụng rất nhanh gọn và dễ dàng Nó được dùng làm nền tảng cho một số thuật toán, như thuật toán Kruskal và Prim, 2 thuật toán tìm cây khung nhỏ nhất trên đồ thị
c) Thuật toán Kruskal, Prim tìm cây khung nhỏ nhất:
1,Thuật toán Kruskal
Thuật toán Kruskal là một thuật toán để tìm cây khung nhỏ nhất trong
đồ thị vô hướng có trọng số Đây là một trong những thuật toán được
sử dụng rộng rãi nhất để giải quyết vấn đề tối ưu hóa trong các hệ thống mạng và điện lực
Các bước thực hiện của thuật toán Kruskal như sau:
Sắp xếp tất cả các cạnh của đồ thị theo thứ tự tăng dần của trọng số Khởi tạo một tập hợp con ban đầu chứa tất cả các đỉnh của đồ thị Duyệt lần lượt các cạnh đã được sắp xếp từ trên xuống dưới, nếu cạnh
đó không tạo thành chu trình với các cạnh trước đó đã được chọn thì đưa cạnh đó vào cây khung nhỏ nhất và gộp hai tập hợp con lại Quá trình duyệt tiếp tục cho đến khi tất cả các đỉnh trong đồ thị được gộp vào một tập hợp con duy nhất
Thuật toán Kruskal là một thuật toán đơn giản và hiệu quả, với độ
phức tạp là O(E log E), trong đó E là số lượng cạnh của đồ thị Mô phỏng thuật toán:
Bước 1: sắp xếp các cặp cạnh trên đồ thị trên theo thứ tự tăng dần: (b, c), (e, d), (a, f), (a, b), (b, e), (f, c), (b, d), (c, e), (f, b), (g, d), (a, d), (e, g)
Bước 2: đặt T =
Bổ sung (b, c), (e, d), (a, f), (a, b), (b, e) vào T
Loại (f, c), (b, d), (c, e), (f, b) vì tạo chu trình con Bổ
sung (g, d) vào T
Thuật toán kết thúc vì đã đã bổ sung 6 cạnh vào cây T