1. Trang chủ
  2. » Luận Văn - Báo Cáo

Ài tập lớn học phần toán rời rạc

24 0 0
Tài liệu đã được kiểm tra trùng lặp

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

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Tiêu đề Bài Tập Lớn Học Phần: Toán Rời Rạc
Tác giả Hà Quang Nhuận, Vũ Ngọc Thức, Phạm Thanh Sơn, Nguyễn Xuân Sơn, Ngô Huy Hoàng
Trường học Trường Đại Học Công Nghệ Đông Á
Chuyên ngành Công Nghệ Thông Tin
Thể loại Bài Tập Lớn
Năm xuất bản 2023
Thành phố Hà Nội
Định dạng
Số trang 24
Dung lượng 1,52 MB

Nội dung

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 1

BỘ 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 2

BỘ 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 3

1

Trang 4

0

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 5

Mộ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 6

2

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 7

IX Đồ 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 8

4

- Đị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 9

5

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 10

vi-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 11

7

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 12

8

//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 13

Có 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 14

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 đỉ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 15

11

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 16

12

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 17

Pseudo 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 18

phỏ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 19

Bà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 20

16

Ứ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

Ngày đăng: 23/12/2024, 12:43

TÀI LIỆU CÙNG NGƯỜI DÙNG

  • Đang cập nhật ...

TÀI LIỆU LIÊN QUAN

w