You are on page 1of 29

Phần 2: Đồ thị Euler và đồ thị Hamilton

1-Đồ thị Euler:


Vấn đề: Bài toán 7 cây cầu
Bài toán bảy cây cầu Euler, còn gọi là Bảy cầu ở Königsberg là bài toán nảy sinh từ nơi chốn cụ thể,
thành phố Königsberg, Phổ (nay là Kaliningrad, Nga) nằm trên sông Pregel, bao gồm hai hòn đảo lớn
nối với nhau và với đất liền bởi 7 cây cầu. Bài toán đặt ra là tìm một tuyến đường mà đi qua mỗi cây
cầu một lần và chỉ đúng một lần sau đó quay chở về điểm xuất phát.

Hình minh hoạ về bài toán 7 cây cầu

Nhà toán học người Thuỵ Sĩ Euler đã giải bài toán này và được xem như là ứng dụng đầu tiên của Lý
thuyết đồ thị. Ông đã mô hình hoá sơ đồ 7 chiếc cầu bằng đa đồ thị (như hình vẽ) với các cầu là các
cạnh. Bài toán tìm đường đi qua 7 chiếc cầu, mỗi chiếc đúng 1 lần có thể tổng quát hoá bằng bài toán :
Có tồn tại chu trình đơn trong đa đồ thị chứa tất cả các cạnh ?
Định nghĩa:
1) Chu trình đơn chưa tất cả các cạnh của đồ thị, mỗi cạnh đúng 1 lần, được gọi là chu trình Euler.
2) Đường đi đơn chứa tất cả các cạnh của đồ thị, mỗi cạnh đúng 1 lần, được gọi là đường đi Euler.
3) Một độ thị có chu trình Euler được gọi là đồ thị Euler.
4) Một đồ thị có đường đi Euler được gọi là đồ thị nửa (bán) Euler.

Từ đây ta có một số chú ý như sau:


1) Đồ thị Euler thì phải là nửa Euler nhưng điều ngược lại chưa chắc đã đúng.
2) Chu trình Euler là 1 đường đi Euler nhưng điều ngược lại chưa chắc đã đúng.
3) Chu trình Euler có thể đi qua một đỉnh 2 lần và có thể không đi qua một số đỉnh nào đó nếu các
đỉnh này là các đỉnh cô lập, tức là các đỉnh có bậc là 0.
4) Chu trình Euler chỉ liên quan đến cạnh của đồ thị, vì vậy ta có thể thêm một số bất kỳ các đỉnh
có bậc bằng 0 (đỉnh cô lập) vào đồ thị thì chu trình Euler của nó vẫn không thay đổi.
Các định lý:
1) Một đồ thị vô hướng liên thông G = (V,E) có chu trình Euler khi và chỉ khi mọi đỉnh của nó đều
có bậc chẵn : deg(v) % 2 == 0.
2) Một đồ thị vô hướng liên thông có đường đi Euler nhưng không có chu trình Euler khi và chỉ khi
chúng có đúng 2 bậc lẻ.
3) Một đồ thị có hướng liên thông yếu G = (V,E) có chu trình Euler thì mọi đỉnh của nó có bán bậc
ra bằng bán bậc vào : deg+(v) = deg–(v). Điều ngược lại cũng hoàn toàn đúng.
4) Một đồ thị có hướng liên thông yếu G = (V,E) có đường đi Euler nhưng không có chu trình Euler
nếu tồn tại đúng 2 đỉnh u, v sao cho :
deg+(u) - deg–(u) = deg–(v) – deg+(v) = 1.
Còn tất cả các đỉnh khác đều có bán bậc ra bằng bán bậc vào.
Thuật toán tìm chu trình Euler:
Bài toán: Cho một đa đồ thị liên thông vô hướng. Kiểm tra xem đồ thị có chu trình Euler hay không,
nếu có thì in ra “YES” và tìm ra chu trình đó. Ngược lại in ra “NO”.
Dữ liệu: Nhập từ file EULER.INP:

+ Dòng đầu tiên gồm 2 số n và m là số đỉnh và cạnh của đồ thị (n ≤ 100, m ≤ 200).
+ M dòng tiếp theo gồm 2 số u và v thể hiện có đường nối từ u đến v.
Kết quả: Ghi ra file EULER.OUT:

+ Nếu đồ thị không có chu trình Euler, in ra “NO”.


+ Ngược lại, in ra “YES” và dòng tiếp theo in ra chu trình đó.

Ví dụ:

EULER.INP EULER.OUT
56 YES
12 1231341
13
14
23
31
34

Hướng dẫn:
Ban đầu, ta sẽ kiểm tra xem đồ thị có chu trình Euler hay không dựa vào định lý (1).
1) Tạo 1 vector để ghi đường đi và một stack (ngăn xếp) để xếp các đỉnh sẽ xét. Đầu tiên xếp một đỉnh
u nào đó của đồ thị vào stack.

2) Xét đỉnh u nằm trên cùng của stack và thực hiện:


+ Nếu u là đỉnh cô lập thì lấy u ra khỏi stack và đưa vào vector.

+ Nếu u liên thông với đỉnh v thì đưa v vào stack và xóa cạnh (u,v).
3) Quay lại bước 2: cho tới khi stack rỗng thì dừng lại. Kết quả đường đi Euler được chứa trong vector
theo thứ tự ngược lại.
Để đảm bảo tính đúng đắn, khi có nhiều đỉnh cùng khả năng lựa chọn thì ưu tiên đỉnh có chỉ số nhỏ
hơn.

Chương trình:
#include <bits/stdc++.h>
using namespace std;
int n, m;
stack <int> s;
vector <int> path;

int a[102][102];
int deg[102];

int main()

{
ios_base::sync_with_stdio(0);

cin.tie(0);cout.tie(0);
freopen("euler.inp" , "r" , stdin);

freopen("euler.out" , "w" , stdout);


cin >> n >> m;

memset(a, 0, sizeof(a));

memset(deg, 0, sizeof(deg));
int u, v;

for (int i = 1;i <= m; i ++) {


cin >> u >> v;

a[u][v]++;
a[v][u]++;
deg[u]++;
deg[v]++;

}
// kiểm tra xem có chu trình Euler không
bool haveEC = true;
for (int i = 1; i <= n; i++){
if (deg[i] % 2 == 1) { // tồn tại đỉnh bậc lẻ
haveEC = false;
break;

}
}

if (! haveEC) { // không có chu trình

cout << "NO";


return 0;

}
// có chu trình

cout << "YES" << '\n';


s.push(1); // đẩy 1 đỉnh bất kì vào stack

while (! s.empty()){

u = s.top();
if (deg[u] == 0) { // đỉnh u bị cô lập

path.push_back(u);
s.pop();

}
else for (int v = 1;v <= n; v++){
if (a[u][v] > 0){ // có đường từ u đến v
s.push(v);

// xoá cạnh (u,v)


a[u][v]--;
a[v][u]--;
deg[u]--;
deg[v]--;
break;
}

}
}

// in kết quả

for (int i = path.size() - 1; i >= 0; i--)


cout << path[i] << " ";

return 0;
}

Ta nhận thấy, với mỗi cạnh của đồ thị, ta sẽ đẩy 1 đỉnh vào stack, sau đó với mỗi đỉnh, ta lại sử dụng 1
vòng for để tìm đỉnh liền kề nên độ phức tạp của thuật toán là O(N*M).
2-Đồ thị Hamilton:
Định nghĩa:
Với đồ thị G = (V,E) có N đỉnh:

1) Chu trình (x1, x2, …, xN, x1) được gọi là chu trình Hamilton nếu xi ≠ xj với 1 ≤ i < j ≤ N.
2) Đường đi (x1, x2, …, xN) được gọi là đường đi Hamilton nếu xi ≠ xj với 1 ≤ i < j ≤ N.
Đường đi được gọi theo tên của William Rowan Hamilton phát biểu vào năm 1859.
Có thể phát biểu bằng lời như sau:
+) Chu trình Hamilton là chu trình xuất phát từ 1 đỉnh, đi thăm tất cả những đỉnh còn lại, mỗi đỉnh
đúng 1 lần, cuối cùng quay trở lại đỉnh xuất phát.
+) Đường đi Hamilton là đường đi xuất phát từ 1 đỉnh, đi thăm tất cả những đỉnh còn lại, mỗi đỉnh
đúng 1 lần.
Từ đây, ta thấy chu trình Hamilton không phải là đường đi Hamilton vì đỉnh xuất phát được thăm 2 lần.

Ví dụ:
Với đồ thị bên cạnh, một chu trình Hamilton
là : 1 – 5 – 2 – 3 – 4 – 1.

Định lý:
1) Định lý Dirac (1952) : Đồ thị vô hướng có N đỉnh (N ≥ 3). Khi đó nếu mọi đỉnh u đều có bậc
deg(u) ≥ N/2 thì đồ thị có chu trình Hamilton. Đây là một điều kiện đủ để đồ thị có chu trình
Hamilton. Nhưng điều ngược lại chưa chắc đã đúng.
2) Đồ thị có hướng liên thông mạnh và có N đỉnh. Nếu deg+(u) ≥ N/2 và
deg–(u) ≥ N/2 với mọi đỉnh u thì đồ thị có chu trình Hamilton.

Thuật toán tìm chu trình Hamilton:


Bài toán: Cho một đồ thị vô hướng không trọng số. Hãy liệt kê tất cả các chu trình Hamilton có trong
đó.
Dữ liệu: Nhập từ file HAMILTON.INP:
+) Dòng đầu tiên gồm 2 số N, M (1 ≤ N ≤ 100) là số lượng đỉnh và số cạnh của đồ thị.
+) M dòng tiếp theo, mỗi dòng gồm 2 số nguyên u,v thể hiện có cạnh nối từ đỉnh u đến đỉnh v.
Kết quả: Ghi ra file HAMILTON.OUT:
+) Liệt kê các chu trình Hamilton bắt đầu từ đỉnh 1.

Ví dụ:

HAMILTON.INP HAMILTON.OUT
56 135241
12 142531
13
24
35
41
52

Hướng dẫn:
+ Hiện tại, người ta vẫn chưa tìm ra một phương án nào thực sự hiệu quả hơn phương pháp quay lui để
tìm chu trình Hamilton trong trường hợp đồ thị tổng quát.
+ Trong phần cài đặt, chúng ta sẽ sử dụng danh sách kề.
+ Thuật toán bắt đầu khi ta chọn đỉnh xuất phát x[1] sau đó gọi hàm quay lui Try(2) để tiến hành tìm
chu trình.
+ Với mỗi hàm Try(id), ta sẽ xét lần lượt các đỉnh v kề với x[id - 1]. Nếu đỉnh v chưa được thăm, ta sẽ
đánh dấu thăm v và chọn x[id] = v. Đến đây, nếu thoả mãn cả 2 điều kiện là id = n (đã đi đủ n đỉnh) và
x[id] kề với x[1] thì ta sẽ ghi nhận chu trình Hamilton.
+ Ngược lại, nếu id < n thì ta tiếp tục gọi đến Try(id + 1).
+ Sau khi thực hiện xong Try(id + 1), ta sẽ bỏ đánh dấu cho đỉnh v và tiếp tục thăm các đỉnh tiếp theo
trong danh sách kề của x[id – 1].

Chương trình:
#include <bits/stdc++.h>

using namespace std;


int x[102],k=0;
vector<int> adj[102];
bool visited[102], check[102][102];
int n, m;

void display() { // in chu trình


for (int i = 1;i <= n; i++)

cout << x[i] << " ";

cout << x[1] << '\n';


}
void Try(int id) {
int u = x[id-1];
for (int i = 0; i < adj[u].size(); i++) {
int v = adj[u][i];
if (! visited[v]) { // v chưa thăm
x[id] = v;
visited[v] = true;
if (id == n) {
if (check[x[1]][x[id]]) // tìm được chu trình
display();
exit(0);
}

}
else Try(id + 1);
visited[v] = false;
}
}
}
int main() {

ios_base::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
freopen("hamilton.inp", "r", stdin);

freopen("hamilton.out", "w", stdout);


cin >> n >> m;

int u, v;
for (int i = 1; i <= m; i++) {

cin >> u >> v;


adj[u].push_back(v);

adj[v].push_back(u);

// đánh dấu đỉnh u kề với v.


check[u][v] = true;

check[v][u] = true;
}

// chọn đỉnh đầu tiên là đỉnh 1.


x[1] = 1;
visited[1] = true;
Try(2);

cout<<-1;
return 0;
}

Bài tập áp dụng


Câu 1: NKPOS.CPP
Một bưu tá ở vùng quê cần chuyển thư cho người dân ở các ngôi làng cũng như ở trên các con đường
nối giữa chúng. Được biết không ngôi làng nào bị cô lập. Bạn cần giúp bưu tá tìm hành trình đi qua
mỗi ngôi làng và mỗi con đường ít nhất một lần (dữ liệu vào đảm bảo một hành trình như vậy tồn tại).
Tuy nhiên, mỗi hành trình còn được gắn với một chi phí. Người dân ở các ngôi làng đều muốn bưu tá
đến làng mình càng sớm càng tốt. Vì vậy mỗi ngôi làng đã thỏa thuận với bưu điện, nếu làng i là làng
thứ k phân biệt được thăm trên hành trình và k ≤ wi, làng i sẽ trả wi – k euros cho bưu điện. Nếu k > wi,
bưu điện đồng ý trả k – wi euros cho ngôi làng. Ngoài ra, bưu điện còn trả bưu tá 1 euro khi đi qua mỗi
con đường trên hành trình.
Có N ngôi làng, được đánh số từ 1 đến N. Bưu điện được đặt ở ngôi làng số 1, do đó hành trình cần bắt
đầu và kết thúc tại ngôi làng này. Mỗi ngôi làng được đặt ở giao điểm của hai, bốn, hoặc tám con
đường. Có thể có nhiều đường nối giữa hai ngôi làng. Con đường có thể là một vòng nối một ngôi làng
với chính nó.
Yêu cầu: Viết chương trình xác định một hành trình đi qua mỗi ngôi làng và mỗi con đường ít nhất một
lần, sao cho tổng lợi nhuận của bưu điện là lớn nhất (hay tổng thiệt hại là bé nhất).
Dữ liệu: Nhập từ file NKPOS.INP:
+ Dòng đầu tiên chứa 2 số nguyên N, M, cách nhau bởi khoảng trắng (1 ≤ N ≤ 200), là số ngôi làng và
M là số con đường. (1 ≤ M ≤ 1000).
+ Dòng tiếp theo gồm N số nguyên dương wi (0 ≤ wi ≤ 1000) xác định chi phí được trả bởi làng i.
+ Mỗi dòng trong số M dòng tiếp theo chứa hai số nguyên dương cách nhau bởi khoảng trắng, mô tả
một con đường nối hai ngôi làng.
Kết quả: Ghi ra file NKPOS.OUT:
+ Dòng đầu tiên là độ dài của hành trình
+ Dòng duy nhất là hành trình thoả mãn với đỉnh đầu và cuối là 1.
Ví dụ:
NKPOS.INP NKPOS.OUT
67 7
1 7 4 10 20 5 15421631
24
15
21
45
36
16
13
Câu 2: EHOUSE.CPP
Sau khi được giải thưởng vô địch vũ trụ về mĩ thuật, Benedict đã quyết định dành tiền thưởng xây một
ngôi nhà siêu to khổng lồ. Ngôi nhà có N phòng và M hành lang nối các phòng, giữa 2 phòng bất kì có
không quá 1 hành lang nối chúng.
Một hôm, anh mời người bạn thân của mình là Wong đến thăm quan ngôi nhà. Ban đầu, Wong xuất
phát từ 1 phòng và đi dọc theo tất cả các hành lang sao cho mỗi hành lang được đi qua đúng một lần,
rồi lại trở về vị trí xuất phát. Mỗi hành lang có một giá trị C cho biết khi đi qua nó thì độ vui vẻ của
Wong sẽ cộng thêm với C (C có thể âm hay dương). Wong bắt đầu xuất phát với năng lượng bằng 0,
anh ta sẽ tức giận và bỏ về nếu sau khi đi hết một hành lang nào đó mà mức năng lượng nhỏ hơn 0. Vì
muốn bạn ở lại chơi, hãy giúp Benedict tìm ra một hành trình an toàn.
Dữ liệu: Nhập từ file EHOUSE.INP:
+ Dòng đầu tiên là 2 số N và M ( 1 ≤ N ≤ 200).
+ M dòng tiếp theo, dòng thứ i gồm 3 số nguyên u, v, c cho biết hành lang nối giữa 2 phòng u và v có
độ vui vẻ là c. ( |c| ≤ 100).
Kết quả: Ghi ra file EHOUSE.OUT:

+ Nếu không có hành trình nào, in ra -1. Ngược lại in ra M + 1 số nguyên là chỉ số phòng trên đường
đi.

Ví dụ:

EHOUSE.INP EHOUSE.OUT
33 2132
122
1 3 -1
2 3 -1
Câu 3: MWORDS.CPP
Trong một lần đi thám hiểm, nhà khảo cổ Stephen đã tìm ra được 1 chiếc cổng cổ xưa dẫn đến kho báu
đáng mơ ước của người Maya. Tuy nhiên, người Maya rất thông minh nên đã tạo ra nhiều lớp bảo mật
cho cánh cửa. Với mỗi lớp, cánh cửa sẽ xuất hiện một số các từ ngẫu nhiên. Câu đố đưa ra là có thể nối
các từ thành một dãy hoàn chỉnh hay không. Cách nối hoàn chỉnh là giữa 2 từ liên tiếp, kí tự kết thúc
của từ trước sẽ là kí tự đầu tiên của từ sau. Ví dụ từ “tose” và “evan” có thể nối với nhau nhưng “tose”
và “fogu” thì không thể. Đây quả thật là một câu hỏi hóc búa. Các bạn hãy giúp Stephen nhé.
Dữ liệu: Nhập từ file MWORDS.INP:
+) Dòng đầu tiên là số T (T ≤ 100) là số lớp bảo mật của cánh cửa.
+) Với mỗi lớp bảo mật, dòng đầu tiên là số N chỉ số lượng từ (1 ≤ N ≤1000).
+) N dòng tiếp theo, mỗi dòng gồm 1 từ bao gồm các chữ cái in thường và có độ dài không quá 100.
Kết quả: Ghi ra file MWORDS.OUT:
+) Với mỗi lớp bảo vệ, in ra “YES” nếu có thể sắp xếp được, “NO” nếu như không thể.
MWORDS.INP MWORDS.OUT
3 NO
2 YES
acm NO
ibm
3
acm
malform
mouse
2
ok
ok

Câu 4: SIONMONA.CPP
Hội tu Sion nổi tiếng với nhiều bí ẩn về tôn giáo và chính trị. Đặc biệt hơn, hội có 2*N hiệp sĩ danh dự
sẽ được triệu tập vào bàn tròn để bàn bạc về những sự việc xảy ra trong mỗi tháng. Tuy vậy, mỗi hiệp
sĩ thường xem một số người hiệp sĩ khác là kẻ thù và không muốn ngồi cạnh họ. Nhưng mỗi người sẽ
có không quá N – 1 kẻ thù. Hãy sắp xếp họ ngồi trong bàn tròn sao cho không có hiệp sĩ nào phải ngồi
cạnh kẻ thù của mình.

Dữ liệu: Nhập từ file SIONMONA.INP:

+ Dòng thứ nhất là số N (1 ≤ N ≤ 50)


+ N dòng tiếp theo, dòng thứ i bao gồm : 1 số Mi là số kẻ thù của hiệp sĩ thứ i, theo sau là Mi số thể
hiện số thứ tự của kẻ thù.
Kết quả: Ghi ra file SIONMONA.OUT:

+ Nếu tồn tại cách xếp thoả mãn, in ra cách xếp đó bắt đầu bằng người thứ 1.

+ Nếu không tồn tại, in ra -1.


Ví dụ:

SIONMONA.INP SIONMONA.OUT
3 143265
232
0
15
242
13
11

Câu 5: BCKNIGHT.CPP
Hôm nay, Baster được thầy giáo dạy về các cách chơi cơ bản cho môn cờ vua. Vì thấy môn này khá dễ,
Baster đã nghĩ ra 1 cách chơi khác. Với một bàn cờ vua kích thước N * N, anh ta đặt sẵn một quân mã
tại ô (x, y) và mong muốn tìm cách đi cho quân mã sao cho quân mã sẽ đi qua tất cả các ô trên bàn cờ
và mỗi ô đi tới chính xác một lần duy nhất. Thật là một bài toán khó. Hãy giúp anh ấy nhé.

Dữ liệu: Nhập từ file BCKNIGHT.INP:


+ Dòng duy nhất gồm 3 số nguyên n, x, y lần lượt là kích thước bàn cờ, và vị trí đặt quân mã (1 ≤ N ≤
8, 1 ≤ x, y ≤ N)

Kết quả: Ghi ra file BCKNIGHT.OUT:


+ Bàn cờ kích thước N * N, mỗi ô (x, y) ghi một số p với ý nghĩa khi quân mã đi tới ô (x, y) thì số bước
đã đi là p.

Chú ý:
+ Ô (x, y) ta đánh số là bước 1.
+ Dữ liệu đảm bảo bài toán luôn có một đáp án duy nhất.

Ví dụ:
BCKNIGHT.INP BCKNIGHT.OUT
623 36 17 6 29 8 11
19 30 1 10 5 28
16 35 18 7 12 9
23 20 31 2 27 4
34 15 22 25 32 13
21 24 33 14 3 26

Gợi ý cách giải

Câu 1: NKPOS.CPP
Ta nhận thấy, dù di chuyển kiểu gì, số tiền lời hoặc lỗ của bưu điện vẫn là không đổi và bằng (tổng các
w) – (1 + 2 + … + N). Thế nên, ta chỉ cần quan tâm đến hành trình thoả mãn. Vì đề bài yêu cầu phải đi
qua N đỉnh ít nhất 1 lần, các cạnh ít nhất 1 lần và mỗi lần đi qua 1 cạnh, bưu điện sẽ mất 1 euro nên ta
cần tìm đường đi đi qua mỗi cạnh đúng 1 lần và mỗi đỉnh đúng 1 lần thì sẽ tiết kiệm chi phí nhất. Điều
này chính là tìm chu trình Euler vì không có đỉnh nào bị cô lập, cũng như các deg của đỉnh luôn là số
chẵn ( vì “Mỗi ngôi làng được đặt ở giao điểm của hai, bốn, hoặc tám con đường”). Từ đây, ta hoàn
toàn có thể giải được bài toán bằng cách tìm chu trình Euler.

Chương trình:
#include <bits/stdc++.h>

using namespace std;


int a[201][201];
int n,m;

stack <int> st;


vector <int> adj[201];

vector <int> res;


int w[201];

void EulerC()
{

st.push(1);

while(! st.empty()) {
int u = st.top();

int total = 0;
for(int i = 1;i <= n; i++)

total += a[u][i];
if(total == 0){ // đỉnh bị cô lập
res.push_back(u);
st.pop();

}
for(int i = 0; i < adj[u].size(); i++){
int v = adj[u][i];
if(a[u][v]>0) {
st.push(v);
a[u][v]--;
a[v][u]--;

break;
}

}
while(! st.empty()){

res.push_back(st.top());
st.pop();

}
}

int main()

{
ios_base::sync_with_stdio(0);

cin.tie(0); cout.tie(0);
freopen("nkpos.inp", "r", stdin);

freopen("nkpos.out", "w", stdout);


cin >> n >> m;
for(int i = 1; i <= n; i++)
for(int j = 1;j <= n; j++)

a[i][j] = 0;
for(int i = 1;i <= n; i++)
cin >> w[i];
for(int i = 1, u, v;i <= m; i++){
cin >> u >> v;
a[u][v]++;
a[v][u]++;

}
for(int i = 1; i <= n; i++)

for(int j = 1; j <= n; j++)

if(a[i][j] > 0){


adj[i].push_back(j);

adj[j].push_back(i);
}

EulerC(); // chu trình Euler


cout << res.size() - 1 << '\n';

for(int i = 0 ;i < res.size(); i++)

cout << res[i] <<' ';


return 0;

Câu 2: EHOUSE.CPP
Ban đầu, ta sẽ tìm một chu trình Euler bất kì. Nếu không tìm được thì hiển nhiên sẽ không có kết quả
cho bài toán này.
Nếu ta tìm được chu trình Euler thoả mãn là u1, u2, … um, u1.
Ta tạo mảng F[i] với ý nghĩa là tổng độ vui vẻ nếu như ta đi từ u1 đến ui hay:

F[i] = ∑𝑖𝑘=2 𝑐(𝑢k – 1, uk). (F[1] = 0).

Lúc này, giả sử đỉnh s có F[s] nhỏ nhất.


+ Nếu đường đi từ s mà tồn tại trường hợp độ vui vẻ < 0 thì rõ ràng, khi đi từ đỉnh nào, ta cũng đều sẽ
gặp kết quả âm và sẽ thất bại.
+ Ngược lại, ta sẽ lấy luôn s là đỉnh xuất phát và đi theo chu trình Euler vừa tìm được.
Độ phức tạp là O(N * M).

Chương trình:
#include <bits/stdc++.h>

using namespace std;


const int N = 203;
const int inf = 1e9 + 7;
int deg[N], a[N][N], res[N * N], f[N * N];

bool link[N][N];
int n, m, cnt;

stack <int> st;


void EulerC() {

st.push(1);

cnt = 0;
while(! st.empty()) {

int u = st.top();
for (int v = 1; v <= n; v++) {

if (link[u][v]) {
link[u][v] = link[v][u] = false;
st.push(v);
break;

}
}
if (u == st.top()) {
cnt++; res[cnt] = u;
st.pop();
}
}

}
bool check(int pos) {

int sum = 0;

for(int i = pos; i < cnt; i++) {


sum += a[res[i]][res[i + 1]];

if (sum < 0)
return false;

}
for(int i = 1; i < pos; i++) {

sum += a[res[i]][res[i + 1]];

if (sum < 0)
return false;

}
return true;

}
int main() {
ios_base::sync_with_stdio(0);
cin.tie(0); cout.tie(0);

freopen("ehouse.inp", "r", stdin);


freopen("ehouse.out", "w", stdout);
cin >> n >> m;
for (int i = 1, u, v, c;i <= m; i++) {
cin >> u >> v >> c;
a[u][v] = c;
a[v][u] = c;

link[u][v] = link[v][u] = true;


deg[u]++; deg[v]++;

for (int i = 1; i <= n; i++)


if (deg[i] % 2){

cout << -1;


return 0;

}
EulerC();

if (cnt != m + 1) {

cout << -1;


return 0;

}
int low = inf, start; f[1] = 0;

for(int i = 2; i <= cnt; i++) {


f[i] = f[i-1] + a[res[i]][res[i - 1]];
if (f[i] < low) {
low = f[i];

start = i;
}
}
if (! check(start)) {
cout << -1;
return 0;
}

for(int i = start; i <= cnt; i++)


cout << res[i] << " ";

for(int i = 2; i <= start; i++)

cout << res[i] << " ";


return 0;

}
Câu 3: MWORDS.CPP
Ta nhận thấy, các từ chỉ có thể kết thúc bằng 1 trong 26 chữ cái theo bảng alphabet.
Giả sử, ta xem mỗi chữ cái là 1 đỉnh. Lại có, với từ có bắt đầu bằng kí tự x1 và kết thúc bằng kí tự x2,
ta xem giữa 2 đỉnh x1 và x2 sẽ có đường nối với nhau. Từ đây, nếu ghép được hết tất cả các từ, tức là
ta sẽ đi qua được tất cả các đường nối, hay nói cách khác là ta đã tìm được 1 đường đi Euler.
Do đó, ta chỉ cần tìm 1 đường đi Euler bằng thuật toán DFS đã được học.
Độ phức tạp : O(100 * 1000).

Chương trình:
#include <bits/stdc++.h>
using namespace std;

const int N = 1003;


const int inf = 1e9 + 7;
int in[30], out[30];

bool conct[30][30], exst[30], vis[30];


string s;
void dfs(int u) {
vis[u] = true;
for (int i =0 ; i < 26; i++)
if (conct[u][i] && ! vis[i])
dfs(i);
}
void init(){

memset(in, 0, sizeof(in));
memset(out, 0, sizeof(out));

memset(conct, 0, sizeof(conct));
memset(exst, 0, sizeof(exst));

}
int main() {
ios_base::sync_with_stdio(0);

cin.tie(0); cout.tie(0);
freopen("mwords.inp", "r", stdin);

freopen("mwords.out", "w", stdout);

int ntest; cin >> ntest;


while(ntest--) {
int n; cin >> n;
init();
for (int i = 1;i <= n; i++) {
cin >> s;
out[s[0] - 'a']++;
in[s[s.size() - 1] - 'a']++;
conct[s[0] - 'a'][s[s.size() - 1] - 'a'] = 1;
exst[s[0] - 'a'] = 1;
exst[s[s.size() - 1] - 'a'] = 1;
}
bool flag = 0;
int cnt = 0, st = -1;
for (int i = 0; i < 26;i ++){
if (abs(in[i] - out[i]) > 1){
flag = 1;
break;
}
if (st == -1 && exst[i]) st = i;
if (abs(in[i] - out[i]) == 1) {
if (out[i] > in[i]) st = i;
cnt++;
}
if (cnt > 2) {
flag = 1;
break;
}
}
if (! flag) {
memset(vis, 0, sizeof(vis));
dfs(st);
for (int i = 0; i < 26; i++){
if (exst[i] && ! vis[i]) flag = 1;
}
}
if (flag) cout << "NO";
else cout << "YES";
cout << '\n';

}
return 0;

Câu 4: SIONMONA.CPP
Ta nhận thấy, nếu xem mỗi hiệp sĩ là 1 đỉnh trong đồ thị, và nếu hiệp sĩ thứ u mà không phải là kẻ thù
của hiệp sĩ thứ v và v cũng không phải là kẻ thù của u thì sẽ có 1 đường nối giữa u và v (tức là hiệp sĩ u
và v có thể ngồi cạnh nhau). Sau khi xây dựng được đồ thị, ta cần xếp 2*N hiệp sĩ vào 1 bàn tròn,
tương đương với tìm 1 chu trình đi qua 2*N đỉnh. Đây chính là tìm chu trình Hamilton.
Chương trình:
#include <bits/stdc++.h>

using namespace std;


int x[102];

bool link[102][102];

vector<int> adj[102];
bool visited[102], haveres;

int n, m;
void display() { // in chu trình

for (int i = 1;i <= n; i++)


cout << x[i] << " ";
haveres = true;
return;

}
void Try(int id) {
int u = x[id - 1];
for (int i = 0; i < adj[u].size(); i++) {
int v = adj[u][i];
if (! visited[v]) { // v chưa thăm
x[id] = v;

visited[v] = true;
if (id == n) {

if (link[x[1]][x[id]]) { // tìm được chu trình

display();
exit(0);

}
}
else Try(id + 1);

visited[v] = false;

}
}

}
int main() {

ios_base::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
freopen("sionmona.inp", "r", stdin);

freopen("sionmona.out", "w", stdout);


cin >> n;
n = n * 2;
memset(link, true, sizeof(link));
for (int i = 1; i <= n; i++) {
cin >> m;
while (m--) {

int x; cin >> x;


link[i][x] = false;

link[x][i] = false;

}
}

for (int i = 1; i <= n; i++) {


for (int j = 1; j <= n; j++) if (i != j) {

if (link[i][j]) {
adj[i].push_back(j);

adj[j].push_back(i);

}
}

}
// chọn đỉnh đầu tiên là đỉnh 1.

x[1] = 1;
visited[1] = true;
Try(2);
if (!haveres) cout << -1;

return 0;
}
Câu 5: BCKNIGHT.CPP
Ta thấy, nếu xem ô của bàn cờ là các đỉnh của đồ thị và các cạnh nối giữa 2 đỉnh tương ứng với hai ô
mã có thể đi lại được thì hành trình của quân mã cần tìm sẽ là một đường đi Hamilton. Do đó với giới
hạn N ≤ 8, ta hoàn toàn có thể sử dụng phương pháp quay lui để tìm đáp án. Đó là kết quả của bài toán
này.

Ngoài ra, khi mở rộng giới hạn lớn hơn với N ≤ 20, khi duyệt quay lui, chúng ta cần thêm phương pháp
ưu tiên Warnsdorff để giảm thời gian chạy chương trình. Ta sẽ thêm mảng deg[x, y] là số ô mã có thể
đi đến được từ (x,y) thì khi duyệt, ta không cần xét lần lượt các hướng đi có thể mà sẽ ưu tiên thử
hướng đi tới ô với deg[] nhỏ nhất trước. Tuy nhiên, phương pháp này chỉ hoạt động tối ưu khi có
đường đi, nếu không thì thời gian chạy vẫn tương đương với phương pháp duyệt quay lui thông
thường.
Chương trình:
#include<bits/stdc++.h>
using namespace std;

int n;
int x,y;

int a[9][9];

int xqX[] = {2,2,1,1,-1,-1,-2,-2};


int xqY[] = {1,-1,2,-2,2,-2,1,-1};

void display(){

for(int i = 1; i <= n; i++){


for(int j = 1; j <= n; j++)
cout << a[i][j] << " ";
cout << '\n';

}
}

void Try(int index, int r, int c){


for(int i = 0;i < 8; i++){
int X = r + xqX[i];
int Y = c + xqY[i];

if( X > 0 && X <= n && Y > 0 && Y <= n && a[X][Y] == 0){
a[X][Y] = index;

if(index < n * n) {

Try(index+1, X, Y);
a[X][Y] = 0;

}
else {

display();
exit(0);

}
}

int main(){
ios_base::sync_with_stdio(0);
cin.tie(NULL);cout.tie(NULL);
freopen("bcknight.inp", "r", stdin);

freopen("bcknight.out", "w", stdout);


cin >> n >> x >> y;
a[x][y] = 1;
Try(2, x, y);
return 0;
}

You might also like