Trai He PN Lan IV-ky Yeu Mon Tin 646eccad98

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 328

SÔÛ GIAÙO DUÏC VAØ ÑAØO TAÏO CAØ MAU

TRÖÔØNG THPT CHUYEÂN PHAN NGOÏC HIEÅN

TRAÏI HEØ PHÖÔNG NAM LAÀN IV - 2017

KYÛ YEÁU HOÄI THAÛO


MOÂN: TIN HOÏC

Caø Mau, ngaøy 21 thaùng 7 naêm 2017


Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

MỤC LỤC

STT CHUYÊN ĐỀ Trang


Thuật toán tham lam
1 4
THPT Chuyên Thủ Khoa Nghĩa, An Giang
Quy Hoạch Động
2 17
Nguyễn Chánh Tín, THPT Chuyên Bạc Liêu
Thuật toán xác định hướng của một điểm so với một vector và
những ứng dụng trong việc giải bài toán hình học
3 47
Đoàn Minh Đức
THPT Chuyên Bến Tre
Xác định thuật toán FLOYD – WARSHALL cho đồ thị dày
4 55
THPT Chuyên Vị Thanh, Hậu Giang
Cấu trúc Heap
5 Thi Thị Thanh Tuyền 63
62
THPT Chuyên Nguyễn Thiện Thành, Trà Vinh
Cấu trúc dữ liệu Heap
6 112
110
THPT Chuyên Long An
Cấu trúc dữ liệu tập rời rạc
7 Nguyễn Hoàng Phú 161
159
THPT Chuyên Lý Tự Trọng, Cần Thơ
Cây khung nhỏ nhất
8 168
166
THPT Chuyên Huỳnh Mẫn Đạt, Kiên Giang
Cây IT – SEGMENT TREE
9 Phan Thành Tâm 203
201
THPT Chuyên Thoại Ngọc Hầu, An Giang
TWO POINTERS
10 Lương Quí Hiệp 215
213
THPT Chuyên Lương Thế Vinh, Đồng Nai
Dãy con chung dài nhất
11 Nguyễn Tấn Phát 235
233
THPT Chuyên Phan Ngọc Hiển, Cà Mau

2
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Chuyên đề Hình Học


12 245
242
THPT Chuyên Nguyễn Bỉnh Khiêm, Vĩnh Long
BINARY INDEX TREE BIT
13 267
264
THPT Chuyên Hùng Vương, Bình Dương
BINARY HEAP
14 Phạm Ngọc Bách 272
269
THPT Chuyên Hoàng Lê Kha, Tây Ninh
Vận dụng thuật toán tìm kiếm nhị phân để giải quyết các bài toán thi
học sinh giỏi
15 278
Nguyễn Trọng Nghĩa 275

THPT Chuyên Tiền Giang


Hình học tính toán
16 Lâm Thanh Phương 293
290
THPT Chuyên Nguyễn Thị Minh Khai, Sóc Trăng
Thuật toán TARJAN và ứng dụng
17 Huỳnh Tấn Thông 316
313
THPT Chuyên Nguyễn Quang Diêu, Đồng Tháp

3
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

PHƯƠNG PHÁP THAM LAM


Trường THPT Chuyên Thủ Khoa Nghĩa – An Giang

I. Ý tưởng của thuật toán tham lam:

Trong mọi khía cạnh của cuộc sống hàng ngày, công việc và nghiên cứu, con
người phải luôn đối mặt với các bài toán để lựa chọn phương án có thể chấp nhận
được. Trong số những bài toán đó, có những bài toán chỉ đơn giản là cần có một giải
pháp nhưng cũng có những bài toán khắt khe hơn, đòi hỏi không chỉ là một giải pháp
mà giải pháp đó còn phải là giải pháp tốt nhất (nhanh nhất, đơn giản nhất, hiệu quả
nhất...) đó được gọi là những bài toán tối ưu. Các bài toán tối ưu thường là một số rất
lớn nghiệm, việc tìm ra nghiệm tối ưu đòi hỏi rất nhiều thời gian.

Ý tưởng phương pháp tham lam là một trong những hướng suy nghĩ được sử
dụng phổ biến trong những tình huống này. Các thuật toán sử dụng phương pháp tham
lam như nền tảng logic để tiếp cận, giải quyết bài toán được gọi là các thuật toán tham
lam. Các thuật toán ứng dụng phương pháp tham lam thường diễn ra qua nhiều giai
đoạn và tại mỗi giai đoạn chúng ta thường không biết thông tin toàn bộ dữ liệu của cả
quá trình mà chỉ biết tình trạng hiện tại và thông tin cho bước đi kế tiếp. Ý tưởng chính
của phương pháp tham lam là chúng ta không cần quan tâm tới dữ liệu tổng thể mà chỉ
từ những dữ liệu tại từng giai đoạn để chọn ra giải pháp tối ưu tại mỗi giai đoạn đó với
hy vọng là tổng hợp những giải pháp tối ưu cục bộ sẽ mang lại một giải pháp tối ưu
cho tổng thể. Từ ý tưởng đó cho thấy, kết quả có được từ thuật toán tham lam chỉ
mang tính tối ưu tương đối, thông thường giải pháp cuối cùng có thể chưa là tối ưu mà
chỉ là tiệm cận với phương án tối ưu. Tuy nhiên với những điều kiện, thông tin thường
không mấy rõ ràng của đầu vào, và cách tiếp cận vấn đề khá “trong sáng” của mình,
phương pháp tham lam thường đưa ra giải pháp tương đối tốt trong giới hạn có thể
chấp nhận và đặc biệt có tốc độ tính toán nhanh (giảm độ phức tạp tính toán của các
thuật toán). Các thuật toán tham lam nói chung là đơn giản và hiệu quả (vì các tính
toán để tìm ra quyết định tối ưu địa phương thường là đơn giản). Tuy nhiên, các thuật
toán tham lam có thể không tìm được nghiệm tối ưu mà cho ra nghiệm tiệm cận tối ưu
(nghiệm tương đối tốt), nhưng thông thường lại giúp giảm độ phức tạp thuật toán của
các thuật toán một cách đáng kể. Bên cạnh đó cũng có nhiều thuật toán được thiết kế

4
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

theo kỹ thuật tham lam vẫn cho ta nghiệm tối ưu, chẳng hạn thuật toán Dijkstra tìm
đường đi ngắn nhất từ một đỉnh tới các đỉnh còn lại trong đồ thị định hướng, các thuật
toán Prim và Kruskal tìm cây bao trùm ngắn nhất trong đồ thị vô hướng.

Lược đồ tổng quát của thuật toán tham lam:

procedure Greedy;

begin

X:=  ;

i:=0;

while (Chua_xay_dung_het_cac_thanh_phan_cua_nghiem) do

begin

i:=i+1;

<Xac_dinh_Si>;

X  Select(Si);

end;

end;

II. Một số bài toán được giải quyết bằng phương pháp tham lam:
1. Bài toán cái ba lô
a. Phát biểu bài toán
Có n vật, mỗi vật i, i∈{1,..,n} được đặc trưng bởi
trọng lượng wi và giá trị vi. Có một ba lô có khả năng
mang được trọng lượng m. Giả sử wi, vi, m ∈N*
∀i∈{1,..,n}. Hãy chọn vật xếp vào ba lô sao cho ba lô
thu được có giá trị nhất.
Dữ liệu: Vào từ file BAG.INP có dạng:
- Dòng đầu tiên gồm n và m cách nhau ít nhất Bài toán cái ba lô
một dấu cách.
- n dòng tiếp theo, mỗi dòng thứ i cho biết trọng lượng và giá trị của vật thứ i.
Kết quả: Ghi vào file BAG.OUT có dạng:
- Dòng đầu tiên ghi giá trị tối ưu của bài toán.

5
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

- n dòng tiếp theo, mỗi dòng cho biết số thứ tự của đồ vật và số lượng lấy của đồ
vật thứ i.
Ví dụ:
BAG.INP BAG.OUT
5 11 15
33 11
46 22
Test 1
54 30
9 10 40
42 50
4 37 83
15 30 10
Test 2 10 25 23
22 31
46 41
25 9
Test 3 35 11
24 21

b. Phân tích và thiết kế thuật toán


Ý tưởng: Ở đây, ta cần quan tâm đến yếu tố “đơn giá” của từng loại đồ vật, tức là
tỉ lệ giá trị/trọng lượng, đơn giá càng cao thì đồ càng quý. Từ đó ta có kỹ thuật tham
lam áp dụng cho bài toán này là:
- Bước 1: Tính đơn giá cho các loại đồ vật.

- Bước 2: Sắp xếp các loại đồ vật theo đơn giá từ lớn đến nhỏ.

- Bước 3: Xét các đồ vật theo thứ tự đã sắp xếp, với mỗi đồ vật được xét sẽ lấy số
lượng tối đa mà trọng lượng còn lại của ba lô cho phép.

- Bước 4: Xác định trọng lượng còn lại của ba lô và quay lại bước 3 cho đến khi
xét hết các đồ vật hoặc trọng lượng còn lại của ba lô là 0.

Minh họa với dữ liệu ở test 1:

Bước 1: Tính đơn giá cho các đồ vật, ta được bảng sau:
6
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Loại đồ vật Trọng lượng w Giá trị v Đơn giá c


1 3 3 3/3 = 1
2 4 6 6/4 = 1.5
3 5 4 4/5 = 0.8
4 9 10 10/9 = 1.1
5 4 2 2/4 = 0.5

Bước 2: Sắp xếp lại các đồ vật theo thứ tự từ lớn đến bé, ta có bảng sau:

Loại đồ vật Trọng lượng w Giá trị v Đơn giá c


2 4 6 6/4 = 1.5
4 9 10 10/9 = 1.1
1 3 3 3/3 = 1
3 5 4 4/5 = 0.8
5 4 2 2/4 = 0.5

Bước 3 và 4:

+ Với m = 11, xét đồ vật 2, ta có thể lấy số lượng là 2. Khi đó m được tính lại là
m = 11 – 8 = 3.

+ Với m = 3, xét đồ vật 4, ta không thể lấy đồ vật 4 do vượt quá trọng lượng còn
lại của cái ba lô.

+ Với m = 3, xét đồ vật 1, ta có thể lấy số lượng là 1. Khi đó m được tính lại là m
= 3 – 3 = 0.

+ Do trọng lượng còn lại của ba lô là 0 nên dừng thuật toán.

Vậy ta lấy được đồ vật 2, 1 với số lượng lần lượt tương ứng là 2, 1 với giá trị tối
ưu là 15.

Nhận xét:

Với thuật toán tham lam, trong một số trường hợp ta chỉ tìm được phương án đủ
“tốt” tiệm cận với phương án tối ưu. Điều đáng lưu tâm ở đây là một số bài toán tối ưu
tổ hợp có thuật toán tìm nghiệm chính xác đòi hỏi thời gian mũ, khi dữ liệu bài toán
lớn thì rất khó áp dụng. Trong trường hợp này ta sử dụng thuật toán tham lam cho

7
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

nghiệm đủ tốt mà thời gian thực hiện rất nhanh, đây cũng chính là lý do tại sao thuật
toán tham lam được mọi người quan tâm.

Với 3 bộ test đã cho, ta tìm được nghiệm tối ưu ở 2 test đầu, test thứ 3 không cho
được nghiệm tối ưu. Muốn tìm được nghiệm tối ưu, ta dùng phương pháp quy hoạch
động cho bài toán này.

Độ phức tạp thuật toán: Quá trình sắp xếp nếu làm theo QuickSort mất
O(nlog(n)), quá trình lặp n đồ vật mất O(n). Vậy tổng độ phức tạp của thuật toán là
O(nlog(n)).

2. Bài toán Chọn hoạt động

a. Phát biểu bài toán

Chúng ta có rất nhiều nhiệm vụ trong ngày. Giả sử có 𝑛 nhiệm vụ và nhiệm vụ


thứ 𝑖 phải bắt đầu ngay sau thời điểm 𝑠𝑖, thực hiện liên tục và kết thúc tại thời điểm fi.
Có thể coi mỗi nhiệm vụ tương ứng với một khoảng thời gian thực hiện(𝑠𝑖, fi]. Hãy
chọn ra nhiều nhất các nhiệm vụ để làm, sao cho không có thời điểm nào chúng ta phải
làm hai nhiệm vụ cùng lúc, hay nói cách khác, khoảng thời gian thực hiện hai nhiệm
vụ bất kỳ là không giao nhau.
Dữ liệu: Vào từ file AS.INP có dạng:

- Dòng đầu tiên là số .

- dòng tiếp theo, dòng thứ cho biết chứa hai số nguyên
( ).

Kết quả: Ghi vào file AS.OUT có dạng:

- Dòng đầu tiên ghi số hoạt động được chọn.

- Ḍng tiếp theo cho biết hoạt động được chọn.

Ví dụ:

AS.INP AS.OUT
5 3
79 351
Test 1
68
13

8
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

06
37
11 4
35 2 5 10 4
14
06
12 14
57
Test 2
38
2 13
6 10
59
8 11
8 12

b. Phân tích và thiết kế thuật toán

Ý tưởng:

Trước tiên ta có nhận xét là trong các phương án tối ưu, chắc chắn có một
phương án mà nhiệm vụ đầu tiên được chọn là nhiệm vụ kết thúc sớm nhất.

Gọi i1 là nhiệm vụ kết thúc sớm nhất. Với một phương án tối ưu bất kỳ, giả sử
thứ tự các nhiệm vụ cần thực hiện trong phương án tối ưu đó là (j1, j2, …, jk). Do i1 là
nhiệm vụ kết thúc sớm nhất nên chắc chắn nó không thể kết thúc muộn hơn j1, vì vậy
việc thay j1 bởi i1 trong phương án này sẽ không gây ra sự xung đột nào về thời gian
thực hiện các nhiệm vụ. Sự thay thế này cũng không làm giảm bớt số lượng nhiệm vụ
thực hiện được trong phương án tối ưu, nên (i1, j2, …, jk) cũng sẽ là một phương án tối
ưu. Yêu cầu của bài toán là chỉ cần đưa ra một phương án tối ưu, vì thế ta sẽ chỉ ra
phương án tối ưu có nhiệm vụ đầu tiên là nhiệm vụ kết thúc sớm nhất trong số 𝑛
nhiệm vụ. Điều này có nghĩa là chúng ta không cần thử 𝑛 khả năng chọn nhiệm vụ đầu
tiên, đi giải các bài toán con, rồi mới đánh giá chúng để đưa ra quyết định cuối cùng.
Chúng ta sẽ đưa ngay ra quyết định tức thời: chọn ngay nhiệm vụ kết thúc sớm nhất i1
làm nhiệm vụ đầu tiên.
Sau khi chọn nhiệm vụ i1, bài toán lớn quy về bài toán con: Chọn nhiều nhiệm
vụ nhất trong số các nhiệm vụ được bắt đầu sau khi i1 kết thúc. Phép chọn tham lam lại
cho ta một quyết định tức thời: nhiệm vụ tiếp theo trong phương án tối ưu sẽ là nhiệm
vụ bắt đầu sau thời điểm f[i1] và có thời điểm kết thúc sớm nhất, gọi đó là nhiệm vụ i2.

9
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Và cứ như vậy chúng ta chọn tiếp các nhiệm vụ i3, i4…. Cứ như vậy cho tới khi không
còn nhiệm vụ nào chọn được nữa.

Đến đây ta có thể thiết kế một thuật toán tham lam như sau:
 Sắp xếp các nhiệm vụ theo thứ tự không giảm của thời điểm kết thúc.
 Khởi tạo thời điểm 𝐹𝑖𝑛𝑖𝑠ℎ𝑇𝑖𝑚𝑒 ∶= 0.
 Duyệt các nhiệm vụ theo danh sách đã sắp xếp (nhiệm vụ kết thúc sớm sẽ được
xét trước nhiệm vụ kết thúc muộn), nếu xét đến nhiệm vụ 𝑖 có [𝑖] ≥ 𝐹𝑖𝑛𝑖𝑠ℎ𝑇𝑖𝑚𝑒 thì
chọn ngay nhiệm vụ 𝑖 vào phương án tối ưu và cập nhật 𝐹𝑖𝑛𝑖𝑠ℎ𝑇𝑖𝑚𝑒 thành thời điểm
kết thúc nhiệm vụ 𝑖: 𝐹𝑖𝑛𝑖𝑠ℎ𝑇𝑖𝑚𝑒 ≔ 𝑓[𝑖].
Minh họa với dữ liệu test 1:

Ta có bảng dữ liệu sau khi sắp xếp là:

Công việc i 3 4 5 2 1
s[i] 1 0 3 6 7
f[i] 3 6 7 8 9

FinishTime = 0.

Với bảng trên, ta chọn công việc 3 trước, khi đó cập nhật lại FinishTime= 3.

Tiếp tục duyệt các công việc còn lại, công việc 4 sẽ không được chọn do
s[4]< FinishTime.

Ta chọn công việc 5 do s[5]  FinishTime, cập nhật lại FinishTime = 7.

Công việc 2 không được chọn do s[4]< FinishTime.

Ta chọn công việc 1 do s[1]  FinishTime, cập nhật lại FinishTime = 9.

Dừng thuật toán lại vì đã xét hết các công việc.

Hình 0.1. Minh họa bài toán Chọn hoạt động

Nhận xét: Thuật toán tham lam cho bài toán này luôn cho ra phương án tối ưu.
10
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Độ phức tạp thuật toán: Dễ thấy rằng ở thủ tục GreedySelection được thực
hiện trong thời gian O(n). Thời gian mất chủ yếu nằm ở thuật toán sắp xếp các công
việc theo thời điểm kết thúc. Như đoạn code ở trên, ta dùng QuickSort, do đó độ phức
tạp thuật toán sẽ là O(nlog(n)).

3. Bài toán máy rút tiền tự động ATM

a. Phát biểu bài toán

Một máy ATM hiện có n (n < 20) tờ tiền có mệnh giá t1, t2,..., tn. Hãy tìm cách trả
ít tờ nhất với số tiền đúng bằng s.

Dữ liệu: Vào từ file ATM.INP có dạng:

- Dòng đầu tiên gồm n và s cách nhau ít nhất một dấu cách.

- Dòng thứ 2 gồm n số t1, t2, …, tn.

Kết quả: Ghi vào file ATM.OUT, nếu không có phương án thì ghi -1, ngược lại
thì:

- Dòng đầu tiên ghi số tờ tiền.

- Dòng tiếp theo ghi cách trả tiền.

Ví dụ:

ATM.INP ATM.OUT
10 390 5
Test 1
200 10 20 20 50 50 50 50 100 100 200 100 50 20 20
11 100 8
Test 2
50 20 20 20 20 20 2 2 2 2 2 50 20 20 2 2 2 2 2
6 100 -1
Test 3
50 20 20 20 20 20

b. Phân tích và thiết kế thuật toán

Ý tưởng: Như đã phân tích ở ví dụ 1.2, ta thiết kế thuật toán tham lam như sau:
tại mỗi bước ta sẽ chọn tờ tiền lớn nhất còn lại không vượt quá lượng tiền còn phải trả,
cụ thể:

Bước 1: Sắp xếp các tờ tiền giảm dần theo giá trị.
11
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Bước 2: Lần lượt xét các tờ tiền từ giá trị lớn đến giá trị nhỏ, nếu vẫn còn chưa
lấy đủ s và tờ tiền đang xét có giá trị nhỏ hơn hoặc bằng s thì lấy luôn tờ tiền đó.

Minh họa với dữ liệu ở test 1:

Bước 1: Sau khi sắp xếp các tờ tiền giảm dần theo giá trị, ta có:

200 100 100 50 50 50 50 20 20 10

Bước 2: Với tiền cần rút là 390, ta chọn được 2 phần tử đầu tiên trong dãy, khi đó
số tiền cần rút còn lại là 90. Do đó ta không chọn tờ 100, mà sẽ xét tiếp phần tử tiếp
theo. Ta chọn được một tờ 50, tiền cần rút còn lại là 40. Vì vậy ta không chọn được
các tờ 50. Xét tiếp ta chọn được hai tờ 20, khi đó đã đủ số tiền cần rút, dừng thuật
toán. Kết quả là ta nhận được là 200 100 50 20 20.

Nhận xét:

Với bộ test 1, thuật toán tham lam cũng cho được nghiệm tối ưu. Tuy nhiên với
bộ test 2, thuật toán tham lam không cho nghiệm tối ưu và bộ test 3 thì thuật toán tham
lam không tìm được nghiệm mặc dù có nghiệm.

Độ phức tạp thuật toán: Thuật toán tham lam qua một lần duyệt, có độ phức tạp
là O(n). Toàn bộ quá trình mất thời gian nhiều nhất là ở quá trình sắp xếp, ở đây ta sắp
xếp theo phương pháp chọn, có độ phức tạp là O(n2). Vậy độ phức tạp của thuật toán
là O(n2).

4. Bài toán băng nhạc

a. Phát biểu bài toán

Người ta cần ghi n bài hát, được mã số từ 1 đến n, vào một băng nhạc có thời
lượng tính theo phút đủ chứa toàn bộ các bài hát đã cho. Với mỗi bài hát ta biết thời
lượng phát của mỗi bài hát đó. Băng sẽ được lắp vào một máy phát nhạc đặt trong một
siêu thị. Khách hàng muốn nghe bài hát nào chỉ cần nhấn phím tương ứng với bài hát
đó. Để tìm và phát bài hát thứ i trên băng, máy xuất phát từ đầu cuộn băng, quay băng
để bỏ qua i-1 bài ghi trước đó. Thời gian quay băng bỏ qua mỗi bài hát và thời gian
phát bài hát đó được tính là như nhau. Tính trung bình, các bài hát trong một ngày
được khách hàng lựa chọn với số lần (tần suất) là như nhau. Hãy tìm cách ghi các bài
trên băng sao cho tổng thời gian quay băng trong mỗi ngày là ít nhất.
12
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Dữ liệu: Vào từ file CASSETTE.INP có dạng:

- Dòng đầu tiên là số tự nhiên n cho biết số lượng bài hát.

- Dòng thứ 2 gồm n số nguyên dương thể hiện dung lượng tính theo phút của mỗi
bài, mỗi số cách nhau một dấu cách.

Kết quả: Dữ liệu ghi vào file CASSETTE.OUT có dạng:

- n dòng đầu tiên thể hiện trật tự ghi bài hát trên băng, mỗi dòng gồm hai số
nguyên dương j và d cách nhau bởi một dấu cách, trong đó j là số thứ tự của bài hát
cần ghi, d là thời gian tìm và phát bài đó theo trật tự này.

- Dòng thứ n+1 ghi tổng số thời gian quay băng nếu mỗi bài hát được phát một
lần trong ngày.

Ví dụ:

CASSETTE.INP CASSETTE.OUT
3 22
723 35
1 12
19

b. Phân tích và thiết kế thuật toán

Ý tưởng:

Giả sử ta có ba bài hát với số phút lần lượt như sau:

Mã số bài hát 1 2 3
Thời gian phát 7 2 3

Ta xét vài tình huống ghi băng để rút ra kết luận cần thiết:

Trật tự ghi trên Thời gian phát


băng (x,y,z) t(x) + t(y) + t(z); t(i): thời gian tìm và phát bài i
(1,2,3) (7) + (7 + 2) + (7 + 2 + 3) = 28 phút
(1,3,2) (7) + (7 + 3) + (7 + 3 + 2) = 29 phút
(2,1,3) (2) + (2 + 7) + (2 + 7 + 3) = 23 phút
(2,3,1) (2) + (2 + 3) + (2 + 3 + 7) = 19 phút (phương án tối ưu)
13
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

(3,1,2) (3) + (3 + 7) + (3 + 7 + 2) = 25 phút


(3,2,1) (3) + (3 + 2) + (3 + 2 + 7) = 20 phút

Vậy phương án tối ưu sẽ là (2,3,1): ghi bài 2 rồi đến bài 3, cuối cùng ghi bài 1.
Tổng thời gian theo phương án này là 19 phút.

Để có phương án tối ưu, ta chỉ cần ghi băng theo trật tự tăng dần của thời lượng.
Bài toán được cho với giả thiết băng đủ lớn để ghi được toàn bộ các bài [3].

Minh họa với dữ liệu đã cho:

Rất đơn giản, ta chỉ cần sắp xếp theo thứ tự không giảm thời gian phát của từng
bài nhạc. Với dữ liệu đã cho ta sẽ có thứ tự là 2, 3, 1 và thời gian phát toàn bộ sẽ được
tính là 2 + (2 + 3) + (2 + 3 + 7) = 19 phút.

Nhận xét:

Thuật toán tham lam trong trường hợp này là các thời gian phát của mỗi bài nhạc
được tính theo trật tự tăng dần. Lời giải tìm được sẽ là lời giải tối ưu.

Độ phức tạp thuật toán: Thuật toán tham lam qua một lần duyệt, có độ phức tạp
là O(n). Toàn bộ quá trình mất thời gian nhiều nhất là ở quá trình sắp xếp, ở đây ta sắp
xếp theo QuickSort, có độ phức tạp là O(nlog(n)). Vậy độ phức tạp của thuật toán là
O(nlog(n)).

5. Bài toán nối điểm đen trắng


a. Phát biểu bài toán
Trên trục số thực cho n điểm đen và n điểm trắng hoàn toàn phân biệt. Các điểm
đen có tọa độ nguyên a1, a2, …, an còn các điểm trắng có tọa độ nguyên b1, b2, …, bn.
Người ta muốn chọn ra k điểm đen và k điểm trắng để nối mỗi một điểm đen với một
điểm trắng sao cho k đoạn thẳng tạo được đôi một không có điểm chung.

Yêu cầu: Cho tọa độ của n điểm đen a1, a2, …, an và tọa độ của điểm trắng b1, b2,
…, bn. Hãy tìm giá trị k lớn nhất thỏa mãn yêu cầu trên [7].

Dữ liệu: Vào từ file BAW.INP có dạng:

- Dòng thứ nhất chứa số nguyên dương n (0 < n ≤ 105).

- Dòng thứ hai chứa các số a1, a2, …, an (|ai| <= 109, i = 1, 2,…, n).

14
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

- Dòng thứ ba chứa các số b1, b2, …, bn (|bi| <= 109, i = 1, 2,…, n).

Các số trên cùng một dòng được ghi cách nhau ít nhất một dấu cách.

Kết quả: Ghi ra một số nguyên duy nhất là số k lớn nhất tìm được.

Ví dụ:

BAW.INP BAW.OUT
3 2
031
-3 5 -1

b. Phân tích và thiết kế thuật toán

Hình 0.2. Minh họa dữ liệu bài toán nối điểm đen trắng

Ý tưởng:

Trộn hai màu lại thành một danh sách trên trục tọa độ và sắp xếp lại theo thứ tự
tăng dần. Dùng ý tưởng tham lam duyệt thứ tự từ trái sang phải, nếu thấy hai cặp liền
nhau khác màu thì tăng biến đếm lên 1, bởi vì nhận thấy rằng cần chọn các cặp điểm
kề nhau khác màu trên trục này thì sẽ được nhiều cặp điểm không giao nhau. Ta cũng
chú ý là tăng bước nhảy để tránh trường hợp 1 điểm đen (trắng) kẹp giữa 2 điểm trắng
(đen) thì tính thành 2 đoạn vì ở đây đề bài nói là các điểm đen trắng phân biệt.

Minh họa với dữ liệu đã cho:

Theo đề bài ta có các biến và các giá trị như sau:

Biến Giá trị


N 3
A 031
B -3 5 -1

Khởi tạo giá trị tất cả các phần tử của mảng color là 0 và res là 0.

15
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Thêm tất cả các giá trị của mảng b thêm sau mảng a, đồng thời ở các vị trí thêm
đó ở mảng color ta đánh dấu là 1 (để phân biệt với các phần tử của mảng a). Ta có
bảng sau:

Biến 1 2 3 4 5 6
a 0 3 1 -3 5 -1
color 0 0 0 1 1 1

Sắp xếp lại mảng a theo thứ tự tăng dần, khi đổi chỗ hai phần tử mảng a ta cũng
đồng thời đổi luôn ở mảng color. Ta có dữ liệu như sau sau khi sắp xếp lại:

Biến 1 2 3 4 5 6
a -3 -1 0 1 3 5
color 1 1 0 0 0 1

Ta duyệt trong mảng color từ trái sang phải, nếu thấy hai cặp liền nhau khác giá
trị thì tăng biến res lên 1. Sau khi duyệt, biến res = 2 là kết quả tối ưu của bài toán.

Nhận xét: Thuật toán tham lam thể hiện ở chỗ sau khi sắp xếp dữ liệu, ta chỉ
cần chọn các cặp điểm liền kề nhau khác màu trên trục là sẽ ra được kết quả tối ưu.

Độ phức tạp thuật toán: Thuật toán tham lam qua một lần duyệt n phần tử có
O(n), sắp xếp dùng QuickSort có O(nlog(n)). Vậy độ phức tạp của thuật toán là
O(nlog(n)).

16
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

QUY HOẠCH ĐỘNG

Nguyễn Chánh Tín - THPT Chuyên Bạc Liêu


Phần 1: CƠ SỞ LÝ THUYẾT
I- CƠ SỞ LÝ THUYẾT:
1) Phương pháp quy hoạch động:
Nhà toán học Richard Bellman đã phát minh phương pháp Quy hoạch động vào
năm 1953. Ngành này đã được thành lập như là một chủ đề về kỹ nghệ và phân tích hệ
thống đã được tổ chức IEEE (Viện kỹ nghệ Điện và Điện tử quốc tế) thừa nhận.
Phương pháp quy hoạch động dùng để giải bài toán tối ưu có tính chất đệ quy,
tức là việc tìm phương án tối ưu cho bài toán đó có thể đưa về tìm phương án tối ưu
của một số hữu hạn các bài toán con. Đối với nhiều thuật toán đệ quy, nguyên lý chia
để trị (devide and conquer) thường đóng vai trò chủ đạo trong việc thiết kế thuật toán.
Để giải quyết một bài toán lớn, ta chia nó thành nhiều bài toán con cùng dạng với nó
để có thể giải quyết độc lập. Trong phương pháp quy hoạch động, nguyên lý này càng
được thể hiện rõ: Khi không biết cần phải giải bài những toán con nào, ta sẽ đi giải
quyết tất cả các bài toán con và lưu trữ những lời giải hay đáp số của chúng với mục
đích sử dụng lại theo một sự phối hợp nào đó để giải quyết những bài toán tổng quát
hơn. Đó chính là điểm khác nhau giữa Quy hoạch động và phép đệ quy và cũng là nội
dung Phương pháp quy hoạch động:
+ Đệ quy bắt đầu từ bài toán lớn phân rã thành nhiều bài toán con và đi giải từng
bài toán con đó. Việc giải từng bài toán con lại đưa về phép phân rã tiếp thành nhiều
bài toán con nhỏ hơn và lại đi giải quyết bài toán nhỏ hơn đó bất kể nó đã được giải
hay chưa.
+ Quy hoạch động bắt đầu từ việc giải tất cả các bài toán nhỏ nhất (bài toán cơ
sở) để từ đó từng bước giải quyết những bài toán lớn hơn, cho tới khi giải được bài
toán lớn nhất (bài toán ban đầu).
Sau đây ta sẽ xét một ví dụ đơn giản và rất quen thuộc:
Ví dụ: Dãy Fibonacci là dãy số nguyên dương được định nghĩa như sau:
F[1]=F[2]=1;
Với mọi i: 3<=i: F = F[i-1] + F[i-2]

17
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Hãy tính F[N]


Xét hai cách cài đặt chương trình:
* Cách 1: Dùng đệ quy
function F (i: longint): longint;
begin
if i<=2 then exit(1)
else exit(F(i-1)+F(i-2));
end;
* Cách 2: Dùng quy hoạch động
F[1]:=1; F[2]:=1;
For i:=3 to MaxN do F[i]:=F[i-1]+F[i-2]
Nhận xét:
Trong cách 1, ta viết một hàm đệ quy F(i) để tính số Fibonacci thứ i. Giả sử ta
cần tính F[6]: Chương trình chính gọi F(6), nó sẽ gọi tiếp F(5) và F(4) để tính … Quá
trình tính toán có thể vẽ như cây dưới đây. Ta nhận thấy để tính F(6) nó phải tính 1 lần
F(5), 2 lần F(4), 3 lần F(3), 5 lần F(2), 3 lần F(1).

Cách 2 thì không như vậy. Trước hết nó tính sẵn F[1] và F[2], từ đó tính tiếp
F[3], lại tính tiếp được F[4], F[5], F[6]. Ðảm bào rằng mỗi giá trị Fibonnaci chỉ phải
tính 1 lần.
(Cách 2 còn có thể cải tiến thêm nữa, chỉ cần dùng 3 biến tính lại giá trị lẫn
nhau).
Trước khi áp dụng phương pháp quy hoạch động ta phải xét xem phương pháp
đó có thỏa mãn những yêu cầu dưới đây hay không:

18
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

+ Bài toán lớn phải phân rã được thành nhiều bài toán con, mà sự phối hợp lời
giải của các bài toán con đó cho ta lời giải của bài toán lớn.
+ Vì quy hoạch động là đi giải tất cả các bài toán con, nên nếu không đủ không
gian vật lý lưu trữ lời giải (bộ nhớ, đĩa,…) để phối hợp chúng thì phương pháp quy
hoạch động cũng không thể thực hiện được.
+ Quá trình từ bài toán cơ sở tìm ra lời giải bài toán ban đầu phải qua hữu hạn
bước.
2) Các khái niệm:
+ Bài toán giải theo phương pháp quy hoạch động gọi là bài toán quy hoạch
động.
+ Công thức phối hợp nghiệm của các bài toán con để có nghiệm của bài toán
lớn gọi là công thức truy hồi của quy hoạch động.
+ Tập các bài toán nhỏ nhất có ngay lời giải để từ đó giải quyết các bài toán
lớn hơn gọi là cơ sở quy hoạch động.
+ Không gian lưu trữ lời giải các bài toán con để tìm cách phối hợp chúng gọi
là bảng phương án của quy hoạch động.
3) Các bước cài đặt một chương trình sử dụng quy hoạch động:
+ Giải tất cả các bài toán cơ sở (thông thường rất dễ), lưu các lời giải vào bảng
phương án.
+ Dùng công thức truy hồi phối hợp những lời giải của các bài toán nhỏ đã lưu
trong bảng phương án để tìm lời giải của những bài toán lớn hơn và lưu chúng vào
bảng phương án. Cho tới khi bài toán ban đầu tìm được lời giải.
+ Dựa vào bảng phương án, truy vết tìm ra nghiệm tối ưu.
Cho đến nay, vẫn chưa có một định lý nào cho biết một cách chính xác những bài
toán nào có thể giải quyết hiệu quả bằng quy hoạch động. Tuy nhiên để biết được bài
toán có thể giải bằng quy hoạch động hay không, ta có thể tự đặt câu hỏi : “Một
nghiệm tối ưu của bài toán lớn có phải là sự phối hợp các nghiệm tối ưu của các
bài toán con hay không?” và “Liệu có thể nào lưu trữ được nghiệm các bài toán
con dưới một hình thức nào đó để phối hợp tìm được nghiệm bài toán lớn”.

19
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Cuối cùng trước khi khảo sát một số bài toán quy hoạch động, ta nhắc lại:
Phương pháp tốt nhất để giải quyết mọi bài toán trong tin học là biết sử dụng và phối
hợp uyển chuyển nhiều thuật toán, không được lạm dụng hay coi thường bất cứ một
phương pháp nào.
II- MỘT SỐ BÀI TOÁN VÍ DỤ ĐIỂN HÌNH:
1) Bài 1: Trước tiên chúng ta hãy xét 1 bài toán thật đơn giản và quen thuộc đó là
tìm giá trị lớn nhất trong n số là a1, a2, ..., an. Giải quyết bài toán này, ta sẽ xây dựng
các cấu hình con tối ưu bằng cách lần lượt tìm số lớn nhất trong k số đầu tiên với k
chạy từ 1 đến n:
K=1: max1:=a1;
K=2: max2:=max(max1,a2);
K=3: max3:=max(max2,a3);
..............................................
K=n: maxn:=max(maxn-1,an);
Như vậy khi k đạt tới n thì maxn chính là giá trị lớn nhất trong n số đã chọ Việc
cài đặt chương trình hết sức đơn giản như sau:
Uses crt;
Var a: array[1..100] of integer;
n,k,max: integer;
Begin
Write('Cho so luong phan tu: ');readln(n);
For i:=1 to n do
begin
write('a[',i,']= ');
readln(a[i]);
end;
Max:=a[1];
For k:=2 to n do
If a[k]>max then max:=a[k];
Write('Gia tri lon nhat cua day cac so da cho la: ',max);
Readln
End.
20
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Bây giờ chúng ta xét đến bài toán 2 có phần hấp dẫn hơn. Đây chính là một trong
những bài toán kinh điển cho giải thuật qui hoạch động:
2) Bài 2: BÀI TOÁN XẾP BA LÔ (một số sách ghi là bài toán cái túi, tương tự
như bài toán xếp vali) là một bài toán tối ưu hóa tổ hợp. Bài toán được đặt tên từ vấn
đề chọn những gì quan trọng có thể nhét vừa vào trong một cái túi (với giới hạn khối
lượng) để mang theo trong một chuyến đi.
Một số cách phát biểu nội dung bài toán:
Một kẻ trộm đột nhập vào một cửa hiệu tìm thấy
có n mặt hàng có trọng lượng và giá trị khác nhau, nhưng
hắn chỉ mang theo một cái túi có sức chứa về trọng lượng tối
đa là M. Vậy kẻ trộm nên bỏ vào ba lô những món nào và số
lượng bao nhiêu để đạt giá trị cao nhất trong khả năng mà
hắn có thể mang đi được.
Một hành khách chỉ được mang theo một vali có khối
lượng hàng hoá tối đa là M. Hành khách ðó ðã chuẩn bị ra N dồ vật được ðánh số từ 1
ðến N ðể chuẩn bị mang theo. Ðồ vật thứ i có trọng lượng là ai và giá trị sử dụng là
ci (i = 1, 2, .. N) . Yêu cầu: Chỉ ra ðồ vật mà hành khách ðó cần mang theo sao cho
tổng giá trị sử dụng là lớn nhất?
* Bài toán:
Trong siêu thị có n gói hàng (n ≤ 100), gói hàng thứ i có trọng lượng là Wi ≤ 100
và trị giá Vi ≤ 100. Một tên trộm đột nhập vào siêu thị, tên trộm mang theo một cái túi
có thể mang được tối đa trọng lượng M ( M ≤ 100). Hỏi tên trộm sẽ lấy đi những gói
hàng nào để được tổng giá trị lớn nhất.
Input: file văn bản BAG.INP
• Dòng 1: Chứa hai số n, M cách nhau ít nhất một dấu cách
• n dòng tiếp theo, dòng thứ i chứa hai số nguyên dương Wi, Vi cách nhau ít nhất
một dấu cách
Output: file văn bản BAG.OUT
• Dòng 1: Ghi giá trị lớn nhất tên trộm có thể lấy
• Dòng 2: Ghi chỉ số những gói bị lấy

21
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

BAG.INP BAG.OUT

5 11 11
3 3 521
4 4
5 4
9 10
44

* Ý týởng giải thuật:


Nếu gọi F[i, j] là giá trị lớn nhất có thể có bằng cách chọn trong các gói {1, 2,
…, i} với giới hạn trọng lượng j. Thì giá trị lớn nhất khi được chọn trong số n gói với
giới hạn trọng lượng M chính là F[n, M].
Công thức truy hồi tính F[i, j].
Với giới hạn trọng lượng j, việc chọn tối ưu trong số các gói {1, 2, …,i – 1, i} để
có giá trị lớn nhất sẽ có hai khả năng:
• Nếu không chọn gói thứ i thì F[i, j] là giá trị lớn nhất có thể bằng cách chọn
trong số các gói {1, 2, …, i – 1} với giới hạn trọng lượng là j. Tức là
F[i, j] = F[i - 1, j]
• Nếu có chọn gói thứ i (tất nhiên chỉ xét tới trường hợp này khi mà Wi ≤ j) thì
F[i, j] bằng giá trị gói thứ i là Vi cộng với giá trị lớn nhất có thể có được bằng cách
chọn trong số các gói {1, 2, …, i – 1} với giới hạn trọng lượng j – Wi. Tức là về mặt
giá trị thu được:
F[i, j] = Vi + F[i - 1, j - Wi]
Vì theo cách xây dựng F[i, j] là giá trị lớn nhất có thể, nên F[i, j] sẽ là max trong
2 giá trị thu được ở trên.
Cơ sở quy hoạch ðộng:
Dễ thấy F[0, j] = giá trị lớn nhất có thể bằng cách chọn trong số 0 gói = 0.
Tính bảng phương án:

22
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Bảng phương án F gồm n + 1 dòng, M + 1 cột, trước tiên được điền cơ sở quy
hoạch động: Dòng 0 gồm toàn số 0. Sử dụng công thức truy hồi, dùng dòng 0 tính
dòng 1, dùng dòng 1 tính dòng 2, v.v… đến khi tính hết dòng n.

Truy vết:
Tính xong bảng phương án thì ta quan tâm đến F[n, M] đó chính là giá trị lớn
nhất thu được khi chọn trong cả n gói với giới hạn trọng lượng M. Nếu F[n, M] = F[n -
1, M] thì tức là không chọn gói thứ n, ta truy tiếp F[n - 1, M]. Còn nếu F[n, M] ≠ F[n -
1, M] thì ta thông báo rằng phép chọn tối ưu có chọn gói thứ n và truy tiếp F[n - 1, M -
Wn]. Cứ tiếp tục cho tới khi truy lên tới hàng 0 của bảng phương án
Chương trình:
const max = 100;
var
w, v: array[1..max] of integer;
f: array[0..max, 0..max] of integer;
n, m: integer;
procedure enter;
var i: integer;
begin
readln(n, m);
for i := 1 to n do readln(w[i], v[i]);
end;
procedure optimize; {tinh bang phuong an}
var i, j: integer;
begin
fillchar(f[0], sizeof(f[0]), 0); {dien co so qhd}
for i := 1 to n do

23
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

for j := 0 to m do
begin {tinh f[i, j]}
f[i, j] := f[i - 1, j]; {Neu khong chon goi thu i}
if (j>= w[i]) and (f[i, j] < f[i-1, j-w[i]]+v[i]) then
f[i, j] := f[i-1, j-w[i]]+v[i];
end;
end;
procedure trace; {truy vet tim nghiem toi uu}
begin
writeln(f[n, m]); {in ra gia tri lon nhat co the tim duoc}
while n <> 0 do {truy vet tu hang n len hang 0}
begin
if f[n, m] <> f[n - 1, m] then {neu co chon goi thu n}
begin
write(n, ' ');
m := m - w[n];
end;
dec(n);
end;
end;
Begin
assign(input, 'bag.inp'); reset(input);
assign(output, 'bag.out'); rewrite(output);
enter;
optimize;
trace;
close(input); close(output);
End.

24
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

3) Bài 3: DÃY CON ĐƠN ĐIỆU TĂNG DÀI NHẤT

Bài toán: Cho dãy số nguyên A = a1, a2, …, an. (n ≤ 10000, -10000 ≤ ai ≤
10000). Một dãy con của A là một cách chọn ra trong A một số phần tử giữ nguyên
thứ tự. Như vậy A có 2n dãy con.
Yêu cầu: Tìm dãy con đơn điệu tăng của A có độ dài lớn nhất.
Ví dụ: A = (1, 2, 3, 4, 9, 10, 5, 6, 7, 8). Dãy con đơn điệu tăng dài nhất là: (1, 2,
3, 4, 5, 6, 7, 8).
* Dữ liệu (Input) vào từ file văn bản INCSEQ.INP
• Dòng 1: Chứa số n
• Dòng 2: Chứa n số a1, a2, …, an cách nhau ít nhất một dấu cách
* Kết quả (Output) ghi ra file văn bản INCSEQ.OUT
• Dòng 1: Ghi độ dài dãy con tìm được
• Các dòng tiếp: ghi dãy con tìm được và chỉ số những phần tử được chọn vào
dãy con đó.

INCSEQ.INP INCSEQ.OUT

11 8
1 2 3 8 9 4 5 6 a[1] = 1
20 9 10 a[2] = 2
a[3] = 3
a[6] = 4
a[7] = 5
a[8] = 6
a[10] = 9
a[11] = 10

* Ý týởng giải thuật:


Bổ sung vào A hai phần tử: a0 = -∞ và an+1 = +∞. Khi ðó dãy con đơn ðiệu tăng
dài nhất chắc chắn sẽ bắt ðầu từ a0 và kết thúc ở an+1.

25
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Với mọi i: 0 ≤ i ≤ n + 1. Ta sẽ tính L[i] = độ dài dãy con đơn điệu tăng dài nhất
bắt đầu tại ai.
Cơ sở quy hoạch ðộng (bài toán nhỏ nhất):
L[n+1] = Độ dài dãy con đơn điệu tăng dài nhất bắt đầu tại an+1 = +∞. Dãy con
này chỉ gồm mỗi một phần tử (+∞) nên L[n+1]=1.
Công thức truy hồi: Giả sử với i từ n đến 0, ta cần tính L[i]: độ dài dãy con tăng
dài nhất bắt đầu tại ai. L[i] được tính trong điều kiện L[i + 1], L[i + 2], …, L[n + 1] đã
biết:
Dãy con đơn điệu tăng dài nhất bắt đầu từ ai sẽ được thành lập bằng cách lấy ai
ghép vào đầu một trong số những dãy con đơn điệu tăng dài nhất bắt đầu tại vị trí aj
đứng sau ai. Ta sẽ chọn dãy nào để ghép ai vào đầu? Tất nhiên là chỉ được ghép ai vào
đầu những dãy con bắt đầu tại aj nào đó lớn hơn ai (để đảm bảo tính tăng) và dĩ nhiên
ta sẽ chọn dãy dài nhất để ghép ai vào đầu (để đảm bảo tính dài nhất). Vậy L[i] được
tính như sau: Xét tất cả các chỉ số j trong khoảng từ i + 1 ðến n + 1 mà aj >ai, chọn
ra chỉ số jmax có L[jmax] lớn nhất. Ðặt L[i] := L[jmax] + 1.
Truy vết: Tại bước xây dựng dãy L, mỗi khi tính L[i] = L[jmax] + 1, ta đặt T[i]
= jmax. Để lưu lại rằng: Dãy con dài nhất bắt đầu tại ai sẽ có phần tử thứ hai kế tiếp là
ajmax.
Sau khi tính xong hay dãy L và T, ta bắt đầu từ 0.
T[0] là phần tử đầu tiên được chọn,
T[T[0]] là phần tử thứ hai được chọn,
T[T[T[0]]] là phần tử thứ ba được chọn …Quá trình truy vết có thể diễn tả như
sau:
Ví dụ: với A = (5, 2, 3, 4, 9, 10, 5, 6, 7, 8). Hai dãy L và T sau khi tính sẽ là:

Chương trình:
program LongestSubSequence;
const max = 10000;
26
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

fi='INCSEQ.INP';
fo='INCSEQ.OUT';
var a, L, T: array[0..max + 1] of Integer;
n: Word;

procedure Enter;
var i: Word;
begin
ReadLn(n);
for i := 1 to n do Read(a[i]);
end;

procedure Optimize; {QHD}


var i, j, jmax: Word;
begin
a[0] := -32768; a[n + 1] := 32767;
L[n + 1] := 1; {Dien co so quy hoach dong}
for i := n downto 0 do {Tinh bang phuong an}
begin
jmax := n + 1;
for j := i + 1 to n + 1 do
if (a[j] > a[i]) and (L[j] > L[jmax]) then jmax := j;
L[i] := L[jmax] + 1;
T[i] := jmax;
end;
WriteLn(L[0] - 2);
i := T[0]; {Truy vet}
while i <> n + 1 do
begin
WriteLn('a[', i, '] = ', a[i]);
i := T[i];
end;
27
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

end;

Begin
Assign(Input, fi); Reset(Input);
Assign(Output,fo); Rewrite(Output);
Enter;
Optimize;
Close(Input); Close(Output);
End.
4) Bài 4: BÀI TOÁN TÌM XÂU CON CHUNG DÀI NHẤT
Bài toán: Cho hai xâu X =x1x2…xm và Y=y1y2…yn. Tìm xâu Z = z1z2…zk là xâu
con chung dài nhất của X và Y. Một xâu con của xâu A là một cách chọn trong A một
số phần tử giữ nguyên thứ tự.
* Ý tưởng giải thuật:
Bảng phương án:
Ta sẽ dùng mảng hai chiều B[0..m, 0..n] làm bảng phương án, trong đó B[i,j] là
độ dài xâu chung dài nhất của các xâu con Xi (phần đầu của X) và Yj (phần đầu của
Y).
Cơ sở: Rõ ràng nếu ít nhất một trong hai xâu X hoặc Y là xâu rỗng (j=0 hoặc
i=0) thì B[i,j]=0.
Công thức truy hồi: Dễ dàng có các nhận xét sau:
- Nếu i,j > 0 và xi=yi thì B[i,j] = B[i-1,j-1] + 1
- Nếu i,j > 0 và xi<>yj thì B[i,j] = max ( B[i,j-1], B[i-1,j] )
Truy vết :
Như vậy B[n,m] cho biết độ dài của xâu con chung dài nhất. Để chi ra tường
minh xâu con chung dài nhất ta cần xây dựng bảng T[1..m, 1..n] để ghi nhận truy vết
đánh dấu B[i,j] được tính từ B[i-1,j-1] hay B[i,j-1] hay B[i-1,j].
Chương trình:
const fi='xaucon.inp';
fo='xaucon.out';
var s1,s2,s:string;
28
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

b,t:array[0..100,0..100] of integer;
m,n,len:integer;
procedure getinput;
var f:text;
begin
assign(f,fi);
reset(f);
readln(f,s1);
readln(f,s2);
close(f);
m:=length(s1);
n:=length(s2);
end;
procedure optimize;
var i,j:integer;
begin
for i:=0 to m do b[i][0]:=0;
for j:=0 to n do b[0][j]:=0;
for i:=1 to m do
for j:=1 to n do
if s1[i]=s2[j] then
begin
b[i,j]:=b[i-1,j-1]+1;
t[i,j]:=0;
end
else
if b[i-1,j]>b[i,j-1] then
begin
b[i,j]:=b[i-1,j];
t[i,j]:=1;
end
else
29
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

begin
b[i,j]:=b[i,j-1];
t[i,j]:=-1;
end;
end;
procedure trace;
begin
len:=b[m,n];
s:='';
while (m>0) and (n>0) do
begin
if t[m,n]=0 then
begin
s:=s1[m]+s;
m:=m-1;
n:=n-1;
end
else
if t[m,n]=1 then m:=m-1 else n:=n-1;
end;
end;
procedure putoutput;
var f:text;
i:integer;
begin
assign(f,fo);
rewrite(f);
writeln(f,len);
write(f,s);
close(f);
end;
Begin
30
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

getinput;
optimize;
trace;
putoutput;
End.
5) Bài 5: BÀI TOÁN XÂU CON ĐỐI XỨNG DÀI NHẤT

Bài toán: Một xâu được gọi là đối xứng (palindrome) nếu như khi đọc xâu này
từ phải sang trái cũng thu được xâu ban đầu.
Yêu cầu: tìm một xâu con đối xứng dài nhất của một xâu s cho trước. Xâu con là
xâu thu được khi xóa đi một số ký tự từ xâu ban đầu.
* Dữ liệu vào: Gồm một dòng duy nhất chứa xâu S, chỉ gồm những chữ cái in
thường.
* Kết quả: Gồm một dòng duy nhất là một xâu con đối xứng dài nhất của xâu S.
Nếu có nhiều kết quả, chỉ cần in ra một kết quả bất kỳ.
Giới hạn: Xâu S có độ dài không vượt quá 2000.
Ví dụ: Dữ liệu vào: lmevxeyzl Kết quả: level
2) Ý tưởng giải thuật:
Ta sẽ chuyển bài toán về một bài toán quy hoạch động cơ bản là: Bài toán tìm
xâu con chung dài nhất.
Với dữ liệu vào là S1.
Ta tạo xâu S2 là xâu ngược của S1bằng cách chép các phần tử của xâu S1 vào
xâu S2 theo thứ tự ngược lại.
Sau đó ta sẽ tìm xâu con chung dài nhất của S1 và S2.
Ta chỉ cần tìm xâu con chung dài nhất của một phần của S1 và nghịch đảo phần
còn lại, tức là ta chỉ xét một phần của bảng phương án với i+j<=chiều dài của S1.
Ví dụ:
S1 = lmevxeyzl
Ta có bảng phương án (hình bên)

31
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Khi đó xâu con chung dài nhất là


le của S14=“lmev” và S24=”lzye” (hoặcS13=”lme” và S25=”lzyex”)
Khi ta truy vết để tìm xâu con chung ta sẽ kiểm tra xem xâu đối xứng của chúng
ta là lẻ hay chẵn (số kí tự).
+ Nếu i+j=chiều dài của S1, tức là 2 kí tự đối xứng đứng liên tiếp nhau trong S1,
vì vậy xâu đối xứng là chẵn.
+ Ngược lại, tức là mọi i+j<chiều dài của S1, thì trong S1, có các kí tự xen giữa
hai kí tự đối xứng, nên xâu đối xứng là lẻ. Trong ví dụ trên thì có hai kí tự v và x xen
giữa xâu đối xứng
Với xâu đối xứng chẵn ta chỉ việc sao chép lại nửa sau dựa vào nửa đầu.
Còn xâu lẻ ta sẽ chọn thêm một kí tự xen giữa. Theo ví dụ trên, có thể
chọn v hoặc x. Như vậy xâu đối xứng dài nhất sẽ là level hoặc lexel
* Chương trình:
const fi='xaucon.inp';
fo='xaucon.oup';
var s1,s2,s: ansistring;
b,t:array[0..2000,0..2000] of integer;
m,n,len:integer;

procedure getinput;
var f:text; i: longint;
begin
assign(f,fi);
reset(f);
readln(f,s1);
close(f);
m:=length(s1);
for i:=m downto 1 do s2:=s2+s1[i];
n:=length(s2);
end;
procedure optimize;

32
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

var i,j:integer;
begin
for i:=0 to m do b[i][0]:=0;
for j:=0 to n do b[0][j]:=0;
for i:=1 to m do
for j:=1 to n do
if s1[i]=s2[j] then
begin
b[i,j]:=b[i-1,j-1]+1;
t[i,j]:=0;
end
else
if b[i-1,j]>b[i,j-1] then
begin
b[i,j]:=b[i-1,j];
t[i,j]:=1;
end
else
begin
b[i,j]:=b[i,j-1];
t[i,j]:=-1;
end;
end;
procedure trace;
begin
len:=b[m,n];
s:='';
while (m>0) and (n>0) do
begin
if t[m,n]=0 then
begin
s:=s1[m]+s;
33
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

m:=m-1;
n:=n-1;
end
else
if t[m,n]=1 then m:=m-1 else n:=n-1;
end;
end;
procedure putoutput;
var f:text;
i:integer;
begin
assign(f,fo);
rewrite(f);
writeln(f,len);
write(f,s);
close(f);
end;
Begin
getinput;
optimize;
trace;
putoutput;
End.
6) Bài 6: BÀI TOÁN DI CHUYỂN TỪ TÂY SANG ĐÔNG

1) Bài toán: Cho một bảng A kích thýớc m x n, trên ðó ghi các số nguyên. Một
người xuất phát tại ô nào ðó của cột 1, cần sang cột n (tại ô nào cũng ðược). Quy tắc:
Từ ô A[i, j] chỉ ðược quyền sang một trong 3 ô A[i, j + 1]; A[i - 1, j + 1]; A[i + 1, j +
1]. Hãy tìm vị trí ô xuất phát và hành trình ði từ cột 1 sang cột n sao cho tổng các số
ghi trên ðường ði là lớn nhất (nhỏ nhất).

34
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Dữ liệu vào: Từ file vãn bản dongtay.inp gồm:


- Dòng ðầu ghi hai số m, n (2<= m, n <=1000).
- Dòng thứ i trong m dòng tiếp theo mỗi dòng ghi n số là các số trong bảng
( |A[I,j]|<=1000).
Kết quả: file vãn bản dongtay.out gồm:
- Dòng ðầu ghi tổng số lớn nhất (nhỏ nhất).
- Dòng thứ hai ghi n số mỗi số là chỉ số dòng ở mỗi býớc ði.
Dongtay.inp Dongtay.out
45 9
12679 12323
70537
12342
47276
2) Ý tưởng giải thuật: Gọi B[i, j] là số điểm lớn nhất (nhỏ nhất) có thể có được
khi tới ô A[i, j]. Rõ ràng đối với những ô ở cột 1 thì B[i, 1] = A[i, 1]:

Với những ô (i, j) ở các cột khác. Vì chỉ những ô (i, j – 1), (i – 1, j – 1), (i + 1, j –
1) là có thể sang được ô (i, j), và khi sang ô (i, j) thì số điểm được cộng thêm A[i, j]
nữa. Chúng ta cần B[i, j] là số điểm lớn nhất có thể nên B[i, j] = max(B[i, j - 1], B[i -
1, j - 1], B[i + 1, j - 1]) + A[i, j] (Hoặc min). Ta dùng công thức truy hồi này tính tất cả
các B[i, j]. Cuối cùng chọn ra B[i, n] là phần tử lớn nhất trên cột n của bảng B và từ đó
truy vết tìm ra đường đi nhiều điểm nhất.
* Chương trình:
const max = 2000000000;

35
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

fi='taydong.inp';
fo='taydong.out';
var a,b,t:array[0..101,1..100] of longint;
tong:longint;
m,n,dongcuoi:integer;
procedure nhap;
var i,j:integer;
begin
readln(m,n);
for i:=1 to m do
begin
for j:=1 to n do read(a[i,j]);
readln;
end;
end;
function min(i,j:integer):integer;
var m:integer;
begin
m:=i-1;
if b[i,j-1] < b[m,j-1] then m:=i;
if b[i+1,j-1] < b[m,j-1] then m:=i+1;
min:=m;
end;
procedure taobang;
var i,j,d:integer;
begin
for j:=1 to n do
begin
b[0,j]:=max;
b[m+1,j]:=max;
end;
for i:=1 to m do b[i,1]:=a[i,1];
36
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

for j:=2 to n do
for i:=1 to m do
begin
d:=min(i,j);
b[i,j]:=b[d,j-1]+a[i,j];
t[i,j]:=d;
end;
tong:=b[1,n];
dongcuoi:=1;
for i:=2 to m do
if tong>b[i,n] then
begin
tong:=b[i,n];
dongcuoi:=i;
end;
end;
procedure truyvet(dong,cot:integer);
begin
if cot=0 then
writeln(tong)
else
begin
truyvet(t[dong,cot],cot-1);
write(dong,' ');
end;
end;
Begin
assign(input,fi); reset(input);
assign(output,fo); rewrite(output);
nhap;
taobang;
truyvet(dongcuoi,n);
37
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

close(input); close(output);
End.
7) Bài 7: BÀI TOÁN CHIA KẸO:
Có N gói kẹo, gói thứ i có Ai cái kẹo. Không được bóc bất kỳ một gói kẹo nào,
cần chia N gói kẹo thành hai phần sao cho độ chênh lệch số kẹo giữa hai gói là ít nhất.
Dữ liệu vào trong file “chiakeo.inp” có dạng :
 Dòng đầu tiên là số N (N<=100);
 Dòng thứ hai là N số Ai (i=1, 2,.., N; Ai <=100).
Kết quả ra file “chiakeo.out” có dạng:
 Dòng đầu là độ chênh lệch nhỏ nhất giữa hai phần có thể được.
 Dòng hai là một dãy N số, nếu si =1 thì gói thứ i thuộc phần 1, nếu si =2 thì gói
thứ i thuộc phần 2.
* Lời giải:
- Với một số M bất kì, nếu ta biết được có tồn tại một cách chọn các gói kẹo để
tổng số kẹo của các gói được chọn bằng đúng M không, thì bài toán được giải sẽ
quyết. Vì đơn giản là ta chỉ cần chọn số M sao cho M gần với S/2 nhất (với i
=1,2,..,N). Sau đó xếp các gói kẹo để tổng bằng M vào phần một, phần thứ hai sẽ gồm
các gói kẹo còn lại.
- Để kiểm tra được điều trên ta sẽ xây dựng tất cả các tổng có thể có của N gói
kẹo bằng cách: ban đầu chưa có tổng nào được sinh ra.
- Làm lần lượt với các gói kẹo từ 1 đến N, với gói kẹo thứ i, ta kiểm tra xem hiện
tại có các tổng nào đã được sinh ra, giả sử các tổng đó là x1, x2,.., xt vậy thì đến bước
này sẽ có thể sinh ra các tổng x1, x2,.., xt và Ai và x1+Ai,x2+Ai,..,xt+Ai.
- Với N gói kẹo, mà mỗi gói có không quá 100 cái kẹo vậy tổng số kẹo không
vượt quá N*100 ≤ 10000 cái kẹo. Dùng mảng đánh dấu D, nếu có thể sinh được ra
tổng bằng k thì D[k] = 1 ngược lại D[k] = 0.
* Chương trình:
const max = 100;
fi = 'chiakeo.inp';
fo = 'chiakeo.out';

38
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

var a,s : array[1..max]of integer;


d1,d2,tr : array[0..max*max]of integer;
n,m,sum : integer;
Procedure docf;
var f: text;
k : integer;
begin
assign(f,fi); reset(f);
readln(f,n);
sum:=0;
for k:=1 to n do
begin
read(f,a[k]);
sum:=sum+a[k];
end;
close(f);
end;
Procedure lam;
var i,j : integer;
Begin
fillchar(d1,sizeof(d1),0);
fillchar(tr,sizeof(tr),0);
d1[0]:=1;
d2:=d1;
for i:=1 to n do
begin
for j:=0 to sum-a[i] do
if (d1[j]=1)and(d2[j+a[i]]=0) then
begin
d2[j+a[i]]:=1;
tr[j+a[i]]:=i;
end;
39
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

d1:=d2;
end;
end;
Procedure ghif;
var m,k : integer;
f :text;
Begin
fillchar(s,sizeof(s),0);
m:=sum div 2;
while d2[m]=0 do
dec(m);
assign(f,fo); rewrite(f);
writeln(f,sum-2*m);
while tr[m]>0 do
begin
s[tr[m]]:=1;
m:=m-a[tr[m]];
end;
for k:=1 to n do
write(f,k+1,#32);
close(f);
end;
BEGIN {main}
docf;
lam;
ghif;
END.
8) Bài 8: DÃY CON CÓ TỔNG CHIA HẾT CHO K
Cho một dãy số nguyên A1, A2,.., AN và một số k. Hãy tìm một dãy con (không
nhất thiết phải liên tiếp nhau) dài nhất có tổng các số chia hết cho số k.
Dữ liệu vào trong file “dayso.inp” có dạng:

40
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

 Dòng 1 gồm 2 số N và k (N<=1000; k<=50)


 Các dòng tiếp theo chứa các số của mảng A.
Kết quả ra file “dayso.out” gồm một dòng ghi số phần tử lớn nhất tìm được.
* Thuật toán: Tìm dãy nhiều phần tử nhất tương đương với việc loại khỏi dãy ít
phần tử nhất để các phần tử còn lại có tổng chia hết cho k. Ta gọi d là số dư của tổng
tất cả các phần tử của dãy A chia cho k. Nếu ta chọn được ít phần tử nhất (thậm chí có
thể không chọn) để tổng các số được chọn chia k cũng dư d thì các phần tử còn lại là
dãy cần tìm.
Với bài này ta cũng sinh ra các tổng có thể có, song ta xếp các tổng có cùng số
dư khi chia cho k làm một lớp. Ta chỉ cần quan tâm tới các lớp mà không cần quan
tâm tới các tổng. Sẽ có k lớp, lớp một là gồm các tổng chia cho k dư 1, lớp 2 là gồm
các tổng chia cho k dư 2,…, lớp k-1 là gồm các tổng chia cho k dư (k-1), lớp k là gồm
các tổng chia hết cho k.
Bài toán này khác với bài toán chia kẹo là phải đòi hỏi tìm ít phần tử nhất. Để
làm được điều này ta sẽ dùng thêm một mảng nhãn.
Như vậy theo thuật toán trên ta có thể làm được với dữ liệu rất lớn so với đề bài.
* Chương trình: Chương trình sau có thể giải với N <=100000 và k <= 100.
uses crt;
const maxK = 100;
fi='dayso.inp';
fo='dayso.out';
var L1,L2 : array[0..maxK]of longint;
n,k,p : longint;
Procedure lam;
var f : text;
i,j,x : longint;
Begin
fillchar(L1,sizeof(L1),0);
L2:=L1;
assign(f,fi); reset(f);
readln(f,n,k);
41
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

p:=0;
for i:=1 to n do
begin
read(f,x);
x:=x mod k+k;
p:=(p+x)mod k;
for j:=0 to k-1 do
if (L1[j]>0) then
if (L2[(x+j)mod k]=0)
or(L2[(x+j)mod k]>L1[j]+1) then
L2[(x+j)mod k]:=L1[j]+1;
L2[x mod k]:=1;
end;
close(f);
end;
Procedure ghif;
var f :text;
Begin
assign(f,fo); rewrite(f);
write(f,n-L2[p]);
close(f);
End;
BEGIN
lam;
ghif;
END.
9) Bài 9: HÌNH VUÔNG 0 1
Cho một bảng kích thước MxN, được chia thành lưới ô vuông đơn vị M dòng N
cột ( 1 <= M, N <= 1000 ). Trên các ô của bảng ghi số 0 hoặc 1. Các dòng của bảng
được đánh số 1, 2… M theo thứ tự từ trên xuống dưới và các cột của bảng được đánh
số 1, 2…, N theo thứ tự từ trái qua phải

42
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Yêu cầu: Hãy tìm một hình vuông gồm các ô của bảng thoả mãn các điều kiện
sau:
1 – Hình vuông là đồng nhất: tức là các ô thuộc hình vuông đó phải ghi các số
giống nhau (0 hoặc 1)
2 – Cạnh hình vuông song song với cạnh bảng.
3 – Kích thước hình vuông là lớn nhất có thể
Input : + Dòng 1: Ghi hai số m, n
+ M dòng tiếp theo, dòng thứ i ghi N số mà số thứ j là số ghi trên ô (i, j) của bảng
Output: Gồm 1 dòng duy nhất ghi kích thước cạnh của hình vuông tìm được
Example
Input:
11 13
0000010000000
0000111000000
0011111110000
0011111110000
0111111111000
1111111111100
0111111111000
0011111110000
0011111110000
0000111000011
0000010000011
Output: 7
* Lời giải: Đây là một bài quy hoạch động hay.
- Gọi f[i,j] là kích thước của hình vuông có góc đáy là ô [i,j].
- Vậy thì nếu bốn ô: bên trái, bên phải, chéo trên đều cùng giá trị với ô này thì nó
sẽ tạo được một ô vuông, nếu không thì xem như ô đó có kích thước chỉ là 1 vì chỉ tự
nó tạo được một ô vuông

43
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

- Bây giờ ta xét đến công thức quy hoạch động đối với ô đang xét ở trên. Đương
nhiên ta chỉ có thể lấy giá trị nhỏ nhất trong các f của các ô ở trên để chắc chắn tạo
thành ô vuông: f[i,j] := min(f[i-1,j],f[i,j-1],f[i-1,j-1]) + 1
-Cơ sở quy hoạch động của bài toán chính là các ô cạnh trên và bên trái có f = 1
vì các ô có góc dưới của nó chính là chính nó và kích thước là 1.
* Chương trình:
uses math;
const fi = 'HV.INP';
fo = 'HV.OUT';
var m,n,i,j,maxf : integer;
a : array[0..1001,0..1001] of 0..1;
f : array[0..1001,0..1001] of integer;

begin
assign(input,fi);
reset(input);
assign(output,fo);
rewrite(output);

readln(m,n);
for i := 1 to m do
for j := 1 to n do
read(a[i,j]);
for j := 0 to n+1 do f[1,j] := 1;
for i := 0 to m+1 do f[i,1] := 1;
for i := 1 to m do
for j := 1 to n do
if (a[i,j] = a[i-1,j]) and
(a[i,j] = a[i,j-1]) and
(a[i,j] = a[i-1,j-1]) then
f[i,j] := min(f[i-1,j],min(f[i,j-1],

44
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

f[i-1,j-1])) + 1
else
f[i,j] := 1;
maxf := 1;
for i := 1 to m do
for j := 1 to n do
maxf := max(maxf,f[i,j]);
writeln(maxf);

close(input);
close(output);
end.

10) Bài 10: CHIA XÂU THÀNH ÍT NHẤT CÁC PALINDROME


Bài toán: Tìm cách chia một xâu thành ít nhất các Palindrome độ dài ≤1000
* Lời giải
-Sử dụng thuật toán quy hoạch động.
-Gọi F[i] là số palindrome ít nhất mà đoạn 1→ j chia thành được.
⇒ Ta có công thức: F[i] = max(F[j] + 1); j < i thỏa mãn: đoạn j+1→ i là
palindrome.
-Đoạn chương trình như sau:
F[0] := 0;
for i := 1 to n do
begin
for j := i-1 downto 0 do
if (đoạn j+1..i là palindrome) then
F[i] := max( F[i], F[j]+1 );
end;
- Hai vòng for lồng nhau mất O(N2), phần kiểm tra đoạn j+1→ i là palindrome
hay không mất O(N), vậy độ phức tạp thuật toán là O(N3). Sẽ không được khả thi nếu
N = 1000.
- Để giảm độ phức tạp thuật toán, ta sử dụng mảng L[i, j] có ý nghĩa tương tự
như mảng F[i, j] ở cách 1 bài Xâu con liên tiếp đối xứng dài nhất. Quy hoạch động lập
45
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

mảng L[i, j] mất N2. Tổng cộng là O(N2) vì mỗi lần kiểm tra chỉ mất O(1). Có thể cải
tiến bằng cách dùng hai mảng một chiều L[i] và C[i] có ý nghĩa:
- L[i] là độ dài lớn nhất của palindrome độ dài lẻ nhận s[i] làm tâm;
- C[i] là độ dài lớn nhất của palindrome độ dài chẵn nhận s[i] và s[i+1] làm
tâm; L[i] và C[i] có thể tính được bằng cách 2 bài Xâu con liên tiếp đối xứng
dài nhất trong O(N2). Phần kiểm tra ta viết lại như sau:
Function is_palindrome(i, j : integer) : boolean;
var t : integer;
Begin
t := j-i+1;
if odd (t) then
is_palindrome := (L[(i+j) div 2] >= n)
else
is_palindrome := (C[(i+j) div 2] >= n)
end;
Vậy thuật toán của chúng ta có độ phức tạp tính toán là O(N2), chi phí bộ nhớ là O(N).

TÀI LIỆU THAM KHẢO

1. Sách giáo khoa Chuyên Tin quyển 1 – NXB Giáo dục – Chủ biên: Hồ Sĩ Đàm

2. Toán rời rạc – Nguyễn Đức Nghĩa, Nguyễn Tô Thành. NXB ĐHQG Hà Nội

3. Thuật toán thông dụng (Dành cho bồi dưỡng HSG Tin học) – Trần Đỗ Hùng.

4. Một số vấn đề chọn lọc trong tin học – Tập 1 – Nguyễn Xuân My

5. Sáng tạo trong thuật toán và lập trình – Nguyễn Xuân Huy.

6. Một số tài liệu, bài viết trên Internet.

46
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

THUẬT TOÁN XÁC ĐỊNH HƯỚNG CỦA MỘT ĐIỂM SO VỚI MỘT
VECTOR VÀ NHỮNG ỨNG DỤNG TRONG VIỆC GIẢI BÀI TOÁN
HÌNH HỌC

Đoàn Minh Đức


Trường THPT Chuyên Bến Tre
I. Đặt vấn đề
- Chuyên đề hình học là một chuyên tương đối “Khó chịu” với học sinh và giáo
viên trong việc bồi dưỡng. Ngoài việc phải nắm 1 số kiến thức toán về hình
học, chúng ta còn phải vận dụng những đặc thù thuật toán về hình học để xử lí.
- Trong một phần nhỏ đóng góp cho trại hè phương nam tôi xin giới thiệu thuật
toán liên quan tới hướng của 1 điểm so với một vector và việc vận dụng nó
trong việc giải một số bài toán hình học.
II. Nội dung
Tổ chức dữ liệu cho các thuật toán

Type

//Điểm:
Point = record
x, y: integer;
end;
//Đường thẳng:
Line = Record
p1, p2: point;
end;
//Đa giác:
Polygon = array[1..n] of point;

1. Thuật toán xác định hướng của điểm C so với Vector (phương pháp
vector)

a. Cơ sở toán học
47
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

- Xét 3 điểm A, B, C trong mặt phẳng tọa độ, vị trí tương đối C so với A, B có 3
khả năng xãy ra:

B C C B C
B

A A A

b. Thuật toán

function CCW(C, A, B: Point): integer;


var
k: integer;
begin

if equal(k,0) then CCW := 0 // C thẳng hàng AB


else
if k > 0 then CCW := 1 // C bên trái AB
else CCW := -1; // C bên phải AB
end;
c. Nhận xét :
- Bất chấp vị trí của 3 điểm A,B,C trên mặt phẳng, thông qua hệ số k cho phép

chúng ta xác định hướng của C so với vector


- Chi phí thuật toán trên là (1)

48
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

2. Vận dụng giải quyết 1 số bài toán cơ bản trong chuyên đề hình học

2.1 Một số bài toán liên quan giữa điểm, đoạn thẳng và đường thẳng
+ Xác định điểm M có thuộc đoạn thẳng nối 2 điểm A,B

Điểm M thuộc đoạn thẳng AB nếu M nằm trên đường thẳng đi qua 2 điểm AB
và tọa của nó khi chiếu các trục tọa độ thì nó thuộc tọa độ chiếu xuống các trục tọa độ
của điểm A và B.

function Inside(M, A, B: Point): boolean;


begin
exit(CCW(M,A,B)and(M.x>=min(A.x,B.x))and(M.x<=max(A.x,B.x))an
d(M.y>=min(A.y,B.y))and(M.y<=max(A.y,B.y)))
end;

+ Xác định vị trí tương đối của 2 điểm M,N so với đường thẳng đi qua 2 điểm A,
B.

- 2 điểm M,N nằm cùng phía hoặc khác phía so với đường thẳng đi qua 2 điểm A,B.
Nếu 1 trong 2 điểm M hoặc N thuộc đường thẳng trên thì ta xem như M và N nằm
cùng phía với nhau.

function Pos2PWithLine(M, N, A, B: Point): boolean;


begin
exit(CCW(M,A,B)*CCW(N,A,B)>=0)
end;

+ Xác định 2 đoạn thẳng AB và CD có cắt nhau?

a. Cách 1
2 đoạn thẳng giao nhau nếu thỏa điều kiện:
- Hai đường thẳng qua 2 điểm đó phải cắt nhau tại I
- Và I thuộc 2 đoạn thẳng
b. Phương pháp vector

49
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

- Trường hợp cắt nhau:


o Đầu mút của đoạn thẳng nằm trên đoạn thẳng kia
o 2 điểm của đoạn thẳng nằm khác phía với đường thẳng kia

function Intersect(A,B,C,D: Point): boolean;


Begin
if (inside(C,A,B)) or (inside(D,A,B)) or (inside(A,C,D)) or (inside(B,C,D))
then exit(true);
Exit(CCW(M,A,B)* CCW(N,A,B)* CCW(A,M,N)* CCW(B,M,N)>=0);
End;
c. Đánh giá:
- Việc giải bài toán trong cách 1 khó cài đặt hơn bằng phương pháp vector. Hơn nữa
còn phải giải quyết vấn đề về số thực trong việc cài đặt do phải xác định 1 điểm có
thuộc đoạn thẳng.

2.2 Một số thuật toán về đa giác lồi

+ Kiểm tra đa giác lồi

* Định nghĩa
Một đa giác lồi được gọi là lồi nếu ta vẽ một đường thẳng đi qua 1 cạnh của đa
giác thì các điểm còn lại luôn nằm cùng 1 phía so với đường thẳng đó.

a. Cách 1: Dựa theo định nghĩa


chi phí thực hiện 0(N2)
b. Theo phương pháp vector

xét theo 1 chiều nào đó (cùng chiều hoặc ngược chiều kim đồng hồ), xét với
mọi 3 điểm liên tiếp nhau thì chiều của đỉnh thứ 3 luôn nằm cùng một phía nhau (trái
hoặc phải) so với 2 đỉnh trước đó.
Function Convex (P:poly;n:index):boolean;
Vari:index;
Begin
If n<3 then exit(false);
Dir:=CCW(p[1],p[2],p[3]);
50
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

For i:=3 to n do
If CCW(p[i-1],p[i],p[i+1])<>Dir then exit(false);
Convex:=true;
End;

c. Đánh giá

- Chi phí thuật toán theo cách 1 là 0(n2) trong khi đó chi phí theo phương
pháp vector là 0(n).

- Việc cài đặt theo cách 1 là phức tạp hơn.

+ Bài toán vị trí tương đối giữa 1 điểm so với đa giác lồi

Điểm thuộc đa giác nếu điểm nằm trên các cạnh hoặc thuộc miền đa giác.
a. Cách 1:
- Vẽ trục song với trục tung với tọa độ x=max{các hoành độ}+1
- Vẽ đoạn thẳng song song với trục hoàng và cắt trục vẽ bên trên. Ta nhận xét
nếu số giao điểm với đa giác là số lẽ thì điểm thuộc đa giác. Còn ngược lại
điểm nằm ngoài đa giác.
- Các trường hợp cắt sau ta chỉ tính cắt tại 1 giao điểm:

P[i-1] P[i]
P[i]
M N
P[i] P[i+1] N
M N
P[i+1] P[i+2] M
P[i+1]
P[i+2]

Trường hợp 1 Trường hợp 2 Trường hợp 3

b. Phương pháp vector:


- Ta thấy nếu xét các cạnh của đa giác theo 1 chiều nào đó thì điểm M thuộc đa
giác nếu nằm cùng 1 bên (trái hoặc phải) với mọi vector cạnh cuả đa giác.

Function InsideP (P:polygon;n:index;M:point):boolean;


Var i:index; VT, VT1:integer;
51
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Begin
If Inside(M,P[1], P[2]) then exit(true);
Dir:=CCW(P[1],P[2],M);
For i:=2 to n do
Begin
If Inside(M,P[i], P[i+1]) then exit(true);
If CCW(P[i],P[i+1],M)<>Dir then exit(false);
end;
exit(true);
End;
c. Đánh giá

- Việc giải bài toán trong cách 1 khó hơn cài đặt hơn bằng phương pháp vector. Hơn
nữa còn phải giải quyết vấn đề về số thực trong việc cài đặt do phải xác định 1 điểm có
thuộc đoạn thẳng.

+ Bài toán tìm bao lồi của 1 đa giác

a. Thuật toán bọc gói

* Điểm có tung độ nhỏ nhất luôn thuộc bao lồi


- pomin(trung độ);
- W={po};
- Lặp lại
 Từ po Quét ngược theo chiều kim đồng hồ gặp đỉnh đầu tiên v
 W=W  v;
 pov
- Until v=đỉnh đầu tiên
b. Grahamscan(phương pháp vector)
- Chọn điểm có tung độ nhỏ nhất. Nếu có tung độ bằng nhau thì chọn điểm có
hoành độ lớn nhất. Điểm này thuộc bao lồi.
- Sắp xếp các điểm còn lại tăng dần về góc so với điểm trên.
- Lần lượt chọn các điểm theo trật tự sắp xếp

52
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

o Nếu điểm được chọn không tạo thành đa giác lồi với các điểm đã chọn
thì lần lượt bỏ các đỉnh đã chọn cho tới khi lập thành đa giác lồi thì thôi
o Bỏ điểm vào bao lồi

Thuật toán Grahamscan


function Grahamscan:integer;
var M,min,i:integer; t:point;
begin
min:=1; // find min(p.y) and max(p.x)
for i:=2 to N do
if (p[i].y<p[min].y) or ((p[i].y=p[min].y) and (p[i].x>p[min].x)) then
min:=i;
t:=p[1];p[1]:=p[min];p[min]:=t;// điểm đầu tiên của bọc gói
pa[1]:=0.0;
for i:=2 to N do pa[i]:=theta1(p[1],p[i]); // tinh goc cac dinh so voi dinh 1
quicksort;
p[0]:=p[n];
M:=3; \\ vì tam giác là đa giác lồi
for i:=4 to N do
begin
while (ccw(p[M-1],p[M],p[i])<>-1) do dec(M);
inc(M);
t:=p[M];p[M]:=p[i];p[i]:=t;// bỏ điểm vào bao lồi
end;
grahamscan:=M;
end;
Thuật toán Grahamscan
c. Đánh giá

Thuật toán bọc gói đòi hỏi một chi phí là O(M*N) (trong đó M là số điểm trên
bao). Vì vậy nó chỉ làm việc tốt trong trường hợp số điểm nằm trên bao nhỏ hơn nhiều

53
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

so với tổng số. Nhưng trong trường hợp xấu nhất (tất cả mọi điểm đều nằm trên bao)
thì chi phí thuật toán sẽ lên tới O(N2).

Chi phí cho thủ tục trên tỷ lệ thuận với N. Đúng vậy, mặc dù trong vòng lặp có
một vòng lặp, nhưng ta để ý là không điểm nào bị loại quá một lần nên vòng lặp này
chỉ hoạt động không đến N lần.Như vậy chi phí cho thuật toán này là O(NlogN) nếu ta
dùng phương pháp sắp xếp tốt (như Quick Sort chẳng hạn).

III. KẾT LUẬN


Thông qua thuật toán CCW cho ta một phương pháp giải một số bài toán dựa
vào đặc thù là hướng của các điểm trên mặt phẳng. Dựa trên phương pháp này ta thấy
hiệu quả hơn một số phương pháp khác như đã đề cập bên trên. Việc giải các bài toán
hình học về kinh nghiệm chúng ta nên sử dụng cách thức là vẽ hình ra và nhận biết các
mối liên hệ để sử dụng phương pháp cho hiệu quả.

Tài liệu tham khảo

Cẩm nang thuật toán 1,2: Tác giả Robert Segewick

54
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

XÂY DỰNG THUẬT TOÁN FLOYD-WARSHALL CHO ĐỒ THỊ DÀY

Trường THPT Chuyên Vị Thanh – Hậu Giang

Thuật toán tìm đường đi ngắn nhất giữa mọi cặp đỉnh của đồ thị G(V,E) (có
hướng, có trọng số và không có chu trình âm). Áp dụng quy hoạch động để giải bài
toán này trong thời gian O(n3) và bộ nhớ O(n2). Một vài cách phát triển thuật toán quy
hoạch động khác nhau, mỗi cách hi vọng sẽ mang đến một cách nhìn khác về bài toán.
Cách phát biểu cuối cùng sẽ đưa chúng ta đến thuật toán nổi tiếng Floyd-Warshall. Tài
liệu tham khảo chính là notes của Jeff Erickson1. Đối với học sinh nên xem lại cách
phát triển thuật toán quy hoạch động cho bài toán đường đi ngắn nhất từ một đỉnh tới
mọi đỉnh từ thuật toán Bellman-Ford.

Ghi chú: Đồ thị G trong bài này sẽ được biểu diễn bằng ma trận kề W, i.e,
W[u,v]=w(u→v) nếu tồn tại một cung từ u tới v với trọng số w(u→v)

Vấn đề: Cho đồ thị có hướng, có trọng số và không có chu trình âm G(V,E), tìm
khoảng cách ngắn nhất giữa mọi cặp đỉnh của đồ thị.

Nếu trọng số của đồ thị G là không âm, ta có thể áp dụng thuật toán Dijkstra V
lần, mỗi lần áp dụng cho một đỉnh của đồ thị. Do đó, ta có:

Định lý: Ta có thể tìm được khoảng cách ngắn nhất giữa mọi cặp đỉnh trong đồ thị
không có trọng số âm trong thời gian O(V2logV) sử dụng O(V2) bộ nhớ.

Tuy nhiên trong đồ thị có trọng số âm, bài toán trở nên phức tạp hơn nhiều.
Như ta đã biết, thuật toán Bellman-Ford có thể tìm đường đi ngắn nhất từ một đỉnh tới
mọi đỉnh khác trong đồ thị trong thời gian O(VE) và bộ nhớ O(V). Do đó, nếu áp dụng
thuật toán Bellman-Ford cho mọi đỉnh của đồ thị, ta sẽ thu được thuật toán với thời
gian O(V2E) và bộ nhớ O(V2). Với đồ thị dầy E=O(V2), thời gian của thuật toán này là
O(V4).

1
Link: https://goo.gl/DbO6hd (Notes on All Pairs Shortest Paths - Jeff Erickson)
55
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Trong các giả mã dưới đây, ta quy ước w(u→u)=0 và w(u→v)=+∞ nếu như
cung u→v không tồn tại trong đồ thị G.

Thuật toán quy hoạch động phong cách Brute-force

Ta sẽ xây dựng bảng bảng quay hoạch động tương tự như bảng quy hoạch động
trong bài thuật toán Bellman-Ford.

Gọi D[1,…,V][1,…,V][0,…,V−1] là mảng 3 chiều trong đó mỗi ô D[u,v,i]


tương ứng với khoách cách ngắn nhất từ u tới v đi qua tối đa i đỉnh, từ đó suy ra công
thức truy hồi:

(1)

Ý nghĩa của công thức truy hồi trên như sau: đường đi ngắn nhất từ u tới v sẽ
phải đi qua một hàng xóm x nào đó của v mà (x→v)∈E

Nếu đường đi ngắn nhất từ u tới v đi qua tối đa i đỉnh thì đường đi con từ u tới x
sẽ đi qua không quá i−1 đỉnh. Do đó D[u,v,i]=D[u,x,i−1]+w(x→v). Do ta không biết
x, ta sẽ duyệt qua tất cả các hàng xóm có thể của v và lấy giá trị nhỏ nhất để tìm
D[u,v,i].

Khoảng cách ngắn nhất từ u tới v chính là D[u,v,V−2] vì đường đi ngắn nhất từ
u tới v sẽ đi qua tối đa V−2 đỉnh.

Mã giả:

56
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Phân tích thời gian: Bước khởi tạo đầu tiên mất thời gian O(V3) để khởi tạo
các phần tử của bảng D thành +∞. Bước khởi tạo thứ hai (2 vòng lặp for) mất thời gian
O(V2). Do đó ta mất tổng thời gian O(V3) để khởi tạo. Tiếp theo ta sẽ phân tích 4 vòng
for cuối cùng. Vòng for trong cùng duyệt qua tất các cảc đỉnh x mà có cung x→v∈E;
do đó, vòng for này có d+(v) vòng lặp trong đó d+(v) là bậc tới của v. Tổng số bước lặp
của hai vòng for trong cùng là: ∑v∈Vd+(v)=O(E) (2)

Do hai vòng lặp ngoài cùng có (V−2)V bước lặp, thời gian của thuật toán là
O(V2E). Như vậy, thuật toán DummyAllPairsSPs có thời gian chạy bằng thời gian
thực hiện V lần thuật toán Bellman-Ford.

Cải tiến giải thuật

Gọi D[1,…,V][1,…,V][0,…,[log2(V−2)]] là mảng 3 chiều trong đó mỗi ô


D[u,v,i] tương ứng với khoách cách ngắn nhất từ u tới v đi qua tối đa 2i đỉnh. Từ đó có
công thức truy hồi: D[u,v,i]=minx∈V(D[u,x,i−1]+D[x,v,i−1]) (3)

Ý nghĩa của công thức truy hồi trên là nếu có một đường đi từ u tới v qua tối đa
2i đỉnh thì đường đi đó sẽ đi qua một đỉnh x sao cho đường đi từ u tới x sẽ qua tối đa
2i−1 đỉnh và đường đi từ x tới v đi qua tối đa 2i−1 đỉnh.

57
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Từ định nghĩa ta suy ra, D[u,v,[log2(V−2)]] sẽ là đường đi ngắn nhất từ u tới v

đi qua tối đa đỉnh. Do đó, D[u,v,[log2(V−2)]] sẽ là


đường đi ngắn nhất từ u tới v.

Chú ý với định nghĩa bảng D như trên, trường hợp cơ bản sẽ là D[u,v,1]. Do đó,
ta sẽ áp dụng một vòng lặp trong thuật toán DummyAllPairsSPs để tính các giá trị đó.

Mã giả:

Phân tích thuật toán: Bước khởi tạo đầu tiên mất thời gian O(V2log2(V)) để
khởi tạo bảng D và bước thứ hai (2 vòng lặp for đầu tiên) mất thời gian O(V2). Phân
tích bước thứ ba (2 vòng lặp for tiếp theo), tương tự như phân tích ở trên, mất thời gian
O(VE). Bước cuối cùng thực hiện 4 vòng lặp lồng nhau, 3 vòng lặp trong cùng có V3
bước lặp và vòng lặp ngoài cùng có log2(V−2) bước lặp. Như vậy, tổng thời gian của
thuật toán trên là O(VE+V3log2(V)) = O(V3log2(V)).

Thuật toán Floyd-Warshall

Gọi D[1,…,V][1,…,V][0,…,V] là mảng 3 chiều trong đó mỗi ô D[u,v,i] tương


ứng với khoách cách ngắn nhất từ u tới v đi qua các đỉnh trung gian nằm trong tập hợp
{1,2,…,i}.

58
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Như vậy, D[u,v,0] sẽ có ý nghĩa là đường đi ngắn nhất từ u tới v mà không đi


qua đỉnh trung gian nào cả và ta sẽ khởi tạo D[u,v,0]=w(u→v). Với định nghĩa như
trên, ta có công thức truy hồi:

D[u,v,i]=min(D[u,v,i−1],D[u,i,i−1]+D[i,v,i−1]) (4)

Ý nghĩa của công thức truy hồi trên là nếu có một đường đi từ u tới v qua các
đỉnh trong tập hợp {1,2,…,i} thì hoặc đường đi đó đi qua đỉnh i hoặc nó không đi qua
đỉnh i

- Nếu đường đi đó không đi qua đỉnh i thì đường đi ngắn nhất đó cũng chính
là đường đi ngắn nhất từ u tới v đi qua các đỉnh trung gian trong
{1,2,…,i−1}. Do đó D[u,v,i]=D[u,v,i−1]
- Nếu đường đi đó đi qua đỉnh i thì đường đi con từ u tới i và đường đi con từ
i tới v sẽ chỉ đi qua các đỉnh thuộc tập {1,…,i−1}. Do đó
D[u,v,i]=D[u,v,i−1]+D[i,v,i−1]
- Ta sẽ lấy min trong hai trường hợp để xác định giá trị D[u,v,i]. Từ định
nghĩa ta suy ra, D[u,v,V] sẽ là đường đi ngắn nhất từ u tới v
Mã giả:

Dễ nhận thấy thời gian chạy là O(V3).


59
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Giảm bộ nhớ:

Thuật toán Floyd-Warshall trình bày ở trên có bộ nhớ O(V3) vì đầu ra của
chúng ta là mảng 3 chiều D. Để giảm bộ nhớ chúng ta có nhận xét: Các giá trị ở bảng
con 2 chiều thứ i (là các giá trị D[u,v,i] với mọi cặp đỉnh u,v) chỉ phụ thuộc vào giá trị
của bảng con hai chiều thứ i−1 (là các giá trị D[u,v,i−1] với mọi cặp đỉnh u,v). Do đó,
thay vì lưu cả bảng 3 chiều, ta có thể lưu hai bảng hai chiều, một bảng tương ứng với
bảng hai chiều thứ i với i chẵn, và một với bảng hai chiều thứ i với i lẻ và tái sử dụng
các bảng hai chiều đó qua các vòng lặp. Ý tưởng này giống như cách giảm bộ nhớ của
thuật toán tính số Fibonacci.

Tuy nhiên, ta có thể đơn giản hóa bằng cách bỏ hẳn chiều thứ 3 của bảng 3
chiều D. Tính chất nhỏ nhất của khoảng cách ngắn nhất sẽ đảm bảo thuật toán luôn
đúng mặc dù ta bỏ hẳn một chiều của bảng quy hoạch động.

Mã giả:

Truy vết đường đi

Để in ra đường đi ngắn nhất giữa hai đỉnh (u,v) bất kì, chúng ta cần phải lưu vết
trong quá trình quy hoạch động. Mỗi khi có thay đổi trong bảng quy hoạch động
(D[u,v]>D[u,i]+D[i][v]), ta phải ghi lại sự thay đổi đó. Ta sẽ dùng một mảng 2 chiều
T[1,…,V][1,…,V] trong đó T[u,v]=i nếu đường đi ngắn nhất từ u tới v đi qua đỉnh i.

Có hai giá trị đặc biệt của T[u,v] mà ta cần phải quan tâm: nếu không tồn tại
đường đi nào từ u tới v, ta sẽ gán T[u,v]= Null. Nếu đường đi ngắn nhất từ u đến v
không đi qua đỉnh trung gian nào, ta sẽ gán T[u,v]=0.

60
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Mã giả:

Khi đã có bảng T[1,…,V][1,…,V], ta có thể in ra đường đi ngắn nhất giữa hai


đỉnh u,v như sau:

Ghi chú: Mặc dù sau rất nhiều năm nghiên cứu, cho đến nay vẫn chưa có thuật toán
nào tốt hơn thời gian O(V3) một cách đáng kể và cho mọi trường hợp. Do đó, người ta
giả thuyết rằng không tồn tại thuật toán như vậy (gọi là giả thuyết đường đi ngắn nhất
giữa mọi cặp đỉnh). Thuật toán (ngẫu nhiên) tốt nhất cho đến nay được phát triển bởi

Ryan Williams có thời gian

Một số bài tập trên spoj: VDANGER, NETACCEL,PVOI14_3, DHSERV.

61
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

CẤU TRÚC HEAP


Thi Thị Thanh Tuyền
THPT Chuyên Nguyễn Thiện Thành – Trà Vinh
I. GIỚI THIỆU
1. Khái niệm
Heap là một trong những cấu trúc dữ liệu đặc biệt quan trọng, nó giúp ta có thể
giải được nhiều bài toán trong thời gian cho phép. Độ phức tạp thông thường khi làm
việc với heap là O(logN). Heap là một cây cân bằng thỏa mãn tính chất heap: nếu B là
nút con của A thì khóa(A)≥khóa(B).
Hệ quả: khóa lớn nhất luôn nằm ở nút gốc, do đó một đống như vậy thường được
gọi là heap-max. Nếu mọi phép so sánh bị đảo ngược khiến cho khóa nhỏ nhất luôn
nằm ở nút gốc thì đống đó gọi là heap-min. Không có hạn chế nào về số lượng nút con
của mỗi nút trong đống nhưng thông thường mỗi nút có không quá hai nút con.
Cấu trúc Heap là một cách thực hiện kiểu dữ liệu trừu tượng mang tên hàng đợi
ưu tiên và có vai trò quan trọng trong nhiều thuật toán cho đồ thị chẳng hạn như thuật
toán Dijkstra, Kruskal, Prim … hay thuật toán sắp xếp Heapsort.
2. Các thao tác thường gặp trên Heap
Mặc dù được mô tả như cây nhưng heap có thể được biểu diễn bằng mảng. Nút
con của nút i là 2*i và 2*i+1. Do Heap là cây cân bằng nên độ cao của 1 nút
luôn <=lgN.

Mô hình biểu diễn heap bằng cây nhị phân và bằng mảng
* Khai báo Heap:
Const
maxn=10000;

62
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Var
nHeap:LongInt;
Heap : array[0..maxn] of LongInt;
* Upheap: Nếu một nút lớn hơn nút cha của nó thì di chuyển nó lên trên
Procedure UpHeap(I : LongInt);
begin
if (i = 1) or (Heap[i] < Heap[i div 2]) then
exit; // Nếu i là nút gốc hoặc nhỏ hơn nút cha thì không làm việc
swap(Heap[i],Heap[i div 2]); // Đổi chỗ 2 phần tử trong Heap;
UpHeap(i div 2); // Tiếp tục di chuyển lên trên
end;
* Downheap: Nếu một nút nhỏ hơn nút con thì đẩy nó xuống dưới
Procedure UpHeap(i : LongInt);
Begin
if (i = 1) or (Heap[i] < Heap[i div 2]) then
exit; // Nếu i là nút gốc hoặc nhỏ hơn nút cha thì không làm việc
swap(Heap[i] , Heap[i div 2]); // Đổi chỗ 2 phần tử trong Heap;
UpHeap(i div 2); // Tiếp tục di chuyển lên trên
end;
* Push: Đưa một phần tử mới vào Heap bằng cách thêm một nút vào cuối Heap và
tiến hành UpHeap từ đây
Procedure Push(x : LongInt);
begin
Inc(nHeap); // Tăng số phần tử của Heap
Heap[nHeap] := x; // Thêm x vào Heap
UpHeap(nHeap);
end;
* Pop:Lấy ra phần tử ở vị trí v trong Heap bằng cách gán Heap[v] := Heap[nHeap] rồi
tiến hành chỉnh lại Heap
Function Pop(v : LongInt) : LongInt;
begin
Pop := Heap[v]; // Lấy phần tử ở vị trí v ra khỏi Heap
63
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Heap[v] := Heap[nHeap]; // Đưa phần tử ở cuối Heap vào vị trí v


Dec(nHeap); // Giảm số phần tử của Heap đi 1
{Chỉnh lại Heap}
UpHeap(v);
DownHeap(v);
end;
Ngoài ra, khi sử dụng thuật toán Dijsktra/Prim kết hợp cấu trúc Heap, bạn còn có
thể sử dụng cách Push và Pop khác thuận lợi hơn so với cách trình bày ở trên
 Update – Dijsktra:
Procedure Update(v:longint);
//Đỉnh v vừa được sữa nhãn, cần chỉnh lại Heap
Var Child, Parent:longint;
Begin
Child:=pos[v];//Child là vị trí của đỉnh trong heap
If child=0 then // Nếu đỉnh v chưa có trong Heap
Begin
Inc(nheap);
Child:=nheap;//Đưa v vào cuối Heap
End;
Parent:=child div 2;
While (parent>0) and (d[heap[parent]]>d[v]) do
//Nếu đỉnh ở nút cha kém ưu tiên hơn v thì bị “kéo xuống” nút child
Begin
Heap[child]:=Heap[parent];//Đẩy đỉnh được lưu trong nút cha xuống nút con
Pos[Heap[child]]:=child;//Ghi nhận lại vị trí mới của đỉnh đó
Child:=parent;//Tiếp tục di chuyển lên
Parent:=child div 2;
End;
Heap[child]:=v;
//Thao tác kéo xuống ở trên sẽ tạo ra 1 ô trống ở nút child để đặt v vào
pos[v]:=child;//Ghi nhận vị trí mới của đỉnh v trong Heap
end;
64
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

 Pop – Dijsktra:
Function Pop:Longint; //Lấy từ Heap đỉnh có nhãn tự do nhỏ nhất
Var r,v,c:longint;
Begin
Pop:=Heap[1];//Nút gốc là nút có nhãn tự do nhỏ nhất
v:=Heap[nHeap];//v là đỉnh ở nút lá cuối Heap, sẽ được đảo lên đầu và vun đống
Dec(Heap);
r:=1;//Bắt đầu từ nút gốc
While r*2<=nheap do //Chừng nào r chưa phải là lá
Begin
c:=r*2; // c là nút con của r
if (c<=nheap) and (d[heap[c]]>d[heap[c+1]]) then
inc(c); //Trong 2 nút con chọn nút con có đỉnh ưu tiên hơn
if d[heap[c]]>=d[v] then
break; //nếu v ưu tiên hơn thì không làm việc nữa
heap[r]:=heap[c];//Chuyển đỉnh lưu ở nút con lên nút cha
pos[heap[r]]:=r;//Cập nhật lại vị trí trong Heap của đỉnh đó
r:=c;//Tiếp tục di chuyển xuống dưới
end;
heap[r]:=v;//Đỉnh v được đặt vào vị trí r để đảm bảo cấu trúc heap
pos[v]:=r;//Ghi nhận vị trí mới của đỉnh v trong heap
end;
II. ỨNG DỤNG CỦA HEAP
1. Ứng dụng của Heap trong giải thuật Heapsort
Tư tưởng thuật toán
Đổi chỗ (Swap): Sau khi mảng a[1..n] đã là đống, lấy phần tử a[1] trên đỉnh của
đống ra khỏi đống đặt vào vị trí cuối cùng n, và chuyển phần tử thứ cuối cùng a[n] lên
đỉnh đống thì phần tử a[n] đã được đứng đúng vị trí.
Vun lại: Phần còn lại của mảng a[1..n-1] chỉ khác cấu trúc đống ở phần tử a[1].
Vun lại mảng này thành đống với n-1 phần tử.
Lặp: Tiếp tục với mảng a[1..n-1]. Quá trình dừng lại khi đống chỉ còn lại một
phần tử.
65
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Thuật toán Heapsort có độ phức tạp O(nlogn).

Tính chất của Heap

Một Heap có thể là một trong hai kiểu sau:


Với dữ liệu đẩy vào  35 33 42 10 14 19 27 44 26 31
 Min-Heap: ở đây giá trị của nút gốc là nhỏ hơn hoặc bằng các giá trị của các
nút con.

 Max-Heap: ở đây giá trị của nút gốc là lớn hơn hoặc bằng giá trị của các nút
con.

Cài đặt HeapSort bằng FreePascal:


Program CaidatthuattoanHeapSort;
Uses Crt;
Const
fi='HEAPSORT.INP';
fo='HEAPSORT.OUT';
nmax=100;
Type

66
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Manga=array[1..nmax] of integer;

Var
A:Manga;
n,q,ri,x:integer;
f,g:text;
Procedure Openf;
Begin
Assign(f,fi); reset(f);
Assign(g,fo); rewrite(g);
end;
Procedure Closef;
Begin
Close(f);Close(g);
end;
Procedure Readinp;
Var i:integer;
Begin
Readln(f,n);
For i:=1 to n do
read(f,a[i]);
End;
//Heap Min
Procedure Heap;
Var i,j:integer; cont:Boolean;
Begin
i:=q;
j:=2*i;
x:=a[i];
cont:=true;
While (j<=ri) and cont do
begin
67
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

if j<ri then //Tìm nút có khóa nhỏ nhất trong 2 con


if a[j]>a[j+1] then
inc(j);
If x<=a[j] then
cont:=false
else
begin
a[i]:=a[j];// Chép nút con j vào nút i
i:=j;
j:=2*i;
end;
a[i]:=x;
end;
End;
Procedure TaoHeapbandau;
Begin
q:=(n div 2) +1;
ri:=n;
While q>1 do
begin
dec(q);
Heap
End;
End;
{Heap Max tương tự}
//Min
Procedure Taodaycothutugiam;
Begin
ri:=n;
While ri>1 do
begin
x:=a[1];
68
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

a[1]:=a[ri];
a[ri]:=x;
dec(ri);
// Tạo a[1]...a[r] là heap
Heap;
end;
End;
Procedure Taodaycothututang;
Begin
For ri:=1 to (n div 2) do
begin
x:=a[ri];
a[ri]:=a[n-ri+1];
a[n-ri+1]:=x;
end;
End;
Procedure Output;
Var i:integer;
Begin
For i:=1 to n do
Write(g,a[i],' ');
Writeln(g);
End;
BEGIN
Openf;
ReadInp;
Writeln(g,'Tao Heap ban dau');
Taoheapbandau;
Output;
Writeln(g,'Tao day co thu tu giam');
Taodaycothutugiam;
Output;
69
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Writeln(g,'Tao day co thu tu tang');


taodaycothututang;
Output;
Closef;
END.
2. Ứng dụng heap trong tổ chức hàng đợi ưu tiên
Sau đây ta xét một số bài tập để thấy ứng dụng của heap trong tổ chức hàng đợi
ưu tiên
Bài 1: Cắt gỗ ( http://vn.spoj.com/problems/HEAP1/ )
Một người nông dân muốn cắt 1 thanh gỗ có độ dài L của mình thành N
miếng,mỗi miếng có độ dài là 1 số nguyên dương A[i] (A[1] + A[2] + … A[N] = L).
Tuy nhiên, để cắt một miếng gỗ có độ dài là X thành 2 phần thì ông ta sẽ mất X tiền.
Ông nông dân này không giỏi tính toán lắm, vì vậy bạn được yêu cầu lập trình giúp
ông ta cho biết cần để dành ít nhất bao nhiêu tiền thì mới có thể cắt được tấm gỗ như
mong muốn .
Input:
- Dòng đầu: một số nguyên dương T là số bộ test .
- T nhóm dòng tiếp theo mô tả các bộ test, mỗi nhóm dòng gồm 2 dòng:
Dòng 1 là số nguyên dương N (1 ≤ N ≤ 20000);
Dòng 2: N số nguyên dương A[1],…, A[N](1 ≤ A[i] ≤ 50000)
Output: Kết quả mỗi test ghi ra trên 1 dòng, ghi ra 1 số nguyên dương duy nhất
là chi phí tối thiểu cần để cắt tấm gỗ.
Input Output
1 19
4
1234
Đầu tiên cắt miếng gỗ thành 2 phần có độ dài 6 và 4 . Sau đó cắt tiếp miếng có
độ dài 6 -> 3 và 3 . Cắt 1 miếng 3 thành 2 phần có độ dài 1 , 2 . Như vậy chi phí là 10
+ 6 + 3 = 19.
Thuật toán:
Đầu tiên cho tất cả các phần độ dài trên vào heap min, thực hiện n-1 lần vòng
lặp như sau: Lấy 2 phần tử đầu tiên (tức là 2 phần tử nhỏ nhất trong heap hiện tại ra),
70
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

sau đó thêm 1 phần tử mới có giá trị bằng tổng 2 phần tử vừa lấy ra cho vào heap. Như
thế sau n-1 lần, heap chỉ còn 1 phần tử duy nhất và giá trị của phần tử này chính là kết
quả cần tìm. Chú ý: kết quả vượt quá longint nên phải để heap là int64.
Chương trình: Code\Catgo.pas)
PROGRAM Catgo;
Uses Crt;
Const
fi='CATGO.Inp';
fo='CATGO.Out';
maxn=20001;
Var N,ntest,nheap,x:longint;
value:int64;
Heap:array[1..maxn] of longint;
f,g:text;
Procedure Openf;
Begin
Assign(f,fi); reset(f);
Assign(g,fo); rewrite(g);
end;
Procedure Closef;
Begin
Close(f);Close(g);
end;
Procedure Update(u:longint);
Var cha,con:longint;
Begin
nheap:=nheap+1;
con:=nheap;
cha:=con shr 1;
While (cha>0) and (u<Heap[cha]) do
begin
Heap[con]:=Heap[cha];
71
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

con:=cha;
cha:=con shr 1;
end;
Heap[con]:=u;
End;
Procedure Pop;
Var cha,con,u:longint;
Begin
u:=Heap[nheap];
Dec(nheap); cha:=1;
While (cha<=nheap shr 1) do
begin
con:=cha shl 1;
If (con<nheap) and (Heap[con+1]<Heap[con]) then
con:=con+1;
If (u<Heap[con]) then
break;
Heap[cha]:=Heap[con];
cha:=con;
end;
Heap[cha]:=u;
End;
Procedure Solve;
Var i:longint;
Begin
Readln(f,n);
nheap:=0;
For i:=1 to n do
Begin
Read(f,Heap[i]);
Update(Heap[i]);
End;
72
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Value:=0;
While (nheap>1) do
Begin
x:=heap[1]; Pop;
X:=x+Heap[1]; Pop;
Value:=Value+Int64(x);{Int64(x) la gia tri cua x}
Update(x);
End;
writeln(g,Value);
End;
BEGIN
Openf;
Solve;
Closef;
END.
Bài 2: KMIN (http://vn.spoj.com/problems/KMIN/cstart=30 )
Cho 2 dãy số nguyên A và B. Với mọi số A[i]thuộc A và B[j] thuộc B người ta
tính tổng nó. Tất cả các tổng này sau khi được sắp xếp không giảm sẽ tạo thành dãy
C.Nhiệm vụ của bạn là: Cho 2 dãy A, B. Tìm K số đầu tiên trong dãy C
Input:
- Dòng đầu tiên gồm 3 số: M, N, K
- M dòng tiếp theo gồm M số mô tả dãy A
- N dòng tiếp theo gồm N số mô tả dãy B
Output: Gồm K dòng tương ứng là K phần tử đầu tiên trong dãy C
Giới hạn:
 1 ≤ M, N, K ≤ 50000
 1 ≤ Ai, Bi ≤ 109
Input Output
446 3
1 4
2 4

73
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

3 5
4 5
2 5
3
4
5
Giới hạn
1 ≤ M, N, K ≤ 50000
1 ≤ Ai, Bi ≤ 109
Thuật toán:
Với thì không thể tạo một mảng rồi sắp xếp lại được, vì
vậy chúng ta cần những thuật toán tinh tế hơn, 1 trong những thuật toán đơn giản mà
hiệu quả nhất là dùng heap.
Đầu tiên sắp xếp 2 mảng A và B không giảm, dĩ nhiên phần tử đầu tiên chính là
A[1] + B[1], vấn đề là phần tử thứ 2 là A[2] + B[1] hay là A[1] + B[2], ta xử lý như
sau:
Trước hết ta tạo một heap min gồm các phần tử A[1]+B[1], A[1]+B[2], …,
A[1]+B[n], mỗi khi lấy ra phần tử ở gốc (tức phần tử có giá trị nhỏ nhất trong heap
hiện tại) giả sử phần tử đó là A[i]+B[j] ta lại đẩy vào heap phần tử mới là A[i+1]+B[j]
(nếu i=m thì không đẩy thêm phần tử nào vào heap). Cứ thế cho đến khi ta lấy đủ K
phần tử cần tìm.
Độ phức tạp thuật toán trong trường hợp lớn nhất là O(50000*log(50000)), có
thể chạy trong thời gian 1s, bộ nhớ là n.
Chương trình: (Code\KMIN.pas)
Bài 3:Hàng đợi có độ ưu tiên (http://vn.spoj.com/problems/QBHEAP/ )
Cho trước một danh sách rỗng. Người ta xét hai thao tác trên danh sách đó:
Thao tác “+V” (ở đây V là một số tự nhiên <= 1000000000): Nếu danh sách đang có ít
hơn 15000 phần tử thì thao tác này bổ sung thêm phần tử V vào danh sách; Nếu
không, thao tác này không có hiệu lực.
Thao tác “-”: Nếu danh sách đang không rỗng thì thao tác này loại bỏ tất cả các
phần tử lớn nhất của danh sách; Nếu không, thao tác này không có hiệu lực

74
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Input
Gồm nhiều dòng, mỗi dòng ghi một thao tác. Thứ tự các thao tác trên các dòng được
liệt kê theo đúng thứ tự sẽ thực hiện
Output
Dòng 1: Ghi số lượng những giá trị còn lại trong danh sách. Các dòng tiếp theo:
Liệt kê những giá trị đó theo thứ tự giảm dần, mỗi dòng 1 số.
Example

Input Output
+1 4
+3 8
+2 7
+3 2
– 1
+4
+4

+2
+9
+7
+8

Chương trình: (Code\QBHeap.pas)


const
fi='';
nmax=15001;
type
data=longint;
var
f:text;
Heap,B:array[0..nmax+1] of data;
75
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

nHeap,n:data;
procedure swap(a,b:data);
var
z:data;
begin
z:=heap[a];
heap[a]:=heap[b];
heap[b]:=z;
end;
procedure UpHeap(i:data);
begin
if (i=1) or (heap[i]<=heap[i div 2]) then
exit;
swap(i,i div 2);
upheap(i div 2);
end;
procedure push(x:data);
begin
inc(nHeap);
heap[nHeap]:=x;
upheap(nHeap);
end;
procedure downheap(i:data);
var j:data;
begin
j:=i*2;
if j>nHeap then
exit;
if (j<nHeap) and (heap[j+1]>heap[j]) then
inc(j);
if heap[i]>=heap[j] then
exit;
76
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

swap(i,j);
downheap(j);
end;
procedure pop;
begin
heap[1]:=heap[nHeap];
dec(nHeap);
downheap(1);
end;
procedure xuli;
var i,j,x,tmp,res:data;
c:char;
begin
assign(f,fi); reset(f);
nheap:=0;
while not seekeof(f) do
begin
read(f,c);
if c='+' then
begin
readln(f,x);
if nHeap<15000 then
begin
push(x);
end;
end
else
begin
readln(f);
if nHeap>0 then
begin
tmp:=heap[1];
77
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

while (nheap>0) and (heap[1]=tmp) do


pop;
end;
end;
end;
res:=0;
while nHeap>0 do
begin
tmp:=heap[1];
while (nHeap>0) and (heap[1]=tmp) do
pop;
inc(res);
b[res]:=tmp;
end;
writeln(res);
for i:=1 to res do
writeln(b[i]);
close(f);
end;
begin
xuli;
end.
Bài 4: Hội pháp sư (http://vn.spoj.com/problems/VOSNSEQ/ )
Ở vương quốc XYZ, phép thuật không còn là điều xa lạ với các cư dân ở đây.
Hằng ngày mọi người đều ăn ở và sinh hoạt với các pháp sư ở đây trong yên bình. Các
pháp sư “tốt” đã tập hợp với nhau và thành lập các “bạch” hội với mục đích giúp đỡ
người dân. Nhưng cái gì có mặt “tốt” thì cũng có mặt “xấu”. Các “hắc” pháp sư là
những pháp sư luôn muốn sử dụng sức mạnh của mình để ức hiếp người dân với tiêu
chí “ai mạnh thì thắng”. Những pháp sư này cũng đã cùng nhau thành lập các “hắc”
hội. Các cuộc giao tranh giữa các pháp sư “tốt” và các “hắc” pháp sư đang ngày một
căng thẳng. Đỉnh điểm là cuộc giao tranh sắp tới, một cuộc tổng tiến công của cả 2
phe pháp sư.
78
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Theo thống kê thì có tất cả N “bạch” hội được đánh số từ 1 tới N theo thứ tự bất
kỳ và mỗi hội có M pháp sư.N hội này đã hội họp lại với nhau để bàn chiến thuật cho
cuộc chiến sắp tới với các “hắc” hội. Vì đây sẽ là cuộc chiến lâu dài nên chiến thuật
được đưa ra là sẽ đưa từng tốp N pháp sư trong đó không có 2 pháp sư nào thuộc
cùng một hội ra đánh thay phiên nhau. Chi tiết chiến thuật thì các tốp có sức mạnh
yếu sẽ được đưa ra trước. Sức mạnh của một tốp pháp sư được đánh giá bằng tổng chỉ
số sức mạnh của các pháp sư được chọn. Khi không cầm cự được nữa thì tốp đó sẽ
quay về để tốp khác lên đánh. Một pháp sư có thể được chọn trong 2 đợt liên tiếp.
Ngoài ra 2 tốp pháp sư liên tiếp nhau có thể mạnh hoặc yếu như nhau.
Yêu cầu: Tính sức mạnh của tốp pháp sư ở lượt thứ K.
Input:
- Dòng đầu chứa 3 số nguyên dương N, M, K.
- N dòng tiếp theo, dòng thứ i sẽ chứa M số nguyên dương là chỉ số sức mạnh
của các pháp sư từ yếu tới mạnh trong “bạch” hội thứ i. Pháp sư u yếu hơn pháp sư v
nếu chỉ số sức mạnh của pháp sư u nhỏ hơn pháp sư v.
Output:Chứa 1 số là tổng chỉ số sức mạnh của tốp pháp sư ở lượt thứ K.
Input Output
234 7
123
456
Ràng buộc:
 K <= MN.
 Sức mạnh của các pháp sư <= 109.
 20% số test sẽ có N = 5 và M <= 20.
 20% số test tiếp theo sẽ có N = 2 và M <= 106.
 20% số test tiếp theo sẽ thỏa mãn: N = 3, M <= 105, tổng chỉ số sức
mạnh trong mỗi hội <= 107.
 40% số test tiếp theo có N = 10, M <= 105 và K <= 106.
Thuật toán:
Với : Sinh số bằng đệ quy, sort lại rồi lấy số thứ .

79
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Với : Chặt nhị phân kết quả rồi đếm trên 2 dãy với độ phức tạp
O(N).
Với Chú ý rằng trong 1 hội thì tổng không vượt quá , vậy

nên số phần tử phân biệt trong 1 hội không vượt quá . Ta co dãy số lại rồi thực
hiện thuật toán như trên.

Với : Ta đưa về bài toán 2 trên N dãy: thực hiện thuật


toán ở bài 2 cho 2 dãy đầu, được K số, tiếp tục áp dụng dãy K số đó với dãy thứ 3,
được K số tiếp theo,… Làm lần lượt như vậy ta sẽ được K số bé nhất, đó là đáp án.
Bài 5: Điều động không lưu (http://vn.spoj.com/problems/MOVE12/ )
Sau khi thực thi quy hoạch của Bộ Giao thông, sơ đồ giao thông của thành phố H
gồm n tuyến đường ngang và n tuyến đường dọc cắt nhau tạo thành một lưới ô vuông
với n x n nút giao thông. Các nút giao thông được gán tọa độ theo hàng từ 1 đến n, từ
trên xuống dưới và theo cột từ 1 đến n, từ trái sang phải. Ban chỉ đạo an toàn giao
thông quyết định điều n cảnh sát giao thông đến các nút giao thông làm nhiệm vụ. Ban
đầu mỗi cảnh sát được phân công đứng trên một nút của một tuyến đường ngang khác
nhau. Đến giờ cao điểm, xuất hiện ùn tắc tại các tuyến đường dọc không có cảnh sát
giao thông. Để sớm giải quyết tình trạng này, Ban chỉ đạo an toàn giao thông quyết
định điều động một số cảnh sát giao thông ở một số nút, từ nút hiện tại sang một nút
khác cùng hàng ngang để đảm bảo mỗi tuyến đường dọc đều có mặt của cảnh sát giao
thông.
Yêu cầu: Biết rằng cảnh sát ở hàng ngang thứ i cần ti đơn vị thời gian để di
chuyển qua 1 cạnh của lưới ô vuông (i = 1, 2, ..., n), hãy giúp Ban chỉ đạo an toàn
giao thông tìm cách điều động các cảnh sát thỏa mãn yêu cầu đặt ra sao cho việc điều
động được hoàn thành tại thời điểm sớm nhất. Giả thiết là các cảnh sát được điều
động đồng thời thực hiện việc di chuyển đến vị trị mới tại thời điểm 0.
Ràng buộc: 50% số tests ứng với 50% số điểm của bài có n ≤ 100.
Input:
- Dòng thứ nhất chứa một số nguyên dương n (n ≤ 10000).
- Dòng thứ i trong số n dòng tiếp theo chứa hai số nguyên dương ci, ti (ti ≤
10000) tương ứng là tọa độ cột và thời gian để di chuyển qua 1 cạnh của lưới ô vuông
của cảnh sát đứng trên tuyến đường ngang thứ i (i = 1, 2, ..., n).

80
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Hai số trên cùng một dòng được ghi cách nhau ít nhất một dấu cách.
Output: Ghi ra một số nguyên duy nhất là thời điểm sớm nhất tìm được.
Input Output
5 10
5 10
3 10
3 20
2 9
2 15
Thuật toán:
Chặt nhị phân kết quả, giả sử là x. Với mỗi cảnh sát ta xác định được lo[i] và
hi[i] là vị trí mà cảnh sát i có thể di chuyển được trong thời gian x. Bài toán quy về xếp
các cảnh sát vào vị trí ans[i] sao cho và

Sắp xếp các cảnh sát theo tăng dần theo lo, với một cột j, ta sẽ tìm cảnh sát i thỏa
mãn để xếp vào và là nhỏ nhất trong các i thỏa mãn điều kiện.
Để thực hiện thao tác này, ta dùng 1 heap min để chứa các hi[i].
i := 1;
for j := 1 to n do
begin
while (i <= n) and (lo[i] <= j) do
begin
push(hi[i]);
inc(i);
end;
u := pop;
ans[u] := j;
end;
Chương trình: (Code\ MOVER12.pas)
Var
fi,fo:text;

81
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

n,nh:longint;
a,b,h,pos,c,t,v:array[0..10000]of longint;
procedure nhap;
var i:longint;
begin
read(fi,n);
for i:=1 to n do
read(fi,c[i],t[i]);
end;
procedure swap(var x,y:longint);
var tg:longint;
begin
tg:=x; x:=y; y:=tg;
end;
procedure upheap(i:longint);
var j:longint;
begin
j:=i div 2;
if (i>1) and (b[h[i]]<b[h[j]]) then
begin
swap(h[i],h[j]);
upheap(j);
end;
end;
procedure downheap(i:longint);
var j:longint;
begin
j:=i+i;
if (j>nh) then exit;
if (j<nh) and (b[h[j]]>b[h[j+1]]) then
inc(j);
if b[h[i]]>b[h[j]] then
82
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

begin
swap(h[i],h[j]);
downheap(j);
end;
end;
procedure push(x:longint);
begin
inc(nh);
h[nh]:=x;
upheap(nh);
end;
function pop:longint;
begin
pop:=h[1];
h[1]:=h[nh];
dec(nh);
downheap(1);
end;
procedure sort(le,r:longint);
var i,j,key:longint;
begin
i:=le;j:=r;
key:=a[le+random(r-le+1)];
repeat
while a[i]<key do inc(i);
while a[j]>key do dec(j);
if i<=j then
begin
swap(a[i],a[j]);
swap(b[i],b[j]);
inc(i);dec(j);dec(j);
end;
83
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

until i>j;
if i<r then sort(i,r);
if le<j then sort(le,j);
end;
function check(x:longint):boolean;
var i,j,u:longint;
begin
nh:=0;
for i:=1 to n do
begin
a[i]:=c[i]-(x div t[i]);
b[i]:=c[i]+(x div t[i]);
end;
sort(1,n);
i:=0;
j:=1;
for i:=1 to n do
begin
while (a[j]<=i) and (j<=n) do
begin
push(j);
inc(j);
end;
if nh>0 then u:=pop else u:=0;
if b[u]<i then exit(false);
end;
exit(true);
end;
procedure xuli;
var le,r,mid,res:longint;
begin
le:=0;
84
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

r:=10000*10000;
while le<=r do
begin
mid:=(le+r) div 2;
if check(mid) then
begin
res:=mid;
r:=mid-1;
end
else le:=mid+1;
end;
writeln(fo,res);
end;
begin
assign(fi,tfi);
assign(fo,tfo);
reset(fi);
rewrite(fo);
nhap;
xuli;
close(fo);
end.
3. Ứng dụng của heap trong lý thuyết đồ thị
Ứng dụng của heap trong lý thuyết đồ thị là giúp cải tiến tốc độ thực hiện các
thuật toán. Chẳng hạn:
a) Trong thuật toán Dijkstra
Trường hợp đồ thị có nhiều đỉnh, ít cạnh, thuật toán cần n lần cố định nhãn và
mỗi lần tìm đỉnh để cố định nhãn sẽ mất một đoạn chương trình với độ phức tạp O(n).
Để tăng tốc độ, người ta thường sử dụng cấu trúc dữ liệu Heap để lưu các đỉnh chưa cố
định nhãn. Heap ở đây là một cây nhị phân hoàn chỉnh thỏa mãn: Nếu u là đỉnh lưu ở
nút cha và v là đỉnh ở nút con thì d[u] ≤ d[v] (đỉnh r lưu ở gốc Heap là đỉnh có d[r] nhỏ
nhất).
85
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Tại mỗi bước lặp của thuật toán Dijkstra có hai thao tác: Tìm đỉnh cố định nhãn
và sửa nhãn
 Thao tác tìm đỉnh cố định nhãn sẽ lấy đỉnh lưu ở gốc Heap, cố định nhãn, đưa
phần tử cuối Heap và thế chỗ và thực hiện việc vun đống.
 Thao tác sửa nhãn, sẽ duyệt danh sách kề của đỉnh vừa cố định nhãn và sửa
nhãn những đỉnh tự do kề với đỉnh này, mỗi lần sửa nhãn một đỉnh nào đó, ta
xác định đỉnh này nằm ở đâu trong Heap và thực hiện việc chuyển đỉnh đó lên
phía gốc Heap nếu cần bảo toàn cấu trúc Heap.
Thuật toán Dijkstra tổ chức trên Heap tìm đường đi ngắn từ đỉnh s đến đỉnh t
- Cập nhật nút 1 của Heap (tương ứng với đỉnh xuất phát s, có giá trị khóa
bằng 0)
- Vòng lặp cho đến khi Heap rỗng (không còn nút nào)
 Begin
o Lấy đỉnh u tại nút gốc của heap (phép loại bỏ gốc Heap)
o Nếu u=t (đỉnh t là đỉnh đích của đường đi) thì thoát khỏi
vòng lặp
o Đánh dấu u là đỉnh đã được cố định nhãn
o Duyệt danh sách cung để tìm các cung có đỉnh đầu bằng u,
đỉnh cuối là v. Nếu v là đỉnh tự do và d[v] > d[u] + khoảng
cách (u,v) thì
Begin
 Sửa nhãn cho v và ghi nhận đỉnh trước v là u
 Trên Heap, cập nhật lại nút tương ứng với
đỉnh v
end
 End
Thuật toán Dijkstra có độ phức tạp là O(n2), việc kết hợp với cấu trúc dữ liệu
Heap giúp thuật toán được cải tiến và có độ phức tạp là O(max(n,m).logn).
Bài :Centre http://vn.spoj.com/problems/CENTRE28/)
Theo thống kê cho biết mức độ tăng trưởng kinh tế của nước Peace trong năm
2006 rất đáng khả quan. Cả nước có tổng cộng N thành phố lớn nhỏ được đánh số tuần
tự từ 1 đến N phát triển khá đồng đều. Giữa N thành phố này là một mạng lưới gồm M
86
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

đường đi hai chiều, mỗi tuyến đường nối 2 trong N thành phố sao cho không có 2
thành phố nào được nối bởi quá 1 tuyến đường. Trong N thành phố này thì thành phố 1
và thành phố N là 2 trung tâm kinh tế lớn nhất nước và hệ thống đường đảm bảo luôn
có ít nhất một cách đi từ thành phố 1 đến thành phố N.
Tuy nhiên,cả 2 trung tâm này đều có dấu hiệu quá tải về mật độ dân số. Vì vậy,
đức vua Peaceful quyết định chọn ra thêm một thành phố nữa để đầu tư thành một
trung tâm kinh tế thứ ba. Thành phố này sẽ tạm ngưng mọi hoạt động thường nhật,
cũng như mọi luồng lưu thông ra vào để tiến hành nâng cấp cơ sở hạ tầng. Nhưng
trong thời gian sửa chữa ấy, phải bảo đảm đường đi ngắn nhất từ thành phố 1 đến
thành phố N không bị thay đổi, nếu không nền kinh tế quốc gia sẽ bị trì trệ.
Vị trí và đường nối giữa N thành phố được mô tả như một đồ thị N đỉnh M
cạnh. Hãy giúp nhà vua đếm số lượng thành phố có thể chọn làm trung tâm kinh tế thứ
ba sao cho thành phố được chọn thỏa mãn các điều kiện ở trên
Input
- Dòng đầu tiên ghi 2 số nguyên dương N và M là số thành phố và số tuyến
đường.
- Dòng thứ i trong số M dòng tiếp theo ghi 3 số nguyên dương xi, yi và di với
ý nghĩa tuyến đường thứ i có độ dài di và nối giữa 2 thành phố xi, yi.
Output
- Dòng đầu tiên ghi số tự nhiên S là số lượng các thành phố có thể chọn làm
trung tâm kinh tế thứ ba.
- S dòng tiếp theo, mỗi dòng ghi 1 số nguyên dương là số thứ tự của thành
phố được chọn ( In ra theo thứ tự tăng dần )
Example
Input Output
66 2
121 4
231 5
361
1 4 100
4 5 100

87
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

5 6 100

Thuật toán: (sưu tầm)


 Tìm đường đi ngắn nhất giữa đỉnh 1 đến mọi đỉnh và giữa đỉnh n đến mọi đỉnh
sử dụng thuật toán Dijkstra. Đồng thời ta tính số đường đi ngắn nhất.
 Gọi d1[i] là đường đi ngắn nhất từ đỉnh 1 đến i
 Gọi d2[i] là đường đi ngắn nhất từ đỉnh n đến i
 Gọi f1[i] là số đường đi ngắn nhất từ đỉnh 1 đến i
 Gọi f2[i] là số đường đi ngắn nhất từ đỉnh n đến i
 Điều kiện để đỉnh i có thể làm trung tâm kinh tế thứ 3 là: (d1[i]+dn[i]<>d1[n])
hoặc (f1[i]*fn[i]<>f1[n])

Chương trình: (Code\Centre28.pas)


const fi = '';
fo = '';
maxn = 30000+3;
maxc = trunc(1e9)+3;
maxm = trunc(1e5);
type arrd = array[1..maxn] of longint;
arrf = array[1..maxn] of int64;
arrk = array[-maxm..maxm] of longint;
arrh = array[1..maxn] of longint;
var d1,dn,d :arrd;
f1,fn,f :arrf;
n,i,j,m :longint;
res :longint;
ans :arrd;
head :array[1..maxn] of longint;
link,ts,ke :arrk;
// dijkstra heap;
h,p :array[1..maxn] of longint;
nh :longint;
procedure add(i,u,v,w:longint);
88
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

begin
link[i]:=head[u];
head[u]:=i;
ke[i]:=v;
ts[i]:=w;
end;
procedure enter;
var u,v,w:longint;
begin
assign(input,fi);reset(input);
readln(n,m);
for i:=1 to m do
begin
read(u,v,w);
add(i,u,v,w);
add(-i,v,u,w);
end;
close(input);
end;
procedure swap(var x,y:longint);
var tg:longint;
begin
tg:=x;x:=y;y:=tg;
end;
procedure upheap(i:longint);
var j:longint;
begin
j:=i div 2;
if i>1 then
if d[h[i]] < d[h[j]] then
begin
swap(h[i],h[j]);
89
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

swap(p[h[i]],p[h[j]]);
upheap(j);
end;
end;
procedure downheap(i:longint);
var j:longint;
begin
j:=i + i;
if j>nh then exit;
if (j<nh) and (d[h[j]] > d[h[j+1]]) then inc(j);
if d[h[j]] < d[h[i]] then
begin
swap(h[i],h[j]);
swap(p[h[i]],p[h[j]]);
downheap(j);
end;
end;
procedure push(i:longint);
begin
inc(nh);
h[nh]:=i;
p[i]:=nh;
upheap(nh);
end;
function pop:longint;
begin
pop:=h[1];
h[1]:=h[nh];
p[h[1]]:=1;
dec(nh);
downheap(1);
end;
90
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

procedure update(i:longint);
begin
if p[i]=0 then push(i) else upheap(p[i]);
end;
procedure dijkstra(s:longint);
var u,v,i:longint;
begin
fillchar(h,sizeof(f),0);
fillchar(p,sizeof(p),0);
nh:=0;
fillchar(f,sizeof(f),0);
for i:=1 to n do
d[i]:=maxc;
d[s]:=0;
f[s]:=1;
push(s);
repeat
u:=pop;
i:=head[u];
while i<>0 do
begin
v:=ke[i];
if d[v]>d[u]+ts[i] then
begin
d[v]:=d[u]+ts[i];
f[v]:=f[u];
update(v);
end
else
if d[v]=d[u]+ts[i] then
begin
f[v]:=f[u]+f[v];
91
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

end;
i:=link[i];
end;
until nh=0;
end;
procedure process;
begin
dijkstra(1);
f1:=f;
d1:=d;
dijkstra(n);
fn:=f;
dn:=d;
for i:=1 to n do
if (d1[i]+dn[i]<>d1[n]) or (f1[i]*fn[i]<>f1[n]) then
begin
inc(res);
ans[res]:=i;
end;
end;
procedure print;
begin
assign(output,fo);rewrite(output);
{for i:=1 to n do writeln(d1[i]);}
writeln(res);
for i:=1 to res do writeln(ans[i]);
close(output);
end;
begin
enter;
process;
print;
92
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

end.
b) Trong thuật toán Kruskal tìm cây khung nhỏ nhất
Một trong những vấn đề quan trọng là làm thế nào để xét được các cạnh từ cạnh
có trọng số nhỏ tới cạnh có trọng số lớn. Ta có thể thực hiện bằng cách sắp xếp danh
sách cạnh theo thứ tự không giảm của trọng số, sau đó duyệt từ đầu tới cuối danh sách
cạnh, nên sử dụng các thuật toán sắp xếp hiệu quả để đạt được tốc độ nhanh trong
trường hợp số cạnh lớn. Trong trường hợp tổng quát, thuật toán Heapsort là hiệu quả
nhất bởi nó cho phép chọn lần lượt các cạnh từ cạnh trọng số nhỏ nhất tới cạnh trọng
số lớn nhất ra khỏi Heap và có thể xử lư (bỏ qua hay thêm vào cây) luôn.
Thuật toán Kruskal có độ phức tạp là O(mlogn), nếu đồ thị có cây khung thì m ≥
n-1 thì chi phí thời gian chủ yếu sẽ nằm ở thao tác sắp xếp danh sách cạnh bởi độ phức
tạp của HeapSort là O(mlogm), do đó độ phức tạp tính toán của thuật toán là
O(mlogm) trong trường hợp xấu nhất.
Bài 1: Net (Test\Net.rar)
Một công ty lập một dự án mắc điện thoại cho N (N<5000) nhân viên của mình
bằng một lưới đoạn nối từ máy của một người đến máy một người khác (không phải
người nào cũng được nối với nhau). Dự án phải đảm bảo yêu cầu sau cho (gọi là yêu
cầu về tính thông suốt của mạng): trên lưới điện thoại đó mỗi nhân viên của Công ty
đều có thể nhắn tin cho bất cứ một nhân viên khác hoặc trực tiếp hoặc thông qua một
số nhân viên trung gian.
Yêu cầu:Kiểm tra xem dự án có đáp ứng yêu cầu về tính thông sống không ?,
nếu có, hãy tìm cách loại bỏ một số đường nối sao cho mạng vẫn là thông suốt đồng
thời giảm đến mức tối thiểu chi phí nối mạng
Dữ liệu vào: Trong File NET.INP
- Dòng đầu tiên chứa hai số n (số nhân viên), m (số đường nối)
- Các dòng tiếp theo chứa các đường nối của dự án: mỗi dòng gồm 3 số
nguyên theo thứ tự là chỉ số của hai máy được nối và chi phí nối hai máy
này
Kết quả: Ghi ra File NET.OUT
- Dòng đầu tiên ghi chi phí tối thiểu nối mạng
- Dòng thứ hai ghi số k là số lượng đoạn nối cần loại bỏ

93
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

- K dòng tiếp theo mô tả thông tin k đoạn nối cần loại bỏ: mỗi dòng chứa hai
số nguyên xác định điểm đầu và điểm cuối đoạn nối.
Ví dụ:
NET.INP NET.OUT
57 91
1 2 16 3
1 4 38 45
1 5 43 15
2 5 50 25
3 5 25
4 3 12
4 5 29

Thuật toán:
- Xây dựng cây khung nhỏ nhất với n-1 cạnh
- Sắp xếp các cạnh của đồ thị theo thứ tự không giảm của độ dài để lựa chọn
cạnh bổ song (hay loại bỏ). Tuy nhiên để xây dựng cây khung nhỏ nhất với
n-1 cạnh, ta không cần phải sắp toàn bộ thứ tự các cạnh mà chỉ cần xét phần
trên của dãy đó chứa r<m cạnh. Để làm việc đó ta có thể sử dụng thủ tục sắp
xếp dạng vun đống (Heap Sort), hoặc sử dụng (Quick Sort)
Việc lựa chọn cạnh (bổ sung hay loại bỏ) đòi hỏi phải có 1 thủ tục hiệu quả kiểm tra
tập cạnh T  {e} có chứa chu trình hay không. Để ý rằng các cạnh trong T ở các bước
lập trung gian sẽ tạo thành 1 rừng. Cạnh e cần khảo sát tạo thành 1 chu trình.

Chương trình (Code\Net.pas):


PROGRAM NetWork;
Uses Crt;
Const
fi='Network.Inp';
fo='Network.Out';
maxn=5000;
maxm=5000;
Type
94
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Arrn=array[1..maxn] of Integer;
Arrm=array[1..maxm] of Integer;
Var
D1:Arrn;D2,W:Arrm;
CuoiT,DauT,Father:Arrm;
m,n,MinL:integer;
noi:Boolean;
f:text;
Procedure Nhap;
Var i:integer;
Begin
Assign(f,fi); Reset(f);
Readln(f,n,m);
For i:=1 to m do
Read(f,D1[i],D2[i],W[i]);
Close(f);
End;
Procedure Heap(First,last:integer);
Var j,k,t1,t2,t3:integer;
Begin
j:=first;
While (j<=trunc(last/2)) do
Begin
If (2*j<last) and (W[2*j+1]<W[2*j]) then
k:=2*j+1
Else
k:=2*j;
If W[k]<W[j] then
begin
t1:=D1[j]; t2:=D2[j]; t3:=W[j];
D1[j]:=D1[k]; D2[j]:=D2[k]; W[j]:=W[k];
D1[k]:=t1; D2[k]:=t2; W[k]:=t3;
95
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

j:=k;
end
Else j:=last;
end;
end;
Function Find(i:integer):integer;
Var tro:integer;
Begin
Tro:=i;
While Father[tro]>0 do
tro:=Father[tro];
Find:=tro;
End;

Procedure Union(i,j:integer);
Var x:integer;
Begin
x:=Father[i]+Father[j];
If Father[i]>father[j] then
Begin
Father[i]:=j;
Father[j]:=x;
end
Else
Begin
Father[j]:=i;
Father[i]:=x;
end;
End;
Procedure Xuli;
Var last,i,c,u,r1,r2,v,Ndinh,Ncanh:integer;
Begin
96
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

For i:=1 to n do
Father[i]:=-1;
last:=m; Ncanh:=0; Ndinh:=0;noi:=true;
For i:=trunc(m/2) downto 1 do Heap(i,m);
MinL:=0; Noi:=true;
While (Ndinh<n-1) and (Ncanh<m) do
Begin
Ncanh:=Ncanh+1; u:=D1[1]; v:=D2[1];
r1:=Find(u);
r2:=Find(v);
If r1<>r2 then
Begin
Ndinh:=Ndinh+1; Union(r1,r2);
DauT[Ndinh]:=u; CuoiT[Ndinh]:=v;
MinL:=MinL+W[1];
end;
D1[1]:=D1[last]; D2[1]:=D2[last];
W[1]:=W[last]; last:=last-1;
Heap(1,last);
end;
If Ndinh<>N-1 then noi:=false;
End;
Procedure Xuat;
Var i,canh:integer;
Begin
Assign(f,fo); Rewrite(f);
If noi then
begin
Writeln(f,MinL);canh:=0;
For i:=1 to n-1 do
If (D1[i]<>DauT[i]) and (D2[i]<>CuoiT[i]) then
inc(canh);
97
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Writeln(f,canh);
For i:=1 to n-1 do
If (D1[i]<>DauT[i]) and (D2[i]<>CuoiT[i]) then
Writeln(f,DauT[i],' ',CuoiT[i]);
end
Else Writeln(f,'Mang chua thong suot');
Close(f);
End;
BEGIN
Clrscr;
Nhap;
Xuli;
Xuat;
END.
Bài 2: Xây dựng thành phố (http://vn.spoj.com/problems/NKCITY/ )
Nước Anpha đang lập kế hoạch xây dựng một thành phố mới và hiện đại. Theo
kế hoạch, thành phố sẽ có N vị trí quan trọng, được gọi là N trọng điểm và các trọng
điểm này được đánh số từ 1 tới N. Bộ giao thông đã lập ra một danh sách M tuyến
đường hai chiều có thể xây dựng được giữa hai trọng điểm nào đó. Mỗi tuyến đường
có một thời gian hoàn thành khác nhau.
Các tuyến đường phải được xây dựng sao cho N trọng điểm liên thông với
nhau. Nói cách khác, giữa hai trọng điểm bất kỳ cần phải di chuyển được đến nhau qua
một số tuyến đường. Bộ giao thông sẽ chọn ra một số tuyến đường từ trong danh sách
ban đầu để đưa vào xây dựng sao cho điều kiện này được thỏa mãn.
Do nhận được đầu tư rất lớn từ chính phủ, bộ giao thông sẽ thuê hẳn một đội thi
công riêng cho mỗi tuyến đường cần xây dựng. Do đó, thời gian để hoàn thành toàn bộ
các tuyến đường cần xây dựng sẽ bằng thời gian lâu nhất hoàn thành một tuyến đường
nào đó.
Yêu cầu: Giúp bộ giao thông tính thời gian hoàn thành các tuyến đường sớm
nhất thỏa mãn yêu cầu đã nêu.
Dữ liệu
- Dòng chứa số N và M (1 ≤ N ≤ 1000; 1 ≤ M ≤ 10000).
98
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

- M tiếp theo, mỗi dòng chứa ba số nguyên u, v và t cho biết có thể xây dựng
tuyến đường nối giữa trọng điểm u và trọng điểm v trong thời gian t. Không
có hai tuyến đường nào nối cùng một cặp trọng điểm.
Kết quả
Một số nguyên duy nhất là thời gian sớm nhất hoàn thành các tuyến đường thỏa
mãn yêu cầu đã nêu.
Chương trình: Code\NKCiTy_Krus.pas
uses math;
const fi ='';
fo ='';
maxn =1000;
maxm =10000;
type data =record
u, v, c :longint;
end;
var f :Text;
n, m :longint;
Edges :array[1..maxm] of Data;
b :Array[1..maxm] of longint;
Father :Array[1..maxn] of longint;
res :longint;
procedure nhap;
var i, u, v, c :longint;
begin
assign(f, fi);
reset(f);
readln(f, n, m);
for i:=1 to m do
begin
readln(f, u, v, c);
Edges[i].u:=u;
Edges[i].v:=v;
99
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Edges[i].c:=c;
end;
close(f);
end;
Procedure init;
var i :longint;
begin
for i:=1 to m do b[i]:=i;
for i:=1 to n do father[i]:=-1;
res:=-1;
end;
procedure QS(l,r:longint);
var i, j, x, tg :longint;
begin
i:=l;
j:=r;
x:=edges[b[ (l+r) div 2] ].c;
repeat
while Edges[b[i]].c < x do inc(i);
while Edges[b[j]].c > x do dec(j);
if i<=j then
begin
tg:=b[i];
b[i]:=b[j];
b[j]:=tg;
inc(i);
dec(j);
end;
until i>j;
if l<j then QS(l,j);
if i<r then QS(i,r);
end;
100
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

function find(i:longint):longint;
var t :longint;
begin
t:=i;
while father[t]>0 do t:=father[t];
exit(t);
end;
procedure union(i, j:longint);
var x :longint;
begin
x:=father[i]+father[j];
if father[i]>father[j] then
begin
father[i]:=j;
father[j]:=x;
end
else begin
father[j]:=i;
father[i]:=x;
end;
end;
procedure Kruskal;
var i, u,v, c, r1, r2 :longint;
begin
init;
QS(1,m);
for i:=1 to m do
begin
u:=Edges[b[i]].u;
v:=Edges[b[i]].v;
c:=Edges[b[i]].c;
r1:=find(u);
101
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

r2:=find(v);
if r1<>r2 then
begin
union(r1,r2);
res:=max(res,c);
end;
end;
end;
procedure xuat;
begin
assign(f, fo);
rewrite(f);
writeln(f, res);
close(f);
end;
BEGIN
nhap;
Kruskal;
xuat;
END.
c) Trong thuật toán Prim tìm cây khung nhỏ nhất
Xét về độ phức tạp tính toán, thuật toán Prim có độ phức tạp là O(n2). Tương tự
thuật toán Dijkstra, nếu kết hợp thuật toán Prim với cấu trúc Heap sẽ được một thuật
toán với độ phức tạp O((m+n)logn).
Bài 1: Xây dựng thành phố
Chương trình: Code\NKCiTy_Pri.pas
const fi='';
fo='';
maxn=1000;
maxc=trunc(1e9);
var d:array[1..maxn] of longint;
i,j,n,m,res:longint;
102
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

u,v,t:longint;
cx:array[1..maxn] of boolean;
c:array[1..maxn,1..maxn] of longint;
procedure init;
begin
for i:=1 to n do d[i]:=maxc;
d[1]:=0;
fillchar(cx,sizeof(cx),true);
end;
procedure prim;
var min,u:longint;
begin
repeat
min:=maxc;u:=0;
for i:=1 to n do
if cx[i] then
if d[i]<min then
begin
min:=d[i];
u:=i;
end;
if (u=0) then exit;
cx[u]:=false;
if d[u]>res then res:=d[u];
for v:=1 to n do
if cx[v] then
if d[v]>c[u,v] then
begin
d[v]:=c[u,v];
end;
until false;
end;
103
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

begin
assign(input,fi);reset(input);
assign(output,fo);rewrite(output);
readln(n,m);
for i:=1 to n do
for j:=1 to n do
if i<>j then
c[i,j]:=maxc
else c[i,j]:=0;
for i:=1 to m do
begin
readln(u,v,t);
c[u,v]:=t;
c[v,u]:=t;
end;
init;
prim;
writeln(res);
close(input);close(output);
end.
Bài 2:Tưới nước đồng cỏ (http://vn.spoj.com/problems/FWATER/ )
Nông dân John quyết định mang nước tới cho N (1 <= N <= 300) đồng cỏ của
mình, để thuận tiện ta đánh số các đồng cỏ từ 1 đến N. Để tưới nước cho 1 đồng cỏ
John có thể chọn 2 cách, 1 là đào ở đồng cỏ đó 1 cái giếng hoặc lắp ống nối dẫn nước
từ những đồng cỏ trước đó đã có nước tới.
Để đào một cái giếng ở đồng cỏ i cần 1 số tiền là W_i (1 <= W_i <= 100,000).
Lắp ống dẫn nước nối 2 đồng cỏ i và j cần 1 số tiền là P_ij (1 <= P_ij <= 100,000; P_ij
= P_ji; P_ii=0).
Tính xem nông dân John phải chi ít nhất bao nhiêu tiền để tất cả các đồng cỏ đều có
nước.
DỮ LIỆU
 Dòng 1: Một số nguyên duy nhất: N
104
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

 Các dòng 2..N + 1: Dòng i+1 chứa 1 số nguyên duy nhất: W_i
 Các dòng N+2..2N+1: Dòng N+1+i chứa N số nguyên cách nhau bởi dấu cách;
số thứ j là P_ij
KẾT QUẢ
 Một số nguyên duy nhất là chi phí tối thiểu để cung cấp nước cho tất cả các
đồng cỏ.
VÍ DỤ
FWATER.INP FWATER.OUT
4 9
5
4
4
3
0222
2033
2304
2340

GIẢI THÍCH:
Có 4 đồng cỏ. Mất 5 tiền để đào 1 cái giếng ở đồng cỏ 1, 4 tiền để đào ở đồng
cỏ 2, 3 và 3 tiền để đào ở đồng cỏ 4. Các ống dẫn nước tốn 2, 3, và 4 tiền tùy thuộc
vào nó nối đồng cỏ nào với nhau. Nông dân John có thể đào 1 cái giếng ở đồng cỏ thứ
4 và lắp ống dẫn nối đồng cỏ 1 với tất cả 3 đồng cỏ còn lại, chi phí tổng cộng là 3 + 2
+ 2 + 2 = 9.

Chương 1. Chương trình: Code\FWATER.pas


const
fi = '';
fo = '';
maxN = 301;
maxC = 10000000;
var

105
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

a : array[1..maxN,1..maxN] of longint;
d,tr,w : array[1..maxN] of longint;
free : array[1..maxN] of boolean;
n : longint;

procedure readf;
var f: text;
i,j: longint;
begin
assign(f,fi); reset(f);
readln(f,n);
for i:=1 to n do
for j:=1 to n do
if i=j then a[i,j] := 0
else a[i,j] := maxC;
for i:=1 to n do readln(f,w[i]);
for i:=1 to n do begin
for j:=1 to n do read(f,a[i,j]);
readln(f);
end;
close(f);
end;

procedure init;
var i: longint;
begin
inc(n);
for i:=1 to n do begin
a[n,i] := w[i];
a[i,n] := w[i];
end;
d[1] := 0;
106
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

for i:=2 to n do d[i] := maxC;


fillchar(free,sizeof(free),true);
end;

procedure Prim;
var k,i,u,v,min: longint;
connected: boolean;
begin
fillchar(tr,sizeof(tr),0);
connected := true;
for k:=1 to n do begin
u := 0;
min := maxC;
for i:=1 to n do
if free[i] and (d[i] < min) then begin
min := d[i];
u := i;
end;
if u=0 then begin
connected := false;
break;
end;
free[u] := false;
for v:=1 to n do
if free[v] and (d[v] > a[u,v]) then begin
d[v] := a[u,v];
tr[v] := u;
end;
end;
end;

procedure writef;
107
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

var f: text;
t,i: longint;
begin
assign(f,fo); rewrite(f);
t := 0;
for i:=2 to n do
t := t + a[tr[i],i];
writeln(f,t);
close(f);
end;

BEGIN
readf;
init;
Prim;
writef;
END.
III. KẾT LUẬN
Có thể thấy cấu trúc dữ liệu Heap là một cấu trúc dữ liệu quan trọng và có tính
ứng dụng cao, kết hợp các thuật toán với cấu trúc Heap giúp cải tiến một cách đáng kể
thời gian thực hiện thuật toán. Ngoài ra, có rất nhiều cấu trúc dữ liệu heap còn có
nhiều dạng biến thể như B-heap, Binary heap, Fibonacci heap….
Qua quá trình nghiên cứu tài liệu từ nhiều nguồn khác nhau, tôi viết chuyên đề
này, mong rằng có thể hữu ích cho các đồng nghiệp, do kinh nghiệm còn hạn hẹp
mong có thêm nhiều góp ý và trao đổi.
IV. TÀI LIỆU THAM KHẢO
[1] Các chuyên đề của nhiều tác giả đã viết trước đây
[2] Tài liệu sách giáo khoa chuyên – Tập 1, 2, 3
[3] Giáo trình Giải thuật và lập trình – Tác giả Lê Minh Hoàng
[4] Sáng tạo thuật toán trong lập trình tập 1, 2, 3 – Nguyễn Xuân Huy
[5] Trang kienthuc24h.com
[6] Trang Vnoi.info
108
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

[7] Trang yeulaptrinh.com

109
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

CẤU TRÚC DỮ LIỆU HEAP

Trường THPT Chuyên Long An


I. Khái niệm
Heap là một trong những cấu trúc dữ liệu đặc biệt quan trọng, nó giúp ta có thể giải
được nhiều bài toán trong thời gian cho phép. Độ phức tạp thông thường khi làm việc
với Heap là O(logN).
Heap thực chất là một cây cân bằng thỏa mãn các điều kiện sau:
 Một nút có không quá 2 nút con.
 Với Heap Max thì nút gốc là nút lớn nhất, mọi nút con đều không lớn hơn nút
cha của nó. Với Heap Min thì ngược lại.
Mặc dù được mô tả như cây nhưng Heap có thể được biểu diễn bằng mảng. Nút
con của nút i là 2*i và 2*i+1. Do Heap là cây cân bằng nên độ cao của 1 nút luôn <=
logN.
Ứng dụng chủ yếu của Heap là tìm Min, Max trong 1 tập hợp động (có thể thay
đổi, thêm, bớt các phần tử) nhưng như vậy đã là quá đủ.

(Mô hình biểu diễn Heap bằng cây nhị phân và bằng mảng)
II. Các thao tác thường dùng đối với Heap Max
1. Khai báo
Ở ví dụ này, Heap sẽ là mảng một chiều kiểu LongInt, nHeap là số phần tử của mảng.

110
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Const
maxn = 100000;
Var
nHeap : LongInt;
Heap : array[0..maxn] of LongInt;
2. UpHeap
Nếu 1 nút lớn hơn nút cha của nó thì di chuyển nó lên trên:

Procedure UpHeap(i : LongInt);


Begin
if (i = 1) or (Heap[i] < Heap[i div 2]) then exit; // Nếu i là nút gốc hoặc
nhỏ hơn nút cha thì không làm việc
swap(Heap[i] , Heap[i div 2]); // Đổi chỗ 2 phần tử trong Heap;
UpHeap(i div 2); // Tiếp tục di chuyển lên trên
end;
3. DownHeap
Nếu 1 nút nhỏ hơn nút con thì đẩy nó xuống dưới:

111
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Procedure DownHeap(i : LongInt);


Var
j : LongInt;
Begin
j := i*2;
if j > nHeap then exit; // Nếu i không có nút con thì không làm việc
if (j < nHeap) and (Heap[j] < Heap[j+1]) then Inc(j); // Nếu i có 2 nút con thì
chọn nút ưu tiên hơn
if Heap[i] < Heap[j] then // Nếu nút cha nhỏ hơn nút con
begin
swap(Heap[i] , Heap[j]); // Đổi chỗ 2 phần tử trong Heap
DownHeap(j); // Tiếp tục di chuyển xuống dưới
end;
end;
4. Push
Đưa 1 phần tử mới vào Heap: Thêm 1 nút vào cuối Heap và tiến hành UpHeap từ đây:
112
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Procedure Push(x : LongInt);


Begin
Inc(nHeap); // Tăng số phần tử của Heap
Heap[nHeap] := x; // Thêm x vào Heap
UpHeap(nHeap);
End;
5. Pop
Rút ra 1 phần tử ở vị trí v trong Heap: Gán Heap[v] := Heap[nHeap] rồi tiến hành
chỉnh lại Heap:
Function Pop(v : LongInt) : LongInt;
Begin
Pop := Heap[v]; // Lấy phần tử ở vị trí v ra khỏi Heap
Heap[v] := Heap[nHeap]; // Đưa phần tử ở cuối Heap vào vị trí v
Dec(nHeap); // Giảm số phần tử của Heap đi 1
{Chỉnh lại Heap}
UpHeap(v);
DownHeap(v);
End;
Ngoài ra, khi sử dụng thuật toán Dijkstra/Prim kết hợp cấu trúc Heap, bạn còn có
thể sử dụng cách Push và Pop khác thuận lợi hơn so với cách trình bày ở trên:
6. Dijkstra Heap
1/ Update – Dijkstra:
Procedure Update(v : LongInt); // Đỉnh v vừa được sửa nhãn, cần chỉnh lại Heap
var
child , parent : LongInt;
begin
child := pos[v]; // child là vị trí của đỉnh v trong Heap
if child = 0 then // Nếu đỉnh v chưa có trong Heap
begin
Inc(nHeap); // Tăng số phần tử của Heap
child := nHeap; // Đưa v vào cuối Heap

113
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

end;
parent := child div 2; // parent là nút cha của child
while (parent > 0) and (d[Heap[parent]] > d[v]) do // Nếu đỉnh ở nút parent kém
ưu tiên hơn v thì bị “kéo xuống” nút child
begin
Heap[child] := Heap[parent]; // Đẩy đỉnh được lưu trong nút cha xuống nút
con
pos[Heap[child]] := child; // Ghi nhận lại vị trí mới của đỉnh đó
child := parent; // Tiếp tục di chuyển lên
parent := child div 2;
end;
Heap[child] := v; // Thao tác “kéo xuống” ở trên sẽ tạo ra 1 ô trống ở nút child
để đặt v vào
pos[v] := child; // Ghi nhận vị trí mới của đỉnh v trong Heap
end;
2/ Pop – Dijkstra:
Function Pop : LongInt; // Lấy từ Heap đỉnh có nhãn tự do nhỏ nhất
var
r , v , c : LongInt;
begin
Pop := Heap[1]; // Nút gốc là nút có nhãn tự do nhỏ nhất
v := Heap[nHeap]; // v là đỉnh ở nút lá cuối Heap, sẽ được đảo lên đầu và vun
đống
Dec(nHeap); // Giảm số phần tử của Heap
r := 1; // Bắt đầu từ nút gốc
while r*2 <= nHeap do // Chừng nào r chưa phải là lá
begin
c := r*2; // c là nút con của r
if (c <nHeap) and (d[Heap[c]] > d[Heap[c+1]]) then Inc(c); // Trong 2 nút con
chọn nút con chứa đỉnh ưu tiên hơn
if d[Heap[c]] >= d[v] then break; // Nếu v ưu tiên hơn thì không làm việc nữa
Heap[r] := Heap[c]; // Chuyển đỉnh lưu ở nút con lên nút cha
114
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

pos[Heap[r]] := r; // Cập nhật lại vị trí mới trong Heap của đỉnh đó
r := c; // Tiếp tục di chuyển xuống dưới
end;
Heap[r] := v; // Đỉnh v được đặt vào vị trí r để đảm bảo cấu trúc Heap
pos[v] := r; // Ghi nhận vị trí mới của đỉnh v trong Heap
end;
III. Một số bài tập ứng dụng
1. Bài tập 1: Heapuri
Cho danh sách gồm N phần tử. Chúng ta thực hiện một số thao tác trên danh sách. Có
3 loại thao tác sau :
- Thao tác 1 : Cho phần tử x vào danh sách
- Thao tác 2 : Xóa phần tử thứ t (theo thứ tự nhập vào)
- Thao tác 3: Lấy ra phần tử nhỏ nhất trong danh sách
Yêu cầu: Hãy cho biết các kết quả của thao tác 3 ?
INPUT: HEAPURI.INP
- Dòng 1: N
- Các dòng tiếp theo là dãy thao tác cần xử lý theo định dạng 1 x, 2 x hoặc 3
tương ứng là thao tác 1, 2 và 3
OUTPUT : HEAPURI.OUT
- Ghi trên nhiều dòng, mỗi dòng là kết quả của thao tác 3 theo thứ tự trong file
input.
Ví dụ :
HEAPURI.INP HEAPURI.OUT
9 4
1 4 2
1 7 7
1 9
3
1 2
2 1
3

115
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

2 4
3

Dễ thấy đây là một danh sách động (số lượng phần tử thay đổi), vì vậy ta xây dựng
một heapmin để lấy ra giá trị nhỏ nhất ở các thao tác 3. Để xóa phần tử thứ t theo thứ
tự nhập vào thì ta lưu thêm một mảng Pos.
#include <stdio.h>
#include <assert.h>

#define maxn 200010

int N, L, NR;
int A[maxn], Heap[maxn], Pos[maxn];

void push(int x)
{
int aux;

while (x/2 && A[Heap[x]]<A[Heap[x/2]])


{
aux = Heap[x];
Heap[x] = Heap[x/2];
Heap[x/2] = aux;

Pos[Heap[x]] = x;
Pos[Heap[x/2]] = x/2;
x /= 2;
}
}

void pop(int x)
{

116
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

int aux, y = 0;

while (x != y)
{
y = x;

if (y*2<=L && A[Heap[x]]>A[Heap[y*2]]) x = y*2;


if (y*2+1<=L && A[Heap[x]]>A[Heap[y*2+1]]) x = y*2+1;

aux = Heap[x];
Heap[x] = Heap[y];
Heap[y] = aux;

Pos[Heap[x]] = x;
Pos[Heap[y]] = y;
}
}

intmain()
{
freopen("heapuri.in", "r", stdin);
freopen("heapuri.out", "w", stdout);

int i, x, cod;

scanf("%d ", &N);

assert(1<=N && N<=200000);

for (i=1; i<=N; i++)


{
scanf("%d ", &cod);
117
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

assert(1<=cod && cod<=3);


if (cod < 3)
{
scanf("%d ", &x);
assert(1<=x && x<=1000000000);
}

if (cod == 1)
{
L++, NR++;
A[NR] = x;
Heap[L] = NR;
Pos[NR] = L;

push(L);
}

if (cod == 2)
{
A[x] = -1;
assert(Pos[x] != 0);
assert(1<=x && x<=NR);
push(Pos[x]);

Pos[Heap[1]] = 0;
Heap[1] = Heap[L--];
Pos[Heap[1]] = 1;
if (L>1) pop(1);
}

if (cod == 3) printf("%d\n", A[Heap[1]]);


}
118
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

return 0;
}
2. Bài tập 2: Thả xốp
Có N hạt xốp, hạt thứ i có khối lượng , được thả lần lượt xuống một ống nước
đặc biệt được thiết kế sao cho tại mỗi thời điểm chỉ có một hạt xốp nhẹ nhất nổi lên
trên bề mặt. Trước mỗi lần thả, hạt xốp đang nổi trên bề mặt sẽ bị ngấm nước và tăng
gấp đôi khối lượng. Hỏi sau khi thả hạt xốp cuối cùng vào ống thì khối lượng xốp tăng
so với tổng khối lượng ban đầu là bao nhiêu ?
INPUT:SPONGE.INP
- Dòng 1: Số nguyên dương N
- Dòng 2: N số nguyên dương
OUTPUT :SPONGE.OUT
- Ghi 1 số duy nhất là đáp án của bài toán
Ví dụ:
SPONGE.INP SPONGE.OUT
3 3
2 1 3
Tương tự bài 1, dễ thấy đây là một danh sách động (giá trị phần tử thay đổi), vì vậy ta
xây dựng một heapmin để lấy ra hạt xốp nhỏ nhất, tăng gấp đôi khối lượng rồi cập
nhật lại heapmin.
COnst
tfi='sponge.in';
tfo='sponge.out';
Type
arr1=array[0..100001] of longint;
Var
fi,fo : text;
w,heap,head,ke,d,c,dd,cost : arr1;
n,nheap,delta : longint;

119
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

{==================================}
Procedure nhap;
Var
i :longint;
Begin
Assign(fi,tfi);Reset(fi);
read(fi,n);
For i := 1 to n do
read(fi,w[i]);
cost := w;
Close(fi);
End;
{==================================}
Procedure doicho(varx,y : longint);
var
tmp :longint;
Begin
tmp := x;
x := y;
y := tmp;
End;
{==================================}
Procedure upheap(i : longint);
Var
j :longint;
Begin
While (i <> 1) and (heap[i] <heap[i div 2]) do
Begin
doicho(heap[i],heap[i div 2]);
i := i div 2;
End;
End;
120
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

{=============================}
Procedure downheap(i : longint);
Var
j :longint;
Begin
While (2*i <= nheap) do
Begin
j := 2*i;
If (heap[j] >heap[j+1]) and (j <nheap) then inc(j);
If heap[i] > heap[j] then
Begin
doicho(heap[i],heap[j]);
i :=j;
End
else exit;
End;
End;
{=============================}
Procedure push(x :longint);
Begin
inc(nheap);
heap[nheap] := x;
upheap(nheap);
End;
{=============================}
Procedure xuly;
Var
i1 :longint;
Begin
For i1 := 1 to n-1 do
Begin
push(w[i1]);
121
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

delta := delta + heap[1];


heap[1] := heap[1]*2;
downheap(1);
End;
End;
{==================================}
BEGIN
Assign(fo,tfo);Rewrite(fo);
nhap;
xuly;
write(fo,delta);
Close(fo);
END.
3. Bài tập 3: PILOT
HT AIRLINE là một hãng hàng không danh tiếng ở Việt Nam, tuy nhiên, để tồn tại
trong cơn bão suy thoái kinh tế, Ban giám đốc quyết định giảm chi phi tiền lương cho
phi công càng nhiều càng tốt.
HT airline có tất cả N phi công (N là số chẵn), các phi công được đánh số từ 1 đến
N (Phi công 1 là phi công trẻ nhất, phi công i là phi công có tuổi cao thứ i,… phi công
N
n là phi công cao tuổi nhất). HT airline cần chính xác phi hành đoàn, mỗi phi hành
2
đoàn gồm 2 phi công (một lái chính và một lái phụ), lái chính phải nhiều tuổi hơn lái
phụ. Hợp đồng mà công ty kí với các phi công có 2 điều khoản rõ ràng: tiền lương khi
là lái chính và tiền lương khi là lái phụ. Rõ ràng, đối với 1 phi công, tiền lương lái
chính bao giờ cũng cao hơn tiền lương khi lái phụ. Tuy nhiên, với một phi hành đoàn,
có thể tiền lương của lái chính lại thấp hơn lái phụ.
N
Để giảm chi phí trả tiền lương, HT phải xác định một cách phân chia tối ưu phi
2
hành đoàn.
Bạn hãy giúp HT viết chương trình xác định số tiền tối thiểu để trả lương cho N phi
công.
INPUT: PILOT.INP
122
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

- Dòng 1 : Số nguyên dương N, là số phi công ở HT airline.(2  N  10000; N là


số chẵn)
- N dòng tiếp theo, dòng thứ i là thông tin về phi công i : gồm hai số a và c viết
cách nhau 1 dấu cách trống, tương ứng là tiền lương khi lái chính và tiền lương
khi lái phụ (1  a < c  100.000).
OUTPUT: PILOT.OUT
- Một số nguyên duy nhất là tiền lương tối thiểu phải trả cho N phi công.
Ví dụ :

PILOT.INP PILOT.OUT PILOT.INP PILOT.OUT

6 32000 6 33000
10000 7000 5000 3000
9000 3000 4000 1000
6000 4000 9000 7000
5000 1000 11000 5000
9000 3000 7000 3000
8000 6000 8000 6000

Time limits / test: 1s


Ta có nhận xét sau: Theo thứ tự i nhập vào từ 1 đến N, thì cứ i lẻ thì phải có 1 lái phụ.
Vì vậy, ta xây dựng một heap max, lần lượt cho vào heap, khi i lẻ thì ta lấy
trong heap ra một nút, đây chính là lái phụ.
CONST
tfi = 'pilot.inp';
tfo = 'pilot.out';
max = 100000;
TYPE
Arr1 = array[1..max] of longint;
VAR
fi,fo : text;
n,nh,sum,kq : longint;
a,b,h : Arr1;

123
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

{*-----------------------------------------------------------------*}
procedurenhap;
var
i : longint;
begin
assign(fi,tfi);reset(fi);
read(fi,n);
For i := 1 to n do
read(fi,a[i],b[i]);
close(fi);
end;
{*-----------------------------------------------------------------*}
proceduredoicho(varx,y : longint);
var
tg : longint;
begin
tg := x;
x := y;
y := tg;
end;
{*-----------------------------------------------------------------*}
procedureupheap(i : longint);
begin
if (i = 1) or (h[i] < h[i div 2]) then exit;
if h[i div 2] < h[i] then
begin
doicho(h[i div 2],h[i]);
upheap(i div 2);
end;
end;
{*-----------------------------------------------------------------*}
proceduredownheap(i : longint);
124
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

var
j : longint;
begin
j := 2 *i;
if j >nh then exit;
if (j <nh) and (h[j] < h[j+1]) then inc(j);
if h[i] < h[j] then
begin
doicho(h[i],h[j]);
downheap(j);
end;
end;
{*-----------------------------------------------------------------*}
procedure push(x : longint);
begin
inc(nh);
h[nh] := x;
upheap(nh);
end;
{*-----------------------------------------------------------------*}
function pop : longint;
begin
pop := h[1];
h[1] := h[nh];
dec(nh);
downheap(1);
end;
{*-----------------------------------------------------------------*}
procedurexuli;
var
i : longint;
begin
125
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

sum := 0;nh := 0;kq := 0;

For i := 1 to n do
begin
sum := sum + a[i];
push(a[i] - b[i]);
if i mod 2 = 1 then kq := kq + pop;
end;
end;
{*-----------------------------------------------------------------*}
procedureinkq;

begin
assign(fo,tfo);rewrite(fo);
write(fo,sum-kq);
close(fo);
end;
{*-----------------------------------------------------------------*}
{*-----------------------------------------------------------------*}
{*-----------------------------------------------------------------*}
BEGIN
nhap;
xuli;
inkq;
END.
4. Bài tập 4: Tiểu thuyết trinh thám
Ivan Đneprôp viết truyện tiểu thuyết trinh thám. Truyện của anh ta không có gì đặc
sắc: không có các tình huống ly kỳ đặc biệt, cũng không có các chi tiết hài hước tế nhị.
Thậm chí một hiệu sách đã bán các sáng tác của Ivan theo cân! Nhưng độc giả lại thích
truyện của Ivan. Nó dễ hiểu và giúp người ta thư giản sau một ngày lao động mệt
nhọc.

126
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Thực ra, bí mật sự thành công của Ivan là ở chổ không phải chính anh nghĩ ra các
tình huống mà là người em trai Alexei. Ivan có nhiệm vụ viết nó thành các bestsellers.
Dĩ nhiên hai anh em chia nhau hợp lý số tiền kiếm được. Điều đáng tiếc là khả năng
sáng tạo các tình huống ly kỳ của Alexei lại phụ thuộc vào chu kỳ sinh học của anh.
Hai anh em phân tích bảng chu kỳ sinh học của Alexei và thấy rằng trong thời gian tới
Alexei sẽ nghĩ được n chủ đề mới, chủ đề thứ i sẽ được nghĩ ra ở ngày ri. Trong cùng
một ngày có thể Alexei nghĩ ra tới vài câu chuyện.
Với mỗi chủ đề, Ivan thời lượng cần thiết để hoàn thành tác phẩm và tính được
rằng chủ đề thứ i cần có pi ngày để viết. Ivan có trí nhớ cực tốt, vì vậy anh có thể tạm
bỏ dở một truyện, chuyển sang viết truyện khác sau đó quay lại hoàn thành nốt các
truyện dở dang.
Dĩ nhiên, hai anh em muốn sách được viết càng nhanh càng tốt, tức là phải cực tiểu
hóa thời gian trung bình từ thời điểm hiện tại tới thời điểm lúc tiểu thuyết hoàn thành.
Vì số sách là cố định, nên điều này tương đương với việc cực tiểu hóa tổng thời gian
viết tất cả các cuốn sách. Điều này có nghĩa là nếu cuốn sách thứ i được hoàn thành
vào ngày ci thì c i phải được cực tiểu hóa. Ví dụ, ở ngày thứ nhất Alexei nghĩ ra một

cốt chuyện mà Ivan cần viết trong 5 ngày, Ivan bắt tay viết ngay. Ngày thứ 2 Alexei
nghĩ thêm một cót chuyện mới cần viết trong một ngày. Ivan chuyển sang viết chuyện
mới, ngày thứ 3: chuện thứ hai hoàn thành và Ivan quay lại viết tiếp chuyện thứ nhất,
mất thêm 4 ngày nữa, đến ngày thứ 7 chuyện thứ nhất hoàn thành. Tổng các thời điểm
hoàn thành là 3+7 = 10.
Yêucầu: Cho n, ri và pi. Hãy xác định tổng thời gian ngắn nhất hoàn thành tất cả các
truyện.
INPUT:PULP.INP:
- Dòng 1: Chứa số nguyên n (1 ≤ n ≤ 100.000)
- Mỗi dòng trong n dòng sau chứa 2 số nguyên ri và pi.
OUTPUT: PULP.OUT
- Một số nguyên – tổng thời gian ngắn nhất tìm được.
Ví dụ:
PULP.INP PULP.OUT
2 10

127
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

15
21
Thuật toán:
1. Sort tăng theo R[i]
2. Ví dụ với các thời điểm , dễ dàng ta thấy
3. Với mỗi khoảng thời gian t = R[i] – R[i-1], ta ưu tiên giải quyết công việc cần ít
thời gian nhất, giả sử là công việc P (Thấy ngay là sử dụng Heap để lấy công
việc có thời gian nhỏ nhất)
a. Nếu thời gian thực hiện công việc P < t thì thực hiện hết công việc P,
thời gian còn lại thực hiện tiếp các công việc có thời gian nhỏ tiếp theo.
b. Nếu thời gian thực hiện công việc P > t thì chúng ta thực hiện công việc
P trong khoảng thời gian t, thời gian còn lại là t[P] – t ta lưu lại trong
Heap để tính tiếp.
4. Sau khi xét đến thời điểm N, lúc này ta sẽ phải làm tất cả các công việc còn lại
tuần tự từ nhỏ đến lớn, hay là lấy tất cả các phần tử trong Heap ra là xong.

CONST
tfi = 'pulp.inp';
tfo = 'pulp.out';
max = 100000;
TYPE
Arr1 = array[1..max] of longint;
VAR
fi,fo : text;
n,nh,sum,res : longint;
r,p,h : Arr1;
{*------------------------------------------------------------*}
procedurenhap;
var
i : longint;
begin
assign(fi,tfi);reset(fi);
128
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

read(fi,n);
For i := 1 to n do
read(fi,r[i],p[i]);
close(fi);
end;
{*------------------------------------------------------------*}
proceduredoicho(varx,y : longint);
var
tg : longint;
begin
tg := x;
x := y;
y := tg;
end;
{*------------------------------------------------------------*}
procedureupheap(i : longint);
begin
if (i = 1) or (h[i] > h[i div 2]) then exit;
if h[i div 2] > h[i] then
begin
doicho(h[i div 2],h[i]);
upheap(i div 2);
end;
end;
{*------------------------------------------------------------*}
proceduredownheap(i : longint);
var
j : longint;
begin
j := 2 * i;
if j >nh then exit;
if (j <nh) and (h[j] > h[j+1]) then inc(j);
129
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

if h[i] > h[j] then


begin
doicho(h[i],h[j]);
downheap(j);
end;
end;
{*------------------------------------------------------------*}
procedure push(x : longint);
begin
inc(nh);
h[nh] := x;
upheap(nh);
end;
{*------------------------------------------------------------*}
function pop : longint;
begin
pop := h[1];
h[1] := h[nh];
dec(nh);
downheap(1);
end;
{*------------------------------------------------------------*}
procedurexuli;
var
i,t,x : longint;
begin
nh := 0;
push(p[1]);
sum := r[1];
res := 0;
For i := 2 to n do
begin
130
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

t := r[i] - r[i-1];
while (nh> 0) and (t > 0) do
begin
x := pop;
if (x <= t) then
begin
t := t - x;
sum := sum + x;
res := res + sum;
end
else
begin
push(x-t);
t := 0;
end;
end;
push(p[i]);sum := r[i];
end;
whilenh> 0 do
begin
x := pop;
sum := sum + x;
res := res + sum;

end;
end;
{*------------------------------------------------------------*}
procedure sort(x,y:longint);
var
i,j,key1,key2 : longint;
begin
i := x;
131
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

j := y;
key1 := r[x+random(y-x+1)];
key2 := p[x+random(y-x+1)];
repeat
while (r[i] < key1) or ((r[i] = key1) and (p[i] < key2)) do inc(i);
while (r[j] > key1) or ((r[j] = key1) and (p[j] > key2)) dodec(j);
if i <= j then
begin
doicho(r[i],r[j]);
doicho(p[i],p[j]);
inc(i);
dec(j);
end;
until i > j ;
if x < j then sort(x,j);
if y > i then sort(i,y);
end;

{*------------------------------------------------------------*}
{*------------------------------------------------------------*}
procedureinkq;
begin
assign(fo,tfo);rewrite(fo);
write(fo,res);
close(fo);
end;

{*------------------------------------------------------------*}
{*------------------------------------------------------------*}
{*------------------------------------------------------------*}
{*------------------------------------------------------------*}
BEGIN
132
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

randomize;
nhap;
sort(1,n);
xuli;
inkq;
END.

5. Bài tập 5: Cezar


Tại HT Land, có tất cả n thượng nghị sĩ ở trong n ngôi biệt thự (đánh số từ 1 đến
n), giữa 2 ngôi nhà có một đường đi duy nhất: đường đi trực tiếp hoặc không trực tiếp
(đi qua các con đường khác). Tất cả các ngôi nhà đều đi được đến nhau.
Mỗi thượng nghị sĩ khi đi từ nhà mình đến thượng viện, phải trả 1 USD khi đi
qua một con phố (phố = đường nối trực tiếp 2 nhà bất kỳ). HT – người đứng đầu
thượng viện - đã nghĩ cách làm sao cho số tiền mà các thượng nghĩ sĩ phải trả là tối
thiểu. Vì vậy, HT quyết định
 Có k con phố miễn phí (thượng nghị sĩ sẽ không phải trả tiền khi đi trên
con phố này)
 Đặt tòa nhà thượng viện ở một trong n ngôi nhà
Bạn hãy viết chương trình tính xem chi phí tối thiểu là bao nhiêu?
INPUT: CEZAR.INP
- Dòng 1: Số nguyên n và k tương ứng là số ngôi nhà ở HT land và số đường phố
miễn phí
- N – 1 dòng tiếp theo, mỗi dòng chứa 2 số i j có nghĩa là có con phố nối ngôi
nhà i và ngôi nhà j
OUTPUT: CEZAR.OUT
Chi phí tối thiểu phải trả

133
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Giới hạn:

6
1 3
Ví dụ
4 5
CEZAR.INP CEZAR.OUT Giải thích
2 8 7
13 3 11
12 Có nhiều phương án 10 13
9
23 giải quyết: Đây là 1
11
28 phương án: 12

78 – 5-7, 7-8, 8-10


75 là những
54 đường miễn
56 phí
89 – 8 là thượng
8 10 viện
10 11 Chi phí tối thiểu là:
10 12 11.
10 13
Time limit:0.5 s/test
Thuật toán:
1. Chúng ta sẽ tìm con đường phải trả phí đi lại.
2. Khởi tạo D[i] = trọng số của đỉnh i với ý nghĩa = số lượng người đi đến thượng
viện phải đi qua con đường này. Ban đầu
3. Tại mỗi bước chúng ta sẽ cho các nút lá vào Heap, mỗi lần lấy 1 phần tử trong
heap chúng ta sẽ update lại trọng số của đỉnh kề với đỉnh vừa lấy ra và nếu đỉnh
vừa update thành nút lá ta lại cho vào trong heap. Sau khi lấy xong đỉnh
thì cập nhật đáp án.
Const
tfi='cezar.inp';
tfo='cezar.out';
oo = 1000000000;

134
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Type
arr1=array[0..100000] of longint;
arr2=array[0..100000] of boolean;
arr3=array[0..15000] of longint;
arr4=array[0..15000] of arr3;
Var
fi,fo : text;
heap,head,ke,d,c,pos,cost,trace,deg : arr1;
free : arr2;
k,n,nheap,f,r,sum,res : longint;
count : arr4;

{===============================}
Procedure nhap;
Var
i,u,v : longint;
Begin
Assign(fi,tfi);Reset(fi);
read(fi,n,k);
For i := 1 to n-1 do
Begin
read(fi,u,v);
d[i] := u;
c[i] := v;
inc(deg[u]);
inc(deg[v]);
ENd;
head[1] := deg[1];
For i := 2 to n+1 do
Begin
head[i] := head[i-1] + deg[i];
End;
135
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

For i := 1 to n-1 do
Begin
u := d[i];
v := c[i];
ke[head[u]] := v;
ke[head[v]] := u;
cost[head[u]] := 1;
cost[head[v]] := 1;
dec(head[u]);
dec(head[v]);
End;
Close(fi);
End;
{===============================}
Procedure khoitao;
Var
i :longint;
Begin
Fillchar(free,sizeof(free),true);
For i := 1 to n do
BEgin
pos[i] := 0;
cost[i] := 1;
End;
nheap := 0;
End;
{===============================}
Procedure downheap(root : longint);
Var
child,u : longint;
BEgin
u := heap[root];
136
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

repeat
child := root*2;
If (child <nheap) and (cost[heap[child]] > cost[heap[child+1]]) then inc(child);
If (child >nheap) or (cost[heap[child]]>= cost[u]) then break;
heap[root] := heap[child];
pos[heap[root]] := root;
root := child;
Until false;
heap[root] := u;
pos[u] := root;
End;
{===============================}
Procedure upheap(child : longint);
Var
root,u : longint;
Begin
u := heap[child];
Repeat
root := child div 2;
If (root=0) or (cost[heap[root]] <= cost[u]) then break;
heap[child] := heap[root];
pos[heap[child]] := child;
child := root;
Until false;
heap[child] := u;
pos[u] := child;
End;
{===============================}
FUnctionpop :longint;
Begin
pop := heap[1];
heap[1] := heap[nheap];
137
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

pos[heap[1]] := 1;
dec(nheap);
downheap(1);
End;
{===============================}
Procedure push(x :longint);
Begin
inc(nheap);
heap[nheap] := x;
pos[heap[nheap]] := nheap;
upheap(nheap);
End;
{===============================}
Procedure update(v : longint);
Var
i :longint;
Begin
For i := head[v] + 1 to head[v+1] do
Begin
dec(deg[ke[i]]);
cost[ke[i]] := cost[ke[i]] + cost[v];
If deg[ke[i]] = 1 then push(ke[i]);
End;
End;
{===============================}
Function dijkstra(x :longint) : longint ;
Var
i,u : longint;
Begin
push(x);
repeat
u := pop;
138
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

free[u] := false;
update(u);
untilnheap = 0;
End;
{===============================}
Procedure xuly;
Var
i,u : longint;
Begin
khoitao;
res := 0;
For i := 1 to n do
Begin
If deg[i]=1 then push(i);
End;
repeat
u := pop;
sum := sum + cost[u];
inc(res);
If res=n-1-k then break;
free[u] := false;
update(u);
until false;

End;
{===============================}
Procedure inkq;
Begin
Assign(fo,tfo);REwrite(fo);
write(fo,sum);
Close(fo);
End;
139
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

{===============================}
BEGIN
nhap;
xuly;
inkq;
END.
6. Bài tập 6: Toy Cars
Bé Tom là một đứa trẻ ba tuổi và rất thích chơi đồ chơi oto. Bé Tom có n chiếc
oto khác nhau, chúng được đặt trên một chiếu giá cao mà Tom không thể tự mình lấy
được. Phòng của Tom cũng rất nhỏ, tại một thời điểm, không thể có nhiều hơn k chiếc
oto đồ chơi ở trên sàn nhà.
Tom chơi với một trong nhưng chiếc oto trên sàn nhà, Mẹ của Ton luôn ở trong
phòng với Tom trong cả thời gian chơi của Tom. Khi Bé Tom muốn chơi với một
chiếc oto khác, nếu chiếc này ở trên sàn nhà, Tom sẽ tự lấy để chơi, còn nếu chiếc oto
này ở trên giá, Mẹ của Tom sẽ lấy xuống cho Tom. (Khi Mẹ của Tom lấy 1 chiếc oto
cho Tom,cùng lúc cô ấy có thể lấy một chiếc oto bất kỳ khác ở sàn nhà để đặt lên giá –
để có đủ khoảng không gian cho k chiếc oto)
Mẹ của Tom là người mẹ rất hiểu ý thích của con mình, cô ta có thể biết được
những chiếc oto nào mà con trai mình muốn chơi. Cô ta muốn biết số lần ít nhất mà cô
ta giúp Tom lấy xe oto từ trên giá.
INPUT: TOYCARS.INP
- Dòng 1: 3 số nguyên dương N, K, P lần
lượt là số lượng oto mà Tom có, số lượng oto có thể đặt trên sàn tại cùng một
thời điểm và độ dài dãy các oto mà Tom muốn chơi. Các oto được đánh số từ 1
đến N.
- P dòng tiếp theo, mỗi dòng 1 số nguyên dương là chiếc oto mà Tom muốn chơi
(theo thứ tự thời gian)
OUTPUT: TOYCARS.OUT
- Một số nguyên duy nhất là số lần ít nhất Mẹ Tom lấy oto từ trên giá xuống.

140
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Ví dụ:
TOYCARS.INP TOYCARS.OUT
327 4
1
2
3
1
3
1
2

Thuật toán :
1. Gọi Next[i] = thời điểm gần nhất sau thời điểm i mà bé Tom muốn chơi món đồ
chơi P[i]. Next[i] = nếu i là thời điểm cuối cùng bé Tom muốn chơi món
P[i]. Như vậy có thể coi Next[i] = độ tồi của việc giữ lại món đồ chơi P[i] ở trên
sàn (Next[i] càng lớn tức là việc giữ lại P[i] càng vô ích).
2. Xét lần lượt các thời điểm i :
a. Nếu món P[i] đang nằm trên sàn : bỏ qua và xét thời điểm tiếp theo.
b. Nếu mon P[i] đang nằm trên giá :
i. Nếu sàn chưa đầy : Ta lấy món P[i] để lên sàn.
ii. Nếu sàn đầy : Ta chọn từ sàn một món đồ chơi có độ tồi lớn nhất
và đặt lên giá, sau đó lấy món P[i] để lên sàn.
3. Dễ dàng nhận thấy có thể sử dụng cấu trúc Heap để thực hiện thuật giải trong
thời gian N.logK.
const
tfi = 'toycars.inp';
tfo = 'toycars.out';
type
arr1 = array[0..1000000] of longint;
arr2 = array[0..100000] of boolean;
var
fi,fo:text;
141
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

n,k,p,res,nheap,x,dem,vc:longint;
c,deg,d,a,pos,b,vt,next:arr1;
free:arr2;
{------------------------------------------------------------------------}
procedurenhap;
vari,j:longint;
begin
assign(fi,tfi);reset(fi);
read(fi,n,k,p);
for i:=1 to p do
read(fi,c[i]);
close(fi);
end;
{------------------------------------------------------------------------}
procedurekhoitao;
vari,j:longint;
begin
res:=0;
for i:=1 to n do
begin
free[i]:=true;
vt[i]:=p+1;
end;
for i:=p downto 1 do
begin
b[i]:=vt[c[i]];
vt[c[i]]:=i;
end;
end;
{------------------------------------------------------------------------}
proceduredoicho(vari,j:longint);
vartg:longint;
142
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

begin
tg:=i;
i:=j;
j:=tg;
end;
{-----------------------------------------------------------------------}
procedureupheap(i:longint);
varj,k:longint;
begin
if (i=1)or(d[a[i]]<=d[a[i div 2]]) then exit;
j:=i div 2;
doicho(a[i],a[j]);
doicho(pos[a[i]],pos[a[j]]);
upheap(j);
end;
{-----------------------------------------------------------------------}
proceduredownheap(i:longint);
var j:longint;
begin
j:=i*2;
if j>nheap then exit;
if (d[a[j]]<d[a[j+1]])and(j<nheap) then inc(j);
if d[a[j]]>d[a[i]] then
begin
doicho(a[i],a[j]);
doicho(pos[a[i]],pos[a[j]]);
downheap(j);
end;
end;
{-----------------------------------------------------------------------}
procedure push(i,j:longint);
begin
143
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

inc(nheap);
a[nheap]:=i;
pos[i]:=nheap;
d[i]:=b[j];
upheap(nheap);
end;
{-----------------------------------------------------------------------}
procedure pop;
vari,j:longint;
begin
x:=a[1];
a[1]:=a[nheap];
pos[a[nheap]]:=1;
dec(nheap);
downheap(1);
end;
{-----------------------------------------------------------------------}
procedurexuly;
vari,j:longint;
begin
for i:=1 to p do
if free[c[i]] then
begin
ifnheap=k then
begin
pop;
free[x]:=true;
end;
push(c[i],i);
free[c[i]]:=false;
inc(res);
end
144
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

else
begin
d[c[i]]:=b[i];
upheap(pos[c[i]]);
end;
end;
{------------------------------------------------------------------------}
procedureinkq;
vari,j:longint;
begin
assign(fo,tfo);rewrite(fo);
write(fo,res);
close(fo);
end;
{------------------------------------------------------------------------}
BEGIN
nhap;
khoitao;
xuly;
inkq;
END.
7. Bài tập 7: BASE3
Cho 3 số ở hệ cơ số 3 (viết bởi 3 số 0, 1, 2). Hãy tìm một chữ số N ở hệ cơ số 3 sao
cho N có một số lẻ chữ số và số 1 nằm ở chính giữa số N. Số N phải thu được từ việc
liên kết 3 số cho trước, mỗi số trong 3 số có thể được sử dụng 0 hoặc nhiều lần:

Yêu cầu:
Xác định số lượng nhỏ nhất các chữ số của N thỏa mãn tính chất đầu bài

INPUT: BASE3.INP
- Gồm 3 dòng, mỗi dòng là một số ở hệ cơ số 3.

145
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

OUTPUT: BASE3.OUT
- Số lượng chữ số nhỏ nhất của N. Nếu không tồn tại N thì ghi số 0.

Giới hạn
- Số chữ số của một số trong 3 số đầu bài cho trong khoảng từ 1 đến 16.000.

Ví dụ:
BASE3.IN BASE3.OUT
001 13
020
2020
Giải thích
N = 2020001001001 .
Time limit/test: 0.4
L 1 R

Ký tự 1 ở giữa của xâu kết quả phải thuộc 1 trong 3 xâu:


Giả sử ký tự 1 của kết quả nằm ở vị trí I của xâu , ta sẽ xây dựng xâu kết
quả bằng cách thêm từng xâu vào trái hoặc phải (phần L là trái, ký tự 1 ở giữa, phần R
là phải)
Ta sẽ xây dựng xâu đến khi thì dừng lại (yêu cầu đề bài)
Dễ thấy, ta chỉ quan tâm tới ghi nhận kết quả. Tức là
nếu tồn tại cách xây dựng L’, R’ mà

thỏa mãn cũng sử dụng cách xây dựng đó được với các cặp
với , chỉ cần lưu 1 cặp có
Quy hoạch động = cách xếp có với
Nếu mỗi lần ta luôn nắp xâu vào nửa ngắn hơn thì luôn đảm bảo

(do ). Với trạng thái , ta cập nhật cho trạng


thái + độ dài xâu nắp thêm vào. Bài toán trở thành Dijkstra
Heap về trạng thái . Các trạng thái cơ sở có thể là các ký tự 1 của 1 trong 3 xâu
ban đầu. Đáp án bài toán là
146
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

{$H+}
uses math ;
const
tfi = 'Base3.inp';
tfo = 'Base3.out';
Nmax = 16001;
vc = 1000000000000000000 ;
type
arr1 = array[0..Nmax] of longint ;
var
fi,fo : text;
n,m,nh,ss,xi,yi : longint;
s : array[1..3] of string ;
len : array[1..3] of longint ;
d : array[-Nmax..Nmax,0..1] of int64 ;
pos : array[-Nmax..Nmax,0..1] of longint;
h,h2 : array[0..4*Nmax] of longint;
procedureNhap;
var
i,j :longint;
begin
ss := 0;
for i := 1 to 3 do
begin
readln(fi,s[i]);
len[i] := length(s[i]);
ss := max(ss,len[i]) ;
end;
end;
procedureUpheap(i:longint) ;
var
x,y:longint ;
147
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

begin
x := h[i] ;
y := h2[i];
while (i > 1) and (d[x,y] < d[h[i div 2],h2[i div 2]]) do
begin
h[i] := h[i div 2] ;
h2[i] := h2[i div 2];
pos[h[i],h2[i]] := i ;
i := i div 2;
end ;
h[i] := x;
h2[i] := y;
pos[x,y] := i;
end;
proceduredownheap(i:longint);
var
j,x,y:longint;
begin
x := h[i];
y := h2[i];
while i * 2 <= nh do
begin
j := i * 2 ;
if (j <nh) and (d[h[j],h2[j]] >d[h[j+1],h2[j+1]]) then inc(j);
if d[x,y] <= d[h[j],h2[j]] then break ;
h[i] := h[j];
h2[i] := h2[j];
pos[h[i],h2[i]] := i;
i := j;
end ;
h[i] := x;
h2[i] := y;
148
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

pos[x,y] := i ;
end;
procedure Push(i,j:longint);
begin
ifpos[i,j] = 0 then
begin
inc(nh) ;
h[nh] := i;
h2[nh] := j;
pos[i,j] := nh;
Upheap(nh) ;
end else Upheap(pos[i,j]) ;
end ;
procedure Pop;
begin
xi := h[1];
yi := h2[1];
h[1] := h[nh];
h2[1] := h2[nh];
pos[h[1],h2[1]] := 1;
dec(nh);
downheap(1) ;
end;
procedure Update ;
var
i,j,k:longint;
begin
for i := 1 to 3 do
begin
if s[i][1] = '0' then k := 0 else k := 1;
j := xi + len[i];
if (j <= ss) and (d[j,k] > d[xi,yi] + len[i]) then
149
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

begin
d[j,k] := d[xi,yi] + len[i] ;
push(j,k) ;
end;
j := xi - len[i] ;
if (j >= -ss) and (d[j,yi] > d[xi,yi] + len[i]) then
begin
d[j,yi] := d[xi,yi] + len[i];
push(j,yi) ;
end ;
end;
end ;
procedureinit;
var
i,j,k,u: longint;
begin
for i := -ss to ss do
for j := 0 to 1 do
d[i,j] := vc ;
for i := 1 to 3 do
for j := 1 to len[i] do if s[i][j] = '1' then
begin
if s[i][1] = '0' then k := 0 else k := 1;
u := (j - 1) - (len[i] - j) ;
d[u,k] := len[i] ;
push(u,k) ;
end;
end;
procedurexuly;
var
i,j,k: longint;
begin
150
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

whilenh> 0 do
begin
pop;
if (xi = 0) then
begin
write(fo,d[xi,yi]);
exit;
end;
update;
end;
write(fo,0) ;
end ;
begin
assign(fi,tfi);reset(fi);
assign(fo,tfo);rewrite(fo);
Nhap;
init;
xuly;
close(fi);
close(fo) ;
end.
IV. Một số bài tập vận dụng
8. Bài tập 1: BOCSOI13
Bước vào tiểu học, Bé Bi được cô giáo chủ nhiệm cho làm lớp trưởng. Nhân dịp kỷ
niệm ngày thành lập trường, Bé Bi tổ chức cho cả lớp chơi 1 trò chơi sau:
Có N đống sỏi xếp thành một hàng, đống thứ i có Ai viên sỏi. Ta có thể ghép hai
đống sỏi bất kỳ thành một đống và mất một chi phí bằng 5% tổng hai đống sỏi đó. Hãy
tìm cách ghép N đống sỏi này thành một đống với chi phí là nhỏ nhất.
Ví dụ: Nếu chúng ta có 4 đống sỏi với số lượng sỏi là 10, 11, 12 và 13.
- Bước 1: Ghép 2 đống 10 và 11 thành 1 đống có số lượng 21 (chi phí là 1.05)

151
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

- Bước 2: Ghép đống 21 vừa thu được với đống 12 thành đống có số lượng 33
(chi phí 1.65)
- Bước 3: Ghép đống 33 vừa thu được với đống 13 thành 1 đống cuối cùng có số
lượng sỏi là 46 (chi phí 2.3)
- Vậy tổng chi phí là 5.00. Tuy nhiên đây không phải là phương án ghép đống tối
ưu, chúng ta có phương án ghép 4 đống này thành 1 đống với chi phí nhỏ nhất
là 4.60.
Các bạn hãy tìm giúp Bé Bi phương án chơi tối ưu nhé!
INPUT:BOCSOI13.INP:
- Dòng 1: Số nguyên dương N là số đống sỏi.
- Dòng tiếp theo, ghi N số nguyên dương, tương ứng là số lượng sỏi trong từng
đống. Số lượng sỏi không vượt quá 10.000.
OUTPUT:BOCSOI13.OUT:
- Ghi 1 số thực duy nhất là chi phí nhỏ nhất phải trả để ghép N đống sỏi thành 1
đống. Kết quả ghi dưới dạng 2 chữ số sau dấu thập phân.
Ví dụ:
BOCSOI13.INP BOCSOI13.OUT
4 4.60
10 11 12 13
2 0.10
11

9. Bài tập 2: Kế hoạch làm bài


Nobita được giao n bài tập về nhà đánh số từ 1 tới n. Mỗi bài tập cần đúng 1 đơn vị
thời gian để làm và tại mỗi thời điểm, Nobita chỉ có thể làm được 1 bài tập. Bài tập thứ
I cần hoàn thành không muộn hơn thời điểm ti và nếu bài thứ I bị nộp muộn thì Nobita
sẽ bị thày giáo cho pi điểm 0.
Giả sử Nobita định làm bài tập từ thời điểm a đến hết thời điểm b. Hãy giúp Nobita
lên kế hoạch làm bài tập để số điểm 0 phải nhận là ít nhất.
INPUT: PENALTY.INP
- Dòng 1: Chứa 3 số nguyên dương n ;
- N dòng tiếp theo, dòng thứ I chứa hai số nguyên dương ti, pi

152
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

OUTPUT: PENALTY.OUT
Một số duy nhất là số điểm 0 tối thiểu phải nhận
Ví dụ:
PENALTY.INP PENALTY.OUT Giải thích
5 1 4 25 Làm bài 1 từ thời điểm 1 đến thời điểm 2.
2 100 Làm bài 4 từ thời điểm 2 đến thời điểm 3.
2 20 Làm bài 5 từ thời điểm 3 đến thời điểm 4.
4 5 Bài 2 và bài 3 bị nộp muộn
4 10
4 6
10. Bài tập 3: CATUN
Tại vương quốc HT có N thị trấn đánh số từ 1 đến N và M con đường nối 2 thị trấn
bất kỳ. Trong số N thị trấn thì có K thị trấn là pháo đài chống giặc ngoại xâm. Khi có
giặc ngoại xâm, 1 pháo đài bất kỳ sẽ bảo vệ những thị trấn gần nó nhất. Nếu co 1 thị
trấn nào đó có khoảng cách tới 2 pháo đài bằng nhau thì thị trấn này được bảo vệ bởi
pháo đài có chỉ số nhỏ hơn?
INPUT: CATUN.IN
- Dòng 1: N, M và K (
- Dòng 2: K số nguyên dương là chỉ số của K pháo đài
- M dòng tiếp theo, mỗi dòng ghi 3 số x, y, z có nghĩa là có con đường 2 chiều
nối thị trấn x với thị trấn y có chiều dài là z.
OUTPUT: CATUN.OUT
- Ghi N số nguyên dương, số thí I là chỉ số của pháo đài bảo vệ thị trấn i. Nếu thị
trấn I không được bảo vệ bởi pháo đài nào thì ghi số 0
Ví dụ:
CATUN.IN CATUN.OUT
8 9 2 5 0 5 0 0 5 0 2
2 5
1 3 6
1 5 3
1 6 1

153
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

2 3 9
5 6 5
6 8 7
3 6 2
4 7 1000
2 8 5
Time limit: 0.2s
11. Bài tập 3: Barbar
Sau cuộc chiến tranh thần thánh, HT bị bắt giam vào ngục tối là một bảng cỡ R*C (R
dòng và C cột). Tại một ô có thể di chuyển sang 4 ô kề cạnh. Trong ngục có 1 số ô là
có thể di chuyển tự do, một số ô là tường không thể di chuyển qua và đặc biệt một số ô
bị chiếm giữ bởi các con rồng lửa (không thể đi lại gần nếu không muốn bị cháy thành
than). Bạn hãy giúp HT di chuyển qua ngục tối sao cho khoảng cách ngắn nhất đến các
con rồng lửa là lớn nhất?
INPUT: BARBAR.IN
- Dòng 1: R và C (
- R dòng tiếp theo, mỗi dòng ghi C ký tự biểu diễn ngục tối.
“.” Ô di chuyển tự do
“*” Tường
“D” Rồng lửa
“I”: Vị trí xuất phát của HT
“O”: Vị trí thoát khỏi ngục tối
OUTPUT: BARBAR.OUT
- Một số duy nhất là giá trị lớn nhất của khoảng cách ngắn nhất đến các con rồng
lửa khi HT di chuyển ra khỏi ngục tối. Nếu không có cách di chuyển ghi -1
Ví dụ:
BARBAR.IN BARBAR.OUT Giải thích
10 10 2 ..........
.......... .Iooo.D...
.I....D... ....o.....
.......... ..D.o.D...

154
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

..D...D... .*..oo....
.*........ D*...ooooo
D*........ *...D....o
*...D..... ..****...o
..****.... ...Ooooooo
...O...... ..........
..........
Time limit: 0.4s
12. Bài tập 4:Cầu cảng
Một cầu cảng có m cầu cảng để tiếp nhận các tàu cập bến. Tại một thời điểm, mỗi cầu
cảng chỉ có thể tiếp nhận không quá 1 tàu. Ban đầu các cầu cảng đều trống và có n tàu
xin đăng ký cập bến, tàu thứ i muốn đậu ở cảng ngày sau thời điểm tới hết thời
điểm . Có thể coi thời gian tàu thứ i muốn đậu ở cảng là một khoảng trên trục
thời gian. Tàu đã vào cầu cảng nào thì sẽ đậu ở đó trong suốt thời gian nằm cảng.
Yêu cầu : Hãy cho biết với m cầu cảng đã cho, có thể tiếp nhận được tối đa bao nhiều
tàu và chỉ ra lịch trình tiếp nhận tại mỗi cầu cảng ?
INPUT:SEAPORTS.INP
- Dòng 1: Chứa 2 số nguyên dương
- N dòng tiếp theo, dòng thứ i chứ 2 số nguyên
OUTPUT:SEAPORTS.OUT
- Dòng 1 : Ghi số lượng tàu được tiếp nhận phục vụ
- Dòng 2 : Ghi n số nguyên, số thứ i là số hiệu cầu cảng sẽ tiếp nhận tàu thứ i
trong trường hợp tàu thứ i được tiếp nhận, còn nếu tàu thứ i không được tiếp
nhận thì số thứ i là 0.
Ví dụ :
SEAPORTS.INP SEAPORTS.OUT
25 4
03 11220
35
02
25

155
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

14

13. Bài tập 5:K TỔNG BÉ NHẤT


Cho 2 dãy số nguyên A và B. Với mọi số A[i] thuộc A và B[j] thuộc B (1 ≤ Ai, Bi ≤
109 ) người ta tính tổng nó. Tất cả các tổng này sau khi được sắp xếp không giảm sẽ
tạo thành dãy C. Nhiệm vụ của bạn là: Cho 2 dãy A, B. Tìm K số đầu tiên trong dãy C
INPUT: KMIN.INP
- Dòng đầu tiên gồm 3 số: M, N, K (1≤M, N, K ≤ 50.000)
- M dòng tiếp theo gồm M số mô tả dãy A
- N dòng tiếp theo gồm N số mô tả dãy B
OUTPUT:KMIN.OUT
- Gồm K dòng tương ứng là K phần tử đầu tiên trong dãy C.
Ví dụ:
KMIN.INP KMIN.OUT
446 3
1 4
2 4
3 5
4 5
2 5
3
4
5
Time limit: 1s
14. Bài tập 6: BINLADEN
Trùm khủng bố Bin Laden trốn trong 1 căn hầm được đào sâu xuống mặt đất M
tầng, mỗi tầng có N phòng. Các phòng được ngăn cách bằng các cửa rất khó phá. Các
phòng có cửa xuống phòng ngay phía dưới và 2 phòng ở 2 bên. Từ trên mặt đất có N
cửa xuống N phòng tầng -1. Bin Laden ở tầng dưới cùng (tầng -M) phòng thứ N
(phòng ở bên phải nhất). Mỗi cửa được làm bằng một kim loại khác nhau với độ dày
khác nhau nên việc phá cửa cần thời gian khác nhau.

156
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Bạn hãy tìm cách đi từ mặt đất xuống phòng của Bin Laden nhanh nhất không hắn
thoát mất.
INPUT: BINLADEN.INP
- Dòng 1 ghi M và N
- Dòng 2 đến 2M + 1, dòng chẵn ghi N số, dòng lẻ ghi N - 1 số là chi phí để phá
cửa. Chi phí của các cánh cửa thuộc [0, 1000].
OUTPUT: BINLADEN.OUT
- Ghi ra 1 số là thời gian nhỏ nhất để đến được phòng của Bin Laden
Ví dụ:
BINLADEN.INP BINLADEN.OUT
42 44 +--99--+--10--+
99 10 | | |
1 | 1 |
10 99 | | |
1 +--10--+--99--+
99 10 | | |
1 | 1 |
10 99 | | |
1 +--99--+--10--+
| | |
| 1 |
| | |
+--10--+--99--+
| | |
| 1 |
| | |
+------+------+
Đi theo đường zigzac

157
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

V. Tài liệu tham khảo


1. Nguồn bài tập: Thầy Nguyễn Thanh Tùng, Thầy Lê Minh Hoàng, Rumania OI,
Poland OI, Russian OI, vn.spoj.pl.
2. Giải thuật và lập trình – T.S Lê Minh Hoàng – ĐHSP Hà Nội
3. Website: www.infoarena.ro
4. Test và solution các Thầy cô có thể liên hệ với tác giả để download.

158
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

CẤU TRÚC DỮ LIỆU TẬP RỜI RẠC

Nguyễn Hoàng Phú


THPT chuyên Lý Tự Trọng – Cần Thơ
Email: nghoangphu@cantho.edu.vn
SĐT: 0917.699.599
1. Tóm tắt

Cấu trúc dữ liệu tập rời rạc (Union-Find Disjoint Sets) là một cấu trúc dữ liệu mô tả
một tập hợp của các tập con rời rạc sao cho trên cấu trúc này ta có thể cài đặt hai phép
toán sau với độ phức tạp xấp xỉ O(1):

- Xác định xem một phần tử thuộc tập nào.


- Hợp hai tập thành một tập.

2. Cài đặt

Ý tưởng chính cho cấu trúc dữ liệu này là sử dụng một rừng cây để biểu diễn các tập
rời rạc, trong đó mỗi cây đại diện cho một tập. Và ứng với mỗi tập, ta sẽ biểu diễn cây
tương ứng bằng cách chọn một phần tử làm đại diện cho tập đó (nút gốc của cây).

Để biểu diễn các nút trong một cây thì thông tin của mỗi nút trong cây sẽ được giữ
trong một vector pSet (thông tin về nút cha của nó trong cây), trong đó pSet[i] = j cho
biết nút thứ thuộc cây chứa nút thứ j (một các hiểu khác là phần tử thứ i sẽ thuộc tập
chứa phần tử thứ j).

vector<int> pSet(1000);

Thủ tục khởi tạo tập rời rạc sẽ tạo một tập hợp gồm n tập rời rạc mà mỗi tập ban đầu
chỉ chứa đúng một phần tử.

void initSet(int n) {

pSet.resize(n);

for (int i = 0; i < n; ++i) pSet[i] = i;

159
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Để xác định một phần tử thuộc tập nào, ta tiến hành gọi đệ quy nút đại diện cho phần
tử đó trong cây để đi tìm nút gốc của cây, trong quá trình gọi đệ quy ta tiến hành điều
chỉnh luôn thông tin được lưu trữ của nút đó để sao cho sau khi kết thúc quá trình gọi
đệ quy mỗi nút đi qua thông tin được lưu trong pSet tương ứng sẽ là nút gốc của cây,
điều này giúp cho độ phức tạp của việc xác định ở các lần tiếp theo sẽ là O(1).

int findSet(int i) {

return (pSet[i] == i) ? i : (pSet[i] = findSet(pSet[i]));

Để hợp 2 tập, ta chỉ đơn giản thay đổi thông tin nút gốc của một tập (hiện tại có giá trị
bằng chính nó) thành thông tin nút gốc của tập còn lại.

void unionSet(int i, int j) {

pSet[findSet(i)] = findSet(j);

Để xác định xem 2 phần tử có thuộc cùng một tập hay không, đơn giản ta chỉ kiểm tra
xem nút gốc của 2 cây chứa 2 phần tử có trùng nhau hay không?

bool isSameSet(int i, int j) {

return findSet(i) == findSet(j);

3. Cải tiến

Một cách cải tiến để cấu trúc dữ liệu có thể hoạt động hiệu quả hơn: khi lưu trữ thông
tin của một nút trong cây, ngoài việc sử dụng vector pSet ta sử dụng thêm một vector
rSet để lưu độ cao của cây, rSet[i] cho biết độ cao của cây với i là nút gốc. Điều này
hữu ích cho ta khi tiến hành hợp 2 tập, lúc này ta luôn chọn tập thuộc cây có độ cao
nhỏ hơn để gắn vào tập thuộc cây có độ cao lớn hơn.

Đoạn code cải tiến được mô tả như sau:

vector<int> pSet(1000);
160
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

vector<int> rSet(1000);

void initSet(int n) {

pSet.resize(n);

rSet.resize(n);

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

pSet[i] = i;

rSet[i] = 0;

int findSet(int i) {

return (pSet[i] == i) ? i : (pSet[i] = findSet(pSet[i]));

bool isSameSet(int i, int j) {

return findSet(i) == findSet(j);

void unionSet(int i, int j) {

if (!isSameSet(i, j)) {

int x = findSet(i);

161
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

int y = findSet(j);

if (rSet[x] > rSet[y]) pSet[y] = x;

else {

pSet[x] = y;

if (rSet[x] == rSet[y]) rSet[y]++;

4. Một số ứng dụng

Cấu trúc dữ liệu này thường được ứng dụng trong thuật toán Kruskal hoặc các bài toán
liên quan đến phân vùng trong đồ thị mà cần lưu vết lại các thành phần liên thông của
chúng.

a) Thuật toán Kruskal


vector< pair<int, pair<int, int> > > edge, mst;

...

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

initSet(n);

int ans = 0;

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

int w = edge[i].first, u = edge[i].second.first, v = edge[i].second.second;

if (isSameSet(u, v)) continue;

mst.pb(edge[i]);

162
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

ans += w;

unionSet(u, v);

...

b) Bài toán: Mạng máy tính

Bình được giao nhiệm vụ quản trị một mạng máy tính của công ty Gbum. Hiện tại bạn
ấy đang giữ một bản ghi các kết nối hai chiều giữa các máy tính trong mạng. Hai máy
tính liên thông với nhau nếu giữa chúng có một đường truyền trực tiếp hoặc chúng liên
thông với cùng một máy khác. Một hôm, Bình muốn biết một cách nhanh chóng rằng
khi cho hai máy tính bất kỳ trong mạng có liên thông với nhau hay không khi biết
được thông tin về các kết nối của mạng máy tính.

Yêu cầu: Viết chương trình đếm số lượng các câu trả lời “CÓ” và số lượng các câu trả
lời “KHÔNG” cho câu hỏi: Máy tính thứ i có liên thông với máy thứ j hay không?

Dữ liệu vào: Cho trong file văn bản network.inp có cấu trúc như sau:

 Dòng đầu ghi 2 số nguyên dương n, m cho biết số lượng máy tính có trong
mạng và số lượng truy vấn.
 m dòng tiếp theo mỗi dòng là một truy vấn thuộc một trong hai dạng sau:
o Dạng 1: c i j cho biết máy tính i được kết nối với máy tính j qua đường
truyền trực tiếp.
o Dạng 2: q i j cho biết câu hỏi máy tính i có liên thông với máy tính j hay
không?

Mạng máy tính được cập nhật mỗi khi xuất hiện truy vấn dạng 1, câu trả lời cho
truy vấn dạng 2 được áp dụng đối với thông tin mạng máy tính hiện tại.

Kết quả: ghi ra file văn bản network.out chỉ gồm 2 số, số đầu tiên cho biết số lượng
câu trả lời “CÓ” và số thứ hai cho biết số lượng các câu trả lời “KHÔNG”.

Ví dụ:

network.inp network.out

163
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

10 7 12

c15

c27

q71

c39

q96

c25

q75

c) Bài toán: Bạn bè

Một thị trấn nọ có n dân và các cư dân trong thành phố này nổi tiếng là thân thiện nên
nếu A là bạn của B và B là bạn của C thì A sẽ là bạn của C.

Yêu cầu: Cho thông tin một số cặp là bạn của nhau hãy xác định xem nhóm có số
lượng lớn nhất là bạn bè.

Dữ liệu vào: Cho trong file văn bản friend.inp có cấu trúc như sau:

 Dòng đầu tiên ghi 2 số nguyên dương n và m cho biết số lượng cư dân và số cặp
là bạn của nhau.
 m dòng tiếp theo, mỗi dòng ghi 2 số nguyên dương a và b cho biết a và b là bạn
của nhau.

Kết quả: Ghi ra file văn bản friend.out chỉ 1 số duy nhất là số lượng lớn nhất tìm
được.

Ví dụ:

friend.inp friend.out

10 12 6

12

164
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

31

34

54

35

46

52

21

7 10

12

9 10

89

5. Tài liệu tham khảo

[1] Competitive programming 3, Steven Halim & Felix Halim, 2013

[2] https://uva.onlinejudge.org/

[3] http://www.spoj.com/

165
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

CÂY KHUNG NHỎ NHẤT

THPT Chuyên Huỳnh Mẫn Đạt – Kiên Giang

A. Lý thuyết

Bài toán cây khung nhỏ nhất của đồ thị là một trong số những bài toán tối ưu trên
đồ thị tìm được ứng dụng trong nhiều lĩnh vực khác nhau của đời sống. Trong mục này
chúng ta trình bày những thuật toán cơ bản để giải bài toán này. Trước hết chúng ta
phát biểu nội dung bài toán.

1. Phát biểu bài toán:

Cho G =(V,E) là đồ thị vô hướng liên thông với tập đỉnh V ={1, 2, . . . ,n} và tập
cạnh E gồm m cạnh. Mỗi cạnh E của đồ thị G được gán với một số không âm c(e), gọi
là độ dài của nó. Giả sử H=(V,T) là cây khung của đồ thị G. Ta gọi độ dài c(H) của
cây khung H là tổng độ dài các cạnh của nó.

Bài toán đặt ra là trong tất cả cây khung của đồ thị G hãy tìm cây khung với độ
dài nhỏ nhất. Cây khung như vậy như vậy được gọi là cây khung nhỏ nhất của đồ thị
và bài toán đặt ra được gọi là bài toán cây khung nhỏ nhất.

2. Thuật toán Kruskal:

Thuật toán Kruskal dựa trên mô hình xây dựng cây khung bằng thuật toán hợp
nhất (§5), chỉ có điều thuật toán không phải xét các cạnh với thứ tự tuỳ ý mà xét các
cạnh theo thứ tự đã sắp xếp: Với đồ thị vô hướng G = (V, E) có n đỉnh. Khởi tạo cây T
ban đầu không có cạnh nào. Xét tất cả các cạnh của đồ thị từ cạnh có trọng số nhỏ đến
cạnh có trọng số lớn, nếu việc thêm cạnh đó vào T không tạo thành chu trình đơn trong
T thì kết nạp thêm cạnh đó vào T. Cứ làm như vậy cho tới khi:

❖ Hoặc đã kết nạp được n - 1 cạnh vào trong T thì ta được T là cây khung nhỏ nhất

❖ Hoặc chưa kết nạp đủ n - 1 cạnh nhưng hễ cứ kết nạp thêm một cạnh bất kỳ
trong số các cạnh còn lại thì sẽ tạo thành chu trình đơn. Trong trường hợp này
đồ thị G là không liên thông, việc tìm kiếm cây khung thất bại.

166
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

1.1.1.1. Như vậy có hai vấn đề quan trọng khi cài đặt thuật toán Kruskal:

Thứ nhất, làm thế nào để xét được các cạnh từ cạnh có trọng số nhỏ tới cạnh có
trọng số lớn. Ta có thể thực hiện bằng cách sắp xếp danh sách cạnh theo thứ tự không
giảm của trọng số, sau đó duyệt từ đầu tới cuối danh sách cạnh. Trong trường hợp tổng
quát, thuật toán HeapSort là hiệu quả nhất bởi nó cho phép chọn lần lượt các cạnh từ
cạnh trọng nhỏ nhất tới cạnh trọng số lớn nhất ra khỏi Heap và có thể xử lý (bỏ qua
hay thêm vào cây) luôn.

Thứ hai, làm thế nào kiểm tra xem việc thêm một cạnh có tạo thành chu trình đơn
trong T hay không. Để ý rằng các cạnh trong T ở các bước sẽ tạo thành một rừng (đồ
thị không có chu trình đơn). Muốn thêm một cạnh (u, v) vào T mà không tạo thành
chu trình đơn thì (u, v) phải nối hai cây khác nhau của rừng T, bởi nếu u, v thuộc cùng
một cây thì sẽ tạo thành chu trình đơn trong cây đó. Ban đầu, ta khởi tạo rừng T gồm n
cây, mỗi cây chỉ gồm đúng một đỉnh, sau đó, mỗi khi xét đến cạnh nối hai cây khác
nhau của rừng T thì ta kết nạp cạnh đó vào T, đồng thời hợp nhất hai cây đó lại
thành một cây.

Nếu cho mỗi đỉnh v trên cây một nhãn Lab[v] là số hiệu đỉnh cha của đỉnh v
trong cây, trong trường hợp v là gốc của một cây thì Lab[v] được gán một giá trị âm.
Khi đó ta hoàn toàn có thể xác định được gốc của cây chứa đỉnh v bằng hàm GetRoot
dưới đây:

function GetRoot(vÎV):
ÎV; begin

while Lab[v] > 0 do v :=


Lab[v]; GetRoot := v;

end;

Vậy để kiểm tra một cạnh (u, v) có nối hai cây khác nhau của rừng T hay không?
ta có thể kiểm tra GetRoot(u) có khác GetRoot(v) hay không, bởi mỗi cây chỉ có duy
nhất một gốc.

Để hợp nhất cây gốc r1 và cây gốc r2 thành một cây, ta lưu ý rằng mỗi cây ở đây
chỉ dùng để ghi nhận một tập hợp đỉnh thuộc cây đó chứ cấu trúc cạnh trên cây thế nào
167
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

thì không quan trọng. Vậy để hợp nhất cây gốc r1 và cây gốc r2, ta chỉ việc coi r1 là
nút cha của r2 trong cây bằng cách đặt:

Lab[r2] := r1.

Tuy nhiên, để thuật toán làm việc hiệu quả, tránh trường hợp cây tạo thành bị suy
biến khiến cho hàm GetRoot hoạt động chậm, người ta thường đánh giá: Để hợp hai
cây lại thành một, thì gốc cây nào ít nút hơn sẽ bị coi là con của gốc cây kia.

Thuật toán hợp nhất cây gốc r1 và cây gốc r2 có thể viết như sau:

{Count[k]làsốđỉnhcủacâygốck}

procedure Union(r1, r2
Î V); begin

if Count[r1] < Count[r2] then {Hợp nhất thành cây gốc r2}

begin

Count[r2] := Count[r1] +
Count[r2]; Lab[r1] := r2;

end

else{Hợpnhấtthànhcâygốcr1}

begin

Count[r1] := Count[r1] +
Count[r2]; Lab[r2] := r1;

end;

end;

Khi cài đặt, ta có thể tận dụng ngay nhãn Lab[r] để lưu số đỉnh của cây gốc r, bởi
như đã giải thích ở trên, Lab[r] chỉ cần mang một giá trị âm là đủ, vậy ta có thể coi
Lab[r] = -Count[r] với r là gốc của một cây nào đó.

procedureUnion(r1,r2Î V);{Hợpnhấtcâygốcr1 vớicâygốcr2}

168
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

begin

x:=Lab[r1]+Lab[r2];{-xlàtổngsốnútcủacảhaicây}

ifLab[r1]>Lab[r2]then{Câygốcr1ítnúthơncâygốcr2,hợpnhấtthànhcâygốcr2}

begin

Lab[r1]:=r2;{r2 làchacủar1}

Lab[r2]:=x;{r2 làgốccâymới,-Lab[r2]giờđâylàsốnúttrongcâymới}

end

else{Hợpnhấtthànhcâygốcr1}

begin

Lab[r1]:=x;{r1 làgốccâymới,-Lab[r1]giờđâylàsốnúttrongcâymới}

Lab[r2]:=r1;{chacủar2 sẽlàr1}

end;

end;

Mô hình thuật toán Kruskal:

for"kÎVdoLab[k]:=-1;

for("(u,v)Î Etheothứtựtừcạnhtrọngsốnhỏtớicạnhtrọngsốlớn)do begin

r1 := GetRoot(u); r2 := GetRoot(v);

if r1 ¹ r2 then {(u, v) nối hai cây khác nhau}

begin

<Kết nạp (u, v) vào cây, nếu đã đủ n - 1 cạnh thì thuật toán dừng>
Union(r1,r2);{Hợpnhấthaicâylạithànhmộtcây}

end;

169
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

end;

Xét về độ phức tạp tính toán, ta có thể chứng minh được rằng thao tác GetRoot
có độ phức tạp là O(lgn), còn thao tác Union là O(1). Giả sử ta đã có danh sách cạnh
đã sắp xếp rồi thì xét vòng lặp dựng cây khung, nó duyệt qua danh sách cạnh và với
mỗi cạnh nó gọi 2 lần thao tác GetRoot, vậy thì độ phức tạp là O(mlgn), nếu đồ thị có
cây khung thì m ³ n-1 nên ta thấy chi phí thời gian chủ yếu sẽ nằm ở thao tác sắp xếp
danh sách cạnh bởi độ phức tạp của HeapSort là O(mlgm). Vậy độ phức tạp tính toán
của thuật toán là O(mlgm) trong trường hợp xấu nhất. Tuy nhiên, phải lưu ý rằng để
xây dựng cây khung thì ít khi thuật toán phải duyệt toàn bộ danh sách cạnh mà chỉ một
phần của danh sách cạnh mà thôi.

3.Thuật toán Prim:

Thuật toán Kruskal hoạt động chậm trong trường hợp đồ thị dày (có nhiều cạnh).
Trong trường hợp đó người ta thường sử dụng phương pháp lân cận gần nhất của Prim.
Thuật toán đó có thể phát biểu hình thức như sau:

Đơn đồ thị vô hướng G = (V, E) có n đỉnh được cho bởi ma trận trong số C. Qui
ước c[u, v] =

+¥ nếu (u, v) không là cạnh. Xét cây T trong G và một đỉnh v, gọi khoảng cách từ
v tới T là trọng số nhỏ nhất trong số các cạnh nối v với một đỉnh nào đó trong T:

d[v] = min{c[u, v] ⎪ uÎT}

Ban đầu khởi tạo cây T chỉ gồm có mỗi đỉnh {1}. Sau đó cứ chọn trong số các
đỉnh ngoài T ra một đỉnh gần T nhất, kết nạp đỉnh đó vào T đồng thời kết nạp luôn cả
cạnh tạo ra khoảng cách gần nhất đó. Cứ làm như vậy cho tới khi:

❖ Hoặc đã kết nạp được tất cả n đỉnh thì ta có T là cây khung nhỏ nhất

❖ Hoặc chưa kết nạp được hết n đỉnh nhưng mọi đỉnh ngoài T đều có khoảng
cách tới T là

+¥. Khi đó đồ thị đã cho không liên thông, ta thông báo việc tìm cây khung thất
bại.

170
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Về mặt kỹ thuật cài đặt, ta có thể bắt đầu từ một cây rỗng và khởi tạo d[1] := 0
còn d[v] := +¥ với "v ¹ 1. Tại mỗi bước chọn đỉnh đưa vào T, ta sẽ chọn đỉnh u nào
ngoài T và có d[u] nhỏ nhất. Khi kết nạp u vào T rồi thì các nhãn d[v] sẽ thay đổi:
d[v]mới := min(d[v]cũ, c[u, v]). Bước lặp đầu tiên sẽ kết nạp đỉnh 1 vào cây, từ bước
lặp thứ hai, trước khi kết nạp một đỉnh vào cây, ta lưu lại cạnh tạo ra khoảng cách gần
nhất từ cây tới đỉnh đó để cuối cùng in ra cây khung nhỏ nhất.

B.Ví dụ:

1. Ví dụ 1:

Cho G = (V, E) là đồ thị vô hướng liên thông có trọng số, với một cây khung T
của G, ta gọi trọng số của cây T là tổng trọng số các cạnh trong T. Bài toán đặt ra là
trong số các cây khung của G, chỉ ra cây khung có trọng số nhỏ nhất, cây khung như
vậy được gọi là cây khung nhỏ nhất của đồ thị (minimum spanning tree) và bài toán đó
gọi là bài toán cây khung nhỏ nhất. Sau đây ta sẽ xét hai thuật toán thông dụng để giải
bài toán cây khung nhỏ nhất của đơn đồ thị vô hướng có trọng số. Bạn có thể đưa vào
một số sửa đổi nhỏ trong thủ tục nhập liệu để giải quyết bài toán trong trường hợp đa
đồ thị.

Input: file văn bản MINTREE.INP:

❖ Dòng 1: Ghi hai số số đỉnh n (≤ 1000) và số cạnh m của đồ thị cách nhau ít nhất 1
dấu cách

❖ m dòng tiếp theo, mỗi dòng có dạng 3 số u, v, c[u, v] cách nhau ít nhất 1 dấu
cách thể hiện đồ thị có cạnh (u, v) và trọng số cạnh đó là c[u, v]. (c[u, v] là số
nguyên có giá trị tuyệt đối không quá 1000).
Output: file văn bản MINTREE.OUT ghi các cạnh thuộc cây khung và trọng số cây
khung
MINTREE.INP MINTREE.OUT

171
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

69 Minimum
121 spanning tree:
1 31 (2, 4) = 1
241 (3, 6) = 1
232 (2, 5) = 1
251 (1, 3) = 1
351 (1, 2) = 1
361 Weight= 5
452
562
Cách giải: áp dụng thuật toán Kruskal như trình bày ở trên.

Code:

type re=record

u,v,c:longint;

kn:boolean;

end;

const fi='bt.inp';

fo='bt.out';

var f:text;

a:array[0..100001] of re;

root:array[0..100001] of longint;

n,m,w,count:int64;

i:longint;

con:boolean;

procedure input;

172
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

begin

assign(f,fi);

reset(f);

readln(f,n,m);

for i:=1 to m do readln(f,a[i].u,a[i].v,a[i].c);

close(f);

end;

procedure sort(l,r: longint);

var

i,j: longint;

x,y:longint;

begin

i:=l;

j:=r;

x:=a[(l+r) div 2].c;

repeat

while a[i].c<x do

inc(i);

while x<a[j].c do

dec(j);

if not(i>j) then

begin

173
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

y:=a[i].c;

a[i].c:=a[j].c;

a[j].c:=y;

y:=a[i].u;

a[i].u:=a[j].u;

a[j].u:=y;

y:=a[i].v;

a[i].v:=a[j].v;

a[j].v:=y;

inc(i);

j:=j-1;

end;

until i>j;

if l<j then

sort(l,j);

if i<r then

sort(i,r);

end;

function getroot(v:longint):longint;

begin

if (root[v]=0) then exit(v);

root[v]:=getroot(root[v]);

174
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

exit(root[v]);

end;

procedure kruskal;

var r1,r2:longint;

begin

con:=false;

count:=0;

for i:=1 to n do root[i]:=0;

for i:=1 to m do

begin

r1:=getroot(a[i].u);

r2:=getroot(a[i].v);

if r1<>r2 then

begin

a[i].kn:=true;

inc(count);

if count=n-1 then

begin

con:=true;

exit;

end;

root[r1]:=r2;

175
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

end;

end;

end;

procedure output;

begin

assign(f,fo);

rewrite(f);

if con=false then writeln(f,'BEEP')

else

begin

count:=0;

w:=0;

for i:=1 to m do

begin

if a[i].kn then

begin

writeln(f,a[i].u,' ',a[i].v,' ',a[i].c);

inc(count);

w:=w+a[i].c;

end;

if count=n-1 then break;

end;

176
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

write(f,w);

end;

close(f);

end;

begin

input;

sort(1,m);

kruskal;

output;

end.

2.Ví dụ 2:

Nước Anpha đang lập kế hoạch xây dựng một thành phố mới và hiện đại. Theo
kế hoạch, thành phố sẽ có N vị trí quan trọng, được gọi là N trọng điểm và các trọng
điểm này được đánh số từ 1 tới N. Bộ giao thông đã lập ra một danh sách M tuyến
đường hai chiều có thể xây dựng được giữa hai trọng điểm nào đó. Mỗi tuyến đường
có một thời gian hoàn thành khác nhau.

Các tuyến đường phải được xây dựng sao cho N trọng điểm liên thông với nhau.
Nói cách khác, giữa hai trọng điểm bất kỳ cần phải di chuyển được đến nhau qua một
số tuyến đường. Bộ giao thông sẽ chọn ra một số tuyến đường từ trong danh sách ban
đầu để đưa vào xây dựng sao cho điều kiện này được thỏa mãn.

Do nhận được đầu tư rất lớn từ chính phủ, bộ giao thông sẽ thuê hẳn một đội thi
công riêng cho mỗi tuyến đường cần xây dựng. Do đó, thời gian để hoàn thành toàn bộ
các tuyến đường cần xây dựng sẽ bằng thời gian lâu nhất hoàn thành một tuyến đường
nào đó.

Yêu cầu: Giúp bộ giao thông tính thời gian hoàn thành các tuyến đường sớm nhất
thỏa mãn yêu cầu đã nêu.
177
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

1.1.2. Dữ liệu

Dòng chứa số N và M (1 ≤ N ≤ 1000; 1 ≤ M ≤ 10000).

M tiếp theo, mỗi dòng chứa ba số nguyên u, v và t cho biết có thể xây dựng tuyến
đường nối giữa trọng điểm u và trọng điểm v trong thời gian t. Không có hai tuyến
đường nào nối cùng một cặp trọng điểm.

1.1.3. Kết quả

Một số nguyên duy nhất là thời gian sớm nhất hoàn thành các tuyến đường thỏa
mãn yêu cầu đã nêu.

1.1.4. Ví dụ

Dữ liệu Kết quả

57 3

122

151

251

143

132

532

344

Cách giải: Một bài toán cây khung nhỏ nhất cơ bản. Ở đây mình dùng thuật
toán Kruskal + Heap sort. Chi phí lớn nhất chính là cạnh thứ n-1 của cây khung khi
kết nạp vào.

Code:

Const
178
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

maxN =1000;

maxM =10000;

Type

TEdge =Record

u,v,c :SmallInt;

end;

Var

n,m,res :SmallInt;

E :Array[1..maxM] of TEdge;

Lab :Array[1..maxN] of SmallInt;

procedure Enter;

var

i :SmallInt;

begin

Read(n,m);

for i:=1 to m do

with (E[i]) do Read(u,v,c);

end;

procedure Init;

var

i :SmallInt;

begin

179
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

for i:=1 to n do Lab[i]:=-1;

end;

function GetRoot(x :SmallInt) :SmallInt;

begin

while (Lab[x]>0) do x:=Lab[x];

Exit(x);

end;

procedure Union(r1,r2 :SmallInt);

var

x :SmallInt;

begin

x:=Lab[r1]+Lab[r2];

if (Lab[r1]>Lab[r2]) then

begin

Lab[r1]:=r2; Lab[r2]:=x;

end

else

begin

Lab[r1]:=x; Lab[r2]:=r1;

end;

end;

procedure DownHeap(root,leaf :SmallInt);

180
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

var

Key :TEdge;

child :SmallInt;

begin

Key:=E[root];

while (root*2<=leaf) do

begin

child:=root*2;

if (child<leaf) and (E[child].c>E[child+1].c) then Inc(child);

if (Key.c<=E[child].c) then Break;

E[root]:=E[child];

root:=child;

end;

E[root]:=Key;

end;

procedure Greedy;

var

count,i,r1,r2 :SmallInt;

Tmp :TEdge;

begin

for i:=m div 2 downto 1 do DownHeap(i,m);

count:=0;

181
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

for i:=m-1 downto 0 do

begin

Tmp:=E[1]; E[1]:=E[i+1]; E[i+1]:=Tmp;

DownHeap(1,i);

r1:=GetRoot(E[i+1].u); r2:=GetRoot(E[i+1].v);

if (r1<>r2) then

begin

Inc(count);

if (count=n-1) then

begin

res:=E[i+1].c; Break;

end;

Union(r1,r2);

end;

end;

end;

Begin

Assign(Input,''); Reset(Input);

Assign(Output,''); Rewrite(Output);

Enter;

Init;

Greedy;

182
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Write(res);

Close(Input); Close(Output);

End.

3.Ví dụ 3:

Nông dân John quyết định mang nước tới cho N (1 <= N <= 300) đồng cỏ của
mình, để thuận tiện ta đánh số các đồng cỏ từ 1 đến N. Để tưới nước cho 1 đồng cỏ
John có thể chọn 2 cách, 1 là đào ở đồng cỏ đó 1 cái giếng hoặc lắp ống nối dẫn nước
từ những đồng cỏ trước đó đã có nước tới.

Để đào một cái giếng ở đồng cỏ i cần 1 số tiền là W_i (1 <= W_i <= 100,000).
Lắp ống dẫn nước nối 2 đồng cỏ i và j cần 1 số tiền là P_ij (1 <= P_ij <= 100,000; P_ij
= P_ji; P_ii=0).

Tính xem nông dân John phải chi ít nhất bao nhiêu tiền để tất cả các đồng cỏ đều
có nước.

1.1.5. DỮ LIỆU

 Dòng 1: Một số nguyên duy nhất: N


 Các dòng 2..N + 1: Dòng i+1 chứa 1 số nguyên duy nhất: W_i
 Các dòng N+2..2N+1: Dòng N+1+i chứa N số nguyên cách nhau bởi dấu cách;
số thứ j là P_ij
1.1.6. KẾT QUẢ

 Dòng 1: Một số nguyên duy nhất là chi phí tối thiểu để cung cấp nước cho tất cả
các đồng cỏ.
1.1.7. VÍ DỤ

Dữ liệu Kết quả


4 9
5
4

183
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

4
3
0222
2033
2304
2340
1.1.8. GIẢI THÍCH

Có 4 đồng cỏ. Mất 5 tiền để đào 1 cái giếng ở đồng cỏ 1, 4 tiền để đào ở đồng cỏ
2, 3 và 3 tiền để đào ở đồng cỏ 4. Các ống dẫn nước tốn 2, 3, và 4 tiền tùy thuộc vào
nó nối đồng cỏ nào với nhau.

Nông dân John có thể đào 1 cái giếng ở đồng cỏ thứ 4 và lắp ống dẫn nối đồng cỏ
1 với tất cả 3 đồng cỏ còn lại, chi phí tổng cộng là 3 + 2 + 2 + 2 = 9.

Cách giải: Dùng thuật toán Prim. Tạo sẵn một đỉnh ảo 0 là đỉnh bắt đầu để kết nạp
các cạnh. Tương ứng C[0, i] là chi phí để đặt giếng nước tại cánh đồng i. Kết quả là
tổng các D[i].
Code:

Const

oo =1000000001;

Var

n :Integer;

D :Array[0..300] of LongInt;

Free :Array[0..300] of Boolean;

C :Array[0..300,0..300] of LongInt;

procedure Enter;

var

184
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

i,j :Integer;

begin

Read(n);

FillChar(Free,SizeOf(Free),true);

for i:=1 to n do Read(C[0,i]);

for i:=1 to n do

for j:=1 to n do Read(C[i,j]);

for i:=1 to n do D[i]:=oo; D[0]:=0;

end;

procedure Optimize;

var

i,u,v :Integer;

min :LongInt;

begin

for i:=0 to n do

begin

min:=High(LongInt); u:=-1;

for v:=0 to n do

if (min>D[v]) and (Free[v]) then

begin

min:=D[v]; u:=v;

end;

185
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

if (u=-1) then Break;

Free[u]:=false;

for v:=1 to n do

if (Free[v]) and (D[v]>C[u,v]) then D[v]:=C[u,v];

end;

end;

procedure Escape;

var

i :Integer;

res :LongInt;

begin

res:=0;

for i:=1 to n do Inc(res,D[i]);

Write(res);

end;

Begin

Assign(Input,''); Reset(Input);

Assign(Output,''); Rewrite(Output);

Enter;

Optimize;

Escape;

Close(Input); Close(Output);

186
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

End.

4.Ví dụ 4:

Singapore sẽ tổ chức một cuộc đua xe Công Thức 1 vào năm 2008. Trước khi
cuộc đua diễn ra, đã xuất hiện một số cuộc đua về đêm trái luật. Chính quyền muốn
thiết kế một hệ thống kiểm soát giao thông để bắt giữ các tay đua phạm luật. Hệ thống
bao gồm một số camera đặt trên các tuyến đường khác nhau. Để đảm bảo tính hiệu quả
cho hệ thống, cần có ít nhất một camera dọc theo mỗi vòng đua.

Hệ thống đường ở Singapore có thể được mô tả bởi một dãy các nút giao thông
và các đường nối hai chiều (xem hình vẽ). Một vòng đua bao gồm một nút giao thông
xuất phát, tiếp theo là đường đi bao gồm ít nhất 3 tuyến đường và cuối cùng quay trở
lại điểm xuất phát. Trong một vòng đua, mỗi tuyến đường chỉ được đi qua đúng một
lần, theo đúng một hướng.

Chi phí để đặt camera phụ thuộc vào tuyến đường được chọn. Các số nhỏ trong
hình vẽ cho biết chi phí để đặt camera lên các tuyến đường. Các số lớn xác định các
nút giao thông. Camera được đặt trên các tuyến đường chứ không phải tại các nút giao
thông. Bạn cần chọn một số tuyến đường sao cho chi phí lắp đặt là thấp nhất đồng thời
vẫn đảm bảo có ít nhất một camera dọc theo mỗi vòng đua.

Viết chương trính tìm cách đặt các camera theo dõi giao thông sao cho tổng chi
phí lắp đặt là thấp nhất.

1.1.9. Dữ liệu

 Dòng đầu tiên chứa 2 số nguyên n, m ( 1 ≤ n ≤ 10000, 1 ≤ m ≤ 100000) là số


nút giao thông và số đường nối. Các nút giao thông được đánh số từ 1 đến n.
 m dòng tiếp theo mô tả các đường nối, mỗi dòng bao gồm 3 số nguyên dương
cho biết hai đầu mút của tuyến đường và chi phí lắp đặt camera. Chi phí lắp đặt
thuộc phạm vi [1, 1000].
1.1.10. Kết qủa

In ra 1 số nguyên duy nhất là tổng chi phí lắp đặt thất nhất tìm được.

187
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

1.1.11. Ví dụ

Dữ liệu: Kết qủa


67 6
125
233
145
454
564
633
523

Cách giải: Dùng thuật toán Kruskal.

Kết quả bằng : Tổng trọng số của đồ thị – Tổng trọng số của cây khung lớn nhất.

Giải thích : Do tính chất của cây khung là khi thêm 1 cạnh ngoài cây vào ta thu được
đúng một chu trình đơn. Vì vậy chọn ra cây khung lớn nhất, những cạnh còn lại là
những cạnh nhỏ nhất. Lắp camera vào các cạnh còn lại này đảm bảo mọi chu trình
trong đồ thị đều phải đi qua một trong các cạnh này, tức là ta không bỏ lỡ một vòng
đua nào cả.

Code:

Const

maxN =10000;

maxM =100000;

Type

TEdge =Record

u,v,c :SmallInt;

end;

188
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Var

n :SmallInt;

m,sum,res :LongInt;

E :Array[1..maxM] of TEdge;

Lab :Array[1..maxN] of SmallInt;

procedure Enter;

var

i :LongInt;

begin

Read(n,m); sum:=0;

for i:=1 to m do

with (E[i]) do

begin

Read(u,v,c); Inc(sum,c);

end;

end;

procedure Init;

var

i :SmallInt;

begin

for i:=1 to n do Lab[i]:=-1;

end;

189
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

function GetRoot(x :SmallInt) :SmallInt;

begin

while (Lab[x]>0) do x:=Lab[x];

Exit(x);

end;

procedure Union(r1,r2 :SmallInt);

var

x :SmallInt;

begin

x:=Lab[r1]+Lab[r2];

if (Lab[r1]>Lab[r2]) then

begin

Lab[r1]:=r2; Lab[r2]:=x;

end

else

begin

Lab[r1]:=x; Lab[r2]:=r1;

end;

end;

procedure DownHeap(root,leaf :LongInt);

var

190
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Key :TEdge;

child :LongInt;

begin

Key:=E[root];

while (root*2<=leaf) do

begin

child:=root*2;

if (child<leaf) and (E[child].c<E[child+1].c) then Inc(child);

if (Key.c>=E[child].c) then Break;

E[root]:=E[child];

root:=child;

end;

E[root]:=Key;

end;

procedure Greedy;

var

Tmp :TEdge;

r1,r2 :SmallInt;

i,count :LongInt;

begin

for i:=m div 2 downto 1 do DownHeap(i,m);

count:=0; res:=0;

191
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

for i:=m-1 downto 0 do

begin

Tmp:=E[1]; E[1]:=E[i+1]; E[i+1]:=Tmp;

DownHeap(1,i);

r1:=GetRoot(E[i+1].u); r2:=GetRoot(E[i+1].v);

if (r1<>r2) then

begin

Inc(count);

Inc(res,E[i+1].c);

if (count=n-1) then Break;

Union(r1,r2);

end;

end;

end;

Begin

Assign(Input,''); Reset(Input);

Assign(Output,''); Rewrite(Output);

Enter;

Init;

Greedy;

Write(sum-res);

Close(Input); Close(Output);

192
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

End.

5. Ví dụ 5:

Bác John dạo này lười đến nỗi không muốn bảo trì các con đường dẫn bác đến
thăm N (5 <= N <= 10,000) cánh đồng (đánh số từ 1 đến N) nữa. Mỗi cánh đồng là nơi
ở của một cô bò. Bác John có kế hoạch loại bỏ nhiều nhất P (N-1 <= P <= 100,000)
con đường sao cho các cánh đồng vẫn liên thông.
Ban phải xác định N-1 con đường cần giữ lại.
Đường nối hai chiều j nối giữa cánh đồng Sj và Ej (1 <= Sj <= N; 1 <= Ej <= N;
Sj # Ej) và cần Lj (0 <= Lj <= 1000) thời gian để di chuyển. Không có hai cánh đồng
nào được nối trực tiếp bởi nhiều hơn một con đường.
Đàn bò buồn vì hệ thống giao thông của chúng sắp bị rút gọn. Bạn phải thăm mỗi
cô bò ít nhất một lần trong ngày để động viên. Mỗi lần thăm cánh đồng i (dù chỉ đi
ngang qua), bạn phải trò chuyện với cô bò trong thời gian Ci (1 <= Ci <= 1000).
Bạn sẽ nghỉ lại đêm trên cùng một cánh đồng (bạn sẽ được chọn) cho đến khi đàn
bò đều đã hết bị suy sụp. Bạn sẽ trò chuyện với cô bò trong cánh đồng mà bạn nghỉ lại
ít nhất 2 lần vào buổi sáng thức dậy và vào buổi tối khi trở về nghỉ.
Giả dụ bác John theo lời khuyên của bạn giữ lại một số con đường và bạn sẽ chọn
cánh đồng tối ưu nhất để nghỉ lại, hãy xác định thời gian nhỏ nhất bạn cần để thăm tất
cả đàn bò ít nhất một lần trong ngày.
1.1.12. Dữ liệu

* Dòng 1: Hai số nguyên N và P cách nhau bởi khoảng trắng


* Dòng 2..N+1: Dòng i+1 chứa một số nguyên duy nhất Ci
* Dòng N+2..N+P+1: Dòng N+j+1 chứa ba số nguyên phân biệt: Sj, Ej và Lj
1.1.13. Kết quả

* Dòng 1: Một số nguyên duy nhất, tổng thời gian cần để thăm tất cả đàn bò (bao gồm
hai lần thăm cô bò ở nơi mà bạn nghỉ).
1.1.14. Ví dụ

Dữ liệu: Kết quả:


57 30 3 4 17 176
10 125 2 5 15
193
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

10 235 356
20 2 4 12 4 5 12
6
Cách giải:

Nhìn hình vẽ, nhận thấy rằng với mỗi cây khung được chọn, mỗi cạnh trong cây
khung đều đi qua đúng hai lần và mỗi đỉnh đi qua đúng số cạnh kề với nó (chỉ tính
những cạnh được chọn). Ví dụ trong hình trên cạnh 1-2 được chọn và giả sử bắt đầu
bằng bất kì đỉnh nào thì cạnh này đi qua đúng hai lần, đi và về; đỉnh 1 sẽ thăm đúng
một lần (không kể trường hợp bắt đầu bằng 1 thì phải thăm hai lần), đỉnh 2 thăm đúng
3 lần (có 3 cạnh kề tô màu tím, và đường nhiên không tính đỉnh bắt đầu là 2). Riêng
đỉnh bắt đầu trong cây khung sẽ thăm thêm một lần là ban đầu đã thăm ở nó.Từ nhận
xét trên ta gán trọng số cạnh (u,v) từ w thành w * 2 + c[u] + c[v]. Khi đó khi ta chọn
cạnh này ta đã tính luôn 2 lần đi qua nó và số lần thăm u, v (các bạn có thể test tay để
nhận ra sự đúng đắn của thuật toán). Lúc này chỉ cần tìm cây khung nhỏ nhất và sau đó
chọn đỉnh bắt đầu nhỏ nhất.

Code:

Uses Math;

Const

maxN =10000;

maxM =100000;

Type

194
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

TEdge =Record

u,v,c :SmallInt;

end;

Var

n :SmallInt;

m :LongInt;

A,Lab :Array[1..maxN] of SmallInt;

E :Array[1..maxM] of TEdge;

res :Int64;

procedure Enter;

var

i :LongInt;

begin

Read(n,m);

for i:=1 to n do Read(A[i]);

for i:=1 to m do

with (E[i]) do

begin

Read(u,v,c);

c:=2*c+A[u]+A[v];

end;

end;

195
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

procedure Init;

var

i :SmallInt;

begin

for i:=1 to n do Lab[i]:=-1;

end;

function GetRoot(x :SmallInt) :SmallInt;

begin

while (Lab[x]>0) do x:=Lab[x];

Exit(x);

end;

procedure Union(r1,r2 :SmallInt);

var

x :SmallInt;

begin

x:=Lab[r1]+Lab[r2];

if (Lab[r1]>Lab[r2]) then

begin

Lab[r1]:=r2; Lab[r2]:=x;

end

else

196
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

begin

Lab[r1]:=x; Lab[r2]:=r1;

end;

end;

procedure DownHeap(root,leaf :LongInt);

var

Key :TEdge;

child :LongInt;

begin

Key:=E[root];

while (root*2<=leaf) do

begin

child:=root*2;

if (child<leaf) and (E[child].c>E[child+1].c) then Inc(child);

if (Key.c<=E[child].c) then Break;

E[root]:=E[child];

root:=child;

end;

E[root]:=Key;

end;

procedure Greedy;

197
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

var

Tmp :TEdge;

i,count :LongInt;

r1,r2 :SmallInt;

begin

for i:=m div 2 downto 1 do DownHeap(i,m);

count:=0; res:=0;

for i:=m-1 downto 0 do

begin

Tmp:=E[1]; E[1]:=E[i+1]; E[i+1]:=Tmp;

DownHeap(1,i);

r1:=GetRoot(E[i+1].u); r2:=GetRoot(E[i+1].v);

if (r1<>r2) then

begin

Inc(count);

Inc(res,E[i+1].c);

if (count=n-1) then Break;

Union(r1,r2);

end;

end;

end;

198
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

procedure Escape;

var

i,minV :SmallInt;

begin

minV:=A[1];

for i:=2 to n do minV:=Min(minV,A[i]);

Write(res+minV);

end;

Begin

Assign(Input,''); Reset(Input);

Assign(Output,''); Rewrite(Output);

Enter;

Init;

Greedy;

Escape;

Close(Input); Close(Output);

End.

C.Tài liệu tham khảo:

- Giải thuật và lập trình (thầy Lê Minh Hoàng).

- Tài liệu giáo khoa chuyên tin (thầy Hồ Sĩ Đàm).

- Bài tập tham khảo VNOI, Codeforce

199
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Ngoài ra bạn có thể tham khảo thêm những cuốn sách sau đây:

❖ Alfred V. Aho, Jeffrey D. Ullman, John E. Hopcroft. Data Structures and


Algorithms, ISBN: 0201000237, Addison Wesley, 1983.

❖ Robert Sedgewick. Algorithms 2nd edition, ISBN: 0201066734, Addison


Wesley, 1988.

❖ Mikhail J. Atallah Ed. Algorithms and Theory of Computation Handbook,


ISBN: 0849326494, CRC Press, 1998.

200
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

CÂY IT - SEGMENT TREE

Phan Thành Tâm, THPT chuyên Thoại Ngọc Hầu – An Giang

1. Giới thiệu
1- Trong những năm gần đây, trong các kỳ thi học sinh giỏi loại cấu trúc dữ
liệu Segment Tree đã được sử dụng rất nhiều đặc biệt là trong những bài toán xử lý
trên dãy số, nó cũng là một loại cấu trúc dữ liệu trừu tượng, khó hiểu, tuy nhiên, tính
hiệu quả thì tương đối cao. Do cây IT được viết rất nhiều trên các tài liệu và khá hay,
vì vậy tôi cũng không dám thay đổi, sửa chữa gì thêm mà chỉ nêu sơ phần lý thuyết và
bổ sung một vài bài tập đơn giản cho các thầy cô mới giảng dạy hay học sinh mới học
tham khảo, nếu cần tham khảo rõ phần lý thuyết, thầy cô có thể vào địa chỉ
http://vnoi.info/wiki/algo/data-structures/segment-tree-extend để tham khảo rõ hơn.

Segment Tree là một cây. Cụ thể hơn, nó là một cây nhị phân đầy đủ (mỗi nút
là lá hoặc có đúng 2 nút con), với mỗi nút quản lý một đoạn trên dãy số. Với một dãy
số gồm N phần tử, nút gốc sẽ lưu thông tin về đoạn [1,N], nút con trái của nó sẽ lưu

thông tin về đoạn [1,⌊N/2⌋] và nút con phải sẽ lưu thông tin về đoạn [⌊N/2⌋+1,N].
Tổng quát hơn: nếu nút A lưu thông tin đoạn [i,j], thì 2 con của nó: A1 và A2 sẽ lưu

thông tin của các đoạn [i,⌊(i+j)/2⌋]và đoạn [⌊(i+j)/2⌋+1,j].

Ví dụ
Xét một dãy gồm 7 phần tử, Segment Tree sẽ quản lý các đoạn như sau:

201
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Cài đặt
Để cài đặt, ta có thể dùng một mảng 1 chiều, phần tử thứ nhất của mảng thể hiện nút
gốc. Phần tử thứ id sẽ có 2 con là 2∗id (con trái) và 2∗id+1 (con phải). Với cách cài
đặt này, người ta đã chứng minh được bộ nhớ cần dùng cho ST không quá 4∗N phần
tử.

Để dễ hình dung, ta lấy 1 ví dụ cụ thể, từ bài VOJ - QMAX


 Cho dãy N phần tử (N≤105). Ban đầu mỗi phần tử có giả trị 0.
 Có Q truy vấn (Q≤105). Mỗi truy vấn có 1 trong 2 loại:
1. Gán giá trị v cho phần tử ở vị trí i.
2. Tìm giá trị lớn nhất cho đoạn [i,j].
Cách đơn giản nhất là dùng 1 mảng A duy trì giá trị các phần tử. Với thao tác 1 thì ta
gán A[i]=v. Với thao tác 2 thì ta dùng 1 vòng lặp từ i đến j để tìm giá trị lớn nhất.

Rõ ràng cách này có độ phức tạp là O(N∗Q) và không thể chạy trong thời gian cho
phép.

Cách dùng Segment Tree như sau:

 Với truy vấn loại 1, ta sẽ cập nhật thông tin của các nút trên cây ST mà đoạn nó
quản lý chứa phần tử i.
 Với truy vấn loại 2, ta sẽ tìm tất cả các nút trên cây ST mà ðoạn nó quản lý nằm
trong [i,j], rồi lấy max của các nút này.

2. Một số bài tập áp dụng

Bài 1. QMAX - Giá trị lớn nhất

Cho một dãy gồm n phần tử có giá trị ban đầu bằng 0.

Cho m phép biến đổi, mỗi phép có dạng (u, v, k): tăng mỗi phần tử từ vị trí u đến vị trí
v lên k đơn vị.

Cho q câu hỏi, mỗi câu có dạng (u, v): cho biết phần tử có giá trị lớn nhất thuộc đoạn
[u, v]

Giới hạn

202
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

 n, m, q <= 50000
 k>0
 Giá trị của một phần tử luôn không vượt quá 231-1

Input: QMAX.INP

 Dòng 1: n, m
 m dòng tiếp theo, mỗi dòng chứa u, v, k cho biết một phép biến đổi
 Dòng thứ m+2: p
 p dòng tiếp theo, mỗi dòng chứa u, v cho biết một phép biến đổi

Output: QMAX.OUT

 Gồm p dòng chứa kết quả tương ứng cho từng câu hỏi.

QMAX.INP:

62

132

463

34

QMAX.OUT:

Chương trình tham khảo

#include<bits/stdc++.h>

using namespace std;

const int maxN=5e4+1;

long long a[maxN+5], it[maxN*4], res, k;

int n,m,i,u,v;
203
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

void built(int l, int r, int k){

if (l>r) return;

if (l==r){it[k]=a[l]; return;}

int m=(l+r)>>1, k2=k<<1;

built(l,m,k2+1); built(m+1,r,k2+2);

it[k]=max(it[k2+1],it[k2+2]);

void lay(int l, int r, int k){

if (l>r or l>v or r<u) return;

if (u<=l and r<=v) {res=max(res,it[k]); return;}

int m=(l+r)>>1; k=k<<1;

lay(l,m,k+1); lay(m+1,r,k+2);

int main(){

cin.tie(0); cout.tie(0); ios::sync_with_stdio(0);

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

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

cin >> n >> m;

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

cin >> u >> v >> k;

a[u]+=k; a[v+1]-=k;

204
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

for (i=1; i<=n; i++) a[i]+=a[i-1];

built(1,n,0);

cin >> m;

while (m--){

res=0; cin>>u>>v; lay(1,n,0);

cout << res <<"\n";

Bài 2. LINEUP - Xếp hàng

Hàng ngày khi lấy sữa, N con bò của bác John (1 ≤ N ≤ 50000) luôn xếp hàng
theo thứ tự không đổi. Một hôm bác John quyết định tổ chức một trò chơi cho một số
con bò. Để đơn giản, bác John sẽ chọn ra một đoạn liên tiếp các con bò để tham dự trò
chơi. Tuy nhiên để trò chơi diễn ra vui vẻ, các con bò phải không quá chênh lệch về
chiều cao.

Bác John đã chuẩn bị một danh sách gồm Q (1 ≤ Q ≤ 200000) đoạn các con bò
và chiều cao của chúng (trong phạm vi [1, 1000000]). Với mỗi đoạn, bác John muốn
xác định chênh lệch chiều cao giữa con bò thấp nhất và cao nhất. Bạn hãy giúp bác
John thực hiện công việc này!

Dữ liệu: LINEUP.INP

 Dòng đầu tiên chứa 2 số nguyên N và Q.


 Dòng thứ i trong số N dòng sau chứa 1 số nguyên duy nhất, là độ cao của con
bò thứ i.
 Dòng thứ i trong số Q trong tiếp theo chứa 2 số nguyên A, B (1 ≤ A ≤ B ≤ N),
cho biết đoạn các con bò từ A đến B.

Kết quả: LINEUP.OUT


205
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Gồm Q dòng, mỗi dòng chứa 1 số nguyên, là chênh lệch chiều cao giữa con bò
thấp nhất và cao nhất thuộc đoạn tương ứng.

Ví dụ

LINEUP.INP:

63

15

46

22

LINEUP.OUT

Chương trình tham khảo

#include<bits/stdc++.h>

using namespace std;

const int maxN=5e4+1, maxdata=1e9+97;

int mi[maxN*4], ma[maxN*4], a[maxN], i,n,u,v,m,res,ans;


206
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

void built(int l, int r, int k){

mi[k]=maxdata; ma[k]=-maxdata; if (l>r) return;

if (l==r){mi[k]=ma[k]=a[l]; return;}

int m=(l+r)>>1, k2=k<<1;

built(l,m,k2+1); built(m+1,r,k2+2);

mi[k]=min(mi[k2+1],mi[k2+2]);

ma[k]=max(ma[k2+1],ma[k2+2]);

void lay(int l, int r, int k){

if(l>r or l>v or r<u) return;

if (u<=l and r<=v){

ans=min(ans,mi[k]); res=max(res,ma[k]); return;

int m=(l+r)>>1; k=k<<1;

lay(l,m,k+1); lay(m+1,r,k+2);

int main(){

cin.tie(0); cout.tie(0); ios::sync_with_stdio(0);

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

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

cin >> n >> m;

for (i=1; i<=n; i++) cin >>a[i];

207
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

built(1,n,0);

while(m--){

cin>>u>>v; ans=maxdata; res=-maxdata;

lay(1,n,0); cout<< res-ans<<"\n";

Bài 3. MINR - Số nhỏ nhất trong dãy con


Cho một mảng gồm n số nguyên a1, a2, a3,..., an. Có m câu hỏi, mỗi câu gồm hai
số nguyên 1 ≤ p ≤ r ≤ n, yêu cầu tìm số nguyên nhỏ nhất trong các số ap, ap+1, ..., ar.
Hãy cho biết kết quả của m câu hỏi trên.

Dữ liệu nhập: File MINR.INP gồm các dòng sau


- Dòng thứ nhất là hai số nguyên n, m cách nhau một khoảng trắng (1 ≤ n, m ≤ 105)

- Dòng thứ hai là n số nguyên a1, a2, a3,..., an cách nhau một khoảng trắng (1 ≤ ai ≤ 109)

- Trong m dòng tiếp theo, tại dòng thứ i là hai số pi và ri của câu hỏi thứ i tương ứng,
hai số cách nhau một khoảng trắng. (1 ≤ pi ≤ ri ≤ n)

Dữ liệu xuất: File MINR.OUT


- Gồm m dòng, mỗi dòng có một số nguyên là kết quả của câu hỏi tương ứng.

Ví dụ
MINR.INP

6 4
7 5 3 8 6 9
1 6
1 4
1 2
46

MINR.OUT

208
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

3
3
5
6

Chương trình tham khảo

#include<bits/stdc++.h>

using namespace std;

const int maxN=1e5+1, maxData=1e9+1;

int a[maxN], it[maxN*4], res, u, v, i, n, q;

void built(int l, int r, int k){

it[k]=maxData; if (l>r) return;

if (l==r){it[k]=a[l]; return;}

int m=(l+r)>>1, k2=k<<1;

built(l,m,k2+1); built(m+1,r,k2+2);

it[k]=min(it[k2+1],it[k2+2]);

void lay(int l, int r, int k){

if (l>r or l>v or r<u) return;

if (u<=l and r<=v){res=min(res,it[k]); return;}

int m=(l+r)>>1; k=k<<1;

lay(l,m,k+1); lay(m+1,r,k+2);

int main(){

209
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

cin.tie(0); cout.tie(0); 0; ios::sync_with_stdio(0);

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

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

cin >> n >> q;

for (i=1; i<=n; i++) cin >> a[i];

built(1,n,0);

while (q--){

cin >> u >> v; res=maxData;

lay(1,n,0); cout<<res<<"\n";

Bài 4. NGHICHTHE - Dãy nghịch thế

Cho một dãy số a1.. aN. Một nghịch thế là một cặp số u, v sao cho u < v và au >
av. Nhiệm vụ của bạn là đếm số nghịch thế.

Dữ liệu : NGHICHTHE.INP

 Dòng đầu ghi số nguyên dương N.


 N dòng sau mỗi dòng ghi một số ai ( 1 ≤ i ≤ N ).

Kết quả: NGHICHTHE.OUT

Ghi trên một dòng số M duy nhất là số nghịch thế.

Giới hạn

 1 ≤ N ≤ 60000
 1 ≤ ai ≤ 60000

210
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Ví dụ

NGHICHTHE.INP

NGHICHTHE.OUT

Chương trình tham khảo

#include<bits/stdc++.h>

using namespace std;

const int maxN=6e4;

int it[maxN*4+5], a[maxN], i, n;

long long res;

void update(int k, int l, int r){

if (l>a[i] or r<a[i] or l>r) return;

if (l==r){ it[k]++; return;}

int m=(l+r) >> 1, k2=k<<1;

update(k2+1,l,m); update(k2+2,m+1,r);

it[k]=it[k2+1]+it[k2+2];

void lay(int k, int l, int r){

if (l>=a[i] or l>r) return;


211
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

if (r<a[i]) {res+=it[k]; return;}

int m=(l+r)>>1; k=k<<1;

lay(k+1,l,m); lay(k+2,m+1,r);

int main(){

cin.tie(0); cout.tie(0); ios::sync_with_stdio(0);

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

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

cin >> n; for (i=1; i<=n; i++) cin >> a[i];

for (i=n; i>0; i--){

lay(0,1,maxN); update(0,1,maxN);

cout<<res;

3. Tài liệu tham khảo


1- Một số vấn đề Đáng chú ý trong Tin học
2- http://vnoi.info/wiki/algo/data-structures/segment-tree-extend

212
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

TWO POINTERS

Lương Quí Hiệp

THPT Chuyên Lương Thế Vinh – Đồng Nai

GIỚI THIỆU

Bài toán tìm kiếm một phần tử trong một dãy các phần tử là bài toán đơn giản và được
giải quyết với hai pương pháp cơ bản, đó là phương pháp “tìm kiếm tuần tự” với độ
phức tạp O(n) và phương pháp “tìm kiếm nhị phân” trên dãy đã sắp với độ phức tạp
O(log n). Bây giờ, chúng ta thử nâng độ khó cho bài toán tìm kiếm lên, không phải tìm
kiếm một phần tử mà tìm kiếm hai phần tử trong một dãy hay hai dãy phần tử (mỗi
phần tử trên một dãy). Nếu sử dụng phương pháp tìm kiếm tuần tự để giải quyết thì độ
phức tạp sẽ là O(n2). Còn nếu sử dụng phương pháp tìm kiếm nhị phân để giải quyết,
độ phức tạp là O(n log n).

Trong chuyên đề này, chúng ta đưa ra giải pháp tìm kiếm đồng thời hai phần tử với
một độ phức tạp O(n) trên dãy đã sắp.

Một kỹ thuật ít được thảo luận trong các bài toán tìm kiếm đó là kỹ thuật sử dụng hai
con trỏ (two pointer) để giải quyết bài toán tìm đồng thời hai đối tượng. Cách tiếp cận
của kỹ thuật này vẫn là duyệt trên các dãy đã sắp theo một đặc tính mà theo đó hai con
trỏ tìm kiếm có thể di chuyển đồng thời trong quá trình tìm kiếm.

Giải thuật tìm kiếm nhị phân là một loại giải thuật tối ưu hóa về số lần thử nghiệm
được thực hiện để đạt được vị trí tối ưu có kỹ thuật sử dụng hai con trỏ nhưng chưa tận
dụng tìm kiếm đồng thời.

Để làm rõ hơn các lập luận trên, chúng ta xem xét bài toán sau đây:

Cho hai dãy số A và B, các số được đánh số từ 1 đến n. Bạn được yêu cầu tìm
hai số, mỗi số từ mỗi dãy, sao cho tổng của hai số là X.

Chúng ta xét hai giải pháp đơn giản sau:

Giải pháp 1: (giải pháp thô)

Dùng phương pháp duyệt tuần tự.

213
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Mỗi số sẽ được duyệt tìm trên một dãy, từ 1 ðến n, ứng với mỗi số trên dãy thứ nhất,
chúng ta tìm số thứ hai cho đến khi tìm được hai số hoặc không tìm được.

For i: 1 .. n // duyệt trên dãy thứ nhất

For j: 1 .. n // duyệt trên dãy thứ hai

Nếu (tổng hai số tại i trên dãy A và tại j trên dãy B bằng X) thì kết
thúc tìm được.

Với giải pháp này, rõ ràng độ phức tạp của thuật toán là O(n2).

Giải pháp 2:

Sử dụng phương pháp tìm kiếm nhị phân.

Trước tiên, ta sắp xếp dãy thứ hai theo thứ tự tăng dần. Thay vì phải tìm hai số ai và bj
thỏa điều kiện ai + bj = X. Nếu biết trước số ai trên dãy A, tìm số X – ai trên dãy B. Do
B đã sắp, dùng phương pháp tìm kiếm nhị phân

Như vậy, duyệt trên dãy A. Với mỗi số ai trên dãy A, chúng ta tìm kiếm nhị phân số
thứ 2 có giá trị là (X-ai) trên dãy B đã sắp.

For i : 1 .. n

If (tìm nhị phân số (X – ai)trên dãy B ) thì kết thúc tìm được.

Giải pháp này có độ phức tạp là O(n log n).

Nhận xét

Thực ra, trong 2 giải pháp trên cũng đã sử dụng kỹ thuật hai con trỏ. Tuy nhiên, trong
giải pháp thứ hai, con trỏ thứ hai là tìm kiếm nhị phân nhưng tìm đi tìm lại nhiều lần
trên dãy thứ hai. Đó là do số thứ nhất phải duyệt tuần tự.

Cách tìm kiếm hai phần tử theo hai phương pháp trên là không đồng thời.

214
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

I. KỸ THUẬT SỬ DỤNG HAI CON TRỎ

Trong giải pháp này, chúng ta sử dụng kỹ thuật hai con trỏ thực hiện tìm kiếm đồng
thời hai phần tử trên hai dãy đã sắp với độ phức tạp O(n).

Để làm đơn giản vấn đề hơn, chúng ta xét lại bài toán chỉ trên một mảng. Sau đó, xét
trên hai mảng.

1. Cặp số có tổng bằng X

Cho một dãy đã sắp A, có N Số nguyên. Tìm một cặp số bất kỳ (i, j ) Có tổng bằng
số X.

Hạn chế: Dãy A Chứa khoảng 105 phần tử số nguyên với mỗi giá trị khoảng 109.

Tổ chức dữ liệu

- Mảng dữ liệu cần xử lý đã được sắp.

- Hai biến con trỏ. Hai biến này luôn trỏ vào hai vị trí đang xét trên dãy xử lý.

Phương pháp

- Duyệt tìm kiếm.

Giải pháp:

Chúng tôi sử dụng hai con trỏ, một ở đầu dãy, gọi là con trỏ trái và một ở cuối dãy, gọi
là con trỏ phải. Bắt đầu di chuyển, tại mỗi bước, chúng ta tính tổng giá trị tại hai vị trí
con trỏ. Trong trường hợp tổng giá trị lớn hơn X, chúng ta cần phải di chuyển con trỏ
phải sang trái một đơn vị để giảm giá trị tổng. Tương tự, nếu tổng giá trị nhỏ hơn X,
chúng ta di chuyển con trỏ trái, sang phải một đơn vị để tăng tổng giá trị đến gần với
số X. Tiếp tục di chuyển con trỏ cho đến khi tổng giá trị bằng X. hoặc hai con trỏ gặp
nhau.

While (con trỏ trái < con trỏ phải)

If (tổng giá trị tại hai con trỏ = X) thì kết thúc tìm được

Else If (tổng giá trị tại hai con trỏ > X) thì

giảm con trỏ phải xuống 1 đơn vị

else giảm con trỏ trái xuống một đơn vị.


215
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Độ phức tạp của giải pháp trên là O(N).

Chúng ta xem xét một số bài toán sau:

2. Cặp số có tổng bằng X

Bây giờ, chúng tai quay trở lại bài toán ban đầu với tìm trên hai dãy.

Cho hai dãy đã sắp A và B. Mỗi dãy có chiều dài N. Tìm hai số, mỗi số từ mỗi dãy, sao
cho tổng của hai số là X.

Hạn chế: Dãy A và B Chứa khoảng 105 Số nguyên , mỗi số có giá trị khoảng 109.

Giải pháp

Gọi i và j hai con trỏ chạy trên dãy AVà B tương ứng. đầu tiên cho con trỏ i
trỏ đến phần tử đầu tiên trên dãy a, và con trỏ j trỏ đến phần tử cuối của dãy
b.

di chuyển con trỏ i tăng dần trên dãy a, và giảm vị trí của con trỏ j trên dãy b nếu cần
thiết. Tại mỗi bước, chúng tôi kiểm tra a[i] + b[j] có bằng X không. Nếu thỏa ghi kết
quả và kết thúc tìm kiếm. ngược lại, nếu a [i] + b [j] > X, thì giảm j (nếu j > 0), bởi vì
chúng tôi cần phải giảm giá trị tổng xuống còn nếu a [i] + b [j] < X thì tăng vị trí con
trỏ i.

i trỏ đầu dãy a

j trỏ cuối dãy b

While (i chưa vượt quá cuối dãy a)

While (tổng giá trị tại i và j > X ) và (j chưa vượt quá đầu dãy b) giảm(j)

If (tổng giá trị tại i và j = x) thì kết thúc và tìm được cặp (i, j)

Tăng (i)

3. Sắp xếp trộn

Cho hai dãy số nguyên đã sắp A và B ,,Mỗi dãy có chiều dài N và M tương
ứng. Yêu cầu tạo một dãy có giá trị được sắp mới của cả hai dãy A và B.

Hạn chế: Hai dãy A và B Chứa khoảng 105 số nguyên. Mỗi phần tử có giá trị
khoảng 109.

216
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Giải pháp: Vì hai dãy được cho theo thứ tự sắp xếp, chúng tôi chắc chắn có thể sử
dụng hai con trỏ để giải quyết.

1. Gọi i, j con trỏ chạy trên dãy AVà B tương ứng. Và một con trỏ k chỉ vị trí
ghi vào dãy kết quả. Ban đầu tất cả i, j và k trỏ vào vị trí đầu các dãy.

2. Duyệt trên hai dãy, nếu hai con trỏ chưa vượt quá dãy ( i <= N , j <= M),
Chọn số nhỏ nhất của cặp số ( Ai, Bj) Và ghi nó lên lên dãy kết quả tại vị trí
con trỏ k, Và tăng các con trỏ thích hợp.

while ( i <= n hoặc j <= m )

Tăng(k);

if ( i <= n và j <= m )

if ( A[i] < B[j] )

C[k] = A[i]; Tăng(i);

else if ( A[i] > B[j] )

C[k] = B[j]; Tăng(j);

else if ( i <= n )

C[k] = A[i]; Tăng(i);

else if ( j <= m )

C[k] = B[j]; Tăng(j);

Độ phức tạp nói chung của giải pháp là O ( N+ M).

217
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

II. DÙNG PHƯƠNG PHÁP 2 CON TRỎ GIẢI QUYẾT BÀI TOÁN TÌM
DÃY CON LIÊN TIẾP THỎA ĐIỀU KIỆN

Trong phần trên, chúng ta sử dụng hai con trỏ trong việc tìm kiếm đồng thời chỉ trên
hai phần tử duy nhất không quan tâm đến các phần tử khác trong dãy. Trong phần này,
chúng ta sẽ mở rộng kỹ thuật sử dụng hai con trỏ để tìm một đoạn, một dãy con liên
tiếp các phần tử, trong một dãy cho trước thỏa điều kiện.

Tương tự phương pháp sử dụng hai con trỏ trong phần trên, Tại mỗi bước, hai con trỏ
một trỏ vào phần tử đầu và một trỏ vào phần tử cuối của một dãy con đang xét.

Chúng ta xem xét một số bài toán cơ bản sau:

1. Dãy con liên tiếp tăng dài nhất

Cho một dãy số nguyên n phần tử a1, a2, .. an.

Tìm dãy con liên tiếp tăng dài nhất.

Giải pháp

Một dãy gọi là dãy tăng khi phần tử đứng trước đều có giá trị nhỏ hơn phần tử
đi71ng sau bất kỳ.

Gọi l và r là hai con trỏ đầu và cuối của một dãy con liên tiếp. Giả sử, dãy con
đang xét thỏa điều kiện dãy tăng. Nếu a[r] < a[r+1] thì dãy con từ l .. r+1 là một dãy
con tăng. Ngược lại không phải là một dãy tăng. Đồng thời, dãy [i,R+1] không phải là
dãy tăng với mọi i : l .. r .

l=1; r=1;

While (r < n)

Tăng(r);

If (a[r-1} < a[r])

ghi nhận dãy con tăng;

Else

l=r;

218
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

2. Dãy con liên tiếp có tổng tối ưu

Cho một dãy N Số nguyên ,,Tìm dãy con liên tiếp có tổng lớn nhất nhưng không
lớn hơn M.

Hạn chế: dãy có thể có không ít hơn 105 phần tử và mỗi số không âm và có thể lớn
đến 109.

Giải pháp:

Vì dãy đã cho có các phần tử dương, nên tổng tích lũy sẽ tăng lên khi bạn đi từ trái
sang phải trong dãy. Do đó, dãy tổng tích lũy là dãy đã sắp, bài toán có thể giải quyết
bằng phương pháp hai con trỏ.

Tạo mảng T, tổng tích lũy của dãy đã cho. Hiệu của hai giá trị tổng tích lũy tại
hai vị trí I và j là tổng giá trị dãy con liên tiếp từ i+1 đến j. Bài toán bây giờ là
tìm hai vị trí I và j sao cho hiệu hai tổng tích lũy tại i và j là lớn nhất nhưng
không quá M.

Chúng ta xem xét hai giải pháp sau:

Giải pháp sử dụng tìm kiếm nhị phân:

Đối với mỗi chỉ số i của mảng tổng tích lũy, chúng ta tìm nhị phân số j tương
ứng sao cho biểu thức sau là đúng.

T[j] - T[i-1] <= M and T[j+1] - T[i-1] > M

Như phần trên đã nói, giải pháp này sẽ có độ phức tạp là O(n log n) cho việc tìm kiếm
hai số tại hai vị trí i và j..

Giải pháp sử dụng kỹ thuật hai con trỏ:

Gọi a là dãy số đã cho và T là dãy tổng tích lũy của dãy a.

Gọi l và r là hai con trỏ biểu thị chỉ số đầu và chỉ số cuối của dãy con liên tiếp
liên tiếp trên dãy tổng tích lũy. Hiệu (T[r] – T[l]) là tổng giá trị của dãy con liên
tiếp a[l+1] .. a[r].

Ban đầu cho l=0 và r=0.

Nếu T[r] – T[l] <= M thì ghi nhận dãy con nếu dãy con có tổng lớn hơn tổng đã
lưu; tăng r
219
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Nếu S[r] - S[l] > M thì tăng l

l=0; r=0 ; tổng = -1;

while (l <= r)

while (r <=n) và (S[r] – S[l] <= M)

ghi nhận dãy con [l,r] nếu dãy con có S[r] – S[l] lớn tổng
lớn hơn tổng đã lưu;

tăng r

tăng l

3. Dãy con liên tiếp nhỏ nhất chứa K phần tử khác nhau

Cho một dãy chứa N Số nguyên, Tìm chiều dài của một dãy con liên tiếp nhỏ nhất có
chứa ít nhất K phần tử khác nhau. Đầu ra "-1" nếu không tìm được.

Hạn chế: Số phần tử trong dãy là khoảng một triệu với mỗi giá trị trong số đó có giá
trị lớn đến 109.

Giải pháp:

Chúng ta sử dụng kỹ thuật hai con trỏ để giải quyết bài toán.

Gọi l và r là hai con trỏ trỏ vào đầu và cuối của dãy con liên tiếp. Mỗi lần di chuyển l
hoặc r, cập nhật lại các thông số của dãy con. Dựa vào điều kiện số phần tử khác nhau
trong dăy con (l,r) mŕ chúng ta di chuyển l hay r. Cụ thể như sau:

Nếu số phần tử khác nhau trong dãy (l,r) < K

Tăng (r); Nếu a[r] chưa tồn tại trong dãy từ l đến r-1 thì tăng số phần tử
khác nhau trong dãy (l,r); Tăng số lần xuất hiện số a[r] trong dãy (l,r)

Nếu số phần tử khác nhau trong dãy (l,r) >= K

Ghi nhận kết quả (độ dài min của dãy (l,r)). Đồng thời, giảm số lần xuất
hiện a[l]. Nếu số lần xuất hiện của a[l] = 0 thì giảm số phần tử khác nhau
trong dãy con và tăng l.

220
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Trong giải pháp trên, cần tạo thêm một mảng đánh mã số của các số trong dãy và
mảng đếm số lần xuất hiện của mã số đó trong dãy.

Gọi C là số phần tử khác nhau trong dãy con liên tiếp từ l đến r.

l=0 ; r=0; C=0;

Khi (l <=n)

Khi (r <= n) và (C < K)

Tăng(r);

Nếu a[r] chưa tồn tại trong dãy từ l đến r-1 thì tăng(C)

Tăng số lần xuất hiện số a[r] trong dãy (l,r)

Nếu C >= K

Ghi nhận độ dài min dãy con (l.r)

Nếu số lần xuất hiện của a[l] = 1 thì giảm (C)

Giảm số lần xuất hiện a[l].

Tăng (l)

III. NHẬN XÉT

Phương pháp two pointer là phương pháp vận dụng kỹ thuật tìm kiếm đồng thời hai
phần tử dựa vào hai cho trỏ duyệt chạy trên một dãy đã sắp.

Tại mỗi bước, hai con trỏ một trỏ vào phần tử đầu và một trỏ vào phần tử cuối của một
dãy con đang xét. Độ phức tạp của thuật toán tại mỗi bước chính là độ phức tạp của
việc kiểm tra dãy con có thỏa điều kiện không?

Với kỹ thuật duyệt đồng thời hai con trỏ như phần trên, độ phức tạp của kỹ thuật duyệt
này là O(n). Do đó, độ phức tạp của bài toán là (n x độ phức tạp tại mỗi bước). Nếu
chúng ta tổ chức dữ liệu tốt, tính toán trước (như trong phân tích bài 2 và 3) có thể tại
mỗi bước kiểm tra dãy con, độ phức tạp chỉ là O(1). Như vậy, độ phức tạp của bài toán
là O(n).

Lưu ý: Tất nhiên, không phải bài toán tìm dãy con liên tiếp nào cũng có thể dung
phương pháp two pointer.

221
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

IV.BÀI TẬP

1. Dãy con liên tiếp có dạng sóng dài nhất

Cho một dãy số nguyên n phần tử a1, a2, .. an. Tìm dãy con liên tiếp có dạng
sóng dài nhất. Một dãy gọi là có dạng sóng khi với mỗi 3 số kề nhau bất kỳ trong
dãy, số đứng giữa hoặc lớn hơn hoặc nhỏ hơn hai số hai bên.

Hạn chế: Số phần tử trong dãy là khoảng một triệu với mỗi giá trị trong số đó có giá
trị lớn đến 109 .

2. Dãy con liên tiếp dài nhất có tổng bằng 0

Cho một dãy số nguyên n phần tử a1, a2, .. an. Tìm dãy con liên tiếp dài nhất có
tổng bằng 0.

Hạn chế: Dãy A chứa khoảng 105 Số nguyên , mỗi số có giá trị khoảng 109.

3. Dãy con liên tiếp có tổng lớn nhất

Cho dãy số nguyên n phần tử a1 a2 … an . Tìm dãy con liên tiếp có tổng lớn
nhất?

Hạn chế: Dãy A chứa khoảng 105 Số nguyên , mỗi số có giá trị khoảng 109.

4. THAM QUAN

Điều kiện làm việc cho phép trong khoảng thời gian n ngày liên tục nhân viên có thể
đăng ký nghỉ phép hoặc nghỉ bù. Steve quyết định tận dụng khả năng hiếm có này để
nghỉ ngơi liên tục tất cả k ngày mà mình được quyền. Sau khi cân nhắc, Steve đăng ký
tham gia một tour du lịch. Mỗi ngày công ty du lịch sẽ đưa khách tới tham quan một
thành phố. Trong bảng giới thiệu chương trình tham quan mỗi thành phố được ghi
tương ứng với một số nguyên dương không vượt quá 109, ngày thứ i sẽ tham thành phố
ai. Các thành phố khác nhau tương ứng với các số nguyên khác nhau. Một thành phố
có thể được tới nhiều lần. Steve có thể tham gia tour du lịch bắt đầu từ ngày tùy chọn
và dĩ nhiên, muốn thăm được càng nhiều thành phố khác nhau càng tốt.

Yêu cầu: Cho n, k và ai (1 ≤ k ≤ n ≤ 105). Hãy xác định ngày bắt đầu kỳ nghỉ để nghỉ
đủ k ngày và số lượng các thành phố Steve có thể tới thăm là lớn nhất. Ngày đầu tiên
222
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

trong chương trình của Công ty được đánh số là 1. Nếu tồn tại nhiều cách lựa chọn thì
đưa ra ngày sớm nhất.

Dữ liệu: Vào từ file văn bản EXCURS.INP:

 Dòng thứ nhất chứa 2 số nguyên n và k,


 Dòng thứ 2 chứa n số nguyên a1, a2, . . ., an.

Kết quả: Đưa ra file văn bản EXCURS.OUT một số nguyên – ngày được chọn để bắt
đầu tham quan.

Ví dụ:

EXCURS.INP EXCURS.OUT

73 2

1213121

5. Xâu con dài nhất

Cho một xâu n ký tự (từ A đến Z).

Tìm xâu con có các phần tử khác nhau dài nhất

Input

Gồm nhiều dòng ( 1000). Mỗi dòng ghi một xâu n ký tự (1  n  1000).

Output

Gồm nhiều dòng, dòng thứ i là kết quả của xâu thứ i, gồm hai số pi và qi, pi là
độ dài xâu con tìm được và qi vị trí ký tự đầu xâu con tìm được.

Nếu có nhiều xâu con dài nhất có độ dài bằng nhau thì xuất xâu có vị trí đầu tiên nhỏ
nhất. Hai xâu con

Ví dụ

ABCBJHEPE 6 3
AAA 11

6. KHỐI LẬP PHƯƠNG


223
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Quà sinh nhật của Jimmy là một bộ khối lập phương xếp hình. Jimmy xếp thành n

tháp, tháp thứ i có độ cao là ai (1 ≤ ai ≤ 109, 1 ≤ n ≤ 105, i =1 ÷ n).

Jimmy rất có cảm tình với số nguyên k, vì vậy dãy liên tục các tháp được coi là hài
hòa nếu chúng có độ cao trung bình là k (1 ≤ k ≤ 109).

Yêu cầu: Cho n, k và ai, i =1 ÷ k. Hãy xác định dãy tháp hài hòa dài nhất, chỉ ra tháp
đầu tiên và độ dài của dãy tìm được. Nếu tồn tại nhiều dãy cùng độ dài thì chỉ ra dãy
tháp có vị trí đầu nhỏ nhất.

Dữ liệu: Vào từ file văn bản CUBICS.INP:

 Dòng đầu tiên chứa 2 số nguyên n và k,


 N dòng sau, dòng thứ i chứa n số nguyên ai.

Kết quả: Đưa ra file văn bản CUBICS.OUT trên một dòng 2 số nguyên: độ dài của
dãy tìm được và số thứ tự của tháp đầu tiên hoặc hai số 0 nếu không tồn tại dãy.

Ví dụ:
CUBICS.INP CUBICS.OUT
53 32
1
2
3
4
6

7. HÁI NẤM
224
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Trước mặt Mario là một dãy n cây nấm xếp thành một hàng dài, mỗi cây nấm có một
giá trị riêng, là một số nguyên dương không vượt quá 1000. Mario không cần phải hái
hết nấm mà chỉ cần đạt tổng giá trị càng gần 100 càng tốt và chỉ được hái liên tục các

cây nấm cạnh nhau. Nếu có hai khả năng gần 100 tương đương nhau ( ví dụ 98 và 102)
Mario sẽ chọn phương án có giá trị lớn hơn.

Yêu cầu: Cho giá trị n cây nấm theo trình tự từ trái sang phải. Hãy xác định tổng giá
trị nấm Mario hái được.
Dữ liệu: Vào từ file văn bản MUSHROOM.INP gồm dòng đầu là số n (n <= 5.105), n
dòng sau, mỗi dòng chứa một số nguyên, dòng thứ i xác định giá trị cây nấm i.
Kết quả: Đưa ra file văn bản MUSHROOM.OUT một số nguyên – tổng giá trị nấm
Mario hái được.
Ví dụ:
MUSHROOM.INP MUSHROOM.OUT
1 110
2
3
5
8
13
21
34
55
79

8. Bài tập trên trang SPOJ


225
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

http://www.spoj.com/problems/HOTELS/

HOTELS - Hotels Along the Croatian Coast

There are N hotels along the beautiful Adriatic coast. Each hotel has its value in Euros.

Sroljo has won M Euros on the lottery. Now he wants to buy a sequence of
consecutive hotels, such that the sum of the values of these consecutive hotels is as
great as possible - but not greater than M.

You are to calculate this greatest possible total value.

Input

In the first line of the input there are integers N and M (1 ≤ N ≤ 300 000, 1 ≤ M < 231).

In the next line there are N natural numbers less than 106, representing the hotel values
in the order they lie along the coast.

Output

Print the required number (it will be greater than 0 in all of the test data).

Example

Input input

5 12 49

21345 7356

output Output

8
12

9. Bài tập trên trang Codeforce

http://codeforces.com/problemset/problem/716/B
B. Complete the Word

time limit per test 2 seconds


memory limit per test 256 megabytes
input standard input

226
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

output standard output

ZS the Coder loves to read the dictionary. He thinks that a word is nice if there exists
a substring (contiguous segment of letters) of it of length 26 where each letter of
English alphabet appears exactly once. In particular, if the string has length strictly
less than 26, no such substring exists and thus it is not nice.

Now, ZS the Coder tells you a word, where some of its letters are missing as he forgot
them. He wants to determine if it is possible to fill in the missing letters so that the
resulting word is nice. If it is possible, he needs you to find an example of such a word
as well. Can you help him?
Input

The first and only line of the input contains a single string s (1 ≤ |s| ≤ 50 000), the word
that ZS the Coder remembers. Each character of the string is the uppercase letter of
English alphabet ('A'-'Z') or is a question mark ('?'), where the question marks denotes
the letters that ZS the Coder can't remember.
Output

If there is no way to replace all the question marks with uppercase letters such that
the resulting word is nice, then print  - 1 in the only line.

Otherwise, print a string which denotes a possible nice word that ZS the Coder
learned. This string should match the string from the input, except for the question
marks replaced with uppercase English letters.

If there are multiple solutions, you may print any of them.

Examples
input
ABC??FGHIJK???OPQR?TUVWXY?
output
ABCDEFGHIJKLMNOPQRZTUVWXYS

227
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

input
WELCOMETOCODEFORCESROUNDTHREEHUNDREDANDSEVENTYTWO
output
-1

input
??????????????????????????
output
MNBVCXZLKJHGFDSAQPWOEIRUYT

input
AABCDEFGHIJKLMNOPQRSTUVW??M
output
-1

Note

In the first sample case, ABCDEFGHIJKLMNOPQRZTUVWXYS is a valid answer


beacuse it contains a substring of length 26 (the whole string in this case) which
contains all the letters of the English alphabet exactly once. Note that there are many
possible solutions, such
as ABCDEFGHIJKLMNOPQRSTUVWXYZ or ABCEDFGHIJKLMNOPQRZTUV
WXYS.

In the second sample case, there are no missing letters. In addition, the given string
does not have a substring of length 26 that contains all the letters of the alphabet, so
the answer is  - 1.

In the third sample case, any string of length 26 that contains all letters of the English
alphabet fits as an answer.

PHỤ LỤC

228
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

1.1.15. Trong phần phụ lục này, chúng tôi xin lấy bài viết về kỹ thuật sử dụng hai
con trỏ của tác giả Dương Thanh Phong đã đăng trên trang blogs của tác giả bổ xung
vào chuyên đề này để thấy rõ hơn mức độ hiệu quả của kỹ thuật sử dụng hai con trỏ
trong giải thuật.

1.1.16.

1.1.17. TWO POINTERS


Người viết: Dương Thành Phong

Two pointers là kĩ thuật đơn giản, code ngắn, nhưng hiệu quả. Nó ít được biết đến vì
nó rất ít được dùng khi làm ứng dụng và trong chương trình dạy. Không mất thời gian,
ta bắt đầu bằng một ví dụ: Bạn được giao nhiệm vụ phân tích trên đóng dữ liệu, dữ liệu
là dãy số tự nhiên có N phần tử a0, a1, …, a(N-1) , bạn phải tính có bao nhiêu dãy con
liên tiếp thỏa mãn tổng của chúng lớn hơn hoặc bằng S(S>0). Chẳng hạn với dãy số
{1, 2, 5, 4, 2}, S=8. Dãy con liên tiếp là dãy được trích từ dãy ban đầu và các phần tử
được lấy ra phải liên tiếp nhau. Ta có các dãy con sau thỏa mãn {1, 2, 5}, {1, 2, 5, 4},
{1, 2, 5, 4, 2}, {2, 5, 4}, {2, 5, 4, 2}, {5, 4}, {5, 4, 2}. Tui chắc chắn bạn đã nghĩ ra
cách để tìm ra có bao nhiêu dãy con như thế. Bạn hãy code ra, chạy thử trên bộ dữ liệu
này input . Nếu code của bạn chạy trong 1s mà cho ra kết quả là 302572338891thì bạn
ko cần đọc tiếp bài viết này, và bạn có thể cho mình xin code tham khảo J. Nếu code
của bạn không thể chạy cho dữ liệu lớn như vậy hoặc chạy sai kết quả, thì bạn cứ tiếp
tục đọc nhé. Có phải cách của bạn là: Với cách này chạy ra kết quả đúng, nhưng trong
trường hợp xấu nhất nó mất thời gian O(N^2), khó mà ra kết quả trong 1s với N=10^6.
Vấn đề ở đoạn code trên chính là nó cộng lại nhiều lần. Sau khi vòng lặp thứ nhất i=0,
nó đã tính Run=a0+a1+…+a(N-1). Đến i=1, nó lại cộng Run=a1+a2+…+a(N-1). Ta phải
tìm cách giảm thiểu số lần tính lại nhưng vẫn đảm bảo kết quả đúng. Ta muốn số lần
cộng vào biến Run được giảm xuống, vì thế giá trị của biến Run ở lần chạy trước phải
được sử dụng lại ở lần chạy sau, có như thế thì mới giảm số lần chạy được. Bạn thử
suy nghĩ một lát rồi hãy tiếp tục đọc. Và đây là lời giải của tui cho bài toán này:

229
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Ý tưởng đoạn code trên là với mỗi chỉ số end, ta tìm chỉ số start lớn nhất sao
cho a[start]+a[start+1]+…+a[end] >=S. Như thế sẽ có start+1 chuỗi con thỏa mãn
điều kiện có chỉ số cuối là end. Các dãy đó là {a0, a1, .., a[end]}, { a1,a2 .., a[end]},
{a[start], a[start+1], .., a[end]}. Vì thế ta cộng dồn vào kết quả start+1.

Nhìn vào đoạn code thứ hai, ta thấy vẫn có 2 vòng lặp chạy lồng nhau. Nhưng trên
thực tế đoạn code này chỉ chạy trong (2*N) cho tất cả các trường hợp, thay vì chạy
trong (N^2) như đoạn code thứ nhất. Ta bắt đầu phân tích:

1. Vòng lặp for sẽ chạy trong N


2. Vòng lặp while có số lần chạy khá phức tạp, nhưng ta để ý: nếu ta cộng
tất cả lần chạy của vòng lặp while, thì chính là số lần chạy của biến start từ 0
đến N-1. Với mỗi lần chạy của vòng lặp for, ta không xác định được số lần
chạy của vòng lặp while. Tuy nhiên, tổng số lần chạy của vòng while luôn nhỏ
hơn N vì giá trị biến start chạy không thể lớn hơn end, đồng nghĩa không thể
lớn hơn N.

Ý tưởng:

Ý tưởng của kĩ thuật này là có hai biến chạy song song nhau, ở đây là start và end. Số
lần chạy của biến này không phụ thuộc vào biến kia. Và yêu cầu bài toán thường là tìm
dãy con liên tiếp có tính chất: nếu ta có dãy con thỏa mãn A, thì thêm vào một phần
tử cũng vẫn thỏa mãn A. Biến startcó nhiệm vụ giữ chỉ số đầu của dãy con,
biến end giữ chỉ số cuối của dãy con. Tùy bài toán mà ta cho biến start chạy rồi tìm

230
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

biến end tương ứng, hoặc cho biến end chạy rồi tìm biến start tương ứng. Trong ví dụ
trên là biến end chạy và ta tìm biến start tương ứng thỏa tính
chất a[start]+a[start+1]+…+a[end] >=S. Trước khi kết thúc ta xét thêm một ví dụ:
Một bản nhạc hơi dài có N (N=10^6) nốt nhạc, độ cao mỗi nốt nhạc là hi (từ 0 đến 20).
Ta cần tìm ra đoạn nhạc có chiều dài nhỏ nhất mà trong đó độ chênh lệch độ cao giữa
nốt cao nhất và nốt thấp nhất phải lớn hơn hoặc bằng H (từ 0 đến 20). Nhiệm vụ trước
tiên là tìm ra chiều dài nhỏ nhất đó, sau khi có chiều dài nhỏ nhất thì việc in ra các dãi
con là dễ dàng. Nhận xét: bài toán cũng thuộc dạng tìm dãy con liên tiếp. Nếu ta có
một dãy thỏa điều kiện độ chênh lệch độ cao giữa nốt cao nhất và nốt thấp nhất phải
lớn hơn hoặc bằng H, ta thêm vào một phần tử vào dãy này, thì ta có một dãy mới
cũng thỏa tính chất đó (diễn đạt khác đi [max{ai, x} -min{ai, x}] >= [max{ai} -min{ai
}] >= H ). Cách giải thì tương tự ví dụ trên, nên sẽ không được giải thích
lại.

231
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Tổng kết: Nếu bạn chưa bao giờ dùng kĩ thuật này, tui nghĩ bạn cũng chưa thật sự
hiểu nó. Bạn hãy tự giải lại bài toán trên. Bài viết cũng chỉ gợi ý tưởng cho bạn, chứ
thực sự mình cũng chưa đủ trình độ để có thể viết một bài cho bạn hiểu liền. Nếu hứng
thú bạn có thể thực hành nhiều hơn về kĩ thuật này
tại http://codeforces.com/problemset/tags/two%20pointers

TÀI LIỆU THAM KHẢO

1. The Two Pointer Algorithm Prateek Gupta

https://tp-iiita.quora.com/The-Two-Pointer-Algorithm
2. The Two pointer Algorithm !!

https://ilovealgorithms.wordpress.com/2012/11/05/the-two-pointer-algorithm/

3. TWO POINTERS Người viết: Dương Thành Phong (phần phụ lục)

4. Bài tập trên trang Codeforces http://codeforces.com/

5. Bài tập trên trang SPOJ http://www.spoj.com/

6. Tài liệu Bài tập của thầy Thanh Tùng.

232
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

DÃY CON CHUNG DÀI NHẤT

Nguyễn Tấn Phát


THPT Chuyên Phan Ngọc Hiển – Cà Mau

Bài 1: QBSTR spoj – Xâu con chung dài nhất


http://vn.spoj.com/problems/QBSTR/
Hướng giải quyết
 Gọi F[i,j] là độ dài xâu con chung dài nhất tìm được khi xét i kí tự đầu tiên
trong A và j kí tự đầu tiên trong B.
 Nếu A[i]=B[j] thì F[i,j]:=F[i-1,j-1]+1.
 ngược lại F[i,j]:=max(F[i-1,j],F[i,j-1]).
Code
program qbstr;
uses math;
const fi='';
nmax=1000;
var f:text; s1,s2:ansistring;
KQ:array[0..nmax+10,0..nmax+10] of word;
procedure docfile;
begin
assign(f,fi);
reset(f);
readln(f,s1);
readln(f,s2);
close(f);
end;
procedure bpa;
var i,j:word;
begin
for i:=0 to length(s1) do kq[i,0]:=0;
for i:=0 to length(s2) do kq[0,i]:=0;
233
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

for i:=1 to length(s1) do


for j:=1 to length(s2) do
if s1[i]=s2[j] then KQ[i,j]:=kq[i-1,j-1] + 1
else KQ[i,j]:=max(kq[i,j-1],kq[i-1,j]);
writeln(kq[length(s1),length(s2)]);
end;
begin
docfile;
bpa;
end.
Bài 2: BLGEN spoj – Chuỗi gen đặc trưng
http://vn.spoj.com/problems/BLGEN/
Hướng giải quyết
– Từ mỗi dãy đã cho ta loại đi những số không phải là chính phương hoặc không
phải là lập phượng của số nguyên tố, có thể dùng SÀNG SNT + (chặt NP hoặc
công thức tính căn bậc 3).
– Tiếp theo dùng QHĐ tìm dãy con chung dài nhất thì sẽ thu được kết quả bài
toán.
Code
uses math;
const fi ='';
fo ='';
oo =1000;
maxc =trunc(3e6);
var n :array[1..2] of longint;
a :array[1..2,1..1000] of qword;
f :array[0..oo,0..oo] of longint;
mp :longint;
g :array[1..maxc] of longint;
nto :Array[1..300000] of qword;
procedure nhap;
var i :longint;
234
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

k :longint;
p :qword;
begin
assign(input,fi);
reset(input);
k:=0;
while not seekeof() do
begin
inc(k);
while not seekeoln() do
begin
inc(n[k]);
read(p);
a[k,n[k]]:=p;
end;
readln;
end;
close(input);
end;
procedure sang;
var i,j :longint;
begin
for i:=2 to trunc(sqrt(maxc)) do
if g[i]=0 then
begin
j:=i*I;
while j<=maxc do
begin
g[j]:=i;
inc(j,i);
end;
end;
235
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

for i:=2 to maxc do


if g[i]=0 then
begin
inc(mp);
nto[mp]:=i;
end;
end;
function find(x:qword):boolean;
var d,c,mid :longint;
t1,t2,t3 :qword;
begin
d:=1; c:=mp;
while d<=c do
begin
mid:=(d+c) div 2;
t1:=x mod nto[mid];
t2:=x div nto[mid];
t3:=nto[mid]*nto[mid];
if (t1=0) and (t2=t3) then exit(true);
if (t3>t2) then c:=mid-1
else d:=mid+1;
end;
exit(false);
end;
function kt(x:qword):boolean;
var x1 :qword;
begin
x1:=trunc(sqrt(x));
if x1*x1=x then exit(true);
if find(x) then exit(true);
exit(false);
end;
236
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

procedure xuli;
var i,j :longint;
begin
for i:=1 to n[1] do
for j:=1 to n[2] do
if (a[1,i]=a[2,j]) and kt(a[1,i]) then f[i,j]:=f[i-1,j-1]+1
else f[i,j]:=max(f[i-1,j],f[i,j-1]);
end;
procedure xuat;
begin
assign(output,fo);
rewrite(output);
writeln(f[n[1],n[2]]);
close(Output);
end;
begin
nhap;
sang;
xuli;
xuat;
end.

Bài 3: STMERGE spoj – VOI 2013 – Trộn xâu


http://vn.spoj.com/problems/STMERGE/
Hướng giải quyết
– Gọi F[i][j][k] là tổng nhỏ nhất tạo thành từ xâu X[1..i] và Y[1..j]
 k = 0 tương ứng trường hợp kí tự cuối là Y[j]
 k = 1 tương ứng kí tự cuối là X[i]
– Trường hợp 1: kí tự cuối là Y[j], tức F[i][j][0], thì kí tự trước đó có hai trường
hợp:
 Y[j-1] Y[j] ⇒ F[i][j][0] = F[i][j-1][0]

237
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

 X[i] Y[j] ⇒ f[i][j][0] = f[i][j-1][1] + c[i][j]


– Trường hợp 2: kí tự cuối là X[i], tức F[i][j][1], thì kí tự trước đó có hai trường
hợp:
 X[i-1] X[i] ⇒ f[i][j][1] = f[i-1][j][1]
 Y[j] X[i] ⇒ f[i][j][1] = f[i-1][j][0] + c[i][j]
Code
#include <bits/stdc++.h>
using namespace std;
int q,m,n,c[1001][1001];
long long f[1001][1001][2];
int main()
{
cin>>q;
for(int i=1; i<=q; i++)
{
cin>>m>>n;
for(int i=1; i<=m; i++)
for(int j=1; j<=n; j++)
{
cin>>c[i][j];
f[i][j][0]=f[i][j][1]=0;
}
f[1][1][0] = f[1][1][1] = c[1][1];
for(int j=2; j<=n; j++)
{
f[1][j][0]=min(f[1][j-1][0],
f[1][j-1][1]+c[1][j]);
f[1][j][1]=c[1][j];
}
for(int i=2; i<=m; i++)
{
f[i][1][0]=c[i][1];
238
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

f[i][1][1]=min(f[i-1][1][1],
f[i-1][1][0]+c[i][1]);
}
for(int i=2; i<=m; i++)
for(int j=2; j<=n; j++)
{
f[i][j][0]=min(f[i][j-1][0],
f[i][j-1][1]+c[i][j]);
f[i][j][1]=min(f[i-1][j][1],
f[i-1][j][0]+c[i][j]);
}
cout<<min(f[m][n][0],f[m][n][1])<<"\n";
}
}

Bài 4: LNACS spoj – VOI2010 Dãy con chung không liền kề dài nhất
http://vn.spoj.com/problems/LNACS/
Hýớng giải quyết
Bài này làm quy hoạch động tương tự bài dãy con chung dài nhất. Tuy nhiên có một
chút khác ở công thức.
Gọi f[i][j] là độ dài dãy con chung dài nhất của 2 dãy a[1…i] và b[1…j]. Ta có:
 nếu a[i]!=b[j] thì: f[i][j]=max(f[i-1][j], f[i][j-1],f[i-1][j-1]).
 nếu a[i]==b[j] thì: f[i][j]=max(f[i-1][j], f[i][j-1],f[i-1][j-1],f[i-2][j-2]+1).
Về phần khởi tạo:
 f[0][0] =0;
 f[1][j] = 1 nếu a[1]=b[j].
 f[1][j] = f[1][j-1] nếu a[1]!=b[j].
 f[i][1] = 1 nếu a[i]==b[1].
 f[i][1] = f[i-1][1] nếu a[i]!=b[1].
code
uses math;

239
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

const maxm=10000;
maxn=1000;
var m,n:longint;
a,b:array[0..maxn] of longint;
f:array[-1..maxn,-1..maxn] of longint;
procedure docfile;
var i:longint;
begin
readln(m,n);
for i:=1 to m do readln(a[i]);
for i:=1 to n do readln(b[i]);
end;
procedure xuli;
var i,j:longint;
begin
for i:=1 to m do
begin
f[i,0]:=0;
if a[i]=b[1] then f[i,1]:=1 else f[i,1]:=f[i-1,1];
end;
for j:=1 to n do
begin
f[0,j]:=0;
if a[1]=b[j] then f[1,j]:=1 else f[1,j]:=f[1,j-1];
end;
for i:=2 to m do
begin
for j:=2 to n do
begin
if a[i]=b[j] then
begin
f[i,j]:=f[i-2,j-2]+1;
240
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

if f[i,j]<f[i-1,j] then f[i,j]:=f[i-1,j];


if f[i,j]<f[i,j-1] then f[i,j]:=f[i,j-1];
if f[i,j]<f[i-1,j-1] then f[i,j]:=f[i-1,j-1];
end
else
begin
f[i,j]:=f[i-1,j-1];
f[i,j]:=max(f[i-1,j],f[i,j-1]);
end;
end;
end;
end;
begin
assign(input,''); reset(input);
assign(output,''); rewrite(output);
docfile;
xuli;
writeln(f[m,n]);
close(input);close(output);
end.
Bài 5: LCS2X spoj VOI2014 – Dãy con chung bội hai dài nhất
http://vn.spoj.com/problems/LCS2X/

241
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

CHUYÊN ĐỀ HÌNH HỌC

THPT Chuyên Nguyễn Bỉnh Khiêm – Vĩnh Long

I. Khái niệm hình học và các đối tượng cơ bản

1. Khái niệm hình học

Đa số các thuật toán đều tập trung vào văn bản và các con số, chúng được thiết kế và
xử lí sẵn trong phần lớn các môi trường lập trình. Đối với các bài hình học thì tình
huống khác hẳn, ngay cả các phép toán sơ cấp trên điểm và đoạn thẳng cũng là một
thách thức về tính toán.

Các bài toán hình học thì dễ hình dung một cách trực quan nhưng chính điều đó lại có
thể là một trở ngại. Nhiều bài toán có thể giải quyết ngay lập tức bằng cách nhìn vào
một mảnh giấy nhưng lại đòi hỏi một chương trình không đơn giản

2. Đối tượng hình học cơ bản

Trong các bài toán tin học thuộc loại hình học có 3 đối tượng cơ bản là: Điểm, đoạn
thẳng và đa giác

- Điểm: được xác định là cặp (x;y) trong tọa độ Đề - các

- Đoạn thẳng: là cặp điểm được nối với nhau bằng một phần của đường thẳng

- Đa giác: là dãy các điểm mà 2 điểm liên tiếp nối với nhau bởi đoạn thẳng và điểm
đầu nối với điểm cuối tạo thành đường gấp khúc khép kín

3. Dữ liệu lưu trữ các đối tượng hình học cơ bản

Type Point=record
x, y: integer; end;
Line=Record
P1,p2: Point; end;
Var Polygon: Array[0..Nmax] of Piont;

II. Một số phép toán cơ bản

1. Vị trí tương đối của điểm so với đường thẳng, tia và đoạn thẳng

Bài toán 1: Cho điểm M(x0;y0) A(xA; yA) ; B(xB; yB).

242
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Yêu cầu:

a. Kiểm tra M có thuộc đường thẳng đi qua 2 điểm A,B hay không?

b. Kiểm tra M có thuộc đoạn AB hay không?

c. Kiểm tra M có thuộc tia AB hay không?

Dữ liệu vào từ file văn bản BAITOAN1.INP gồm 1 dòng duy nhất chứa tọa độ của
các điểm M, A, B.

Kết quả ghi ra file văn bản BAITOAN1.OUT là kết quả của đề bài

Ví dụ:

BAITOAN1.INP BAITOAN1.OUT

301020 M(3,0) thuoc duong thang AB

M(3,0) khong thuoc doan thang


AB

M(3,0) thuoc tia AB

-1 -1 0 1 1 3 M(-1,-1) thuoc duong thang AB

M(-1,-1) khong thuoc doan thang


AB

M(-1,-1) khong thuoc tia AB

Phương pháp

Đặt F(X;Y)=(yA- yB)X+(xB- xA)Y+(xAyB- xByA).

- Điểm M thuộc đường thẳng AB khi F(x0;y0)=0.

- Điểm M thuộc đoạn AB khi:

F(x0;y0)=0 và Min(xA, xB)x0 Max(xA, xB) và Min(yA, yB)y0 Max(yA, yB)

- Điểm M thuộc tia AB khi: F(x0;y0)=0 và

AM  k AB có nghĩa là M phải thuộc mãn điều kiện: F(x0;y0)=0

243
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

và (x0- xA)( xB- xA)≥0, (y0- yA)( yB- yA)≥0

2. Giao của các đoạn thẳng, đường thẳng và tia

Bài toán 2: Cho 2 đường thẳng có phương trình a1x+b1y+c1=0 và a2x+b2y+c2=0. Tìm
giao điểm (nếu có) của hai đường thẳng trên.

Dữ liệu vào từ file BAITOAN2.INP gồm các hệ số a1, b1, c1, a2, b2, c2.

Kết quả ghi ra file BAITOAN2.OUT là vị trí tương đối của hai đường thẳng.

Ví dụ:

BAITOAN2.INP BAITOAN2.OUT

123123 Hai duong thang trung nhau

3 -2 1 3 -2 -1 Hai duong thang song song

1 2 -3 3 5 -1 Hai duong thang cat nhau tai M(-


13.0,8.0)

Phương pháp

B1: Tính D=a1b2- a2b1, Dx=c2b1- c1b2, Dy=a2c1- a1c2.

B2: Xét 3 khả năng:

- Nếu D=Dx=Dy=0 thì kết luận 2 đường thẳng trùng nhau.

- Nếu D=0 và ((Dx0) hoặc (Dy≠0)) thì kết luận hai đường thẳng song song

- Nếu D≠ 0 thì kết luận 2 đường thẳng cắt nhau tại điểm có tọa độ  Dx Dy 
 ; 
 D D 

Bài toán 3: Cho 2 đoạn thẳng AB, CD với A(xa;ya), B(xb;yb), C(xc;yc), D(xd,yd) .
Tìm giao điểm (nếu có) của 2 đoạn thẳng.

Dữ liệu vào từ file BAITOAN3.INP gồm tọa độ các điểm A(xa;ya), B(xb;yb),
C(xc;yc), D(xd,yd)

Kết quả ghi ra file BAITOAN3.OUT là giao điểm của hai đoạn thẳng AC và CD (nếu
có).

Ví dụ:
244
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

BAITOAN3.INP BAITOAN3.OUT

12322023 Giao diem cua hai doan AB va CD la:


M(2.0,2.0)

-3 -2 1 -2 -2 -1 3 2 Hai doan AB va CD khong giao nhau

Phương pháp

B1: Tìm giao điểm M của 2 đường thẳng AB và CD

B2: Kiểm tra M có thuộc đồng thời cả 2 đoạn AB và CD hay không. Nếu có đó là
giao điểm cần tìm, ngược lại kết luận không có.

Bài toán 4: Cho tia AM chứa điểm B (khác A) và đoạn thẳng CD với A(xa;ya),
B(xb;yb), C(xc;yc), D(xd;yd). Tìm giao điểm (nếu có) của tia AM với đoạn thẳng CD.

Dữ liệu vào từ file BAITOAN4.INP gồm tọa độ các điểm A(xa;ya), B(xb;yb),
C(xc;yc), D(xd,yd)

Kết quả ghi ra file BAITOAN4.OUT là giao điểm của hai đoạn thẳng AC và CD (nếu
có).

Ví dụ:

BAITOAN4.INP BAITOAN4.OUT

12223034 Giao diem cua tia AB va doan CD la:


N(3.0,2.0)

1 2 4 5 -1 1 3 1 Tia AB va doan CD khong giao nhau

Phương pháp

B1: Tìm giao điểm N của 2 đường thẳng AB và CD

B2. Kiểm tra N có thuộc tia AM và đoạn thẳng CD hay không. Nếu có đó là giao điểm
cần tìm, ngược lại kết luận không có.

II. Một số dạng bài toán hình học thường gặp

Dạng 1. Mối quan hệ giữa điểm, đoạn thẳng, đa giác


245
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Phương pháp: Đây là một trong số dạng bài toán hình học đơn giản nhất. Việc giải bài
toán dạng này chủ yếu sử dụng các kiến thức hình học cơ bản (đã trình bày đầy đủ
trong phần trên)

Ví dụ 1. Ba điểm thẳng hàng

Cho N điểm, hãy kiểm tra xem có bao nhiêu bộ 3 điểm thẳng hàng.

Input: Cho trong file văn bản THANGHANG.INP gồm:

- Dòng thứ 1 ghi số N

- N dòng tiếp theo, mỗi dòng ghi tọa độ của một điểm.

Output: ghi vào file văn bản THANGHANG.OUT gồm:

- Dòng 1 chứa một số duy nhất k là số bộ 3 điểm thẳng hàng.

- k dòng tiếp theo mỗi dòng là tọa độ của 3 điểm thẳng hàng.

Ví dụ:

THANGHANG.INP THANGHANG.OUT

6 3

00 (0,0) (0,1) (0,2)

01 (0,0) (1,1) (2,2)

02 (0,2) (1,2) (2,2)

11

12

22

Ví dụ 2. Đường thẳng cắt nhau

Cho n đường thẳng AiBi (1<=i<=n) phân biệt với Ai, Bi là các điểm cho trước.

Yêu cầu: Hãy cho biết các cặp đường thẳng đôi một cắt nhau.

246
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Dữ liêu vào từ file văn bản DT.INP gồm n dòng (n không biết trước). Dòng thứ i ghi
4 số thực XAi, YAi, XBi, YBi. Các số trên cùng một dòng ghi cách nhau ít nhất một dấu
cách.

Kết quả ghi ra file văn bản DT.OUT là kết quả của bài toán.

Ví dụ:

DT.INP DT.OUT

0011 Duong thang 1 cat duong thang 3

0112 Duong thang 2 cat duong thang 3

-1 -1 8 9

Ý tưởng:

B1. Mỗi đường thẳng được đặc trưng bởi 3 thông số a, b, c được xác định:

a:=y1-y2; b:=x2-x1; c:=x1*y2-x2*y1;

B2. Hai đường thẳng cắt nhau khi: D<>0 ( với D=a1*b2-a2*b1)

Ví dụ 3. Điểm thuộc đa giác

Cho đa giác không tự cắt (để cho dễ ở đây ta xét đa giác lồi) A1A2...AN với các đỉnh
Ai(xi, yi) nguyên. Với điểm A(XA, YA) cho trước.

Lưu ý: Tọa độ các đỉnh của đa giác theo chiều kim đồng hồ

Yêu cầu: Hãy xác định xem A có nằm trong đa giác đã cho hay không

Dữ liệu: Cho trong tệp DAGIAC.INP gồm:

- Dòng đầu là số N

- N dòng tiếp theo mỗi dòng ghi xi, yi là tọa độ của Ai

- Dòng N + 2 ghi số XA và YA (Dữ liệu là các số nguyên)

Kết quả: ghi ra tệp DAGIAC.OUT điểm A có nằm trong đa giác hay không

247
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Ví dụ:

DAGIAC.INP DAGIAC.OUT

4 Diem A(6,5) khong nam trong da giac

00

50

55

05

65

Ý tưởng:

- Lưu tọa độ các đỉnh đa giác vào mảng A

- Kiểm tra xem điểm A có trùng với các đỉnh của đa giác

- Kiểm tra xem điểm A có nằm trên các cạnh của đa giác

- Tìm giao điểm nếu có của tia Ax (Ax//Ox và Ax hướng theo phần dương trục hoành)
với các cạnh của đa giác. Cụ thể:

+ Giả sử điểm A(X0, Y0), chọn điểm B(Xb, Yb) với Xb=X0+1 (vì điểm B thuộc tia
Ax và theo chiều dương của trục Ox, ta có thể chọn Xb=X0 cộng với một giá trị
khác thay vì chọn 1), Yb=Y0

 Nếu tia AB có cắt đoạn thẳng CD (xem lại phương pháp ở phần trên), thì tăng
số giao điểm lên 1;

 Nếu đỉnh của đa giác thuộc tia AB thì tăng số giao điểm lên 1 (điều kiện đỉnh
i thuộc tia AB là (a[i].y=a[n+2].y)and(a[n+2].x<=a[i].x);

+ Đếm số giao điểm, nếu số giao điểm lẻ thì A thuộc đa giác ngược lại thì A
không thuộc đa giác.

248
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Lưu ý: Kiểm tra các đỉnh có nằm trên tia MN hay không

//Tham khảo thêm

Tư tưởng cho bài toán này nói qua thì rất đơn giản và dễ hiểu: Từ điểm cần kiểm
tra ta kẻ một tia bất kỳ, nếu tia đó giao với đa giác một số chẵn lần thì có nghĩa là nó
nằm ngoài đa giác, một số lẻ lần thì nó nằm trong đa giác.

Nhưng trên thực tế có rất nhiều trường hợp cần phải được giải quết triệt để:

Cho một đa giác lồi, hãy xác định xem một điểm có nằm trong đa giác đó hay không?

Ta hãy tận dụng tính “lồi” của đa giác. Vì là đa giác lồi nên nó chỉ giao với một
đường thẳng bất kỳ tại không quá hai điểm (chính xác thì là 2 hoặc là 0). Chú ý, “giao”
ở đây là khi ta đi vòng quanh đa giác, ta đi từ một bên của đường thẳng sang bên kia
của nó.

Đến đây, hẳn nhiều bạn cũng đã rõ thuật toán. Thay bằng việc ta đi vòng quanh
đa giác, ta chỉ tìm hai cạnh của đa giác cắt đường thẳng chứa điểm cần xét. Tất nhiên
trường hợp không đoạn nào như vậy là tầm thường. Cũng có trường hợp có nhiều hơn
hai đoạn như vậy nếu đường thẳng y = t.y đi qua đỉnh của đa giác. Nhưng cũng chẳng
đáng chú ý, ta chọn một trong hai đoạn bất kỳ chứa đỉnh đó thôi. Sau khi chọn được
hai cạnh của đa giác rồi, ta xét xem có bao nhiêu cạnh (trong hai cạnh) cắt tia cần xét.

249
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Để đơn giản, ta cũng trọn đường thẳng là đường y = t.y, và tia có hướng cùng chiều
với chiều dương của trục Ox (Với cách trọn bất kỳ cũng không thực sự phức tạp hơn,
bạn đọc hãy thử coi nó như là một trò giải trí?)

y = t.y

Chú ý rằng chi phí chủ yếu cho thuật toán này lại là phép tìm kiếm. Vì vậy, nếu ta
áp dụng phép tìm kiếm tuần tự vào đây thì đúng là vô nghĩa. Bằng cách chia đa giác
làm hai nửa, phân các nhau bởi hai điểm thấp “nhất” và “cao” nhất (hai đỉnh tô đậm
trên hình vẽ). Ta chỉ phải tìm kiếm nhị phân (xin đọc phần “cấu trúc dữ liệu và giải
thuật”) trên hai nửa này thôi, mỗi nửa sẽ tìm ra một cạnh thoả mãn. Ta tìm điểm i có
toạ độ lớn nhất nhưng nhỏ hơn t.y, cạnh (p[i], p[i + 1]) sẽ là cạnh cần tìm. Như vậy,
chi phí thuật toán cho bài toán này tỷ lệ với LogN. Đến đây, mọi việc gần như đã được
giải quyết, các công việc còn lại là bài tập thực hành của bạn đọc.

Thuật toán chẳng có ý nghĩa gì khi câu hỏi “một điểm có nằm trong đa giác hay
không?” chỉ được dùng có một lần. Nhưng trong trường hợp phải trả lời nhiều lần câu
hỏi như vậy thì thuật toán thực sự đã phát huy tác dụng.

Dạng 2: Tính diện tích của một đa giác

Phương pháp Giả sử cho đa giác có N đỉnh và tọa độ các đỉnh lưu vào mảng a. Để
tính diện tích đa giác ta làm như sau:

B1. Gắn thêm đỉnh phụ:

250
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

A[n+1].x:=a[1].x; A[n+1].y:=a[1].y

B2. Diện tích đa giác được tính theo công thức

n
S=   a i  1.x  a i .x   a i  1. y  a i . y  / 2
i 1

Lưu ý: Có thể áp dụng công thức khác để tính diện tích đa giác trong các trường hợp
đặc biệt:

- Nếu đa thức là tam giác (n=3) thì diện tích tính theo công thức

S p  p  a  p  b  p  c  với p=(a+b+c)/2

- Nếu đa giác là hình chữ nhật (n=4) có các cạnh là a,b thì diện tích là: S=a.b

- Nếu đa giác là hình vuông (n=4) có cạnh là a thì diện tích là: S= a2

- Nếu đa giác là hình tròn có bán kính R thì diện tích S= R 2

Ví dụ 1: Xác định diện tích đa giác

Cho N đa giác lồi A1A2…AN với các đỉnh Ai(xi;yi) có tọa độ nguyên. Hãy tính diện
tích đa giác trên

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

- Dòng 1: Chứa số nguyên dương N

- Dòng 2: chứa 2xN số nguyên dương x1y1x2y2…xNyN là tọa độ các đỉnh của đa giác.
Mỗi số ghi cách nhau một dấu cách.

Kết quả ghi ra file DIENTICH.OUTgồm một dòng duy nhất chứa diện tích đa giác
(có hai chữ số ở phần thập phân)

Ví dụ:

DIENTICH.INP DIENTICH.OU
T

251
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

5 32.00

-8-80010-24-5
0

Ý tưởng:

B1. Lưu tọa độ các đỉnh đa giác vào mảng toado

B2. Sử dụng công thức tính diện tích đa giác

B3. Kết quả cần tìm là abs(s)

Ví dụ 2. Dãy hình chữ nhật

Trong mặt phẳng tọa độ trực chuẩn, cho N hình chữ nhật có các cạnh song song với
trục tọa độ. Mỗi hình chữ nhật được xác định bởi tọa độ đỉnh dưới bên trái và tọa độ
đỉnh trên bên phải của nó. Hãy đưa ra dãy các hình chữ nhật theo thứ tự tăng dần diện
tích.

Dữ liệu: Cho trong file HCN.INP gồm N+1 dòng

- Dòng 1: Chứa số N

- Dòng i+1 ( 1 I  N): ghi 4 số nguyên x1, y1, x2, y2 lần lượt là tọa độ đỉnh dưới bên
trái và đỉnh trên bên phải của hình chữ nhật i (các số ghi trên một dòng cách nhau ít
nhất một dấu cách)

Kết quả: Ghi ra file HCN.OUT dãy các hình chữ nhật sau khi được sắp xếp.

Ví dụ:

HCN.INP HCN.INP

5 -3-40-2

-3-40-2 -6-2-30

-6-400 -6-4 00

-6-2-30 -6 0 07

252
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

0077 0 0 77

-6007

Ý tưởng:

B1. Lưu tọa độ các đỉnh đa giác vào mảng a

B2. Tính diện tích hình chữ nhật theo công thức: S=  x2  x1  y2  y1 

B3. Sắp xếp mảng a tăng dần theo diện tích.

Dạng 3: Xác định diện tích phủ bởi các hình chữ nhật

Phương pháp

Giả sử có N hình chữ nhật. Để tính diện tích phủ bởi N hình chữ nhật ta làm như sau:

B1: Sử dụng a, b lần lượt là các mảng lưu hoành độ và tung độ các đỉnh hình chữ nhật
(mỗi hình chữ nhật chỉ cần lưu tọa độ 2 đỉnh đối diện nhau qua tâm hình chữ nhật)

B2. Sắp xếp mảng a, b theo thứ tự tăng dần

B3. Lần lượt kiểm tra các hình chữ nhật có tọa độ đỉnh trên bên phải là (xi+1;yi+1) và
tạo độ đỉnh dưới bên phải là (xi;yi) với 1 i  n- 1. Nếu hình chữ nhật này thuộc một
trong các hình chữ nhật ban đầu thì cộng thêm vào phần diện tích đang cần tìm diện
tích của hình chữ nhật con này.

Ví dụ 1. Diện tích phủ bởi các hình chữ nhật

Trong mặt phẳng tọa độ trực chuẩn, cho N hình chữ nhật có các cạnh song song với
trục tọa độ. Mỗi hình chữ nhật được xác định bởi tọa độ đỉnh dưới bên trái và đỉnh trên
bên phải của nó. Hãy tính diện tích phần mặt phẳng bị phủ bởi các hình chữ nhật trên.

Dữ liệu: Cho trong file DT_HCN.INP gồm N+1 dòng

- Dòng 1: Chứa số N

- Dòng i+1 (1 i  N): ghi 4 số nguyên x1, y1, x2, y2 lần lượt là tọa độ đỉnh dưới bên
trái và đỉnh trên bên phải của hình chữ nhật i

Các số ghi trên một dòng cách nhau ít nhất một dấu cách

253
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Kết quả: Ghi ra file DT_HCN.OUT diện tích phần mặt phẳng bị phủ bởi hình chữ
nhật trên.

Ví dụ:

DT_HCN.INP DT_HCN.OUT

2 9

0124

1032

3 11

0124

1032

-1 2 1 4

Ý tưởng:

B1. Lập mảng X[1...2N], Y[1..2N] lần lượt chứa hoành độ, tung độ các hình chữ nhật

B2. Lưu tọa độ ban đầu các hình chữ nhật vào mảng a

B3. Sắp xếp mảng X, Y tăng dần.

B4. Lần lượt kiểm tra các hình chữ nhật có tọa độ đỉnh dưới bên trái (xi;yi) và tọa độ
đỉnh trên bên phải (xi+1;yi+1) với 1 i  n- 1. Nếu hình chữ nhật này thuộc một trong
các hình chữ nhật ban đầu thì cộng thêm vào phần diện tích đang cần tìm diện tích của
hình chữ nhật con này.

Ví dụ 2. Phủ S

Trên mặt phẳng tọa độ, một hình chữ nhật với các cạnh song song với các trục tọa độ
được xác định bởi 2 điểm đối tâm: đỉnh góc trên bên trái và đỉnh góc dưới bên phải.
Cho N hình chữ nhật song song với các trục tọa độ . Phủ S của các hình chữ nhật có
diện tích nhỏ nhất chứa N hình chữ nhật đã cho

Dữ liệu vào: Đọc từ file PHU_CN.INP gồm:

254
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

- Dòng đầu tiên chứa N ( N 30 )

- Trong N dòng tiếp theo, mỗi dòng ghi 4 số là tọa độ là tọa độ của 2 đỉnh đối tâm của
hình chữ nhật, các số này là các số nguyên có trị tuyệt đối không quá 100

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

- Dòng 1 ghi tọa độ hai đỉnh đối tâm của phủ S các hình chữ nhật

- Dòng 2 ghi diện tích các phần hình S không nằm trong hình chữ nhật nào trong N
hình đã cho

Ví dụ:

PHU_CN.IN PHU_CN.OU
P T

2 -2 0 5 4

-2 0 1 3 28

2254

Ý tưởng:

B1. Xác định hình chữ nhật H nhỏ nhất bao tất cả các hình chữ nhật ban đầu bằng
cách: Gọi minx, maxx lần lượt là hoành độ nhỏ nhất và lớn nhất trong các hoành độ
các đỉnh hình chữ nhật đã cho; miny, maxy lần lượt là tung độ nhỏ nhất và lớn nhất
trong các tung độ đã các đỉnh của hình chữ nhật đã cho. Khi đó hình H có tọa độ đỉnh
dưới trái là (minx;miny) và đỉnh trên phải là ( maxx;maxy). Đó là phủ S cần tìm

B2. Tính diện tích S phủ bởi các hình chữ nhật

Ví dụ 3. Diện tích phủ bởi các hình tròn

Trên mặt phẳng cho N hình tròn. Tính diện tích phần mặt phẳng bị phủ bởi các hình
tròn trên.

Dữ liệu cho trong file PHU_HT.INP gồm:

- Dòng đầu là số lượng lượng hình tròn

255
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

- Từ dòng 2 trở đi mỗi dòng chứa 3 số nguyên dương là tọa độ (x,y) của tâm và bán
kính của từng hình tròn (các trên cùng một dòng ghi cách nhau ít nhất 1 dấu cách)

Kết quả ghi ra file PHU_HT.OUT một dòng duy nhất chứa diện tích cần tìm (lấy 2
chữ số thập phân).

Ví dụ:

PHU_HT.INP PHU_HT.OUT

3 95.72

005

115

701

256
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Ý tưởng:

B1. Tìm hình chữ nhật nhỏ nhất có các cạnh song song với các trục tọa độ và chứa
toàn bộ N hình tròn.

B2. Chia hình chữ nhật này thành lưới các ô vuông có cạnh 0.1 đơn vị, với mỗi ô thuộc
hình chữ nhật kiểm tra xem ô này có thuộc vào hình tròn (đã cho) nào đó hay không,
nếu có thì tăng diện tích cần tính lên 0.01 đơn vị.

B3. Xuất kết quả vừa tìm được.

Dạng 4: Xác định đa giác nhỏ nhất bao tất cả các điểm, đa giác đã cho

Phương pháp Cho N điểm A1,A2,…,AN trên mặt phẳng. Để xác định một đa giác
không tự cắt chứa một số điểm đã cho và bao tất cả điểm còn lại ta làm như sau:

B1: Tìm điểm có tung độ nhỏ nhất. Điểm đó sẽ là đỉnh của đa giác

B2: Giả sử ta đã chọn được điểm PM. Tìm điểm Pi sao cho góc hợp bởi PMPi và trục
hoành là nhỏ nhất và đồng thời góc này phải lớn hơn góc hợp bởi PMPM- 1 và trục
hoành. Điểm Pi sẽ là một đỉnh của đa giác

B3: Lấy kết quả là dãy các đỉnh P tìm được

Lưu ý: Với bài toán tìm đa giác bao nhau thì cần ghi nhớ đa giác a bao đa giác b khi
mọi điểm trong đa giác b đều nằm trong đa giác a.

Ví dụ 1: Đa giác không tự cắt

Cho N điểm A1,A2,…,AN trên mặt phẳng. Các điểm đều có tọa độ nguyên và không có
3 điểm bất kì trong chúng thẳng hàng. Hãy viết chương trình thực hiện các công việc
sau đây: Xác định một đa giác không tự cắt có đỉnh là một số điểm trong các điểm đã
cho và chứa tất cả các điểm còn lại và có chu vi nhỏ nhất. Hãy tính diện tích đa giác
này.

Dữ liệu: Cho trong tệp HCN.INP gồm n+1 dòng

- Dòng 1: Chứa số N

257
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

- Dòng i+1 ( 1≤ i ≤ N ): ghi 2 chữ số nguyên xi, yi là tọa độ Ai. Các số trên cùng một
dòng cách nhau một khoảng trẳng.

Kết quả: Xuất ra file HCN. OUT gồm:

- Dòng 1: ghi 3 số K, C, S với K là số đỉnh đa giác tìm được, C là chu vi, S là diện tích
của nó

- Dòng i+1 (1≤ i ≤ K ): Ghi tọa độ của đỉnh đa giác

Ví dụ:

HCN.INP HCN. OUT

5 4 15.12 14.00

01 44

44 04

04 01

40 40

22

Ý tưởng:

B1. Tìm điểm có tung độ nhỏ nhất. Điểm đó sẽ là đỉnh đa giác.

B2. Giả sử ta chọn được điểm PM. Tìm một điểm Pi sao cho góc hợp bởi PMPi và trục
hoành là nhỏ nhất và đồng thời góc này phải lớn hơn góc hợp bởi PMPM- 1 và trục
hoành. Điểm Pi sẽ là một đỉnh của đa giác.

B3. Xuất kết quả tìm được.

Ví dụ 2: Đa giác bao nhau

Cho N đa giác thỏa mãn các tính chất:

- Với 2 đa giác bất kì luôn có một đa giác mà mọi điểm của nó nằm trong đa giác kia

- Các cạnh của chúng không có điểm chung. Bài toán đặt ra là: Với mỗi đa giác i, có
bao nhiêu đa giác bao nó ? (i nằm trong bao nhiêu đa giác)
258
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Dữ liệu vào: Ghi trong tập tin văn bản DAGIAC.INP gồm:

- Dòng đầu tiên ghi số tự nhiên N (3≤ N ≤ 10000)

- Trên N dòng tiếp theo: dòng thứ i+1 ghi thông tin về đa giác có số hiệu thứ i. Bao
gồm số đầu tiên Si là số đỉnh của đa giác ( Si=3), Si cặp số nguyên tiếp theo lần lượt là
hoành độ và tung độ các đỉnh của đa giác.

Các số trên cùng dòng cách nhau ít nhất một dấu cách

Kết quả: ghi ra file DAGIAC.OUT gồm N dòng

- Dòng thứ i: ghi số lượng đa giác bao đa giác i

DAGIAC.INP DAGIAC.OUT Ví dụ:

4 0

4 1 1 15 1 15 8 1 8 2

493964643 1

4 3 2 11 2 11 7 3 7 3
Ý tưởng:
3848565
B1. Sử dụng các mảng a, vt, kq
(với a[i] lưu giá trị hoành độ nhỏ nhất của các đỉnh của đa giác thứ i, vt[i] chỉ đa giác
thứ i, mảng kq lưu kết quả)

B2. Thực hiện sắp xếp các đa giác theo thứ tự tăng dần của các giá trị hoành độ của
đỉnh của đa giác

B3. Do theo điều kiện bài toán là với 2 đa giác bất kì luôn có một đa giác mà mọi điểm
của nó nằm trong đa giác kia nên KQ[vt[i]]=i- 1 (hoặc KQ[i]:=vt[i]-1).

Ví dụ 3: Hình chữ nhật bao nhau

Cho N hình chữ nhật trên mặt phẳng mà các cạnh song song với các trục tọa độ. Biết
hình chữ nhật i bao hình chữ nhật j nếu cả 4 đỉnh của hình chữ nhật j đều nằm trong
hình chữ nhật i hoặc nằm trên cạnh hình chữ nhật i. Một dãy các hình chữ nhật được
gọi là hình chữ nhật bao nhau chiều dài k ( k≥ 1) nếu dãy này bao gồm các hình chữ

259
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

nhật H1, H2,…,Hk sao cho hình chữ nhật i bao hình chữ nhật i+1 với i=1…(k- 1). Hãy
tìm số k lớn nhất nói trên

Dữ liệu vào cho trong file HCN.INP gồm:

- Dòng thứ nhất ghi số N ( 1≤ N≤ 1000).

- N dòng tiếp theo, dòng thứ i ghi 4 số nguyên x1,y1,x2,y2 ( - 10000< x1,y1,x2,y2
<10000), lần lượt là hoành độ, tung độ của các đỉnh trái trên, phải dưới của hình chữ
nhật.

Kết quả ghi ra file văn bản HCN.OUT gồm một dòng chứa số nguyên duy nhất là số
k tìm được hoặc ghi số -1 nếu không tồn tại số k thỏa điều kiện đề bài

Ví dụ:

HCN.INP HCN.OUT
HCN.INP HCN.OUT
7 4
5 -1
6675
1522
1432
2433
1532
4361
2433
5574
1552
1341
5684

6685

Ý tưởng:

B1. Lập hàm kiểm tra hình chữ nhật j bao hình chữ nhật i thỏa mãn điều kiện:
(x1[j]<=x1[i]) and(y1[j]>=y1[i]) and (x2[j]>=x2[i]) and ( y2[j] <=y2[i]).

B2. Với mỗi hình chữ nhật i (i=1..n) ta kiểm tra xem có bao nhiêu hình chữ nhật j
(j=1..n) bao hình chữ nhật j, sau đó ta lưu số lượng hình chữ nhật j bao hình chữ nhật i
vào mảng kq (giả sử có k hình chữ nhật j bao hình chữ nhật i thì kq[i]=k //k tính bằng
260
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

cách: khởi tạo k:=0; nếu kiểm tra (sử dụng hàm kiểm tra ở trên) đúng thì tăng k lên 1
đơn vị)

B3. Tìm giá trị lớn nhất trong mảng kq, nếu tất cả các giá trị của mảng kq đều bằng 1
thì xuất ra -1.

Ví dụ 4: Hình chữ nhật bao nhau

Hình chữ nhật A bao hình chữ nhật B nếu mọi điểm thuộc hình chữ nhật B đều nằm
trong hoặc thuộc hình chữ nhật A.

Trong mặt phẳng Oxy cho n hình chữ nhật có các cạnh song song với các trục tọa độ

Yêu cầu:

Câu 1. Tìm số hình chữ nhật bao nhau nhiều nhất (số lượng hình phải lớn hơn 1).

Câu 2. Tìm hiệu diện tích của hình chữ nhật nhỏ nhất bao tất cả các hình chữ nhật đã
cho với diện tích của phần mặt phẳng bị phủ bởi các hình chữ nhật đã cho.

Dữ liệu vào cho trong file văn bản HCN.INP gồm:

- Dòng đầu: ghi số n ( số lượng hình chữ nhật ,1≤ N ≤ 1000).

- n dòng tiếp theo: Mỗi dòng chứa 4 số dạng a, b, c, d (a,b,c,d là các số nguyên) với
(a;b) là tọa độ đỉnh góc trên trái và (c,d) là tọa độ của đỉnh góc dưới phải hình chữ
nhật.

Kết quả xuất ra file văn bản HCN.OUT

- Dòng đầu: cho biết tập các hình chữ nhật bao nhau nhiều nhất theo dạng k l m n…
với ý nghĩa – hình chữ nhật thứ k thuộc hình chữ nhật thứ l, hình chữ nhật thứ l thuộc
hình chữ nhật thứ m, hình chữ nhật thứ m thuộc hình chữ nhật thứ n… nếu không có
thì ghi từ -1

- Dòng thứ 2: chứa con số biểu diễn hiệu diện tích hình chữ nhật nhỏ nhất bao tất cả
hình chữ nhật đã cho với diện tích của phần mặt phẳng bị phủ bởi các hình chữ nhật
đã cho.

261
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Ví dụ:

HCN.INP HCN.OUT HCN.INP HCN.OUT

3 31 6 6215

0044 4 3285 11

5064 3255

0022 1032

3 -1 0335

1032 9 3 0 10 7

0335 3355

3255

Ý tưởng:

Câu 1.

B1. Dùng mảng a để lưu các hình chữ nhật ban đầu.

B2. Sử dụng mảng dt, vt để lưu diện tích và vị trí các hình chữ nhật ban đầu.

B3. Sắp xếp mảng dt theo thứ tự không giảm (lưu ý nếu hoán đổi dt thì phải hoán đổi
vị trí, ví dụ: nếu hoán đổi dt[i] và dt[j] thì phải hoán đổi vt[i] và vt[j], mảng vt rất quan
trọng, đó cũng chính là đáp án của bài toán).

B4. Sử dụng mảng tg để lưu các hình chữ nhật sau khi đã sắp xếp theo diện tích (lưu ý
mang tg[i]:=a[vt[i]]).

B5. Sử dụng hàm kt(i,j) để kiểm tra xem hình chữ nhật i có nằm trong hình chữ nhật j
(việc kiểm tra này đã biết, lưu ý hình chữ nhật ở đây là mảng tg) hay không. Ta sử
dụng mảng hai chiều kq, nếu kt(i,j) đúng thì kq[i,j]:=j; đồng thời inc(d); tức là:

for i:=1 to n-1 do

262
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

begin

d:=1;

for j:=i+1 to n do

if kt(i,j) then

begin

inc(d);

kq[i,j]:=j;

end;

Lưu ý: Vì đề yêu cầu tìm số hình chữ nhật bao nhau nhiều nhất nên d phải lớn nhất và
d phải lớn hơn 1.

B6. Với i nào có d lớn nhất (giả sử k:=i) thì:

Write(vt[k],‘ ’);

For i:=1 to n do

If kq[k,i]<>0 then write(vt[i])

Câu 2.

B1. Tính diện tích S1 phủ S (đã biết).

B2. Tính diện tích S2 phủ bởi các hình chữ nhật (đã biết).

B3. Kết quả là S1-S2

263
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

BINARY INDEX TREE BIT


THPT Chuyên Hùng Vương – Bình Dương

Mọi số tự nhiên đều có thể biểu diễn được dưới dạng nhị phân. Đối với máy tính, các
giá trị số được lưu bởi các biến dưới dạng chuỗi nhị phân có độ dài tương ứng với kiểu
biến đó. Trong ngôn ngữ Pascal, một số kiểu phổ biến như: byte : độ dài 8 BIT –
integer: độ dài 2*8 = 16 BIT –longint : độ dài 4*8 = 32 BIT Các BIT của biến được
đánh số từ phải sang trái bắt đầu từ 1.

Ưu điểm của xử lý BIT:

1/ Bộ nhớ: Xử lý BIT có thể được dùng làm mảng đánh dấu. Thay vì sử dụng 1 biến
Boolean chỉ đánh dấu được 1 phần tử là True hay False, ta có thể xử lý BIT để đánh
dấu 8 BIT tương ứng với 8 phần tử.

2/ Tốc độ: Các phép xử lý BIT có tốc độ nhanh hơn các phép xử lý khác. Ví dụ: Hai
phép (x div 2) và (x shr 1) là như nhau nhưng phép (x shr 1) có tốc độ nhanh hơn.
Trong các bài toán đòi hỏi việc thay đổi trạng thái nhiều lần thì người ta vẫn hay dùng
xử lý BIT để cải thiện tốc độ chương trình.

Sau đây là một số phép xử lý BIT cơ bản:

1/ Cộng BIT (or): Kết quả bằng tổng giá trị 2 BIT. Trường hợp 2 BIT đều có giá trị
bằng 1 thì kết quả là 1.Ví dụ: x1 = 9 (00001001) x2 = 18 (00010010) x1 or x2 = 27
(00011011)

2/ Đảo BIT (not): Đảo BIT 0 thành BIT 1 và BIT 1 thành BIT 0. Ví dụ: x = 13
(00001101) not (x1) = 242 (11110010)

3/ Nhân BIT (and): Kết quả bằng tích giá trị 2 BIT.Ví dụ: x1 = 9 (00001001) x2 =
12 (00001100) x1 and x2 = 8 (00001000)

4/ Loại trừ BIT (xor): Kết quả là 0 nếu 2 BIT có cùng giá trị, là 1 nếu 2 BIT khác
giá trị.Ví dụ: x1 = 9 (00001001) x2 = 18 (00010010) x1 xor x2 = 27 (00011011)

5/ Dịch sang trái k BIT (shl): Ví dụ: x = 27 (00001101) x shl 2 = 108 (01101100)

264
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

6/ Dịch sang phải k BIT (shr):Ví dụ: x = 27 (00001101)x shr 2 = 6 (00000110)

Từ các phép toán trên, ta có các công thức mở rộng sau:

7/ Lấy giá trị BIT: Cho biết BIT thứ k của số x có giá trị bao nhiêu:

Function GetBIT(k , x : LongInt) : LongInt; begin GetBIT := (x shr (k-1)) and 1; end;

8/ Gán giá trị BIT: Gán giá trị c cho BIT thứ k của số x: Procedure SetBIT(c , k :
LongInt; var x : LongInt); begin if c = 1 then x := x or (1 shl (k-1)) else x := x and
(not (1 shl (k-1))); end; Số bù 2: là số âm được biểu diễn trên máy tính.

- Cho số a là nguyên dương để chuyển thành –a thì ta làm :

Lấy phủ định của a sau đó cộng vào bit cuối bên phải của a bit 1: (NOT a) + 1

Vd: số 6 để chuyển thành -6.

610=1102; not 610=not 01102=1001+1=10102=-6

BINARY INDEX TREE

Tổng quát, đặt m = 2k.p (với p là số lẻ). Hay nói cách khác, k là vị trí của bít 1 bên
phải nhất của m. Trong cây BIT, nút có số hiệu m sẽ là nút gốc của một cây con gồm
2k nút có số hiệu từ m- 2k+1 đến m.

Ví dụ:

- 8 = 23.1, vậy 8 là nút gốc của các nút 1, 2, 3, …, 8.


265
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

- 12 = 22.3, vậy 12 là nút gốc của các nút 9, 10, 11, 12

- 10 = 21.5, vậy 10 là nút gốc của các nút 9, 10.

- 7 = 20.7, vậy 7 là nút gốc của chỉ nút 7.

- 16 = 24.1, vậy 16 là nút gốc của các nút 1, 2, 3, …, 16.

Trong cây BIT, nút gốc đại diện cho tất cả các nút con của nó. Ý nghĩa của từ đại
diện ở đây thường dùng là nút gốc lưu tổng giá trị của các nút con. Vì vậy khi tính
toán, ta chỉ cần truy xuất nút gốc là đủ mà không cần thiết phải truy xuất đến các nút
con. Xét ví dụ:

Cho mảng gồm n phần tử a1, a2, …, an. Hãy tính tổng Am = a1 + a2 + … + am (m ≤
n).

Thay vì sử dụng vòng lặp từ 1 đến m để truy xuất từng phần tử ai một (độ phức tạp
O(m)), ta sử dụng cấu trúc BIT như sau:

- t1 = a1

- t2 = a1 + a2

- t3 = a3

- t4 = a1 + a2 + a3 + a4

- t5 = a5

- t6 = a5 + a6

- t7 = a7

- t8 = a1 + a2 + a3 + a4+ a5 + a6 + a7 + a8

-…

- t12 = a9 + a10 + a11 + a12

- ... (tiếp tục như vậy theo cách xây dựng cây BIT)

266
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

* Để tính A15 (m=15), thay vì phải duyệt từ a1 đến a15, ta chỉ cần tính t8 + t12 +
t14 + t15 .

* Để tính A10, chỉ cần tính t8 + t10

* Để tính A13, chỉ cần tính t8 + t12 + t13

* Để tính A16, lấy ngay giá trị t16

Tổng quát với m bất kỳ, biểu diễn m thành dạng nhị phân, sau đó lần lượt xóa các
bít 1 của m theo thứ tự từ phải sang trái, tại mỗi bước trung gian chính là chỉ số nút
cần truy xuất trong cây BIT.

Ví dụ, m = 13 có biểu diễn nhị phân là 1101:

1) 1101 -> truy xuất nút 13

2) Xóa bít 1 bên phải nhất còn 1100 -> truy xuất nút 12

3) Xóa bít 1 bên phải nhất còn 1000 -> truy xuất nút 8

4) Xóa bít 1 bên phải nhất và dừng.

Thao tác truy xuất các nút như trên được gọi là getBIT. May mắn là ta có một công
thức rất đơn giản để xóa bít 1 bên phải dùng phép toán AND. Thủ tục getBIT như sau:

int getBIT(int m)

int result = 0;

for(; m> 0; m &= m-1)

{ result += t[m]; } return result;

Vấn đề còn lại là làm thế nào để xây dựng được cây BIT như trên? Cách thực hiện
là ban đầu khởi tạo các nút của cây BIT là 0. Sau đó ứng với mỗi giá trị am thì cập
nhật các nút cha liên quan trong cây.

267
Trại hè Phương Nam lần IV năm 2017 Kỷ yếu hội thảo môn Tin học

Ví dụ:

- Cập nhật giá trị a5 -> cần cập nhật các nút t5, nút cha t6, nút cha t8, nút cha
t16,….

- Cập nhật giá trị a9 -> cần cập nhật các nút t9, nút cha t10, nút cha t12, nút cha
t16,…

- Cập nhật giá trị a4 -> cần cập nhật các nút t4, nút cha t8, nút cha t16,…

Tổng quát với m bất kỳ, biểu diễn m thành dạng nhị phân, nếu cộng 1 vào bít bên
phải nhất của m thì ta được nút cha của m.

Ví dụ, m = 5 có biểu diễn nhị phân là 101:

1) 101 -> cập nhật nút 5

2) Cộng 1 vào bít phải nhất thành 0110 -> cập nhật nút 6

3) Cộng 1 vào bít phải nhất thành 0111 -> cập nhật nút 8

4) Cộng 1 vào bít phải nhất thành 10000 -> cập nhật nút 16.

Thao tác cập nhật các nút từ con đến cha như trên được gọi là updateBIT. Ta cũng có
một công thức rất đơn giản để cộng 1 vào bít 1 bên phải nhất dùng phép toán AND.
Thủ tục updateBIT như sau:

void updateBIT(int m, int value)

for(; m<= n; m += m & -m)

{ t[m] += value; }

268
Binary Heap
BINARY HEAP
Phạm Ngọc Bách – Trường Chuyên Hoàng Lê Kha Tây Ninh

MỞ ĐẦU
Cấu trúc Heap được dùng để biểu diễn hàng đợi ưu tiên (Priority Queue), có
nhiều dạng cấu trúc Heap khác nhau như: Binary Heap, Fibonacci Heap, Relax
Heap,… Nhưng chuyên đề tập trung vào Binary Heap và ứng dụng cấu trúc này trong
giải bài toán thực tế.

1. BINARY HEAP
Về hình thức, Heap như một cấu trúc vun đống, trong đó đỉnh là cực trị, tùy
theo bài toán yêu cầu mà ta có thể lưu cực trị bé nhất hoặc lớn nhất. Mục này trình bày
cực trị lớn nhất. Binary Heap là cấu trúc cây nhị phân nhưng được biểu diễn khéo léo
trên mảng một chiều gọi là H. Trong đó, đỉnh cực trị là H[1], phần tử thứ i có thể có
hai con là 2i và 2i+1 và H[i] ≥ max(H[2i], H[2i+1]). Ở hình họa, giá trị lớn nhất
H[1]=18 và lớn hơn H[2] và H[3], tương tự cho bất cứ vị trí i nào đó.

Như vậy, để lấy giá trị lớn nhất thì đơn giản chỉ cần xuất H[1], độ phức tạp
O(1). Nhưng vấn đề là mỗi khi ta thêm một phần tử (push) hay xóa cực trị (H[1])
(pop) thì cực trị mới là gì.
Thêm một phần tử x: push(x)
procedure swap(iH,jH:longint);
var t:longint;
begin
t:=H[iH]; H[iH]:=H[jH]; H[jH]:=t;
1
269
Binary Heap
end;
procedure upHeap(iH:longint);
begin
if (iH=1) or (H[iH] < H[iH div 2]) then exit;
swap(iH,iH div 2);
upHeap(iH div 2);
end;
procedure push(x:longint);
begin
if nH<maxn then
begin
inc(nH); H[nH]:=x;
upHeap(nH);
end;
end;
Thủ tục upHeap(iH) có chức năng so sánh H[iH] với cha H[iH div 2], nếu lớn
hơn thì đổi chỗ và gọi đệ quy upHeap(iH div 2). Độ phức tạp O(logN), do thao tác
upHeap nhị phân (iH div 2). Chi phí bộ nhớ O(N), do biểu diễn trên mảng một chiều
và N bằng số phần tử của Heap nH.
Xóa phần tử cực trị: pop()
procedure downHeap(iH:longint);
var jH:longint;
begin
jH:=iH*2;
if jH<nH then
if (H[jH+1]>H[jH]) then jH:=jH+1; //Làm trội
if (jH<=nH) and (H[iH]<H[jH]) then
begin
swap(iH,jH);
downHeap(jH);
end;
end;
function pop:longint;
var t:longint;
begin
t:=H[1]; H[1]:=H[nH]; dec(nH);
downHeap(1);
exit(t);
end;
Thủ tục downHeap(iH), iH có hai con là iH*2 và iH*2+1, chọn con có giá trị
lớn hơn gán cho jH, so sánh H[iH] với H[jH], nếu bé hơn thì đổi chỗ và gọi đệ quy
downHeap(jH). Độ phức tạp thao tác pop bằng O(logN), do tại iH ta chỉ lựa chọn một
trong hai con iH*2 và iH*2+1.

2
Binary Heap
2. VÍ DỤ 1: IO
Cho trước một danh sách rỗng. Người ta xét hai thao tác trên danh sách đó:

 Thao tác "+V" (ở đây V là một số tự nhiên  109): Nếu danh sách đang có ít
hơn 15000 phần tử thì thao tác này bổ sung thêm phần tử V vào danh sách; Nếu
không, thao tác này không có hiệu lực.

 Thao tác "-": Nếu danh sách đang không rỗng thì thao tác này loại bỏ tất cả
các phần tử lớn nhất của danh sách; Nếu không, thao tác này không có hiệu lực.
Vấn đề đặt ra là cho trước một dãy không quá 100 000 thao tác, hãy xác định
những giá trị số nào còn lại trong danh sách theo giá trị giảm dần.
IO.INP

 Gồm nhiều dòng, mỗi dòng ghi một thao tác. Thứ tự các thao tác trên các dòng
được liệt kê theo đúng thứ tự sẽ thực hiện.
IO.OUT

 Dòng 1: Ghi số lượng những giá trị còn lại trong danh sách.

 Dòng 2: Liệt kê những giá trị đó.


Thí dụ
IO.INP IO.OUT
+1 5
+3 87221
+2
+3
-
+4
+4
-
+2
+9
+7
+8
-
Gợi ý: Thao tác "+V" ta dùng push(V), còn thao tác "-" ta dùng pop. Cuối cùng, liên
tục pop để xuất dãy giá trị giảm dần. Chương trình và bộ test được đính kèm trong phụ
lục.

3
Binary Heap
3. KẾT HỢP HEAP VỚI DIJKSTRA
Trong Dijkstra có hai việc chúng ta đặc biệt quan tâm:
a) Lấy u để d[u] nhỏ nhất.
b) Cập nhật d[v] theo d[u].
Với a), nếu dùng vòng lặp duyệt qua tất cả các đỉnh sẽ tốn O(N), nhưng nếu
biểu diễn trong Heap thì u:=pop, chỉ có O(logN). Nhưng việc biểu diễn Heap có khác
so với mục 1) trên, Heap chỉ lưu tên đỉnh đồ thị, do đó để so sánh giữa hai node i và j
trong Heap thực chất là so sánh d[H[i]] với d[H[j]].
Với b), d[v] có thể thay đổi bất cứ lúc nào, do đó phải cập nhật lại Heap, nhưng
cần xác định xem v nằm ở vị trí nào trong Heap, do đó tổ chức thêm pos[v] trả về vị
trí (position) của v trong Heap. Thao tác cập nhật, thực chất là upHeap(pos[v]) sẽ tốn
O(logN).
Do đó các chương trình con bị thay đổi như sau:
procedure swap(iH,jH:longint);
var t:longint;
begin
t:=H[iH]; H[iH]:=H[jH]; H[jH]:=t;
pos[H[iH]]:=iH;
pos[H[jH]]:=jH;
end;
procedure upHeap(iH:longint);
begin
if (iH=1) or (d[H[iH]] > d[H[iH div 2]]) then exit;
swap(iH,iH div 2);
upHeap(iH div 2);
end;
procedure downHeap(iH:longint);
var jH:longint;
begin
jH:=iH*2;
if jH<nH then
if (d[H[jH+1]]<d[H[jH]]) then jH:=jH+1;
if (jH<=nH) and (d[H[iH]]>d[H[jH]]) then
begin
swap(iH,jH);
downHeap(jH);
end;
end;
procedure push(x:longint);
begin
inc(nH); H[nH]:=x; pos[x]:=nH;
upHeap(nH);
4
Binary Heap
end;
function pop:longint;
var t:longint;
begin
if nH = 0 then exit(0);
t:=H[1];
H[1]:=H[nH]; dec(nH); pos[H[1]]:=1;
downHeap(1);
exit(t);
end;
procedure update(v:longint);
begin
if pos[v] = 0 then push(v)
else upHeap(pos[v]);
end;
Còn một lưu ý: Heap được tổ chức để lấy cực trị bé nhất cho phù hợp với d[u]
bé nhất. Đề bài, chương trình và bộ test được đính kèm trong phụ lục.
4. VÍ DỤ 2: SPY
Trùm gián điệp trốn trong 1 căn hầm được đào sâu xuống mặt đất M tầng, mỗi
tầng có N phòng. Các phòng được ngăn cách bằng các cửa rất khó phá. Các phòng có
cửa xuống phòng ngay phía dưới và 2 phòng ở 2 bên. Từ trên mặt đất có N cửa xuống
N phòng tầng 1. Trùm gián điệp ở tầng dưới cùng (tầng M) phòng thứ N (phòng ở bên
phải nhất). Mỗi cửa được làm bằng một kim loại khác nhau với độ dày khác nhau nên
việc phá cửa cần thời gian khác nhau.
Bạn hãy tìm cách đi từ mặt đất xuống phòng của trùm gián điệp nhanh nhất
không hắn thoát mất.
SPY.INP

 Dòng 1 ghi M và N

 Dòng 2 đến 2M + 1, dòng chẵn ghi N số, dòng lẻ ghi N - 1 số là chi phí để phá
cửa.
SPY.OUT

 Ghi ra 1 số là thời gian nhỏ nhất để đến được phòng của Trùm gián điệp
Thí dụ

SPY.INP SPY.OUT
42 44
99 10
1
5
Binary Heap
10 99
1
99 10
1
10 99
1
Giới hạn
1 ≤ M ≤ 2222
1 ≤ N ≤ 100
Chi phí của các cánh cửa thuộc [0, 1000].
Gợi ý: Chúng ta xem mỗi ô như một đỉnh trong đồ thị, một đỉnh sẽ kề với 4 ô kề cạnh,
ta có thể chuyển bài toán thành tìm đường đi ngắn nhất từ 1 đỉnh (M,N) đến mọi đỉnh
bằng thuật toán Dijkstra, với d[u,v] là đường đi ngắn nhất từ (M,N) đến (u,v). Kết hợp
với Heap, mỗi node trong Heap sẽ lưu tọa độ ô (u,v), để biết được vị trí của ô (u,v)
trong Heap ta lưu thêm pos[u,v]. Đề bài, chương trình và bộ test được đính kèm trong
phụ lục.

KẾT LUẬN
Cấu trúc Heap được ứng dụng trong giải nhiều bài toán, đặc biệt là kết hợp với
thuật toán Dijkstra trong bài toán tìm đường đi ngắn nhất, làm giảm độ phức tạp tính
toán. Kinh nghiệm học tốt nhất là nắm chắc kiến thức về Binary Heap và Dijkstra, sau
đó mới kết hợp cả hai và chỉ cần lưu ý thêm: Heap lưu tên đỉnh và pos[v] lưu vị trí
của đỉnh v trong Heap.

PHU LỤC
Tất cả nội dung của chuyên đề được chia sẻ tại địa chỉ sau:
https://drive.google.com/open?id=0B6ZzQE6AlCKDd0Nra3EzbFJPNG8

TÀI LIỆU THAM KHẢO


[1] Hồ Sĩ Đàm, (2009), Tài liệu giáo khoa chuyên tin quyển 1, 2, NXB Giáo dục.
[2] www.spoj.com.
[3] www.codeforces.com

6
VẬN DỤNG THUẬT TOÁN TÌM KIẾM NHỊ PHÂN
ĐỂ GIẢI QUYẾT CÁC BÀI TOÁN THI HỌC SINH GIỎI
Nguyễn Trọng Nghĩa
THPT Chuyên Tiền Giang
1. LÝ THUYẾT CHUNG
Thuật toán tìm kiếm nhị phân là một trong những thuật toán được áp dụng nhiều
trong khoa học cũng như trong thực tế. Trong các kỳ thi học sinh giỏi các cấp của môn Tin
học thì bài toán tìm kiếm nhị phân là một trong những bài toán thường được các tác giả
chọn làm đề bài của mình. Hôm nay chúng ta cùng nhau tìm hiểu thuật toán này.
Đầu tiên chúng ta cùng tìm hiểu khái niệm bài toán tìm kiếm và thuật toán tìm kiếm
tuyến tính.
1.1. Bài toán tìm kiếm
Bài toán xác định vị trí của một phần tử trong một dãy liệt kê sắp thứ tự thường gặp
trong nhiều trường hợp khác nhau. Chẳng hạn chương trình kiểm tra chính tả của các từ,
tìm kiếm các từ này trong một cuốn từ điển, mà từ điển chẳng qua cũng là một dãy liệt kê
sắp thứ tự của các từ. Các bài toán thuộc loại này được gọi là các bài toán tìm kiếm.
Bài toán tìm kiếm tổng quát được mô tả như sau: xác định vị trí của phần tử X trong
một dãy liệt kê các phần tử phân biệt A1, A2, ..., An hoặc xác định rằng nó không có mặt
trong dãy liệt kê đó. Lời giải của bài toán trên là vị trí của số hạng của dãy liệt kê có giá trị
bằng X (tức là i sẽ là nghiệm nếu X = Ai hoặc là 0 nếu X không có mặt trong dãy liệt kê).
1.2. Thuật toán tìm kiếm tuyến tính
Tìm kiếm tuyến tính hay tìm kiếm tuần tự là bắt đầu bằng việc so sánh X với A1; khi
X = A1, nghiệm là vị trí A1, tức là 1; khi X  A1, so sánh X với A2. Nếu X = A2, nghiệm là
vị trí của A2, tức là 2. Khi X  A2, so sánh X với A3. Tiếp tục quá trình này bằng cách tuần
tự so sánh X với mỗi số hạng của dãy liệt kê cho tới khi tìm được số hạng bằng X, khi đó
nghiệm là vị trí của số hạng đó. Nếu toàn dãy liệt kê đã được kiểm tra mà không xác định
được vị trí của X, thì nghiệm là 0.
Mã giả đối với thuật toán tìm kiếm tuyến tính được cho dưới đây:
procedure tìm kiếm tuyến tính (X: integer, A1, A2, ..., An: integer phân biệt)
i := 1
while (i  n and X  Ai)
i := i + 1
if i  n then Vitri := i
else Vitri := 0
{Vitri là chỉ số dưới của số hạng bằng X hoặc là 0 nếu không tìm được X}
Ví dụ: Xét bài toán sau: Cho một dãy gồm N = 10 số nguyên A[1..N] như sau:
1 2 3 4 5 6 7 8 9 10
2 4 5 6 9 21 22 30 31 33
Đối với thuật toán tìm kiếm tuần tự thì giá trị X = 21 được tìm thấy sau 6 phép so
sánh.
1
275
Độ phức tạp tính toán của thuật toán trên được tính như sau:
Trường hợp tốt nhất là O(1): khi A[1] = X; trường hợp xấu nhất là O(N) khi không
tìm được X trong dãy.
 Độ phức tạp chung của thuật toán là O(N).
Với dãy số trên có cách nào tìm kiếm nhanh hơn không? Tại sao thực hiện được
cách này?
Ta thấy dãy liệt kê A ở trên có các số hạng đã được sắp xếp theo thứ tự tăng dần.
Chẳng hạn, nếu các số hạng là các số thì chúng được sắp từ số nhỏ nhất đến số lớn nhất
hoặc nếu chúng là các từ hay xâu ký tự thì chúng được sắp theo thứ tự từ điển. Thuật toán
thứ hai này gọi là thuật toán tìm kiếm nhị phân. Nó được tiến hành bằng cách so sánh phần
tử cần xác định vị trí với số hạng ở giữa dãy liệt kê. Sau đó dãy này được tách làm hai dãy
liệt kê con nhỏ hơn có kích thước như nhau, hoặc một trong hai dãy con ít hơn dãy con kia
một số hạng. Sự tìm kiếm tiếp tục bằng cách hạn chế tìm kiếm ở một dãy liệt kê con thích
hợp dựa trên việc so sánh phần tử cần xác định vị trí với số hạng giữa dãy liệt kê.
Trong ví dụ trên, bằng thuật toán tìm kiếm nhị phân mà ta tìm được giá trị X = 21
chỉ sau 3 phép so sánh.
Chúng ta cùng nhau tìm hiểu cặn kẽ về thuật toán Tìm kiếm nhị phân và các biến
thể của nó.
1.3. Tìm kiếm nhị phân
1.3.1 Tìm kiếm nhị phân đơn giản
Ý tưởng:
Sử dụng tính chất dãy A đã sắp xếp tăng, ta tìm cách thu hẹp nhanh phạm vi tìm
kiếm bằng cách so sánh X với số hạng ở giữa dãy (Agiữa), khi đó chỉ xảy ra một trong ba
trường hợp:
 Nếu Agiữa= X => tìm được chỉ số, kết thúc;
 Nếu Agiữa < X => do dãy A đã được sắp xếp tăng nên việc tìm kiếm thu hẹp chỉ
xét từ Agiữa+1  AN;
 Nếu Agiữa > X => do dãy A đã được sắp xếp tăng nên việc tìm kiếm thu hẹp chỉ
xét từ A1  Agiữa-1.

Đoạn chương trình minh họa:


Function Tknpcb(X: longint): longint;
Var d, c, g: Longint;
Begin
d := 1; c := N;
While d <= c Do
Begin
g := (d + c) Div 2;
if A[g] = X then exit(g);
if A[g] < X then d := g+1 else c := g-1;
End;
Exit(0);
End;
2
Đánh giá độ phức tạp:
Để đơn giản, ta giả sử rằng có N = 2k phần tử trong dãy liệt kê A1, A2, ..., An, với k
là số nguyên không âm (nếu N không là lũy thừa của 2, ta có thể xem dãy là một phần của
dãy gồm 2k+1 phần tử, trong đó k là số nguyên nhỏ nhất sao cho N < 2k+1).
Ở mỗi giai đoạn của thuật toán vị trí của số hạng đầu tiên d và số hạng cuối cùng c
của dãy con hạn chế tìm kiếm ở giai đoạn đó được so sánh để xem dãy con này còn nhiều
hơn một phần tử hay không. Nếu d < c, một phép so sánh sẽ được làm để xác định X có lớn
hơn số hạng ở giữa của dãy con hạn chế hay không. Như vậy ở mỗi giai đoạn, có sử dụng
hai phép so sánh. Khi trong dãy chỉ còn một phần tử, một phép so sánh sẽ cho chúng ta biết
rằng không còn một phần tử nào thêm nữa và một phép so sánh nữa cho biết số hạng đó có
phải là X hay không. Tóm lại cần phải có nhiều nhất 2k+2=2log2N+2 phép so sánh để thực
hiện phép tìm kiếm nhị phân (nếu N không phải là lũy thừa của 2, dãy gốc sẽ được mở
rộng tới dãy có 2k+1 phần tử, với k = [log2N] và sự tìm kiếm đòi hỏi phải thực hiện nhiều
nhất 2[log2N]+2 phép so sánh). Do đó thuật toán tìm kiếm nhị phân có độ phức tạp là
O(log2N). Từ sự phân tích ở trên suy ra rằng thuật toán tìm kiếm nhị phân, ngay cả trong
trường hợp xấu nhất, cũng hiệu quả hơn thuật toán tìm kiếm tuyến tính.
Tóm lại:
- Trường hợp tốt nhất là O(1): khi A[g] = X;
- Trường hợp xấu nhất là O(lgN) khi không tìm thấy.
 Độ phức tạp chung: O(lgN).
1.3.2. Mở rộng phương pháp tìm kiếm nhị phân
Trên thực tế không phải bao giờ người ta cũng yêu cầu tìm kiếm một phần tử bằng
X mà có thể yêu cầu tìm phần tử gần bằng X nhất. Khi đó ta phải chỉnh sửa thuật toán trên
ở một số bước để phù hợp với yêu cầu của bài toán. Cụ thể, với bài toán này ta chia thành
ba bài toán con:
1.3.2.1. Tìm phần tử lớn nhất nhưng nhỏ hơn hoặc bằng X
Ý tưởng:
 So sánh phần tử giữa với X, nếu nó nhỏ hơn hoặc bằng X ta sẽ xác nhận kết quả
tạm thời.
 Tìm trong đoạn cuối để có nghiệm tốt hơn (gần bằng X hơn).
Đoạn chương trình minh họa:
Function Tknp1(X: longint): longint;
Var d, c, g, kq: Longint;
Begin
kq := 0;
d := 1;
c := N;
While d <= c Do
Begin
g := (d + c) Div 2;
if A[g] <= X then
begin
3
kq := g;
d := g +1;
end
Else c := g-1;
End;
Exit(kq);
End;
1.3.2.2. Tìm phần tử nhỏ nhất nhưng lớn hơn hoặc bằng X
Ý tưởng:

 So sánh phần tử giữa với X, nếu nó lớn hơn hoặc bằng X ta sẽ xác nhận kết quả
tạm thời.
 Tìm trong đoạn đầu để có nghiệm tốt.

Đoạn chương trình minh họa:


Function Tknp2(X: longint): longint;
Var d, c, g, kq: Longint;
Begin
kq := 0;
d := 1;
c := N;
While d <= c Do
Begin
g := (d + c) Div 2;
if A[g] >= X then
begin
kq := g
c := g - 1;
end
Else d := g + 1;
End;
Exit(kq);
End;
1.3.2.3. Tìm phần tử có giá trị là X sao cho độ lệch của |X – A[i]| là nhỏ nhất
function TKNP3(X: longint): longint;
var d, c, g: longint;
begin
d := 1;
c := n;
while abs(d - c) <> 1 do
begin
g := (d + c) div 2;
if X = A[g] then exit(g);
if X < A[g] then c := g
else d := g;
end;
if (abs(X - A[d]) < abs(X - A[c])) then exit(d)
else exit(c);
end;

4
1.3.3. Tìm kiếm nhị phân trên các đối tượng có nhiều thông tin (các bản ghi)
Những bài tập đòi hỏi phải tìm kiếm các bản ghi nhiều trường thông tin thường rất
phức tạp và dễ nhầm lẫn trong quá trình cài đặt chương trình. Để đơn giản ta sử dụng một
hàm để định nghĩa việc so sánh hai bản ghi với nhau.
1.3.3.1. Xây dựng hàm so sánh hai bản ghi với nhau
Giả sử có hai bản ghi X và Y gồm hai trường p, q trong đó trường p ưu tiên trước
được viết như sau:
Function sosanh(X,Y: Banghi): shortint;
Begin
If (X.p < Y.p) then exit(-1);
If (X.p = Y.p) and (X.q < Y.q) then exit(-1);
If (X.p = Y.p) and (X.q = Y.q) then exit(0);
Exit(1);
End;
Hàm trên cho kết quả -1 nếu bản ghi X nhỏ hơn bản ghi Y; cho giá trị 0 nếu X bằng
Y và cho giá trị 1 nếu X lớn hơn Y.
1.3.3.2. Tìm kiếm nhị phân dựa trên dãy các bản ghi
Thay phép so sánh trực tiếp bằng hàm so sánh.
Đoạn chương trình minh họa:
Function Tknp_record(X: longint): longint;
Var d, c, g: Longint;
Begin
d := 1;
c := N;
While d <= c Do
Begin
g := (d + c) Div 2;
if Sosanh(A[g],X) = 0 then exit(g);
if Sosanh(A[g],X) = -1 then
d := g+1
else c := g-1;
End;
Exit(0);
End;

2. BÀI TẬP VẬN DỤNG


Bài 1. Đoán số
Hai người chơi như sau: Người thứ nhất sẽ nghĩ ra một số nguyên dương trong
khoảng từ 1 đến N (N được cho biết trước). Người thứ hai sẽ lần lượt đưa ra các số dự
đoán. Với mỗi số dự đoán này, người thứ hai sẽ nhận được câu trả lời cho biết số mình vừa
nêu ra lớn hơn, nhỏ hơn, hay bằng với số mà người thứ nhất đã nghĩ. Em hãy giúp người
thứ hai chọn đúng số cần tìm với số lần đoán càng ít càng tốt.
Thuật toán:
Người thứ hai muốn chọn đúng số mà người thứ nhất nghĩ với số lần đoán ít nhất thì
người thứ hai chắc chắn phải sử dụng đến thuật toán tìm kiếm nhị phân. Các bước sẽ lần
lượt như sau:
5
Bước 1.
d := 1; c := n; A := 0;
Bước 2.
A := (d+c) div 2
Bước 3.
Lần đoán thứ i: A
Bước 4.
Nếu A lớn hơn số cần tìm thì gán c := A - 1
Nếu A nhỏ hơn số cần tìm thì gán d := A + 1
Bước 5. Nếu số lần đoán vượt quá log2N thì chấm dứt
Ngược lại thì trở lại bước 2

Bài 2. BCFRIEND
http://www.spoj.com/PTIT/problems/BCFRIEND/
Theo quan niệm của người Á Đông cổ, mỗi cá nhân khi sinh ra đều ứng với một
ngôi sao, được gọi là sao chiếu mệnh. Các hoạt động của cá nhân đều bị chi phối bởi ngôi
sao này, kể cả quá trình kết bạn – hẹn hò. Theo thuyết Âm dương – Ngũ hành, hai người
chỉ có thể tạo lập mối quan hệ bền vững khi các sao chiếu mệnh của họ không có các thuộc
tính tương khắc. Qua hàng nghìn năm quan sát và chiêm nghiệm, các chiêm tinh gia đã ghi
nhận được N sao và hầu hết các tính chất tương sinh – tương khắc giữa chúng. Để có thể
nhanh chóng đáp ứng nhu cầu kiểm tra độ tương hợp của các sao, hiệp hội ABS
(Association of Broker for Single) tạo lập cơ sở dữ liệu ghi nhận tính chất của tất cả các
sao đã khảo sát được. Trong cơ sở dữ liệu này, các sao được đánh số từ 1 tới N; sao
thứ i có một giá trị Si thể hiện khả năng thích nghi của sao gọi là độ thích nghi. Hai sao
khác nhau có thể có cùng độ thích nghi. Thông qua độ thích nghi của các sao, người ta xác
định khả năng tương hợp của chúng. Khả năng tương hợp của 2 sao được tính bằng tổng 2
độ thích nghi của chúng.
Bài toán: Cho số nguyên dương N, dãy s1, s2, …, sn là độ thích nghi của các sao và
số nguyên B. Hãy xác định số lượng T các cặp sao (i, j) mà si + sj = B, với 1≤i<j≤ n.
Ví dụ: trong 5 sao với độ thích nghi là 3, 5, 6, 5, 3 thì có 4 cặp có khả năng tương
hợp bằng 8.
DỮ LIỆU:
5 9
 Dòng đầu tiên ghi 2 số nguyên n, B (2 ≤ n ≤ 10 , |B| ≤ 10 )
 Mỗi dòng trong N dòng tiếp theo ghi một số nguyên là độ thích nghi của một sao,
độ thích nghi có trị tuyệt đối bé hơn 231.
Hai số trên cùng dòng cách nhau ít nhất một dấu cách.
KẾT QUẢ:
 Gồm một dòng là số nguyên T duy nhất
Ví dụ:
bcfriend.inp bcfriend.out
5 8 5 5 4
3 6 3
Hướng dẫn:
Có thể sort lại rồi, với mỗi giá trị Ai, tìm kiếm nhị phân xem có bao nhiêu giá trị Aj
thỏa mãn Ai+Aj = B
Bài 3. Xếp trứng
Cho N quả trứng được đưa vào dây chuyền theo thứ tự (quả trứng thứ i có thể
tích là ai). Ở cuối dây chuyền đã có sẵn M thùng chứa trứng. Các thùng này nhận
6
trứng theo quy tắc: chứa trứng cho đến khi đầy thì chuyển sang thùng khác. Hãy tính
sức chứa K tối thiểu của mỗi thùng để M thùng này có thể chứa hết trứng theo quy
trình trên.
Dữ liệu
 Dòng đầu: Ghi 2 số nguyên n, m (0 < n, m ≤ 109)
 Các dòng tiếp theo: dãy ai (0 < ai ≤ 106).
Kết quả
 Một số duy nhất là số k tìm được.
Ví dụ:
DỮ LIỆU KẾT QUẢ GIẢI THÍCH
5 3 4 12 Thùng 1: a1, a2
6 8 Thùng 2: a3, a4
5 9
Thùng 3: a5
Gợi ý:
Gọi sum là tổng các ai, dmax là giá trị ai lớn nhất, vậy kết quả bài toán nằm trong
đoạn [dmax .. sum]. Dãy ai được chọn phải theo thứ tự, với mỗi giá trị x của phép tìm kiếm
nhị phân ta kiểm tra xem có thể xếp vào đúng M thùng được không.
Function Check(x: longint): Boolean;
Var dem, i: longint;
s: int64;
Begin
dem := 1; s := 0;
for i := 1 to n do
if s+a[i] <= x then s := s+a[i]
else
begin
if a[i] > x then
exit(false);
inc(dem);
if dem > M then exit(false);
s := a[i];
end;
Check := true;
End;
Đoạn chương trình tìm kiếm nhị phân trong trường hợp tìm giá trị nhỏ nhất:
Procedure solve;
Var Dau, Cuoi, Giua: longint;
Begin
Res := sum;
{Chat nhi phan ket qua}
Dau := dmax;
Cuoi := sum;
while (dau < cuoi) do
begin
Giua := (Dau+Cuoi) div 2;
if Check(Giua) then
7
begin
Res := Giua;
Cuoi := Giua-1;
end
else Dau := Giua+1;
end;
End;
Độ phức tạp thuật toán là O(Nlog(sum))
Bài 4. WIRES - Dây dẫn
Cho n đoạn dây điện (1  n  105 ). Đoạn dây thứ i có độ dài li cm ( 0  li  105 ). Cần
phải cắt các đoạn đã cho thành các đoạn sao cho có được k đoạn dây bằng nhau có độ dài
nguyên. Có thể không cần cắt hết các đoạn dây đã cho. Mỗi đoạn dây bị cắt có thể có phần
còn thừa khác 0.
Yêu cầu: Xác định độ dài lớn nhất của đoạn dây có thể nhận được. Nếu không có cách cắt
thì đưa ra số 0.
Dữ liệu: Vào từ file văn bản WIRES.INP có dạng:
 Dòng đầu tiên chứa hai số nguyên n, k
 Dòng thứ i trong n dòng sau chứa số nguyên li .
Kết quả: Đưa ra file văn bản WIRES.OUT, kết quả trên một dòng dưới dạng số nguyên.
Ví dụ:
WIRES.INP WIRES.QUT
4 11 547 200
802 539
743
Lời giải O(nL)
Ta thử lần lượt độ dài cần tìm ( x1 ) từ nhỏ đến lớn (hoặc lớn về nhỏ), sau đó tiến
hành kiểm tra xem có cắt được k đoạn có độ dài x1 không?
Const MAX = 100000; fi = 'wires.inp'; fo = 'wires.out';
var n, k, res: longint;
L: array[1..MAX]of longint;
procedure ReadFile;
var f: text; i: longint;
Begin
assign(f, fi); reset(f);
Readln(f, n, k);
for i := 1 to n do Readln(f, L[i]);
close(f);
End;
function Check(x1: longint): boolean;
var i, count: longint;
Begin
count := 0;
for i := 1 to n do
count := count + L[i] div x1;
Check := count >= k;
End;
8
procedure Try;
var i, x1, lmax: longint; sum: int64;
Begin
sum := 0;
for i := 1 to n do sum := sum+ L[i];
lmax := sum div k;
res := 0;
for x1 := 1 to lmax do
if Check(x1) then res := x1;
End ;
procedure WriteResult;
var f: text;
Begin
assign(f,fo); ReWrite(f);
Writeln(f,res);
close(f);
End ;
BEGIN
ReadFile;
Try;
WriteResult;
END.
Trong chương trình trên, hàm Check sẽ kiểm tra xem với độ dài x1 có thể có cách
cắt được thành k đoạn hay không? Chi phí kiểm tra là O(n).
Số lượng thử x1 phụ thuộc vào độ dài của các đoạn dây mà độ dài các đoạn dây có
thể lên tới 109.
Lời giải O(nlogL)
Ta có nhận xét sau: nếu độ dài x1 có thể cắt được thành k đoạn thì đương nhiên ta
có thể cắt được thành k đoạn có độ dài x1  1 . Từ nhận xét này ta sẽ giảm thiểu số trường
hợp phải thử bằng thuật toán tìm kiếm nhị phân, cụ thể thay thủ tục Try bằng thủ tục
bsearch_Try như sau:
procedure bsearch_Try;
var i, x1, lmin, lmax:longint;
sum: longint;
Begin
sum := 0;
for i := 1 to n do sum := sum+ L[i];
lmax := sum div k;
lmin := 1;
while lmax > lmin do
Begin
x1 :=(lmax+lmin) div 2;
if Check(x1) then
Begin
res := x1;
lmin := x1+1;
End
else lmax := x1 - 1;
End;
End;

9
Bài 5. NKSGAME - Trò chơi với dãy số (Đề thi HSG QG bảng B – 2007-2008).
Hai bạn học sinh trong lúc nhàn rỗi nghĩ ra trò chơi sau đây. Mỗi bạn chọn trước
một dãy số gồm n số nguyên. Giả sử dãy số mà bạn thứ nhất chọn là: b1, b2, ..., bn còn dãy
số mà bạn thứ hai chọn là c1, c2, ..., cn. Mỗi lượt chơi mỗi bạn đưa ra một số hạng trong dãy
số của mình. Nếu bạn thứ nhất đưa ra số hạng bi (1 ≤ i ≤ n), còn bạn thứ hai đưa ra số hạng
cj (1 ≤ j ≤ n) thì giá của lượt chơi đó sẽ là |bi+cj|.
Ví dụ: Giả sử dãy số bạn thứ nhất chọn là 1, -2; còn dãy số mà bạn thứ hai chọn là
2, 3. Khi đó các khả năng có thể của một lượt chơi là (1, 2), (1, 3), (-2, 2), (-2, 3). Như vậy,
giá nhỏ nhất của một lượt chơi trong số các lượt chơi có thể là 0 tương ứng với giá của lượt
chơi (-2, 2).
Yêu cầu: Hãy xác định giá nhỏ nhất của một lượt chơi trong số các lượt chơi có thể.
Dữ liệu: vào từ file văn bản NKSGAME.INP
 Dòng đầu là số nguyên dương (1 ≤ n ≤ 105)
 Dòng thứ hai chứa các số là dãy b (|bi| ≤ 109)
 Dòng thứ hai chứa các số là dãy c (|ci| ≤ 109)
Kết quả: ghi ra file văn bản NKSGAME.OUT giá trị nhỏ nhất tìm được
Ví dụ:
NKSGAME.INP NKSGAME.OUT
2 0
1 -2
2 3
Ràng buộc: 60% số test ứng với 60% số điểm có 1 ≤ n ≤ 1000
Hướng dẫn giải : Bài toán có thể giải bằng 2 cách:
Cách 1: Sử dụng 2 vòng lặp lồng nhau để tìm giá trị nhỏ nhất. Độ phức tạp là O(N2)
Cách 2: Sử dụng phương pháp tìm kiếm nhị phân như sau:
 Sort lại 2 dãy tăng dần.
 Với mỗi phần tử thứ i của dãy B tìm phần tử đầu tiên lớn hơn hoặc bằng (-B[i])
trong C, gọi vị trí đó là k, cập nhật res = min(abs(C[k]+B[i]), abs(C[k-1]+B[i]),
abs(C[k+1]+B[i]));
 Độ phức tạp là O(NlogN).
Chương trình minh họa:
Uses Math;
Const fi = 'nksgame.inp'; fo = 'nksgame.out'; maxN = 100001;
Type mang = Array[0..maxN] of Longint;
Var n, ans, i, j, x: Longint;
b, c: mang;
Procedure QSort(Var m: mang; L, R: Longint);
Var i, j, x, temp: Longint;
Begin
i := L; j := R;
x := m[(L+R) div 2];
10
Repeat
While m[i] < x do Inc(i);
While x < m[j] do Dec(j);
If i <= j then
Begin
temp := m[i]; m[i] := m[j]; m[j] := temp;
Inc(i); Dec(j);
End;
Until i > j;
If L < j then QSort(m, L, j);
If i < R then QSort(m, i, R);
End;
Function TimNP_Chinhxac(x: Longint): Longint;
Var mid, L, R: Longint;
Begin
L := 1;
R := n;
Repeat
mid := (L+R) div 2;
If c[mid] = x then Exit(mid);
If c[mid] > x then R := mid-1 Else L := mid+1;
Until L > R;
Exit(0);
End;
Function TimNP_gandung(x: Longint): Longint;
Var L, R, mid: Longint;
Begin
L := 1;
R := n;
Repeat
mid := (L+R) div 2;
If c[mid] < x then
Begin
If (c[mid] < x) and (c[mid+1] > x) then Exit(mid+1);
L := mid+1;
End;
If c[mid] > x then
Begin
If (c[mid] > x) and (c[mid-1] < x) then Exit(mid);
R := mid-1;
End;
Until L > R;
Exit(0);
End;
Procedure Xuly;
Var i, j, temp, k: Longint;
Begin
If (b[1] >= 0) and (c[1]>= 0) then
Begin Writeln(b[1]+c[1]); Exit; End;
If (b[n] <= 0) and (c[n]<= 0) then
Begin Writeln(Abs(b[n]+c[n])); Exit; End;
ans := MaxLongint;
For i := 1 to n do
Begin
If TimNP_chinhxac(-b[i]) <> 0 then
Begin
Writeln(0); Exit;
End;
k := TimNP_gandung(-b[i]);

11
temp := Min(Abs(b[i]+c[k-1]), Abs(b[i]+c[k]));
{C[k] > -B[i] và C[k-1] < -B[i] do TimNP_chinhxac ở trên}
ans := Min(ans, temp);
End;
Writeln(ans);
End;
Begin
Assign(input, fi); Reset(input);
Assign(output, fo); Rewrite(output);
Readln(n);
ans := MaxLongint;
For i := 1 to n do Read(b[i]);
Readln;
For i := 1 to n do Read(c[i]);
b[0] := -MaxLongint; c[0] := -MaxLongint;
b[n+1] := MaxLongint; c[n+1] := MaxLongint;
QSort(b, 1, n); QSort(c, 1, n);
Xuly;
Close(input);
Close(output);
End.
Bài 6. BCSEQ1 – Đoạn số có tổng bằng nhau
Một đoạn số có tổng bằng nhau trong một dãy số là một nhóm các số theo đúng thứ
tự ban đầu trong dãy mà nếu nhóm với nhau thì sẽ cho ra cùng một giá trị tổng. Ví dụ với
dãy: 2 5 1 3 3 7 thì ta có thể nhóm thành: (2 5) (1 3 3) (7) cùng cho giá trị tổng là 7.
Chú ý: đoạn đặc biệt chứa tất cả các phần tử của dãy cũng được coi là một đoạn có
tổng bằng nhau với chính giá trị tổng các số của dãy đó.
Yêu cầu: viết chương trình nhận vào các dãy số nguyên dương và trả về giá trị tổng
nhỏ nhất có thể của một đoạn tổng bằng nhau trong dãy.
Dữ liệu vào
Dòng đầu tiên chứa một số nguyên 1 ≤ t ≤ 1000 là số lượng bộ test. Mỗi bộ test bao
gồm:
 Dòng đầu tiên chứa thứ tự bộ test và số M (1≤ M ≤ 10000) là số phần tử của dãy.
 Các dòng tiếp theo mỗi dòng ghi 10 số của dãy phân cách bởi 1 dấu cách. Dòng
cuối cùng có thể có ít hơn 10 số. (Các số trong dãy đều nhỏ hơn 20000).
Dữ liệu ra
 Với mỗi bộ test, in ra trên một dòng gồm số thứ tự bộ test và tổng nhỏ nhất có thể
đạt được của các đoạn số có tổng bằng nhau.
Ví dụ:
Bcseq1.inp Bcseq1.out
3 1 7
1 6 2 21
2 5 1 3 3 7 3 2
2 6
1 2 3 4 5 6
3 20
1 1 2 1 1 2 1 1 2 1
1 2 1 1 2 1 1 2 1 1

12
Hướng dẫn:
Đầu tiên xây dựng mảng A, với A[i] là tổng của A[1]..A[i] tức là
A[i] := A[i-1]+A[i].
Ta nhận xét, nếu N là số phần tử thì ta chỉ có thể chia tối đa [1..n] nhóm. Vậy chỉ có
thể chia thành X nhóm thì tổng của mảng A chia hết cho X. Từ đó ta sẽ thử các trường hợp
đó.
Việc còn lại là xây dựng hàm kiểm tra, có thể chia thành X nhóm hay không. Để xử
lí được vấn đề này, như ở bước đầu, tôi đề nghị xây dựng mảng A, chỉ cần dùng tìm kiếm
nhị phân để kiểm tra là được.
Đầu tiên đứng ở vị trí 0 (gọi là H), tìm kiếm nhị phân tìm vị trí (gọi là vị trí k) mà
A[k] - A[H] = tổng đoạn con được chia, rồi từ vị trí đó cứ tiếp tục tìm như vậy. Nếu tìm có
đủ X lần (số lượng đoạn được chia) thì hàm trả về TRUE, còn không thì thoát ngay hàm và
trả về FALSE.
Chương trình tham khảo:
const fi = 'bcseq1.inp';
fo = 'bcseq1.out';
nmax = 10000;
type data = longint;
var f, g: text;
A: array[0..nmax+1] of data;
n, test: data;
function tknp(dau, cuoi, x: data): data;
var giua: data;
begin
while dau <= cuoi do
begin
giua := (dau+cuoi) div 2;
if A[giua] = x then
exit(giua)
else
if A[giua] > x then
cuoi := giua-1
else
dau := giua+1;
end;
exit(0);
end;

function check(X, sl: data): boolean;


var i, j, id, vt: data;
begin
id := 0;
for i := 1 to sl do
begin
vt := tknp(id, n, A[id]+x);
if vt = 0 then exit(false);
id := vt;
end;
exit(true);
end;

13
procedure xuli;
var i, j: data;
begin
i := a[n];
if A[n] mod i = 0 then
if check(A[n] div i, i) then
begin
writeln(g, test, ' ', A[n] div i);
exit;
end;
i := A[n] div 2;
if A[n] mod i = 0 then
if check(A[n] div i, i) then
begin
writeln(g, test, ' ', A[n] div i);
exit;
end;
for i := trunc(sqrt(A[n])) downto 2 do
if A[n] mod i = 0 then
if check(A[n] div i, i) then
begin
writeln(g, test, ' ', A[n] div i);
exit;
end;
writeln(g, test, ' ', A[n]);
end;
procedure docdl;
var i, j, sl: data;
begin
assign(f, fi); reset(f); assign(g, fo); rewrite(g);
read(f, sl);
A[0] := 0;
for i := 1 to sl do
begin
read(f, test, n);
for j := 1 to n do
begin
read(f, A[j]);
A[j] := A[j-1] + A[j];
end;
xuli;
end;
close(f);
close(g);
end;
Begin docdl; End.
Trên đây tôi đã trình bày một số dạng bài của kỹ thuật “tìm kiếm nhị phân” thông
qua các ví dụ và bài tập minh họa. Những bài được đưa vào thường áp dụng được ngay cấu
trúc tìm kiếm nhị phân đã nêu. Nhưng trong thực tế có nhiều bài chúng ta phải vận dụng
linh hoạt kỹ thuật trên, việc thay đổi giá trị của hai biến đầu và cuối sau mỗi lần kiểm tra
sẽ quyết định tính đúng đắn của thuật toán, nếu không kiểm soát được hai biến này chương
trình có thể dẫn tới lặp vô hạn, đây là điều mà các học sinh rất ngại khi cài đặt bằng kỹ
thuật trên.
Muốn học tốt Tin học thì phải biết vận dụng những kết quả và những suy luận,
chứng minh từ toán học làm cơ sở cho việc tìm kiếm sẽ làm cho những bài toán tin có
14
những giải thuật đơn giản, thời gian chạy chương trình được rút ngắn lại, tìm được kết quả
nhanh chóng và đáng tin cậy hơn. Do đó luyện tập với nhiều bài tập vẫn là cách tốt nhất để
rút ra cấu trúc tìm kiếm nhị phân cho riêng mình.
Tôi viết tham luận này nhằm mục đích cùng trao đổi với Quý Thầy Cô giảng dạy
chuyên đề bồi dưỡng học sinh giỏi Tin học về việc “hệ thống” các kiến thức, một vài kỹ
năng, ứng dụng toán học vào lập trình giải quyết các bài toán tin. Vì kiến thức và thời gian
còn nhiều hạn chế nên chắc rằng tham luận còn có thiếu sót, tôi chân thành đón nhận sự
góp ý của Quý Thầy Cô. Xin chân thành cảm ơn.
Tiền Giang, tháng 7 năm 2017.

15
SỞ GIÁO DỤC VÀ ĐÀO TẠO SÓC TRĂNG
TRƢỜNG THPT CHUYÊN NGUYỄN THỊ MINH KHAI

CHUYÊN ĐỀ

HÌNH HỌC TÍNH TOÁN

BIÊN SOẠN: LÂM THANH PHƢƠNG

Sóc Trăng, 6/2017

290
Chuyên đề hình học tính toán Tổ Tin học - Trường THPT Chuyên NTMK

LỜI NÓI ĐẦU


Hình học tính toán là một nhánh của ngành khoa học máy tính, chuyên nghiên cứu
về thuật toán giải quyết các bài toán liên quan tới các đối tượng hình học. Trong toán
học và công nghệ hiện đại, hình học tính toán có ứng dụng khá rộng rãi trong lĩnh vực
về đồ họa máy tính, thiết kế, mô phỏng…

Có những bài toán hình học giải trên giấy thì rất đơn giản nhưng thể hiện nó trên
máy tính thì cần những chương trình không đơn giản chút nào. Chính vì đều đó làm
cho chuyên đề "Hình học tính toán" trở thành một chuyên đề hay và nó góp phần
không nhỏ cho sự thành công của học sinh trong đội tuyển học sinh giỏi.

Hi vọng nhận được những ý kiến đóng góp của quý thầy cô về nội dung, chất lượng
và hình thức trình bày để cho chuyên đề hoàn thiện hơn.

Biên soạn: Lâm Thanh Phương Trang 1


Chuyên đề hình học tính toán Tổ Tin học - Trường THPT Chuyên NTMK

CHUYÊN ĐỀ . HÌNH HỌC TÍNH TOÁN

1 TỔNG QUAN
1.1 Mục tiêu
Sau khi học chuyên đề này, chúng ta nắm được những kiến thức sau:
- Hiểu khái niệm về các đối tượng hình học cơ bản.
- Nắm được thuật toán thực hiện các bài toán hình học cơ bản.
- Nắm được thuật toán giải một số bài toán giải một số bài toán hình học cơ
bản: tính diện tích đa giác, bài toán tìm bao lồi.
- Hiểu được sai số làm tròn.

1.2 Kiến thức cơ bản cần thiết

Các kiến thức cơ bản cần thiết để học chương này bao gồm:
- Kiến thức toán học: công thức tính khoảng cách giữa 2 điểm, tính góc, đa
giác, phương trình đường thẳng, vị trí tương đối giữa hai đoạn thẳng.
- Kĩ thuật lập trình và lập trình đệ quy.

1.3 Nội dung cốt lõi

Trong chuyên đề này chúng ta sẽ nghiên cứu các vấn đề sau:


• Điểm, đường thẳng và đoạn thẳng; giao các đường thẳng và đoạn thẳng.
• Đa giác.

2 NỘI DUNG
2.1 ĐIỂM, ĐƯỜNG THẲNG VÀ ĐOẠN THẲNG; GIAO CÁC ĐƯỜNG
THẲNG VÀ ĐOẠN THẲNG

2.1.1 Đối tượng hình học cơ bản


Trong các bài toán tin học thuộc loại hình học có 3 đối tượng cơ bản là: Điểm, đoạn
thẳng và đa giác.
- Điểm: Được xác định là cặp (x,y) trong hệ toạ độ đề các.
- Đoạn thẳng: Là cặp điểm được nối với nhau bằng một phần của đường thẳng.
- Đa giác: Là dãy các điểm mà 2 điểm liên tiếp nối với nhau bởi đoạn thẳng và
điểm đầu nối với điểm cuối tạo thành đường gấp khúc khép kín.

2.1.2 Dữ liệu lưu trữ các đối tượng hình học cơ bản
Điểm (Point):

Trong hình học, chúng ta xét trong hệ Đề các Oxy, thì một điểm có toạ độ: (x,y).

Biên soạn: Lâm Thanh Phương Trang 2


Chuyên đề hình học tính toán Tổ Tin học - Trường THPT Chuyên NTMK

Chính vì thế ta lưu tọa độ một điểm trong một bản ghi Record:
Kiểu nguyên:
Type
Point = Record
x , y : longint ;
End ;
Kiểu thực:
Type
Point = Record
x ,y : Real ;
End ;
Chính vì vậy khi xét tới toạ độ của P(x,y) thì ta xét P.x, P.y
Chúng ta biết khoảng cách giữa hai điểm P(x1,y1)và Q(x2,y2) trong mặt phẳng:
Function Khoang_Cach(P,Q: Point) : Real ;
Begin
Khoang_Cach:=Sqrt(Sqr(P.x-Q.x)+Sqr(P.y-Q.y));
End ;
Đường thẳng (line):

Trong hình học, chúng ta có phương trình của một đường thẳng trong mặt phẳng:
Ax+By+C =0. Chúng ta coi A, B, C là biểu diễn cho đường thẳng đó. Nếu một đường
thẳng (d):
Ax + By + C = 0, đi qua 2 điểm A (x1,y1) và B(x2,y2) thì nó có:
A:= y1- y2;
B:= x2 - x1;
C:= -(A.x1+B.y1).
Chính vì thế chúng ta dùng thủ tục xác định A, B, C của một đường thẳng đi qua 2
điểm như sau:
Procedure Xac_DinhABC(P, Q: Point, var A , B , C: Longint );
Begin
A := P.y-Q.y ;

Biên soạn: Lâm Thanh Phương Trang 3


Chuyên đề hình học tính toán Tổ Tin học - Trường THPT Chuyên NTMK

B := Q.x-P.x ;
C := -(A*P.x+B*P.y ) ;
End ;
Và chúng ta có thể coi đường thẳng là một kiểu:
Type
Lines = Record
a , b , c : Longint;
End ;
Chú ý:
* ″Lines″ chứ không phải là ″Line″, vì trong Pascal có thủ tục Line để vẽ đường
thẳng. Thông thường chúng ta làm bài hình học để quan sát trực quan thì chúng ta
thường biểu diễn lên hình vẽ. Chính vì thế chúng ta cần phải tránh những sai sót
không đáng có.
* Sở dĩ chúng ta phải khai báo A, B, C trong Longint vì nếu chúng ta lưu A, B, C
trong Longint thì lúc chúng ta tính toán phương trình không tràn bộ nhớ số học.
Đoạn thẳng:
Đoạn thẳng là một phần của đường thẳng, bị giới hạn x, y. Chúng ta xét đoạn thẳng
thông thường cho đi qua 2 điểm. Chính vì thế toạ độ x, y bị giới hạn trong khoảng đó.
Góc :

Góc định hướng α giữa hai vectơ u =(xu; yu) và v =(xv; yv):
xu yv  xv yu
sin α =
x 2
u  yu2  xv2  yv2 

xu xv  yv yu
cos α =
x 2
u  yu2  xv2  yv2 

Đa giác:
Type
Polygon = array[1..n] of Point;

2.1.3 Vị trí tương đối của điểm so với đường thẳng và đoạn thẳng
Bài toán 1: Cho điểm M(xM,yM), A(xA,yA), B(xB,yB). Yêu cầu:
a) Kiểm tra M có thuộc đường thẳng đi qua 2 điểm A, B hay không?
b) Kiểm tra M có thuộc đoạn thẳng AB hay không
c) Kiểm tra M có thuộc tia AB hay không
Biên soạn: Lâm Thanh Phương Trang 4
Chuyên đề hình học tính toán Tổ Tin học - Trường THPT Chuyên NTMK

Dữ liệu: Cho trong file BT1.INP gồm 3 dòng


- Dòng 1 chứa 2 số nguyên xM,yM là tọa độ điểm M.
- Dòng 2 chứa 2 số nguyên xA,yA là tọa độ điểm A.
- Dòng 3 chứa 2 số nguyên xB,yB là tọa độ điểm B.
Các số trên mỗi dòng ghi cách nhau 1 dấu cách.
Kết quả: : Cho trong file BT1.OUT gồm 3 dòng
- Dòng 1: M thuoc duong thang AB hay M khong thuoc duong thang AB
- Dòng 2: M thuoc doan thang AB hay M khong thuoc doan thang AB
- Dòng 3: M thuoc tia AB hay M khong thuoc tia AB
Giới hạn:
Tọa độ các điểm là số nguyên nằm trong phạm vi từ -106 đến 106.
Ví dụ 1:
BT1.INP BT1.OUT
34 M thuoc duong thang AB
12 M khong thuoc doan thang AB
23 M thuoc tia AB
Ví dụ 2:
BT1.INP BT1.OUT
01 M thuoc duong thang AB
12 M khong thuoc doan thang AB
23 M khong thuoc tia AB
Ví dụ 3:
BT1.INP BT1.OUT
12 M thuoc duong thang AB
01 M thuoc doan thang AB
23 M thuoc tia AB
Ví dụ 4:
BT1.INP BT1.OUT
00 M khong thuoc duong thang AB
01 M khong thuoc doan thang AB
23 M khong thuoc tia AB

Phƣơng pháp:
Đặt F(X,Y) = (yA-yB)X + (xB-xA)Y + (xAyB - xByA)
- Điểm M thuộc đường thẳng AB khi F(xM,yM) = 0
- Điểm M thuộc đoạn thẳng AB khi:
F(xM,yM)=0 và Min(xA,xB)xM Max(xA,xB) và Min(yA,yB) yM  Max(yA,yB)

- Điểm M thuộc tia AB khi F(xM,yM) = 0 và AM  k AB có nghĩa là M


phải thoả mãn điều kiện: F(xM,yM) = 0 và (xM-xA)(xB-xA)0 và (yM-yA)(yB-yA)0
Chƣơng trình:
program bt1;
uses math;
const
fi='BT1.INP';
fo='BT1.OUT';
Type
Biên soạn: Lâm Thanh Phương Trang 5
Chuyên đề hình học tính toán Tổ Tin học - Trường THPT Chuyên NTMK

Point = Record
x ,y : longint ;
End ;

var A, B, M:Point;
Procedure Nhap;
begin
readln(M.x,M.y);
readln(A.x,A.y);
readln(B.x,B.y);
end;
{=============}
Function F(x,y:longint):longint;

begin
F:= (A.y-B.y)*x+(B.x-A.x)*y +(A.x*B.y - B.x*A.y);
end;
{=============}
Procedure kiemtra;
begin
{====Kiem tra M thuoc duong thang====}
if F(M.x,M.y)=0 then writeln('M thuoc duong thang AB')
else writeln('M khong thuoc duong AB');
{====Kiem tra M thuoc doan thang====}
if (F(M.x,M.y)=0) and (min(A.x,B.x)<=M.x) and (M.x<=max(A.x,B.x))
and (min(A.y,B.y)<=M.y) and (M.y<=max(A.y,B.y))
then writeln('M thuoc doan thang AB')
else writeln('M khong thuoc doan thang AB');
{====Kiem tra M thuoc tia====}
if (F(M.x,M.y)=0) and ((M.x-A.x)*(B.x-A.x)>=0)
and ((M.y-A.y)*(B.y-A.y)>=0)
then writeln('M thuoc tia AB')
else writeln('M khong thuoc tia AB');
end;
Begin
assign(input,fi); reset(input);
assign(output,fo); rewrite(output);
nhap;
kiemtra;
close(input); close(output);
End.

2.1.4 Giao của các đường thẳng


Bài toán 2. Cho 2 đường thẳng L1, L2 lần lượt có phương trình a1x+b1y+c1=0 và
a2x+b2y+c2=0. Tìm giao điểm (nếu có) của 2 đường thẳng trên.
Dữ liệu: Cho trong file BT2.INP gồm 2 dòng
- Dòng 1 chứa 3 số nguyên a1, b1, c1 là hệ số của đường thẳng L1.

Biên soạn: Lâm Thanh Phương Trang 6


Chuyên đề hình học tính toán Tổ Tin học - Trường THPT Chuyên NTMK

- Dòng 2 chứa 3 số nguyên a2, b2, c2 là hệ số của đường thẳng L2.


Các số trên mỗi dòng ghi cách nhau 1 dấu cách.
Kết quả: : Cho trong file BT2.OUT gồm:
Nếu đường thẳng L1 cắt đường thẳng L2 tại M(xM,yM) thì ghi là:
Dòng 1: Ghi số 1.
Dòng 2: Ghi tọa độ điểm M (làm tròn 2 chữ số thập phân) , hai số cách nhau
một khoảng cách.
Nếu đường thẳng L1 song song L2 thì ghi một dòng duy nhất là:
Ghi số 2.
Nếu đường thẳng L1 trùng L2 thì ghi một dòng duy nhất là:
Ghi số 3.
Giới hạn:
a1, b1, c1, a2, b2, c2 là số nguyên nằm trong phạm vi từ -106 đến 106.

Ví dụ 1:
BT2.INP BT2.OUT
123 1
2 -1 3 -1.80 -0.60
Ví dụ 2:
BT2.INP BT2.OUT
123 2
5 10 8
Ví dụ 3:
BT2.INP BT2.OUT
123 3
246

Phƣơng pháp:
B1. Tính D = a1b2 - a2b1, Dx = c2b1 - c1b2, Dy = a2c1 - a1c2
B2. Xét 3 khả năng:
+ Nếu D=Dx=Dy=0 thì kết luận 2 đường thẳng trùng nhau
+ Nếu D=0 và ((Dx0) hoặc (Dy0)) thì kết luận 2 đường thẳng song song
+ Nếu D0 thì kết luận 2 đường thẳng cắt nhau tại điểm có (Dx/D, Dy/D)
Chương trình:
program bt2;
uses crt;
const maxn=100;
fi='BT2.INP';
fo='BT2.OUT';
Type
Lines = Record
a , b , c : Longint;
End ;

var L1,L2:Lines;
D,Dx,Dy:real;

Biên soạn: Lâm Thanh Phương Trang 7


Chuyên đề hình học tính toán Tổ Tin học - Trường THPT Chuyên NTMK

Procedure nhap;
begin
readln(L1.a,L1.b,L1.c);
readln(L2.a,L2.b,L2.c);
end;
procedure tinhtoan;
begin
D:=L1.a*L2.b-L2.a*L1.b;
Dx:=L2.c*L1.b-L1.c*L2.b;
Dy:=L2.a*L1.c-L1.a*L2.c;
if (D=0)and(Dx=0)and(Dy=0) then write(3)
else
if (D=0)and((Dx<>0)or(Dy<>0)) then write(2)
else
begin
writeln(1);
writeln(Dx/D:0:2,' ',Dy/D:0:2);
end;
end;
Begin
assign(input,fi); reset(input);
assign(output,fo); rewrite(output);
nhap;
tinhtoan;
close(input); close(output)
End.

2.1.5 Giao của các đoạn thẳng


Bài toán 3. Cho 2 đoạn thẳng AB và PQ với A(xA,yA), B(xB,yB) ; P(xP,yP), Q(xQ,yQ).
Tìm giao điểm (nếu có) của 2 đoạn thẳng.
Dữ liệu: Cho trong file BT3.INP gồm 4 dòng
- Dòng 1 chứa 2 số nguyên xA,yA là tọa độ điểm A.
- Dòng 2 chứa 2 số nguyên xB,yB là tọa độ điểm B.
- Dòng 3 chứa 2 số nguyên xP,yP là tọa độ điểm P.
- Dòng 4 chứa 2 số nguyên xQ,yQ là tọa độ điểm Q.
Các số trên mỗi dòng ghi cách nhau 1 dấu cách.
Kết quả: : Cho trong file BT3.OUT gồm:
Nếu đoạn thẳng AB cắt PQ tại M(xM,yM) thì ghi 2 dòng:
Dòng 1: Ghi số 1
Dòng 2: Ghi tọa độ điểm M (làm tròn 2 chữ số thập phân), hai số cách nhau một
khoảng cách.
Nếu đoạn thẳng AB không cắt PQ thì ghi một dòng duy nhất:
Ghi số 0
Giới hạn:
Tọa độ các điểm là số nguyên nằm trong phạm vi từ -106 đến 106.
Ví dụ 1:
BT3.INP BT3.OUT

Biên soạn: Lâm Thanh Phương Trang 8


Chuyên đề hình học tính toán Tổ Tin học - Trường THPT Chuyên NTMK

01 1
23 1.00 2.00
03
21
Ví dụ 2:
BT3.INP BT3.OUT
01 0
23
11
21
Phƣơng pháp:
Bước 1. Tìm giao điểm M của 2 đường thẳng AB và CD
Bước 2. Kiểm tra M có thuộc đồng thời cả 2 đoạn AB và CD hay không. Nếu có đó là
giao điểm cần tìm, ngược lại kết luận không có.
Chƣơng trình:
program bt3;
uses math;
const
fi='BT3.INP';
fo='BT3.OUT';
Type
Point = Record
x ,y : Real ;
End ;
Lines = Record
a , b , c : Real;
End ;
var
A, B, P, Q, M:Point;
L1,L2:Lines;
D,Dx,Dy:real;
Procedure nhap;
begin

Biên soạn: Lâm Thanh Phương Trang 9


Chuyên đề hình học tính toán Tổ Tin học - Trường THPT Chuyên NTMK

readln(A.x,A.y);
readln(B.x,B.y);
readln(P.x,P.y);
readln(Q.x,Q.y);
end;
{===Kiem tra M thuoc doan ==}
Function Ktra(M,A,B:Point):boolean;
begin
if (min(A.x,B.x)<=M.x) and (M.x<=max(A.x,B.x))
and (min(A.y,B.y)<=M.y) and (M.y<=max(A.y,B.y))
then Ktra:=true else Ktra:=false;
end;
{=====Xac dinh he so a,b, c cua phuong trinh duong thang=====}
Procedure Xac_DinhABC(P, Q: Point; var a , b , c: real);
Begin
a := P.y-Q.y ;
b := Q.x-P.x ;
c := -(A*P.x+B*P.y ) ;
End ;
{===Xac dinh D, Dx, Dy===}
procedure xac_dinh_D_Dx_Dy(A,B,P,Q:Point; Var D,Dx,Dy:real);
begin
Xac_DinhABC(A,B,L1.a,L1.b,L1.c);
Xac_DinhABC(P,Q,L2.a,L2.b,L2.c);
D:=L1.a*L2.b-L2.a*L1.b;
Dx:=L2.c*L1.b-L1.c*L2.b;
Dy:=L2.a*L1.c-L1.a*L2.c;
end;
{===Tim giao diem neu co ===}
Biên soạn: Lâm Thanh Phương Trang 10
Chuyên đề hình học tính toán Tổ Tin học - Trường THPT Chuyên NTMK

procedure xuat;
begin
xac_dinh_D_Dx_Dy(A,B,P,Q,D,Dx,Dy);
M.x:= Dx/D;
M.y:=Dy/D;
if(D<>0)and(ktra(M,A,B))and (ktra(M,P,Q))
then
begin
writeln(1);
writeln(M.x:0:2,' ',M.y:0:2);
end
else writeln(0);
end;
{==============}
Begin
assign(input,fi); reset(input);
assign(output,fo); rewrite(output);
nhap;
xuat;
close(input); close(output);
End.

2.2 ĐA GIÁC

2.2.1 Diện tích đa giác


Bài toán 4.
Cho N đa giác lồi A1A2A3...AN-1AN với các đỉnh Ai(xi,yi) có toạ độ nguyên. Hãy tính
diện tích đa giác trên.
Dữ liệu: Cho trong file BT4.INP gồm 2 dòng
- Dòng 1: Chứa số nguyên dương N
- Dòng 2: Chứa 2xN số nguyên x1 y1 x2 y2...xN yN là toạ độ các đỉnh của đa giác.
Mỗi số ghi cách nhau một dấu cách.
Kết quả: Xuất ra file BT4.OUT một số duy nhất là diện tích đa giác làm tròn 2 chữ số
thập phân.
Biên soạn: Lâm Thanh Phương Trang 11
Chuyên đề hình học tính toán Tổ Tin học - Trường THPT Chuyên NTMK

Ví dụ :
BT4.INP BT4.OUT
5 32.00
-8 -8 0 0 1 0 -2 4 -5 0
Giới hạn:
N là số nguyên không quá 100.
Tọa độ các điểm là số nguyên nằm trong phạm vi từ -106 đến 106.
Phƣơng pháp:
Lưu toạ độ các đỉnh đa giác vào mảng A. Chia đa giác thành các hình thang bằng cách
chiếu các cạnh xuống trục hoành (hoặc trục tung). Hình thang được xác định bởi cạnh
A[i]A[i+1] có diện tích là Abs(S) với:
S = (A[i].x - A [i+1].x).(A[i].y + A[i+1].y) /2
Sau khi gán đỉnh A[n+1] = A[1], ta tính diện tích toàn phần của đa giác như sau:
S: = 0;
For i: =1 to n do
S := S + (A[i].x - A [i+1].x).(A[i].y + A[i+1].y);
S: = (1/2) * Abs(S);
Chƣơng trình:
program bt4;
const maxn=100;
fi='BT4.INP';
fo='BT4.OUT';
type point=record
x,y:longint; end;
var A:array[1..maxn+1] of point;
n,i:longint; s:real;
Procedure Nhap;
begin
readln(n);
for i:=1 to n do read(A[i].x,A[i].y);
end;
procedure Dientich;
begin
s:=0;
A[n+1].x:=A[1].x;
A[n+1].y:=A[1].y;
for i:=1 to n do
s:=s+(A[i+1].x-A[i].x)*(A[i+1].y+A[i].y)/2;
writeln(abs(s):0:2);
end;
Begin
assign(input,fi); reset(input);
assign(output,fo); rewrite(output);
nhap;
dientich;
close(input); close(output);
Biên soạn: Lâm Thanh Phương Trang 12
Chuyên đề hình học tính toán Tổ Tin học - Trường THPT Chuyên NTMK

End.

2.2.2 Điểm thuộc đa giác


Bài toán 5.
Cho đa giác không tự cắt A1A2...AN với các đỉnh Ai(xi,yi) nguyên. Với điểm
M(xM,yM) cho trước, hãy xác định xem M có nằm trong đa giác đã cho hay không
(Trong trường hợp trên cạnh đa giác xem như nằm trong đa giác)
Dữ liệu: Cho trong tệp BT5.INP
+ Dòng đầu là số N
+ N dòng tiếp theo mỗi dòng ghi xi,yi là toạ độ Ai
+ Dòng n+2 ghi 2 số xM và yM
Dữ liệu là các số nguyên.
Kết quả: Đưa ra tệp BT5.OUT một con số duy nhất: 0 là điểm A không nằm
trong đa giác còn 1 là điểm M nằm trong đa giác.
Ví dụ 1:
BT5.INP BT5.OUT
4 0
00
20
22
02
33
Ví dụ 2:
BT5.INP BT5.OUT
5 1
00
20
03
-2 2
-3 -2
00
Giới hạn:
N là số nguyên không quá 100.
Tọa độ các điểm là số nguyên nằm trong phạm vi từ -106 đến 106.
Phƣơng pháp:
Bước 1: Lưu toạ độ các đỉnh đa giác vào mảng A
Bước 2: Gọi S là diện tích đa giác A1A2...AN. Cách tình diện tích đa giác:
Chia đa giác thành các hình thang bằng cách chiếu các cạnh xuống trục
hoành (hoặc trục tung). Hình thang được xác định bởi cạnh A[i]A[i+1] có diện
tích là Abs(S) với:
S = (A[i].x - A [i+1].x).(A[i].y + A[i+1].y) /2
Sau khi gán đỉnh A[N+1] = A[1], ta tính diện tích toàn phần của đa giác như
sau:
S: = 0;
Biên soạn: Lâm Thanh Phương Trang 13
Chuyên đề hình học tính toán Tổ Tin học - Trường THPT Chuyên NTMK

For i: =1 to n do
S := S + (A[i].x - A [i+1].x).(A[i].y + A[i+1].y);
S: = (1/2) * Abs(S);
Bước 3: Gọi S2 tổng diện tích các tam giác tạo bởi các điểm MA1A2, MA2
A3….MAN A1. Việc tính S2 ta có thể áp dụng công thức tích diện tích đa giác S.
Bước 4: So sánh S và S2 nếu S=S2 thì điểm M nằm trong đa giác ngược lại
thì nằm ngoài đa giác.
Chƣơng trình:
program bt5;
const
fi='BT5.inp';
fo='BT5.out';
nmax=1000;
type
diem=record
x,y: real;
end;
var
a:array[1..nmax] of diem;
i,n: longint;
s,s2:real;
m:diem;
procedure nhap;
begin
readln(n);
for i:=1 to n do
readln(a[i].x,a[i].y);
readln(m.x,m.y);
end;
function stg(c,d,e: diem) : real;
var tam: real;
begin
tam:=0;
tam:= tam + (c.x - d.x)*(c.y + d.y);
tam:= tam + (d.x - e.x)*(d.y + e.y);
tam:= tam + (e.x - c.x)*(e.y + c.y);
stg:= abs(tam)/2;
end;
procedure tinh;
begin
a[n+1]:= a[1];
s:=0; s2:=0;
for i:=1 to n do
s:=s + (a[i].x-a[i+1].x)*(a[i].y+a[i+1].y);
s:= abs(s)/2;
for i:=1 to n do
s2:=s2+ stg(a[i],a[i+1],m);
Biên soạn: Lâm Thanh Phương Trang 14
Chuyên đề hình học tính toán Tổ Tin học - Trường THPT Chuyên NTMK

end;
procedure xuat;
begin
if abs(s-s2)< 0.0000000001 then write(1) else write(0);
end;
begin
assign(input,fi); reset(input);
assign(output,fo); rewrite(output);
nhap;
tinh;
xuat;
close(input); close(output);
end.

2.2.3 Đa giác không tự cắt (bao lồi)


Bài toán 6.
Cho N điểm A1, A2, ..., AN trên mặt phẳng. Các điểm đều có toạ độ nguyên và
không có 3 điểm bất kỳ trong chúng thẳng hàng. Hãy viết chương trình thực hiện các
công việc sau đây: Xác định một đa giác không tự cắt có đỉnh là một số điểm trong các
điểm đã cho và chứa tất cả các điểm còn lại và có chu vi nhỏ nhất.
Dữ liệu: cho trong tệp BT6.INP gồm N+1 dòng BT6.INP BT6.OUT
+ Dòng 1: Chứa số N 5 4
+ Dòng i+1 (1 i  N): Ghi 2 chữ số nguyên xi,yi là 0 1 44
toạ độ đỉnh Ai. 4 4 04
04 01
Các số trên cùng một dòng cách nhau một khoảng 4 0 40
trắng. 22
Kết quả: Xuất ra tệp BT6.OUT
+ Dòng 1: Ghi số K với K là số đỉnh đa giác tìm được
+ Dòng i+1(1 i  K): Ghi toạ độ của đỉnh đa giác.
Giới hạn:
N là số nguyên không quá 100.
Tọa độ các điểm là số nguyên nằm trong phạm vi từ -106 đến 106.
Phƣơng pháp:
- Tìm điểm có hoành độ và tung độ nhỏ nhất. Điểm đó sẽ là đỉnh đa giác
- Giả sử ta đã chọn được điểm PM. Tìm điểm Pi sao cho góc hợp bởi PMPi và
trục hoành là nhỏ nhất và đồng thời góc này phải lớn hơn góc hợp bởi P MPM-1 và trục
hoành. Điểm Pi sẽ là một đỉnh của đa giác.
Chƣơng trình:
program bt1;
uses crt;
const eps=1e-6;pi=3.14;maxn=100;max=1000;
fi='BT6.INP';
fo='BT6.OUT';
Type
Point = Record

Biên soạn: Lâm Thanh Phương Trang 15


Chuyên đề hình học tính toán Tổ Tin học - Trường THPT Chuyên NTMK

x ,y : longint ;
End ;

var p:array[1..maxn+1] of Point;


L:array[1..maxn+1] of longint;
n,i,m:longint;
procedure nhap;

begin

readln(n);
fillchar(p,Sizeof(p),0);
for i:=1 to n do
readln(p[i].x,p[i].y);
end;
{=so sanh hai so thuc}
Function sosanh( a1,a2:real):boolean ;
begin
sosanh:=abs(a1-a2)<= eps;
end;

Function goc( p1,p: Point):real;


var rs,c,dx,dy:real;
begin
dx:=p.x-p1.x;
dy:=p.y-p1.y;
rs:=sqrt(sqr(dx)+sqr(dy));
if sosanh(rs,0) then goc:=0
else
begin
c:=dx/rs;
if sosanh(c,0) then c:=pi/2
else c:=arctan(sqrt(abs(1-sqr(c)))/c);
if dx<0 then c:=pi+c;
if dy<0 then c:=2*pi-c;
goc:=c;
end;
end;
procedure tinhtoan;
var
tam,min: Longint;
minangle, tmp,v: Real;
t: Point;
begin

min:= 1;
for i := 2 to n do
if (p[i].y < p[min].y) or (p[i].y = p[min].y) and (p[i].x < p[min].x)
Biên soạn: Lâm Thanh Phương Trang 16
Chuyên đề hình học tính toán Tổ Tin học - Trường THPT Chuyên NTMK

then
min:=i; m := 0;
p[n + 1] := p[min]; {de phat hien diem ket thuc}
{m la so diem tren bao}
minangle:=0.0;
repeat
Inc(m);
t:=p[m];
p[m]:=p[min];
p[min]:=t;
{min:=n+1;}v:=minangle; minangle:=360.0;
for i := m + 1 to n + 1 do
begin
if goc(p[m], p[i]) > v then
if goc(p[m], p[i]) < minangle then {trong nhung goc lon hon goc da chon thi chon
goc nho nhat}
begin
min:=i;
minangle := goc(p[m],p[min]);
L[m] := i;
end;
end;
until min= n + 1;
end;
procedure xuat;

begin
writeln(m);
for i:=1 to m do
writeln(p[l[i]].x,' ',p[l[i]].y);
end;
Begin
assign(input,fi); reset(input);
assign(output,fo); rewrite(output);
nhap;
tinhtoan;
xuat;
close(input); close(output);
End.

3 BÀI TẬP
3.1 Trên mặt phẳng cho một tam giác và một đoạn thẳng. Tính độ dài của đoạn thẳng
nằm trong tam giác.
3.2 Trên mặt phẳng cho N điểm. Tìm hai điểm xa nhất trong N điểm đó.
3.3 Trên mặt phẳng cho N điểm. Tìm hai điểm gần nhất trong N điểm đó.

Biên soạn: Lâm Thanh Phương Trang 17


Chuyên đề hình học tính toán Tổ Tin học - Trường THPT Chuyên NTMK

3.4 Cho hai hình chữ nhật với các cạnh song song với hệ trục tọa độ. Mỗi hình chữ
nhật xác định bởi tọa độ hai đỉnh đối. Tìm diện tích phần mặt phẳng được phủ bởi hai
hình chữ nhật và diện tích phần chung của chúng.
3.5 DIỆN TÍCH N HÌNH CHỮ NHẬT (Tên file: SHCN.PAS)
Trên mặt phẳng tọa độ cho N hình chữ nhật (HCN) có diện tích khác 0 và có các
cạnh song song với các trục tọa độ. Mỗi HCN được mô tả bằng bộ bốn số nguyên
(x1,y1) và (x2,y2) biểu thị tọa độ nguyên của hai đỉnh đối diện.
Yêu cầu: xác định diện tích phần mặt phẳng bị các HCN phủ.

Dữ liệu: tệp văn bản SHCN.INP Dòng đầu tiên: số tự nhiên 1 < N
<= 1000.
Dòng thứ i trong N dòng tiếp theo, mỗi dòng chứa 4 số nguyên x1, y1, x2, y2 cách
nhau qua dấu cách.
Kết quả: tệp văn bản SHCN.OUT chứa tổng đơn vị diện tích trên mặt phẳng bị
các HCN phủ.
Ví dụ:
SHCN.INP SHCN.OUT
5 35
0024
0246
5337
4165
7390
3.6 BÀI DIỆN TÍCH HÌNH HÌNH CHỮ NHẬT 2 (Tên file: HCN2.PAS)
Cho N hình chữ nhật (2<N<500) có các cạnh song song với hai trục toạ độ và
toạ độ các đỉnh đều nguyên. Các hình chữ nhật được đánh số từ 1 đến N.

Yêu cầu: Hãy tìm hai hình chữ nhật mà phần giao nhau của chúng có diện tích
lớn nhất.

Dữ liệu: Cho trong file văn bản có tên HCN2.INP:

Dòng đầu chứa số N.

Dòng thứ i trong N dòng tiếp theo mô tả hình chữ nhật thứ i, chứa 4 số nguyên
x1, y1, x2, y2 trong đó (x1, y1) là toạ độ đỉnh trái dưới và (x2, y2) là toạ độ đỉnh phải
trên của các hình chữ nhật (-1000<x1< x2 <1000;-1000<y1< y2 <1000).

Kết quả: xuất ra file HCN2.OUT gồm 1 dòng duy nhất, chứa 2 số nguyên dương
cho biết chỉ số của hình chữ nhật tìm được.

Ví dụ:
HCN2.INP HCN2.OUT

Biên soạn: Lâm Thanh Phương Trang 18


Chuyên đề hình học tính toán Tổ Tin học - Trường THPT Chuyên NTMK

3 12
1155
-5 -5 5 5
10 10 1000 1000
3.7 HÌNH CHỮ NHẬT LỒNG NHAU (Tên file: HCN3.PAS)
Cho N hình chữ nhật có các cạnh song song với hai trục toạ độ và toạ độ các đỉnh
đều nguyên. Các hình chữ nhật được đánh số từ 1 đến N. Hình chữ nhật thứ i được cho
bởi 4 số nguyên xi1, yi1, xi2, yi2 trong đó (xi1, yi1) là toạ độ đỉnh trái dưới và (xi2, yi2 ) là
toạ độ đỉnh phải trên của các hình chữ nhật . Ta nói hình chữ nhật thứ i nằm trong hình
chữ nhật thứ j nếu trên mặt phẳng toạ độ, mọi điểm của hình chữ i đều thuộc hình chữ
nhật j.
Yêu cầu: Với N hình chữ nhật cho trước, hãy tìm k hình chữ nhật với chỉ số i1, i2,
...., ik sao cho hình i1 nằm trong hình i2, hình i2 nằm trong hình i3,..., hình ik-1 nằm trong
hình ik và k là lớn nhất. Biết rằng hai hình chữ nhật bất kì trong N hình chữ nhật đã
cho hoặc rời nhau hoặc một trong hai hình nằm trong hình còn lại.
Dữ liệu: Vào từ file văn bản HCN3.INP:
Dòng đầu tiên chứa số nguyên dương N (2<N<100)
N dòng tiếp theo, dòng thứ i chứa 4 số nguyên xi1, yi1, xi2, yi2
Kết quả: Ghi ra file văn bản HCN3.OUT số k tìm được.
Ví dụ:
HCN3.INP HCN3.OUT
3 2
1174
3166
2254
3.8 QBPOINT - Bộ ba điểm thẳng hàng (http://vn.spoj.com/problems/QBPOINT/)
Trong các cuộc thi tin học, sự xuất hiện của những bài toán hình học làm đội tuyển
CBQ khá lúng túng. Do đó thầy Thạch quyết định cho đội tuyển luyện tập các bài toán
hình học. Bắt đầu từ điểm, thầy đưa ra bài toán:
Cho n điểm trong mặt phẳng Oxy, hãy đếm số bộ 3 điểm thằng hàng

Input
Dòng thứ nhất ghi số N là số điểm trên mặt phẳng.
N dòng tiếp theo, mỗi dòng ghi tọa độ của một điểm.

Output
Một số duy nhất là số bộ 3 điểm thẳng hàng.

Example
Input:

Biên soạn: Lâm Thanh Phương Trang 19


Chuyên đề hình học tính toán Tổ Tin học - Trường THPT Chuyên NTMK

6
00
01
02
11
20
22
Output:
3

Giới hạn:
1 ≤ N ≤ 2000.
Tọa độ các điểm có trị tuyệt đối không quá 10000.

Biên soạn: Lâm Thanh Phương Trang 20


Chuyên đề hình học tính toán Tổ Tin học - Trường THPT Chuyên NTMK

TÀI LIỆU THAM KHẢO


1. Tài liệu chuyên tin quyển 3 .............................................................. Hồ Sĩ Đàm
2. Website: http://vnoi.info/
3. Website: http://vn.spoj.com

Biên soạn: Lâm Thanh Phương Trang 21


Chuyên đề hình học tính toán Tổ Tin học - Trường THPT Chuyên NTMK

MỤC LỤC
LỜI NÓI ĐẦU ............................................................................................................................ 1
CHUYÊN ĐỀ . HÌNH HỌC TÍNH TOÁN ................................................................................ 2
1 TỔNG QUAN .................................................................................................................... 2
1.1 Mục tiêu ....................................................................................................................... 2
1.2 Kiến thức cơ bản cần thiết ........................................................................................... 2
1.3 Nội dung cốt lõi ........................................................................................................... 2
2 NỘI DUNG ........................................................................................................................ 2
2.1 ĐIỂM, ĐƯỜNG THẲNG VÀ ĐOẠN THẲNG; GIAO CÁC ĐƯỜNG THẲNG VÀ
ĐOẠN THẲNG...................................................................................................................... 2
2.1.1 Đối tượng hình học cơ bản ................................................................................... 2
2.1.2 Dữ liệu lưu trữ các đối tượng hình học cơ bản ..................................................... 2
2.1.3 Vị trí tương đối của điểm so với đường thẳng và đoạn thẳng .............................. 4
2.1.4 Giao của các đường thẳng .................................................................................... 6
2.1.5 Giao của các đoạn thẳng ....................................................................................... 8
2.2 ĐA GIÁC ................................................................................................................... 11
2.2.1 Diện tích đa giác ................................................................................................. 11
2.2.2 Điểm thuộc đa giác ............................................................................................. 13
2.2.3 Đa giác không tự cắt (bao lồi) ............................................................................ 15
3 BÀI TẬP ........................................................................................................................... 17
TÀI LIỆU THAM KHẢO ........................................................................................................ 21

Biên soạn: Lâm Thanh Phương Trang 22


TRẠI HÈ PHƯƠNG NAM - LẦN THỨ IV – CÀ MAU NĂM 2017

THUẬT TOÁN TARJAN VÀ ỨNG DỤNG


Huỳnh Tấn Thông
Trường THPT chuyên Nguyễn Quang Diêu, tỉnh Đồng Tháp
Email: huynhtanthongtinhoc@gmail.com

I. THUẬT TOÁN TARJAN:


Thuật Toán Tarjan là một thuật toán trong lý thuyết đồ thị dùng để tìm thành phần
liên thông mạnh/song liên thông trong một đồ thị.
- Đồ thị liên thông mạnh: là đồ thị có hướng nếu có đường đi từ a tới b và từ b tới
a với mọi cặp đỉnh a và b của đồ thị.
- Đồ thị song liên thông: là Đồ thị vô hướng, liên thông, không có khớp được gọi
là đồ thị song liên thông (bỏ đi một đỉnh bất kỳ thì các đỉnh còn lại vẫn liên thông). Đồ thị
chỉ gồm 1 đỉnh được coi là song liên thông.

- Ý tưởng của thuật toán Tarjan: Tìm kiếm theo chiều sâu (DFS) bắt đầu từ một
đỉnh tùy ý, và sau đó tìm kiếm sâu dần trên bất kỳ các đỉnh nào chưa được xét để tìm “cung
chốt”. Việc tìm kiếm sẽ không xét đến bất kỳ đỉnh nào đã được xét trước đó, quá trình tìm
kiếm tạo nên các cây con của cây tìm kiếm, gốc của những cây con đó chính là gốc của các
thành phần liên thông mạnh nếu có.

- Cài đặt thuật toán Tarjan: Dựa trên thứ tự duyệt đến, gọi Color[u] là màu đỉnh
u, nếu Color[u] là white (màu trắng) thì đỉnh u chưa được thăm, nếu là Gray (màu xám)
thì đỉnh u đã được thăm nhưng chưa duyệt xong, nếu là Black (màu đen) thì đỉnh u đã bị
xóa khỏi nhánh cây DFS. Gọi Number[u] là thứ tự duyệt đến của đỉnh u, Low[u] là giá trị
Number[.] nhỏ nhất trong các đỉnh mà có thể đến được từ một đỉnh v nào đó của nhánh
DFS gốc u bằng một cung.
+ Nếu đỉnh v đã thăm thì Low[u]=Min(Low[u], Number[v])
+ Nếu đỉnh v chưa thăm thì Low[u]=Min(Low[u], Low[v])
+ Nếu Low[u]>=Number[u] thì cung (u,v) là cung chốt. => Các đỉnh trong cây
DFS gốc u là các thành phần song liên thông của đồ thị.

- Thuật toán:
Procedure Tarjan(u);
Begin
Time:=Time+1; Number[u]:=Time;
Low[u]:=+∞; Color[u]:=Gray;
Push(u);//đẩy u vào stack
For vV; (u,v)E do
If Color[v]=Gray then //đỉnh v đã thăm
Begin
Low[u]:=Min(Low[u],Number[v]);

Chuyên đề: Thuật toán Tarjan và ứng dụng Trang: 1/16


313
TRẠI HÈ PHƯƠNG NAM - LẦN THỨ IV – CÀ MAU NĂM 2017
End
Else
If Color[v]=White then //đỉnh v chưa thăm
Begin
Tarjan(v);
Low[u]:=Min(Low[u],Low[v]);
End;
If Low[u]>=Number[u] then //u là chốt
Begin
//thông báo thành phần liên thông manh
Repeat
v:=pop; Outputv
Color[v]:=Black;
//xóa các đỉnh trong một tplt vừa tìm được
Until v=u;
End;
End;
BEGIN
Time:=0;
For i:=1 to n do Number[i]:=0;
For i:=1 to n do
If Number[i]=0 then
Tarjan(i);
END.

II. ỨNG DỤNG:

1. Bài toán tìm thành phần liên thông mạnh của đồ thị:
Bài 1: Bảo vệ
Một thành phố có N địa điểm chiến lược và M con đường một chiều giữa các địa
điểm. Là thị trưởng của thành phố, bạn sẽ phải bảo vệ an toàn cho N địa điểm này.
Để có thể bảo vệ cho các địa điểm, bạn phải xây dựng các đồn cảnh sát tại một vài
địa điểm. Đồn cảnh sát tại địa điểm i có thể bảo vệ cho địa điểm j nếu i = j hoặc cảnh sát có
thể đi tuần tới địa điểm j từ i và có thể quay trở lại đồn tại địa điểm i.
Để có thể xây dựng được các đồn cảnh sát cần phải mất chi phí, do địa hình mỗi địa
điểm là khác nhau nên chi phí xây dựng đồn cũng có thể khác nhau.
Bạn phải xác định số tiền nhỏ nhất để xây dựng các đồn cảnh sát để có thể bảo vệ
được tất cả N địa điểm, hơn nữa bạn phải đưa ra có bao nhiêu cách xây dựng để đảm bảo
chi phí nhỏ nhất đó.
INPUT: SECURITY.INP
 Dòng 1 chứa số nguyên dương N (1 ≤ N ≤ 105)
 Dòng 2 chứa N số nguyên, trong đó số nguyên thứ i là chi phí để xây dựng đồn cảnh
sát tại địa điểm i (chi phí  ≤ 109).
 Dòng 3 chứa số nguyên M (0 ≤ M ≤ 3*105)

Chuyên đề: Thuật toán Tarjan và ứng dụng Trang: 2/16


TRẠI HÈ PHƯƠNG NAM - LẦN THỨ IV – CÀ MAU NĂM 2017
 M dòng tiếp theo, mỗi dòng chứa hai số nguyên dương u và v (1 ≤ u, v ≤ n; u ≠ v)
biểu diễn một con đường một chiều nối từ địa điểm u tới v. Không có nhiều hơn 1
con đường nối giữa 2 địa điểm.
OUTPUT: SECURITY.OUT
 Một dòng duy nhất chứa hai số, số thứ nhất là chi phí nhỏ nhất để xây dựng các đồn
cảnh sát, số thứ hai là số phương án xây dựng (mod (109+7)).
Ví dụ:
SECURITY.INP SECURITY.OUT
5 82
28060
6
14
13
24
34
45
51

Hướng dẫn thuật toán:


Sử dụng thuật toán Tajan để tìm các thành phần liên thông mạnh, tại mỗi thành phần
liên thông mạnh sẽ xây dựng một đồn cảnh sát có chi phí xây dựng là nhỏ nhất, và đếm số
lượng các đồn có cùng chi phí nhỏ nhất đó.
- Tổng số tiền xây dựng là tổng số tiền xây dựng các đồn có chi phí nhỏ nhất ở mỗi
thành phần liên thông.
- Số cách xây dựng là tích của số lượng các đồn cùng chi phí nhỏ nhất ở mỗi thành
phần liên thông.

#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
const int oo = INT_MAX;
const long long e=1000000007;
int n, m;
vector<int> a[N];
stack<int> st1, st2;
int cp[N], Num[N], Low[N];
int id, mi;
long long sl, kq1, kq2;
void nhap(){
ifstream f("security.inp");
f>>n;
for (int i=1; i<=n;i++) f>>cp[i];
f>>m;
for (int i=1; i<=m; i++) {

Chuyên đề: Thuật toán Tarjan và ứng dụng Trang: 3/16


TRẠI HÈ PHƯƠNG NAM - LẦN THỨ IV – CÀ MAU NĂM 2017
int x, y;
f>>x>>y;
a[x].push_back(y);
}
f.close();
}

void Tarjan(int u) {
id=id+1;
Num[u]=Low[u]=id; //thu tu duyet den dinh u
st1.push(u);//them u vao stack
for (int i=0; int v=a[u][i]; i++) // duyet cac canh ke u
if (Num[v]>0) Low[u]=min(Low[u], Num[v]); // v da tham
else { // dinh v chua tham
Tarjan(v);
Low[u]=min(Low[u], Low[v]);
}
if (Low[u]>=Num[u]){ // u la chot vung lien thong manh
int v;
mi=oo;
sl=0;
st2=st1;
do { //tinh chi phi nho nhat
v=st1.top();
mi=min(mi, cp[v]);
st1.pop();
Num[v]=Low[v]=oo; // v ra khoi vung lien thong manh
} while (v!=u);
// tinh so dinh = min
do {
v=st2.top();
st2.pop();
//Num[v]=Low[v]=oo; // remove v from graph
if (cp[v]==mi) sl=sl+1;
} while (v!=u);
}
}

main(){
nhap();
for (int i=1; i<=n; i++)
a[i].push_back(0);
for (int i=1; i<=n; i++) Num[i]=0;
kq2=1; kq1=0;
for (int k=1; k<=n; k++)
if (Num[k]==0) {
id=0;
Tarjan(k);
kq1=kq1+mi; // cong don gia nho nhat

Chuyên đề: Thuật toán Tarjan và ứng dụng Trang: 4/16


TRẠI HÈ PHƯƠNG NAM - LẦN THỨ IV – CÀ MAU NĂM 2017
kq2=kq2*sl; // so cach xay dung

}
ofstream g("security.out");
g <<kq1 <<" "<<kq2%e; // chia lay du
g.close();
}

2. Bài toán tìm thành phần song liên thông của đồ thị:
Bài 2. Đường đua dài nhất
Mạng giao thông của thành phố Cao Lãnh có N nút giao thông. Giữa hai nút giao
thông có tối đa một đường phố hai chiều nối trực tiếp giữa chúng. Nhân dịp chào mừng kỷ
niệm 30 năm ngày thành lập, Lãnh đạo thành phố quyết định tổ chức một cuộc đua xe đạp.
Đường đua xe đạp sẽ xuất phát từ một nút bất kỳ, qua một số nút khác và trở lại nút ban
đầu sao cho không có nút nào (trừ nút xuất phát) đường đua qua đó hai lần. Thật ngạc nhiên,
mạng lưới giao thông của thành phố cho phép lập nhiều đường đua xe đạp như vậy tuy
nhiên: mỗi một đường phố sẽ thuộc không quá một đường đua thỏa mãn điều kiện nêu trên.
Hãy tìm đường đua có số đường phố khác nhau đi qua nhiều nhất.
INPUT: MAXCYCLE.INP
 Dòng đầu ghi hai số nguyên N, M là số nút giao thông và số đường phố trong thành
phố (N≤5000, M≤100000)
 M dòng tiếp theo, mỗi dòng ghi hai số nguyên u, v thể hiện hai nút của một đường
phố
OUTPUT: MAXCYCLE.OUT
 Ghi một số nguyên duy nhất là độ dài (số lượng đường phố khác nhau) của đường
đua dài nhất.
Ví dụ:
MAXCYCLE.INP MAXCYCLE.OUT
78 4
34
14
13
71
27
75
56
62
Hướng dẫn thuật toán:
Tìm các thành phần song liên thông, trong mỗi thành phần song liên thông ta đếm
số đỉnh của nó. Kết quả bài toán là thành phần song liên thông có nhiều đỉnh nhất.
Chuyên đề: Thuật toán Tarjan và ứng dụng Trang: 5/16
TRẠI HÈ PHƯƠNG NAM - LẦN THỨ IV – CÀ MAU NĂM 2017
#include <bits/stdc++.h>
using namespace std;
const int nmax = 5000;
int n,m,num[nmax],low[nmax],id = 0,maxx;
vector <int> a[nmax];
struct canh
{
int u,v;
};
stack <canh> s;

void nhap(){
ifstream f("MAXCYCLE.inp");
f >> n >> m;
for(int i=1; i<=m; i++){
int u,v;
f >> u >> v;
a[u].push_back(v);
a[v].push_back(u);
}
f.close();
}

void Tarjan(int u){


canh e;
int v;
id++;
num[u] = low[u] = id;
for(int i=0;v=a[u][i];i++)
// dinh chieu
if(num[v]>0) low[u]=min(low[u], num[v]);
else {
e.u = u;
e.v = v;
s.push(e);
Tarjan(v);
low[u] = min(low[u],low[v]);
if(low[v] >= num[u]){
int d = 1;
do{
e = s.top();
s.pop();
d++;
}while(e.u != u || e.v != v);
if (d>2) maxx = max(maxx,d);
}
}
}

Chuyên đề: Thuật toán Tarjan và ứng dụng Trang: 6/16


TRẠI HÈ PHƯƠNG NAM - LẦN THỨ IV – CÀ MAU NĂM 2017
main(){
nhap();
for(int i=0; i<=n; i++) a[i].push_back(0);
for(int i=1; i<=n; i++)
if(num[i]== 0)Tarjan(i);

ofstream g("MAXCYCLE.out");
g<< maxx;
g.close();
}

3. Bài toán tìm cạnh cầu của đồ thị:


Một cạnh được gọi là cạnh cầu trên đồ thị vô hướng nếu như bỏ cạnh này thì số
thành phần liên thông của đồ thị mới tăng lên. Dễ thấy rằng trong thuật toán Tarjan cạnh
(u,v) là cạnh cầu của đồ thị khi và chỉ khi 𝑙𝑜𝑤[𝑣] >= 𝑛𝑢𝑚ber[v]
procedure Tarjan(u:longint);
var v:longint;
begin
time:=time+1;
numr[u]:=time; //thu tu duyet den
low[u]:=vc; //mang trajan
for v:=1 to n do
if a[u,v]<>0 then
begin
a[v,u]:=0; // dinh chieu do thi
if Number[v]>0 then low[u]:=min(low[u], number[v]) //dinh v da tham
else
begin
Tarjan(v);
low[u]:=min(low[u],low[v]); //cuc tieu theo trajan
if low[v]>=number[v] then
// u, v là cạnh cầu.
end ;
end;
end;

Bài 3: Đường truyền (Đề 11 Olympic 30-4-2017)


Cho một mạng gồm n máy tính (được đánh số từ 1 đến n) và m đường truyền hai chiều
nối giữa các máy. Hiện thời mạng là thông suốt, tức là mọi máy trong mạng đều có thể kết
nối với nhau một cách trực tiếp hoặc gián tiếp nhờ các đường truyền và các máy trong
mạng. Một số máy trong mạng cung cấp dịch vụ A còn một số máy khác cung cấp dịch vụ
B cho tất cả các máy khác và chính nó. Có thể có một số máy cung cấp cả hai dịch vụ A và
B nói trên. Mỗi máy của mạng, có nhu cầu truy cập mỗi dịch vụ A hoặc B từ càng nhiều
máy (có cung cấp dịch vụ tương ứng) càng tốt.

Chuyên đề: Thuật toán Tarjan và ứng dụng Trang: 7/16


TRẠI HÈ PHƯƠNG NAM - LẦN THỨ IV – CÀ MAU NĂM 2017
Một đường truyền trực tiếp giữa hai máy nào đó, được gọi là đường truyền quan trọng
nếu khi nó bị ngắt kết nối, sẽ xảy ra tình trạng có một số máy trong mạng không thể truy
cập ít nhất một trong hai dịch vụ A,B.
Một đường truyền trực tiếp giữa hai máy nào đó, được gọi là đường truyền cốt yếu
nếu khi nó bị ngắt kết nối (vì lý do nào đó), sẽ khiến cho một số máy trong mạng không
thể kết nối tới quá bán (quá nửa) số máy cung cấp một trong các dịch vụ A,B.
Yêu cầu: Hãy xác định số đường truyền quan trọng và số đường truyền cốt yếu trong
toàn mạng.
Dữ liệu vào: Từ file văn bản ABNET.INP, có nội dung như sau :
- Dòng đầu tiên ghi 4 số nguyên : n,m,k,l lần lượt là số máy tính, số đường truyền
trực tiếp, số máy cung cấp dịch vụ A và số máy cung cấp dịch vụ B (1≤n≤10 4,1 ≤
m≤104,1 ≤k,l≤m),
- Dòng thứ hai ghi k số là số máy cung cấp dịch vụ A.
- Dòng thứ hai ghi l số là số máy cung cấp dịch vụ B.
- Mỗi dòng trong số m dòng tiếp theo ghi 2 số p,q (1≤ p,q ≤n, p≠q) thể hiện một đường
truyền trực tiếp nối máy p và máy q.
- Các số ghi trên cùng một dòng đều cách nhau bởi dấu cách.
Kết quả: Ghi ra file văn bản ABNET.OUT trên một dòng hai số nguyên, lần lượt là
số đường truyền quan trọng và số đường truyền cốt yếu của toàn mạng
Ví dụ :
ABNET.INP ABNET.OUT GIẢI THÍCH
9 10 3 4 34 Các đường truyền quan trọng gồm
245
(2,3) , (5,6) và (7,9)
4983
12 Các đường truyền cốt yếu gồm
41
(2,3) ,(1,5), (5,6) và (7,9)
23
42
15
56
67
68
79
87
Ghi chú : 50% số test, ứng với 50 số điểm của bài, có n≤200
Hướng dẫn thuật toán:
Theo mô tả của đề bài thì đường truyền quan trọng và đường truyền cốt yếu phải là
cầu của đồ thị kèm thêm một số điều kiện.
Giả sử một cầu nối hai thành phần liên thông X và Y. Cầu này sẽ là đường truyền
quan trọng nếu thành phần liên thông X chứa K hoặc o máy cung cấp dịch vụ A hay chứa

Chuyên đề: Thuật toán Tarjan và ứng dụng Trang: 8/16


TRẠI HÈ PHƯƠNG NAM - LẦN THỨ IV – CÀ MAU NĂM 2017
L hoặc 0 máy cung cấp dịch vụ B. Việc đếm số máy cung cấp dịch vụ A và B trong thành
phần liên thông X có thể tích hợp trong đoạn chương trình tìm cầu.
Tương tự ta có thể đếm đường truyền cốt yếu với lưu ý một cầu sẽ là đường truyền cốt yếu
nếu trong thành phần liên thông X số máy cung cấp dịch vụ A ít hơn (K+2) div 1 và số máy
cung cấp dịch vụ B ít hơn (L div 2) + 1 hoặc số máy cung cấp dịch vụ A nhiều hơn hay
bằng (K div 2)+1 và số máy cung cấp dịch vụ B nhiều hơn hay bằng (L div 2) + 1

#include <bits/stdc++.h>
using namespace std;
const int N=1e6; // de cho 1e4;

vector<int> a[N];
int aa[N], bb[N], fa[N], fb[N], num[N], low[N];
int n, m, k,kk, l,ll, id;
long dem1=0, dem2=0;
void nhap(){
ifstream f("ABNET.INP");
int p, q;
f>>n>>m>>k>>l;

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


f>>p;
aa[p]=1;
}
for (int i=1; i<=l; i++){
f>>q;
bb[q]=1;
}

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


f>>p>>q;
a[p].push_back(q);
a[q].push_back(p);
}
f.close();

}
void Tarjan(int u){
id=id+1;
num[u]=low[u]=id;
fa[u]=aa[u];
fb[u]=bb[u];
int v;
for (int i=0; v=a[u][i]; i++){
//a[v][u]=0;
for(int j=0; j<a[v].size(); j++){
if (a[v][j]==u) a[v].erase(a[v].begin()+j);
}

Chuyên đề: Thuật toán Tarjan và ứng dụng Trang: 9/16


TRẠI HÈ PHƯƠNG NAM - LẦN THỨ IV – CÀ MAU NĂM 2017
if(num[v]>0) low[u]=min(low[u], num[v]);
else{
Tarjan(v);
low[u]=min(low[u], low[v]);
fa[u]=fa[u]+fa[v];
fb[u]=fb[u]+fb[v];
if(low[v]>=num[v]){ //cau do thi
if
((fa[v]==k)||(fa[v]==0)||(fb[v]==l)||(fb[v]==0)) dem1=dem1+1;
if ((fa[v]<kk && fb[v]<ll) || (k-fa[v]<kk
&& l-fb[v]<ll)) dem2=dem2+1;
}
}
}
}
main(){
nhap();
kk=1+k/2;
ll=1+l/2;
for(int i=0; i<=n; i++) a[i].push_back(0);
Tarjan(1);
ofstream f("ABNET.OUT");
f<< dem1<<" " <<dem2;
f.close();
}

4. Bài toán tìm đỉnh khớp


Một đỉnh được gọi là một đỉnh khớp (hay còn gọi là đỉnh bản lề) nếu như bỏ đỉnh
và các cạnh liên thuộc với khỏi đồ thị thì số thành phần liên thông của đồ thị tăng lên.
Nếu 𝑙𝑜𝑤[𝑣] ≥ 𝑛𝑢𝑚ber[𝑢] thì đỉnh u là đỉnh khớp.
Procedure Tarjan(u: Longint);
Var v, k: Longint;
Begin
Time:=Time+1;
number[u]:=Time;
low[u]:=Number[u];
for v:=1 to n do
if a[u,v]<>0 then
Begin
If number[v]=0 then
Begin
if v=root then con:=con+1;
prev[v]=u;
Tarjan(v);
low[u]:=min(low[u], number[v])
if low[v]>=number[u] then
k[u]:=true; // dinh u la khop
end;

Chuyên đề: Thuật toán Tarjan và ứng dụng Trang: 10/16


TRẠI HÈ PHƯƠNG NAM - LẦN THỨ IV – CÀ MAU NĂM 2017
if low[v]>=number[u] then
low[u]:=min(low[u], number[v])
end;
End;
Chú ý: Cần xử lí thêm đỉnh gốc có 2 con là đỉnh khớp

Bài 4. Liên thông


Cho một đồ thị vô hướng gồm 𝒏 đỉnh đánh số từ 1 tới 𝒏 và 𝒎 cạnh đánh số từ 1 tới
𝒎. Cạnh thứ 𝒊 nối giữa hai đỉnh 𝒖𝒊 , 𝒗𝒊 . Nếu ta xoá đi một đỉnh nào đó của đồ thị, số thành
phần liên thông của đồ thị có thể tăng lên. Nhiệm vụ của bạn là với mỗi đỉnh, hãy tính xem
nếu ta xoá đỉnh đó đi thì đồ thị mới nhận được có bao nhiêu thành phần liên thông.
Dữ liệu: Vào từ file văn bản GRAPH.INP
 Dòng đầu chứa hai số nguyên dương 𝒏, 𝒎 (𝒏 ≤ 𝟐𝟎𝟎𝟎𝟎; 𝒎 ≤ 𝟓𝟎𝟎𝟎𝟎)
 𝒎 dòng sau, dòng thứ 𝒊 chứa hai số nguyên dương 𝒖𝒊 , 𝒗𝒊 .
Kết quả: Ghi ra file văn bản GRAPH.OUT
𝒏 dòng, dòng thứ 𝒋 cho biết số thành phần liên thông của đồ thị nếu ta xóa đi đỉnh 𝒋.
Ví dụ
GRAPH.INP GRAPH.OUT
43 1
12 3
23 1
24 1
Chú ý: Ít nhất 60% số điểm ứng với các test có 𝒏 ≤ 𝟏𝟎𝟎𝟎; 𝒎 ≤ 𝟐𝟎𝟎𝟎
* Hướng dẫn thuật toán:
- Đây là bài toán điển hình về tìm khớp của đồ thị. Có thể giải quyết vấn đề này như
sau: trong quá trình DFS sử dụng thêm một mảng để lưu số lượng đỉnh con của đỉnh u là
slcon[u]. Khi đó nếu số lượng thành phần liên thông ban đầu của đồ thì là k thì tiếp theo sẽ
có hai khả năng như sau:
+ Khả năng 1: u là khớp nhưng không phải đỉnh gốc của DFS thì số lượng thành
phần liên thông sau khi xóa đỉnh khớp u là: k + slcon[u].
+ Khả năng 2: u là khớp nhưng lại là đỉnh gốc của DFS thì số lượng thành phần liên
thông sau khi xóa đỉnh khớp u là: k + slcon[u] – 1.

#include <bits/stdc++.h>
#define pii pair<int,int>
#define fi first
#define se second
#define mp make_pair
using namespace std;
const int nmax = 1e5+5;
vector <int> adj[nmax];

Chuyên đề: Thuật toán Tarjan và ứng dụng Trang: 11/16


TRẠI HÈ PHƯƠNG NAM - LẦN THỨ IV – CÀ MAU NĂM 2017
map <int,int> M;
int dd[nmax];
int n, s, t, res=0;
pii dsc[nmax];
void doc()
{
map <ll, ll> :: iterator it;
int dem=0;
cin >> n >> s >> t;
for (int i=1; i<=n; i++)
{
int u, v;
cin >> u >> v;
it = M.find(u);
if (it == M.end())
{
dem++;
M.insert(mp(u,dem));
dsc[i].fi = dem;
}
else dsc[i].fi = it->se;
it = M.find(v);
if (it == M.end())
{
dem++;
M.insert(mp(v,dem));
dsc[i].se = dem;
}
else dsc[i].se = it->se;
}
it = M.find(s);
s = it->se;
it = M.find(t);
t = it->se;
}
void DFS(int u)
{
dd[u] = 1;
for (int i=0; i<adj[u].size();i++)
{
int v = adj[u][i];
if (dd[v] == 0) DFS(v);
}
}
bool check(int k)
{
memset(dd,0,sizeof(dd));
for (int i=1; i<=n; i++) adj[i].clear();
for (int i=1; i<=k; i++)

Chuyên đề: Thuật toán Tarjan và ứng dụng Trang: 12/16


TRẠI HÈ PHƯƠNG NAM - LẦN THỨ IV – CÀ MAU NĂM 2017
{
int u = dsc[i].fi;
int v = dsc[i].se;
adj[u].push_back(v);
}
DFS(s);
return (dd[t] == 1);
}
void xuly()
{
int d=1,c=n;
while (d <= c)
{
int g = (d + c)/2;
if (check(g))
{
res = g;
c = g - 1;
}
else d = g + 1;
}
cout << res;
}
main()
{
freopen("GRAPH.INP","r",stdin);
freopen("GRAPH.OUT","w",stdout);
doc();
xuly();
}

5. Bài toán định chiều đồ thị:


Cho đồ thị vô hướng, liên thông, hãy thay mỗi cạnh bằng một cung để được đồ thị
có hướng liên thông mạnh.
Phép định chiều dùng thuật toán tìm kiếm theo DFS với bổ sung: mỗi khi xét qua
cạnh (i;j) thì thay cạnh (i;j) chỉ bằng một cung (i;j)- nghĩa là phải xóa đi cung (j; i).

Bài 5. Đường 1 chiều


Một hệ thống giao thông gồm có N nút giao thông đánh số từ 1 đến N và M đường
hai chiều nối một số cặp nút, không có hai đường nối cùng một cặp nút. Hệ thống đảm bảo
đi lại giữa hai hút bất kì. Để đảm bảo an toàn, người ta quyết định rằng các đường hai
chiều trước đây nay sẽ thành một chiều, và vấn đề ở chỗ chọn chiều cho mỗi đường như
thế nào.
Hãy tìm cách định hướng các cạnh sao cho hệ thống vẫn đảm bảo đi lại giữa hai cặp nút bất
kì.
INPUT: ONEWAY.INP
Chuyên đề: Thuật toán Tarjan và ứng dụng Trang: 13/16
TRẠI HÈ PHƯƠNG NAM - LẦN THỨ IV – CÀ MAU NĂM 2017
- Dòng đầu ghi hai số nguyên dương N, M (1 <= N <= 50000 , 1 <= M <= 100000).
- M dòng tiếp theo, mỗi dòng thể hiện một đường hai chiều gồm u, v là chỉ số hai nút
mà nó nối tới .
OUTPUT: ONEWAY.OUT
- Dòng đầu ghi 1/0 tương ứng với có tìm được phương án thoả mãn hay không .
- Nếu có, M dòng tiếp theo mỗi dòng thể hiện sự định hướng một cạnh bao gồm hai số
u, v với ý nghĩa định hướng cạnh (u,v) thành đường một chiều từ u đến v.
Ví dụ
ONEWAY.INP ONEWAY.OUT ONEWAY.INP ONEWAY.OUT
45 1 44 0
12 12 12
23 23 23
24 24 34
34 34 31
14 41

Hướng dẫn thuật toán:


- Sử dụng DFS để định chiều các cạnh của đồ thị. Cạnh nào thuộc cây DFS sẽ được
định chiều từ gốc xuống lá (tạm gọi là hướng xuôi), những cạnh không thuộc cây DFS sẽ
được định chiều theo hướng ngược lại.
- Sau khi định chiều xong ta được một đồ thị 1 chiều mới, kiểm tra đồ thị mới có
liên thông không (sử dụng thuật toán Tarjan). Nếu có thì trả lời có phương án, ngược lại thì
không có phương án.
#include <bits/stdc++.h>
#define pii pair<int, int>
#define fi first
#define se second
#define mp make_pair
using namespace std;
const int nmax = 50001;
pii dsc[nmax*2];
vector <int> adj[nmax], adj1[nmax];
int dd[nmax], d[nmax], cha[nmax], num[nmax], low[nmax];
int n, m, id, dem=0;
void doc()
{
cin >> n >> m;
for (int i=1; i<=m; i++)
{
int u,v;
cin >> u >> v;
dsc[i] = mp(u,v);
adj[u].push_back(v);
adj[v].push_back(u);
}
}
Chuyên đề: Thuật toán Tarjan và ứng dụng Trang: 14/16
TRẠI HÈ PHƯƠNG NAM - LẦN THỨ IV – CÀ MAU NĂM 2017

void DFS(int u)
{
dd[u] = 1;
for (int i=0; i<adj[u].size(); i++)
{
int v = adj[u][i];
if (dd[v] == 0)
{
d[v] = d[u] + 1;
cha[v] = u;
DFS(v);
}
}
}

void dinh_chieu()
{
for (int i=1; i<=m; i++)
{
int u = dsc[i].fi;
int v = dsc[i].se;
if (cha[v] == u) adj1[u].push_back(v);
else if (cha[u] == v) adj1[v].push_back(u);
else if (d[u] < d[v]) adj1[v].push_back(u);
else adj1[u].push_back(v);
}
}

void DFS1(int u)
{
dd[u] = 1;
id++;
num[u] = id;
low[u] = id;
for (int i=0; i<adj1[u].size();i++)
{
int v=adj1[u][i];
if (dd[v] == 0)
{
DFS1(v);
low[u] = min(low[u],low[v]);
}
else if (dd[v] == 1) low[u] = min(num[v],low[u]);
}
if (low[u] == num[u]) dem++;
}
void tajan(){
memset(dd,0,sizeof(dd));

Chuyên đề: Thuật toán Tarjan và ứng dụng Trang: 15/16


TRẠI HÈ PHƯƠNG NAM - LẦN THỨ IV – CÀ MAU NĂM 2017
for (int i=1; i<=n; i++)
if (dd[i] == 0) DFS1(i);
}

void ghi(){
if (dem != 1) cout << 0;
else
{
cout << 1 << endl;
for (int u=1; u<=n; u++)
for (int i=0; i<adj1[u].size(); i++)
{
int v = adj1[u][i];
cout << u << " " << v << endl;
}
}
}
main(){
freopen("oneway.inp","r",stdin);
freopen("oneway.out","w",stdout);
doc();
DFS(1);
dinh_chieu();
tajan();
ghi();
}

KẾT LUẬN
Hiểu rõ được cơ chế hoạt động của phương pháp tìm kiếm theo chiều sâu (DFS)
được thể hiện thông minh bằng thuật toán Tarjan cho ta cách cài đặt rất ngắn gọn, rõ ràng.
Những cải tiến nhỏ trong thuật toán có thể đem lai nhiều điều thú vị, giải quyết được nhiều
lớp bài toán khác nhau, phần nào cho thấy được tầm quan trọng của thuật toán Tarjan.
Với khả năng và năng lực còn hạn chế, các bài giải có thể chưa sáng, chưa tối ưu và
bài tập trong chuyên đề này được tổng hợp từ nhiều nguồn tài liệu với hy vọng thông qua
chuyên đề này tôi đã truyển tải đến đồng nghiệp một phần nào đó cách sử dụng thuật toán
Tarjan.
Thành phố Cao Lãnh, ngày 30/5/2017
Người viết
Huỳnh Tấn Thông

[] Tải bộ test:
https://drive.google.com/file/d/0Bz7R_T5dzekpaG52SWtvNUIxeXM/view?usp=sharing

Chuyên đề: Thuật toán Tarjan và ứng dụng Trang: 16/16


328

You might also like