You are on page 1of 53

BỒI DƯỠNG HỌC SINH GIỎI

CHUYÊN ĐỀ THUẬT TOÁN

CÁC THUẬT TOÁN CƠ BẢN

1
THUẬT TOÁN SẮP XẾP QUICKSORT
Bài toán:
Cho dãy số A1, A2, …, An. Hãy sắp xếp dãy số đã cho tăng dần.

Ý tưởng:
Giả sử dãy A(n) đã sắp xếp tăng dần.

Nhận xét: Với mỗi đoạn con (L .. H) thuộc dãy, chọn 1 phần tử bất
kỳ thuộc đoạn làm “chốt”, ta luôn có:
Mọi phần tử bên trái chốt không vượt quá chốt;
Mọi phần tử bên phải chốt không bé hơn chốt

Ví dụ:
4 5 6 7 8 8 10
1 2 3 4 5 6 7
L Chot H
THUẬT TOÁN SẮP XẾP QUICKSORT

Từ nhận xét trên, thuật toán QuickSort sắp xếp dãy tăng dần theo ý
tưởng:
Trong dãy A(n) , chọn 1 phần tử bất kỳ làm “chốt”, sau đó sắp xếp
sao cho:
Mọi phần tử bên trái chốt không vượt quá chốt;
Mọi phần tử bên phải chốt không bé hơn chốt
Ví dụ:

5 10 7 8 4 6 8
1 2 3 4 5 6 7
Chot
Chot
5 8 7 6 4 8 10
1 2 3 4 5 6 7
THUẬT TOÁN SẮP XẾP QUICKSORT
Trong đoạn con trái A(1..j) , chọn 1 phần tử bất kỳ làm “chốt”, sau
đó sắp xếp sao cho: Mọi phần tử bên trái chốt không vượt quá chốt;
mọi phần tử bên phải chốt không bé hơn chốt.

Trong đoạn con phải A(i..n) , chọn 1 phần tử bất kỳ làm “chốt”, sau
đó sắp xếp sao cho: Mọi phần tử bên trái chốt không vượt quá chốt;
mọi phần tử bên phải chốt không bé hơn chốt.
Quá trình cứ thế tiếp diễn đến khi sắp xếp xong.

Ví dụ:
Chot

5 8 7 6 4 8 10
1 2 3 4 5 6 7
j i

Đoạn con trái Đoạn con phải


THUẬT TOÁN SẮP XẾP QUICKSORT
Bài toán:
Cho dãy số A1, A2, …, An. Hãy sắp xếp dãy số đã cho tăng dần.

Thuật toán QuickSort:


Thuật toán QuickSort là cách sắp xếp theo kiểu phân đoạn, cách làm
như sau:

Để sắp xếp trong một đoạn (L .. H):


- Chọn 1 phần tử “ở giữa” đoạn làm “chốt”, thường chọn là phần
tử có chỉ số (L+H) div 2. Ta có Chot = A[(L+H) div 2]
Ví dụ:

5 10 7 8 4 6 8
1 2 3 4 5 6 7
5
L Chot H
THUẬT TOÁN SẮP XẾP QUICKSORT
- Tìm cách sắp xếp sao cho các phần tử nhỏ hơn (hoặc bằng) “chốt”
được đặt về bên trái “chốt”, các phần tử lớn hơn (hoặc bằng) “chốt”
được đặt bên phải “chốt”. Cách làm: thực hiện lặp các bước
B1. từ trái sang phải, tìm i sao cho A[i] ≥ chot
B2. từ phải sang trái, tìm j sao sao A[j] ≤ chot
B3. Nếu i < j (tồn tại nghịch thế) thì đổi chỗ A[i] với A[j]; rồi tăng i
và giảm j
Nếu i = j, tức A[i] và A[j] chỉ là một, thì chỉ tăng i và giảm j
B4. Nếu i ≤ j, quay lại B1, còn i>j thì thoát lặp.

5 10 7 8 4 6 8
1 2 3 4 5 6 7
B1, B2 Chot
i j

5 8 7 8 4 6 10
1 2 3 4 5 6 7
B3, B4 L Hj
i
THUẬT TOÁN SẮP XẾP QUICKSORT

5 8 7 8 4 6 10
1 2 3 4 5 6 7
B1, B2
L Chot H
i j

5 8 7 8 4 6 10
1 2 3 4 5 6 7
B1, B2
L i j H

5 8 7 6 4 8 10
1 2 3 4 5 6 7
B3, B4
L i j H

5 8 7 6 4 8 10
1 2 3 4 5 6 7
B3, B4
L j H
i
THUẬT TOÁN SẮP XẾP QUICKSORT
- Tới đây, do i > j nên thoát vòng lặp.
- Dãy được phân chia thành 2 đoạn con:
+ Đoạn bên trái gồm các phần tử (L .. j) ≤ “chốt”.
+ Đoạn bên phải gồm các phần tử [i .. H) ≥ “chốt”

Chot = 8

5 8 7 6 4 8 10
1 2 3 4 5 6 7
j i
L H
Đoạn con trái
Đoạn con phải

8
THUẬT TOÁN SẮP XẾP QUICKSORT
- Tiếp tục sắp xếp hai đoạn con được tạo thành (có độ dài ngắn hơn độ
dài đầu) bằng phương pháp tương tự.
- Quá trình cứ thế tiếp tục cho tới khi đoạn con cần sắp xếp có độ dài
nhỏ hơn 2 (đoạn con nhỏ nhất).

Độ phức tạp:
- Thuật toán QuickSort có độ phức tạp trung bình là O(nlogn).
- Có thể sắp xếp dãy số gồm 105 phần tử trong thời gian không quá 1
giây.

5 8 7 6 4 8 10
1 2 3 4 5 6 7
L j i H

Đoạn con trái 9


Đoạn con phải
THUẬT TOÁN SẮP XẾP QUICKSORT
Procedure QuickSort(L,H:longint);
Var i,j, Chot: longint;
Begin
If L>=H then Exit; {Nếu độ dài đoạn con < 2 thì dừng đệ qui}
Chot := A[(L+H) div 2]; {Xác định giá trị chốt}
i := L; {i là vị trí đầu đoạn bên trái}
j := H; {j à vị trí đầu đoạn bên phải}
Repeat {vòng lặp phân đoạn}
While A[i] < Chot do inc(i); {tìm A[i] ≥ Chot. Nếu gặp, dừng lại}
While A[j] > Chot do dec(j); {tìm A[j] ≤ Chot. Nếu gặp, dừng lại}

If i<=j then
Begin
If i<j then DoiCho(A[i],A[j]);
Inc(i); Dec(j);
End;
Until i>j;

if L<j then QuickSort(L,j); {Sắp xếp đoạn con bên trái}


if i<H then QuickSort(i,H); {Sắp xếp đoạn con bên phải}
End;
10
/*
#include <iostream> // std::cin, cout
#include <algorithm> // std::sort
#include <vector> // std::vector
*/

#include <bits/stdc++.h> // include này bao gồm các include trên

using namespace std;


#define lap(i,a,b) for (int i=a;i<=b;i++)
#define oo -2000000000

// hàm so sánh lớn hơn


bool mycomp (int i,int j) { return (i>j); }

int main()
{
// tham chiếu mở file input
freopen("sort.inp","r",stdin);
// tham chiếu mở file output
freopen("sort.out","w",stdout);
// khai báo
int n;
int x;
vector <int> a;
a.push_back(oo); // gán a[0] = oo

// đọc dữ liêu
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>x;
a.push_back(x);
}

// sắp xếp tăng dần, toàn bộ vector từ a[0] đến a[n]


sort(a.begin(),a.end());

// sắp xếp tăng dần từ a[2] đến a[4]


//sort(a.begin()+2, a.end()-1);
// sắp xếp giảm dần, từ a[1] đến a[5]
//sort(a.begin()+1,a.end(),mycomp);

// sắp xếp giảm dần, từ a[2] đến a[4]


//sort(a.begin()+2,a.end()-1,mycomp);

// xuất kết quả


lap(i,1,n) cout<<a[i]<<" ";

return 0;
}
THUẬT TOÁN TÌM KIẾM NHỊ PHÂN
Bài toán: Cho dãy số A1, A2, …, An và số X. Hãy tìm phần tử của dãy
có giá trị bằng X.

Giải:
- Sắp xếp dãy số đã cho tăng dần (giảm dần).
- Đặt L := 1; H := n;
- Bước lặp:
B1. Chọn vị trí “ở giữa” dãy.
Mid := (L+H) div 2;

B2. So sánh A[Mid] với X.


Nếu A[Mid] = X; đã tìm thấy, ghi nhận kết quả, dừng tìm kiếm;
Nếu A[Mid] < X; tiếp tục tìm X trong đoạn con bên phải
(Mid+1 .. n)  đặt L := Mid + 1;
Nếu A[Mid] > X; sẽ tìm X trong đoạn con bên trái (1 .. Mid-1)
 đặt H := Mid – 1;
Nếu L ≤ H quay lại bước 1.
14
THUẬT TOÁN TÌM KIẾM NHỊ PHÂN
Minh họa thuật toán

Tìm X = 10

5 10 7 8 4 6 8
1 2 3 4 5 6 7

Sắp xếp

4 5 6 7 8 8 10
1 2 3 4 5 6 7

15
THUẬT TOÁN TÌM KIẾM NHỊ PHÂN

Tìm X = 10
A[Mid] = 7 < 10

4 5 6 7 8 8 10
1 2 3 4 5 6 7
L Mid = 4 H
THUẬT TOÁN TÌM KIẾM NHỊ PHÂN

Tìm X = 10 A[Mid] = 8 < 10

4 5 6 7 8 8 10
1 2 3 4 5 6 7
L Mid = 6 H
THUẬT TOÁN TÌM KIẾM NHỊ PHÂN

Tìm X = 10
A[Mid] = 10

4 5 6 7 8 8 10
1 2 3 4 5 6 7
Mid = 7
THUẬT TOÁN TÌM KIẾM NHỊ PHÂN
Hàm tìm kiếm giá trị X trong dãy A1…An, trả về chỉ số tìm thấy:

Function BinarySearch(X:longint):longint;
Var L,H,Mid: longint;
Begin X
1 Mid n
L := 1; H := n;
While (L<=H) do
Begin 1 Mid X n
Mid := (L+H) div 2;
If A[Mid] = X then Exit(Mid);
If A[Mid] < X then L := Mid+1
Else H := Mid - 1;
End; 1 X Mid n
Exit(0);
End;
THUẬT TOÁN TÌM KIẾM NHỊ PHÂN
#include <iostream> // std::cin, cout
#include <algorithm> // std::sort
#include <vector> // std::vector
/*
#include <bits/stdc++.h> // include này bao gồm các include trên
*/

using namespace std;


#define lap(i,a,b) for (int i=a;i<=b;i++)
#define oo -2000000000

// hàm so sánh lớn hơn


bool mycomp (int i,int j) { return (i>j); }

int n;
int x;
int ans;
THUẬT TOÁN TÌM KIẾM NHỊ PHÂN
int main()
{
// tham chiếu mở file input
freopen("BinarySearch.inp","r",stdin);
// tham chiếu mở file output
freopen("BinarySearch.out","w",stdout);

// đọc file
cin>>n>>x; // x là giá trị cần tìm
int ai;
vector <pair<int,int>> a; // vector của cặp

for(int i=0;i<n;i++)
{
cin>>ai;
a.push_back(make_pair(ai,i));
}
THUẬT TOÁN TÌM KIẾM NHỊ PHÂN
// sắp xếp vector a tăng dần
sort(a.begin(), a.end());
ans = -1;
// tìm x bằng TKNP
int L = 0;
int H = n-1;
int Mid;

while (L <= H)
{
Mid = (L + H) / 2;
if (x == a[Mid].first)
{
ans = a[Mid].second;
break;
}
else
{
if (x < a[Mid].first) H = Mid - 1;
if (x > a[Mid].first) L = Mid + 1;
}
}
THUẬT TOÁN TÌM KIẾM NHỊ PHÂN

// ghi kết quả


if (ans == -1) cout<<ans; else cout<<ans+1;

return 0;
}
THUẬT TOÁN TÌM KIẾM NHỊ PHÂN

Nhận xét:

- Sắp xếp dãy số có độ phức tạp O(nlogn);


- Tìm kiếm:
Trong mỗi lần lặp, lệnh
Mid := (L+R) div 2;
đã thu hẹp phạm vi tìm kiếm còn một nửa, nên độ
phức tạp thuật toán tìm kiếm là: O(logn)

 Độ phức tạp chung của lời giải là O(nlogn). Có


thể tìm kiếm trong dãy gồm 105 phần tử vói thời
gian không quá 1 giây.
25
BÀI TẬP ÁP DỤNG

Tìm kiếm
Cho a(n) là dãy số nguyên a1,a2,...,an và dãy các giá trị p1,p2,...,pm.
Hãy tìm vị trí xuất hiện của mỗi giá trị pi trong dãy a(n).
Dữ liệu vào từ file văn bản TIMKIEM.INP gồm nhiều dòng:
Dòng đầu tiên chứa hai số nguyên n, m (1 ≤ n, m ≤ 105);
Dòng thứ hai ghi dãy số nguyên a1,a2,...,an (|ai| ≤ 106, ai ≠ aj với i ≠j) ;
Dòng thứ ba ghi dãy giá trị p1,p2,...,pm (|pi| ≤ 106);
Các số trên cùng dòng viết các nhau một dấu cách.
Kết quả ghi ra file văn bản TIMKIEM.OUT gồm m dòng, dòng thứ i
ghi vị trí tìm gặp giá trị pi xuất hiện trong dãy a1,a2,...,an . Nếu không
tìm thấy pi thì ghi số 0.
Ví dụ:
TIMKIEM.INP TIMKIEM.OUT
75 3
6 -3 1 8 -1 7 -7 5
1 -1 8 2 -7 4
0
7
65 2
6 -7 -1 8 3 7 3
-7 -1 8 2 -7 4
0
2

Ràng buộc: có 50% test ứng với 50% số điểm với n ≤ 103
có 50% test ứng với 50% số điểm với n ≤ 105

26
27
BÀI TẬP ÁP DỤNG

Tìm kiếm theo vị trí nhỏ nhất


Cho a(n) là dãy số nguyên a1,a2,...,an và dãy các giá trị p1,p2,...,pm.
Hãy tìm vị trí xuất hiện của mỗi giá trị pi trong dãy a(n).
Dữ liệu vào từ file văn bản TIMKIEM2.INP gồm nhiều dòng:
Dòng đầu tiên chứa hai số nguyên n, m (1 ≤ n, m ≤ 105);
Dòng thứ hai ghi dãy số nguyên a1,a2,...,an (|ai| ≤ 106) ;
Dòng thứ ba ghi dãy giá trị p1,p2,...,pm (|pi| ≤ 106);
Các số trên cùng dòng viết các nhau một dấu cách.
Kết quả ghi ra file văn bản TIMKIEM2.OUT gồm m dòng, dòng thứ
i ghi vị trí tìm gặp giá trị pi xuất hiện trong dãy a1,a2,...,an. Nếu không
tìm thấy pi thì ghi số 0. Nếu trong dãy a(n) có nhiều phần tử bằng
nhau thì tìm phần tử đứng ở vị trí nhỏ nhất.
Ví dụ:
TIMKIEM2.INP TIMKIEM2.OUT
75 2
6 -7 -1 8 -1 7 -7 3
-7 -1 8 2 -7 4
0
2
65 1
111111 0
10101 1
0
1

Ràng buộc: có 50% test ứng với 50% số điểm với n ≤ 103
có 50% test ứng với 50% số điểm với n ≤ 105

28
29
BÀI TẬP ÁP DỤNG

Doanh thu
Công ty L là một công ty liên doanh nước ngoài, chuyên sản xuất
các mặt hàng giày da thuộc Khu công nghiệp H, họ theo dõi việc
kinh doanh bằng cách ghi lại doanh thu đạt được ở mỗi ngày.
Theo đó, doanh thu đạt được của công ty sau N ngày là một dãy
số gồm N số nguyên a1, a2, ..., aN; trong đó ai là doanh thu của
công ty ở ngày thứ i (lưu ý doanh thu có thể là số âm).
Trước diễn biến phức tạp của việc đại dịch Covid-19 tái bùng phát
ở các nước châu Âu, công ty cần đánh giá lại hiệu quả kinh doanh
trong N ngày nêu trên. Giám đốc công ty muốn biết khoảng thời
gian ngắn nhất gồm những ngày liên tiếp nhau có tổng doanh thu
đạt được không thấp hơn mức doanh thu phải đạt S.
Yêu cầu: Cho trước dãy số a1, a2, ..., aN­ và S, bạn hãy giúp giám đốc công
BÀI TẬP ÁP DỤNG
ty tìm câu trả lời cho vấn đề đặt ra.
Dữ liệu: vào từ file văn bản DOANHTHU.INP có nội dung:
- Dòng đầu tiên chứa hai số nguyên dương N, S (1 ≤ N ≤ 104, 0 ≤ S ≤
109);
- Dòng thứ hai là dãy số nguyên a1, a2, ..., aN (|ai| ≤ 104).
Các số trên cùng dòng được ghi cách nhau bởi dấu cách.
Kết quả: ghi ra file văn bản DOANHTHU.OUT một số duy nhất là độ dài
dãy con ngắn nhất tìm được thỏa yêu cầu nêu trên. Nếu không tìm được dãy
con nào thì ghi -1.
Ví dụ:
DOANHTHU.INP DOANHTHU.OUT Giải thích
6 10 2 Dãy con thỏa yêu cầu là
0 0 -2 0 5 6 dãy a5 .. a6.
20 -1 Không tìm được dãy con
-4 -6 thỏa yêu cầu.

30
BÀI TẬP ÁP DỤNG
Lời giải O(n ) 2

• Chuẩn bị trước mảng Sum


1. fillchar(sum,sizeof(sum),0);
2. for i:=1 to n do
3. begin
4. sum[i] := sum[i-1] + a[i];
5. end;
BÀI TẬP ÁP DỤNG
Lời giải O(n ) 2

• Tìm đáp số bằng 2 vòng lặp lồng nhau


1. ans := oo;
2. for i:=1 to n do
3. for j:=i to n do
4. begin
5. SS := sum[j] - sum[i-1];
6. if SS >= s then
7. begin
8. if (j-i+1) < ans then ans := j - i + 1;
9. end;
10. end;
Cải tiến lời giải O(n2)
• Thay vòng lặp tìm j (thao tác 3) thành TKNP
1. for i:=1 to n do
2. begin
3. k := 0; // k la vi tri nho nhat cua sum_sort thoa sum_sort[k] >= sum[i-1]+s
4. search(sum[i-1] + s,1,n); // Neo i, tim sum[i-1]+s trong sum_sort
5. if (k > 0) then // neu tim thay k
6. begin
7. j_min := n+1; // j_min la chi so nho nhat can tim ung voi moi i
8. for j:=k to n do // trong đoạn (k..n) của pos, tim j_min khong nho hon i
9. if (pos[j]<j_min) and (pos[j]>=i) then j_min := pos[j];
10. if (j_min >=i) and (j_min<=n) then // tìm được j_min cap nhat ans
11. begin
12. if (j_min-i+1 < ans) then ans := j_min-i+1;
13. end;
14. end;
15. end;
Ví dụ minh họa lời giải cải tiến
DÃY NHỊ PHÂN ĐỘ DÀI N
Bài toán:
Cho số nguyên dương n. Hãy tìm tất cả các dãy nhị phân độ dài n.
Ví dụ: Với n = 3, ta liệt kê được 8 dãy như sau:
000, 001, 010, 011, 100, 101, 110, 111.
- Dữ liệu vào từ file văn bản DAYNP.INP gồm một dòng duy nhất là
số nguyên dương n (2 ≤ n ≤ 20).
- Kết quả ghi ra file văn bản DAYNP.OUT gồm nhiều dòng, mỗi dòng
ghi một dãy nhị phân tìm được.
Ví dụ:
DAYNP.INP DAYNP.OUT

3 000
001
010
011
100
101
110
111
Lời giải:

- Mỗi cấu hình có n phần tử, mỗi phần tử có 2 khả năng chọn lựa là 0 hoặc 1.
- Thủ tục Try duyệt toàn bộ sinh ra tất cả các cấu hình như sau:

Procedure Try(i:longint); {Thủ tục xây dựng phần tử thứ i của cấu hình}
Var j: longint;
Begin
For j:=0 to 1 do {Xét các giá trị có thể chọn cho phần tử i}
Begin
a[i]:= j; {Thử gán phần tử thứ i giá trị là j}
If (i=n) then Ketqua {Nếu đã xây dựng đến phần tử cuối cùng thì
thông báo kết quả}
Else Try(i+1); {xây dựng phần tử kế tiếp của cấu hình}
End;
End;
Hình minh họa:
Cây tìm kiếm gốc Try(1) với n = 3 có dạng sau:
- Thủ tục thông báo một cấu hình tìm được:
Procedure Ketqua;
Var i: longint;
Begin
For i:=1 to n do Write(a[i],#32);
End;

- Lời gọi bắt đầu thủ tục duyệt các cấu hình là Try(1):
BEGIN
Nhap;
Try(1);
END.

- Thuật toán có độ phức tạp là O(2n). Xử lí hiệu quả với độ


lớn của n ≤ 20
39
VOI 2016 Day 1 - SEQ198
Mã bài: SEQ198
 Con số 198 có gợi cho bạn điều gì không? Khi học lịch sử Việt Nam,
Vinh biết rằng ngày 19-8-1945 là ngày Tổng khởi nghĩa, ngày nhân dân cả
nước ta nhất tề đứng lên làm cuộc Cách mạng Tháng Tám vĩ đại. Hiện
nay, 198 được đặt tên cho nhiều bệnh viện, công viên, đường phố trong cả
nước. Con số này đã gợi ý cho Vinh khảo sát dãy số SEQ98 sau đây: Dãy
số nguyên không âm a1, a2, ..., an được gọi là dãy SEQ198 nếu không tồn
tại hai chỉ số i và j (1 ≤ i,j ≤ n, i≠j) mà |ai-aj| hoặc là bằng 1 hoặc là bằng 8
hoặc là bằng 9.
Ví dụ: Dãy số nguyên 1, 3, 5, 7 là dãy SEQ198.
Dãy số nguyên 7, 3, 5, 1, 9, 21 không phải là dãy SEQ198 bởi vì có hai
phần tử 1 và 9 có hiệu 9 - 1 = 8. Tuy nhiên, sau khi xóa bớt phần tử 1, ta
thu được dãy 7, 3, 5, 9, 21 là một dãy SEQ198.
Vinh quan tâm tới bài toán sau đây: Cho dãy số nguyên không âm b1,
b2, ..., bm, hãy tìm cách loại bỏ một số ít nhất phần tử của dãy để được dãy
còn lại là SEQ198.
40

Yêu cầu: Hãy giúp Vinh giải quyết bài toán đặt ra
 Dữ liệu vào:
Dòng đầu chứa số nguyên dương m;
Dòng thứ hai chứa m số nguyên không âm b1, b2, ..., bm (bi ≤ 109).
Kết quả: Ghi ra số nguyên k là số phần tử bị loại bỏ. Ghi số 0 nếu dãy đã
cho là SEQ198.
Ví dụ:
Input
6
731921
Output
2
 
Ràng buộc:
Có 50% số test ứng với 50% số điểm của bài có m ≤ 20.
Có 50% số test còn lại ứng với 50% số điểm của bài có m ≤ 2000.
41

Hướng dẫn:

Sinh các dãy nhị phân độ dài m


Ta quy ước ý nghĩa của các bit trong mỗi dãy nhị phân sinh ra:
- Bit thứ i bằng 0 có nghĩa bi sẽ bị xóa.
- Bit thứ i bằng 1 có nghĩa bi sẽ không xóa.

Với mỗi dãy nhị phân sinh ra, ta kiểm tra xem dãy con được giữ lại có là
dãy SEQ198 không?
Nếu có, ta đếm số phần tử bị xóa (đếm số bit 0), rồi cập nhật “kỷ lục”
mới (nếu có).
IF Dem < Smin THEN Smin := Dem;
TỔ HỢP CHẬP K CỦA N PHẦN TỬ
Một tổ hợp chập k của n phần tử là một tập con gồm k phần tử của
tập n phần tử.
Chẳng hạn tập {1,2,3,4} có các tổ hợp châp 2 là:
{1,2}, {1,3}, {1,4}, {2,3}, {2,4}, {3,4}.
Chú ý, tổ hợp không phân biệt thứ tự giữa các phần tử, do đó tập
{1,2} và tập {2,1} chỉ được coi là một tổ hợp. Ta thường liệt kê các
phần tử của tổ hợp theo thứ tự tăng dần.
Yêu cầu: Cho trước tập A = {1,2,...n} và số k, hãy xác định tất cả các
tổ hợp chập k của n phần tử của A.
- Dữ liệu vào từ file văn bản TOHOP.INP gồm một dòng duy nhất
chứa hai số nguyên dương n và k (1 ≤ k ≤ n ≤ 20).
- Kết quả ghi ra file văn bản TOHOP.OUT gồm nhiều dòng, mỗi
dòng ghi một tổ hợp tìm được.
Ví dụ:

TOHOP.INP TOHOP.OUT
42 12
13
14
23
24
34
Lời giải:
Mỗi tổ hợp là một tập con thỏa các điều kiện sau:
- Tập con gồm k phần tử {x1,x2,...,xk};
- xi lấy giá trị trong tập {1,2,...,n} và thỏa điều kiện:
+ xi-1 < xi với mọi 1 ≤ i ≤ k; (vì tổ hợp không phân biệt thứ tự
nên ta liệt kê theo thứ tự tăng dần)
+ xi ≤ (n-(k-i)) // giải thích: phía sau i còn (k-i) phần tử, do đó giá
trị của xi chỉ có thể lấy tối đa là (n-(k-i)).
Để tính luôn được cho x1, ta bổ sung x0 = 0.
• Số tổ hợp chập k của n phần tử được tính bởi công thức:
n!
Cnk 
k!(n  k )!

• Thuật toán chạy hiệu quả với n =25 và k = 10


Thủ tục Try liệt kê các tổ hợp:
Procedure Try(i:longint); {Xay dung phan tu thu i cua to hop}
Var j: longint;
Begin
For j:=x[i-1]+1 to (n-k+i) do {Xet tat ca cac gia tri co the chon
cho phan tu thu i}
Begin
x[i]:= j;
If (i=k) then Ketqua {Neu da xay dung den phan tu
thu k thi in ket qua}
Else Try(i+1); {Goi de qui xay dung phan tu ke tiep
cua to hop}
End;
End;
Thủ tục in kết quả các tổ hợp:
Procedure Ketqua;
Var i: longint;
Begin
For i:=1 to k do Write(x[i],#32);
End;
Kho hàng

Một công ty thương mại có n cửa hàng dọc theo một đoạn đường quốc lộ được
đánh số từ 1 đến n (tính từ đầu đường). Li là khoảng cách của cửa hàng thứ i đến
đầu đường. Hiện tại, không có cửa hàng nào trong số n cửa hàng của công ty có
kho chứa hàng cả. Thời gian tới, công ty sẽ đặt K kho hàng tại một số cửa hàng để
thuận tiện cho việc phân phối hàng hóa. Sau khi đặt các kho hàng; nếu một cửa
hàng có kho thì không cần phải đi lấy hàng, ngược lại thì phải đến lấy hàng ở cửa
hàng có kho gần nhất.

Yêu cầu: Cho trước n và số K, hãy giúp công ty chọn K cửa hàng để đặt kho sao
cho quãng đường đi lấy hàng dài nhất của các cửa hàng không được đặt kho là
ngắn nhất.

Dữ liệu: vào từ file văn bản KHOHANG.INP gồm nhiều dòng:


- Dòng đầu là 2 số n và K (2 ≤ n ≤ 1000, 1 ≤ K < n);
- Trong n dòng tiếp theo, dòng thứ i là số nguyên dương Li (Li ≤ 109);
Các số trên cùng dòng viết cách nhau ít nhất 1 dấu cách.
Kết quả: ghi ra file văn bản KHOHANG.OUT gồm 1 số nguyên duy nhất là quãng
đường đi lấy hàng ngắn nhất của các cửa hàng không được đặt kho theo yêu cầu
đề bài.
Ví dụ:
KHOHANG.INP KHOHANG.OUT
32 1
3 (đặt kho ở cửa hàng 1, 2 hoặc 1, 3)
10
11
51 2
1 (đặt kho ở cửa hàng 3)
2
3
4
5

Giới hạn: Có ít nhất


40% số test có n ≤ 20
30% số test có n ≤ 100
30% số test có n ≤ 1000
Hướng dẫn

- Lời giải “ăn” 40% test


+ Sinh tổ hợp cập k của n phần tử
+ Với mỗi tổ hợp sinh ra, kiểm tra thỏa yêu cầu bài toán, ghi nhận nghiệm tối ưu

- Lời giải “ăn” 70% test. Độ phức tạp O(n3)


+ Gọi D khoảng đi lấy hàng tối ưu của các cửa hàng.
 Ta có Dmin ≤ D ≤ Dmax. Trong đó Dmin = 0, Dmax = Ln - L1
+ Tìm D nhỏ nhất sao cho: (tìm D bằng tìm kiếm tuần tự)
Nếu đặt kho tại các cửa hàng sao cho đảm bảo “Mỗi của hàng không có kho, có
đường đi lấy hàng không quá D” thì Số kho đặt tại các cửa hàng không quá k.

- Lời giải “ăn” 100% test. Độ phức tạp O(n2logn)


+ Gọi D khoảng đi lấy hàng tối ưu của các cửa hàng.
 Ta có Dmin ≤ D ≤ Dmax. Trong đó Dmin = 0, Dmax = Ln - L1
+ Tìm D nhỏ nhất sao cho: (tìm D bằng tìm kiếm nhị phân)
Nếu đặt kho tại các cửa hàng sao cho đảm bảo “Mỗi của hàng không có kho, có
đường đi lấy hàng không quá D” thì Số kho đặt tại các cửa hàng không quá k.
HOÁN VỊ

Một hoán vị của tập n phần tử {1,2,3,…,n} là một cách xếp đặt có thứ
tự vị trí của n phần tử của tập hợp.
Chẳng hạn tập {1,2,3} có các hoán vị là:
(1,2,3), (1,3,2), (2,1,3), (2,3,1), (3,1,2), (3,2,1).
Yêu cầu: Cho trước tập A={1,2,...,n}, hãy xác định tất cả các hoán vị
của n phần tử của A.
- Dữ liệu vào từ file văn bản HOANVI.INP gồm một số nguyên
dương n (1 ≤ n ≤ 10)
- Kết quả ghi ra file văn bản HOANVI.OUT gồm nhiều dòng, mỗi
dòng ghi một hoán vị tìm được.
Ví dụ:

HOANVI.INP HOANVI.OUT
3 123
132
213
231
312
321
Lời giải:
Mỗi hoán vị là một cấu hình thỏa các điều kiện sau:
- Là cấu hình gồm n thành phần (x1,x2,...,xn);
- xi lấy giá trị trong tập {1,2,...,n};
- xi ≠ xj với mọi i ≠ j. Có phân biệt thứ tự các thành phần.

* Số các hoán vị của tập n phần tử được tính bởi công thức:
P = n!
* Độ phức tạp thuật toán là O(n!). Chạy hiệu quả với độ lớn
của n ≤ 10.
Procedure Try(i:longint);
Var j: longint;
Begin
For j:=1 to n do
If Free[j] then
Begin
x[i]:= j;
Free[j] := False;
If (i=n) then Ketqua
Else Try(i+1);
Free[j] := True;// quay lui
End;
End;

You might also like