Professional Documents
Culture Documents
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.
+ 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:
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.
+ 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);
memset(a, 0, sizeof(a));
memset(deg, 0, sizeof(deg));
int 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;
}
}
}
// có chu trình
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);
}
}
// in kết quả
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.
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>
}
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);
int u, v;
for (int i = 1; i <= m; i++) {
adj[v].push_back(u);
check[v][u] = true;
}
cout<<-1;
return 0;
}
+ 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.
+ 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.
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é.
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
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>
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);
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++)
adj[j].push_back(i);
}
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:
Chương trình:
#include <bits/stdc++.h>
bool link[N][N];
int n, m, cnt;
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;
if (sum < 0)
return false;
}
for(int i = 1; i < pos; i++) {
if (sum < 0)
return false;
}
return true;
}
int main() {
ios_base::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
}
EulerC();
if (cnt != m + 1) {
}
int low = inf, start; f[1] = 0;
start = i;
}
}
if (! check(start)) {
cout << -1;
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;
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);
}
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>
bool link[102][102];
vector<int> adj[102];
bool visited[102], haveres;
int n, m;
void display() { // in chu trình
}
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) {
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);
link[x][i] = false;
}
}
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];
void display(){
}
}
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);