You are on page 1of 101

Tài liệu chuyên Tin – PTNK ĐHQG TPHCM

MỤC LỤC
CHƯƠNG 1. CÁC BƯỚC GIẢI BÀI TOÁN TIN HỌC ..................................................................... 5
1.1. Bài toán tin học .................................................................................................................................. 5
1.2. Xác định bài toán ............................................................................................................................... 5
Bài tập áp dụng 1.2 ............................................................................................................................. 5
1.3. Thiết kế thuật toán............................................................................................................................. 6
1.3.1. Khái niệm thuật toán (algorithm) ................................................................................................. 6
1.3.2. Các tính chất của thuật toán .......................................................................................................... 7
1.3.3. Biểu diễn thuật toán ...................................................................................................................... 8
Liệt kê các bước thực thi..................................................................................................................... 8
Sơ đồ thuật toán .................................................................................................................................. 8
Bài tập áp dụng 1.3.3. ......................................................................................................................... 9
1.3.4. Các cấu trúc cơ bản của thuật toán ............................................................................................. 10
Định lý Bohn-Jacopini ...................................................................................................................... 10
Các cấu trúc của thuật toán ............................................................................................................... 10
BÀI TẬP THIẾT KẾ SƠ ĐỒ THUẬT TOÁN ..................................................................................... 14
1.4. Lập trình C/C++ (chuyển đổi sơ đồ thành chương trình)............................................................ 17
1.4.1. Nguyên tắc chung ....................................................................................................................... 17
1.4.2. Biến và kiểu dữ liệu .................................................................................................................... 18
1.4.3. Hằng số (constant) ...................................................................................................................... 19
1.4.4. Câu lệnh gán (assignment) ......................................................................................................... 20
1.4.5. Các phép toán ............................................................................................................................. 21
1.4.5.1. Phép toán số học .................................................................................................................. 21
1.4.5.2. Phép toán quan hệ ................................................................................................................ 22
1.4.5.3. Phép toán logic..................................................................................................................... 22
1.4.5.4. Phép gán phức hợp (compound) .......................................................................................... 23
1.4.5.5. Phép tăng/giảm giá trị của biến............................................................................................ 23
1.4.6. Chuyển đổi các cấu trúc thuật toán thành câu lệnh .................................................................... 24
1.4.6.1. Câu lệnh nhập/xuất .............................................................................................................. 24
1.4.6.2. Lệnh rẽ nhánh ...................................................................................................................... 27
1.4.6.3. Lệnh lặp ............................................................................................................................... 29
1.4.7. Hàm (function) ........................................................................................................................... 31

Trương Phước Hải 1


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
1.4.7.1. Phân loại hàm....................................................................................................................... 32
1.4.7.2. Phạm vi của biến .................................................................................................................. 36
BÀI TẬP LẬP TRÌNH ........................................................................................................................... 40
CHƯƠNG 2. KIỂU MẢNG ................................................................................................................... 48
2.1. Định nghĩa mảng .............................................................................................................................. 48
2.2. Mảng một chiều................................................................................................................................ 48
2.2.1. Khai báo mảng ............................................................................................................................ 49
2.2.2. Truy xuất phần tử mảng.............................................................................................................. 49
2.2.3. Một số ví dụ minh họa ................................................................................................................ 50
2.3. Mảng hai chiều ................................................................................................................................. 52
2.3.1. Khai báo mảng ............................................................................................................................ 52
2.3.2. Truy xuất phần tử mảng.............................................................................................................. 53
2.3.3. Một số ví dụ minh họa ................................................................................................................ 53
2.4. Mảng động với kiểu vector trong C++ ........................................................................................ 55
2.4.1. Khai báo vector ........................................................................................................................... 55
2.4.2. Một số phương thức hàm thông dụng trên vector ...................................................................... 56
2.4.3. Một số ví dụ minh họa ................................................................................................................ 56
BÀI TẬP CHƯƠNG 2 ............................................................................................................................ 63
CHƯƠNG 3. ĐÁNH GIÁ THUẬT TOÁN ........................................................................................... 67
3.1. Lý do cần đánh giá thuật toán ........................................................................................................ 67
3.2. Độ phức tạp của thuật toán............................................................................................................. 68
Phép toán logarit ................................................................................................................................... 69
3.3. Một số quy tắc xác định độ phức tạp ............................................................................................. 69
3.3.1. Quy tắc bỏ hệ số ......................................................................................................................... 69
3.3.2. Quy tắc tổng................................................................................................................................ 70
3.3.3. Quy tắc lấy max .......................................................................................................................... 71
3.3.4. Quy tắc nhân ............................................................................................................................... 72
3.4. Độ phức tạp một số bài toán thông dụng ....................................................................................... 72
Bài toán đếm số ước dương .................................................................................................................. 72
Bài toán tìm ước chung lớn nhất........................................................................................................... 73
Bài toán tìm đoạn con có tổng lớn nhất ................................................................................................ 74
CHƯƠNG 4. SẮP XẾP VÀ TÌM KIẾM............................................................................................... 77
4.1. Bài toán sắp xếp ............................................................................................................................... 77

Trương Phước Hải 2


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
4.1.1. Phát biểu bài toán ....................................................................................................................... 77
4.1.2. Phương pháp đổi chỗ trực tiếp (interchange sort) ...................................................................... 77
4.1.3. Phương pháp sắp xếp nhanh (quick sort).................................................................................... 78
4.1.4. Sắp xếp trên nhiều khóa.............................................................................................................. 80
4.2. Bài toán tìm kiếm ............................................................................................................................. 81
4.2.1. Phát biểu bài toán ....................................................................................................................... 81
4.2.2. Tìm kiếm tuần tự ........................................................................................................................ 82
4.2.3. Tìm kiếm nhị phân ...................................................................................................................... 82
BÀI TẬP CHƯƠNG 4 ............................................................................................................................ 86
CHƯƠNG 5. MỘT SỐ KỸ THUẬT CƠ BẢN .................................................................................... 91
5.1. Kỹ thuật mảng thống kê .................................................................................................................. 91
Đếm số giá trị phân biệt của dãy .......................................................................................................... 91
Tìm giá trị xuất hiện nhiều nhất............................................................................................................ 91
Sắp xếp dãy (phương pháp distributing sort) ....................................................................................... 92
5.2. Kỹ thuật 2 con trỏ ............................................................................................................................ 92
Hai con trỏ cùng chiều .......................................................................................................................... 92
Hai con trỏ ngược chiều........................................................................................................................ 95
5.3. Kỹ thuật mảng hằng ........................................................................................................................ 97
5.4. Kỹ thuật xử lý số nguyên lớn .......................................................................................................... 99
Phép cộng 2 số nguyên lớn ................................................................................................................... 99
Phép trừ 2 số nguyên lớn .................................................................................................................... 100
Phép nhân nguyên số lớn với số nguyên có 1 chữ số ......................................................................... 100
Phép nhân 2 số nguyên lớn ................................................................................................................. 101

Trương Phước Hải 3


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM

Trương Phước Hải 4


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM

Chương 1. CÁC BƯỚC GIẢI BÀI TOÁN TIN HỌC

1.1. Bài toán tin học


Bài toán tin học là một vấn đề/công việc mà con người yêu cầu máy tính thực hiện hoàn toàn hoặc thực
hiện một phần: gởi một email, chỉnh sửa một bức ảnh, chơi một file nhạc, soạn thảo văn bản, nhận dạng
mặt người, … Bài toán tin học được mô tả bởi dữ liệu được cho (input) và kết quả đầu ra (output) cần
tìm.

1.2. Xác định bài toán


Để chuyển một bài toán thực tế thành chương trình xử lý trên máy tính ta cần xác định thông tin input và
output của bài toán
- Input (dữ liệu đầu vào): dữ liệu cần xử lý/dữ liệu được cung cấp để phục vụ giải bài toán.
- Output (kết quả đầu ra): dữ liệu cần đạt sau xử lý/kết quả cần tìm của bài toán.

Ví dụ 1.2.1:
Cho 2 số nguyên 𝑎, 𝑏. Tính tổng của chúng.
Input: 𝑎, 𝑏 ∈ 𝑍
Output: 𝑐 ∈ 𝑍
Có 2 input và 1 output

Ví dụ 1.2.2:
Cho số nguyên 𝑛. Kiểm tra 𝑛 có phải là số dương chẵn?
Input: 𝑛 ∈ 𝑍
Output: true/false
Có 1 input và 1 output

Bài tập áp dụng 1.2

Xác định input, output và số lượng của chúng ở mỗi bài toán.
1) Cho số nguyên dương 𝑛. Đếm số ước dương của 𝑛.
2) Cho số nguyên 𝑛. Kiểm tra 𝑛 có phải là số nguyên tố?
3) Cho 2 số nguyên 𝑎, 𝑏. Tìm 𝑈𝐶𝐿𝑁(𝑎, 𝑏).
4) Cho số nguyên dương 𝑛. Liệt kê tất cả số chính phương có giá trị không vượt quá 𝑛.

Trương Phước Hải 5


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
5) Cho 2 số nguyên 𝑎, 𝑏(𝑏 ≠ 0) là tử và mẫu của phân số 𝑎/𝑏. Tìm 𝑐, 𝑑 là tử và mẫu tối giản.
6) Cho số nguyên dương 𝑛. Cho biết chữ số lớn nhất trong biểu diễn của 𝑛 và số lượng chữ số lớn
nhất.
7) Cho 2 số thực 𝑎, 𝑏. Giải và biện luận phương trình bậc nhất: 𝑎𝑥 + 𝑏 = 0.

1.3. Thiết kế thuật toán


1.3.1. Khái niệm thuật toán (algorithm)
Thuật toán là dãy hữu hạn các thao tác thi hành được sắp theo một trình tự xác định để xử lý input và tìm
ra output của bài toán.

Ví dụ 1.3.1:
Cho 3 số nguyên 𝑎, 𝑏, 𝑐. Tìm giá trị lớn nhất.
Thuật toán:
Bước 1: Input 𝑎, 𝑏, 𝑐
Bước 2: Gán 𝑚𝑎𝑥𝑉𝑎𝑙𝑢𝑒 = 𝑎
Bước 3: Kiểm tra 𝑏 > 𝑚𝑎𝑥𝑉𝑎𝑙𝑢𝑒
- Nếu đúng thì gán 𝑚𝑎𝑥𝑉𝑎𝑙𝑢𝑒 = 𝑏
Bước 4: Kiểm tra 𝑐 > 𝑚𝑎𝑥𝑉𝑎𝑙𝑢𝑒
- Nếu đúng thì gán 𝑚𝑎𝑥𝑉𝑎𝑙𝑢𝑒 = 𝑐
Bước 5: Output 𝑚𝑎𝑥𝑉𝑎𝑙𝑢𝑒. Kết thúc.

Ghi chú: Thao tác gán 𝑎 = 𝑏 gán giá trị của 𝑏 cho 𝑎, nghĩa là sau thao tác, giá trị của 𝑎 sẽ bằng giá trị
của 𝑏. Ví dụ sau thao tác 𝑎 = 𝑎 + 5 thì giá trị của 𝑎 sẽ tăng thêm 5.

Ví dụ 1.3.2:
Cho 3 số nguyên 𝑎, 𝑏, 𝑐. Đếm số lượng số chẵn trong 3 số.
Thuật toán:
Bước 1: Input 𝑎, 𝑏, 𝑐.
Bước 2: Gán 𝑐𝑛𝑡 = 0
Bước 3: Kiểm tra 𝑎 mod 2 = 0?
- Nếu đúng thì 𝑐𝑛𝑡 = 𝑐𝑛𝑡 + 1 (tăng cnt thêm 1 đơn vị)
Bước 4: Kiểm tra 𝑏 mod 2 = 0?
- Nếu đúng thì 𝑐𝑛𝑡 = 𝑐𝑛𝑡 + 1 (tăng cnt thêm 1 đơn vị)

Trương Phước Hải 6


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Bước 5: Kiểm tra 𝑐 mod 2 = 0?
- Nếu đúng thì 𝑐𝑛𝑡 = 𝑐𝑛𝑡 + 1 (tăng cnt thêm 1 đơn vị)
Bước 7: Output 𝑐𝑛𝑡. Kết thúc

1.3.2. Các tính chất của thuật toán


Tính đúng đắn: đảm bảo thuật toán luôn cho output đúng trong mọi trường hợp input.
Tính xác định: đảm bảo mọi bước thi hành không mập mờ và có thể thực hiện được.
Tính dừng: đảm bảo thuật toán luôn dừng lại sau một số bước thực thi.
Tính hiệu quả: đảm bảo thuật toán thực thi nhanh, nói cách khác số bước thực thi của thuật toán phải nằm
trong giới hạn chấp nhận được.

Ví dụ 1.3.3:
Cho số nguyên dương 𝑛. Đếm số lượng ước dương của 𝑛.
Cách 1:
- Nhận xét: Nếu 𝑎 là ước dương của 𝑛 thì 1 ≤ 𝑎 ≤ 𝑛. Do đó, để đếm số ước dương của 𝑛 ta xét 𝑎
lần lượt mang các giá trị từ 1 đến 𝑛. Với mỗi giá trị của 𝑎, kiểm tra xem 𝑛 có chia hết cho 𝑎.
- Số trường hợp phải xét: 𝑛.
Cách 2:
- Nhận xét: Nếu 𝑎 là ước dương của 𝑛 thì luôn tồn tại số nguyên dương 𝑏 cũng là ước dương của 𝑛
sao cho 𝑎 × 𝑏 = 𝑛, khi đó 𝑏 = [𝑛/𝑎] (kí hiệu [𝑥] với ý nghĩa lấy giá trị phần nguyên của số thực
𝑥).
- Giả sử 𝑎 ≤ 𝑏 ⟹ 𝑎2 ≤ 𝑎 × 𝑏 = 𝑛 ⟹ 𝑎2 ≤ 𝑛 ⟹ 1 ≤ 𝑎 ≤ [√𝑛]. Như vậy khi 𝑎 là ước của 𝑛 thì
ta tìm được 2 ước là 𝑎 và 𝑏(𝑎 < 𝑏). Khi nào xảy ra trường hợp 𝑎 = 𝑏?
- Số trường hợp phải xét: [√𝑛].

Ví dụ 1.3.4:
Cho số nguyên dương 𝑛. Giải phương trình nghiệm nguyên không âm 2𝑥 + 𝑦 = 𝑛(∗).
Việc giải phương trình trên chính là liệt kê tất cả cặp giá trị (𝑥, 𝑦) nguyên không âm thỏa (*).
Từ phương trình (∗) ta có 0 ≤ 𝑥 ≤ [𝑛/2] và 0 ≤ 𝑦 ≤ 𝑛.
Cách 1:
- Thử lần lượt từng cặp giá trị (𝑥, 𝑦) trong miền giá trị của chúng và kiểm tra thỏa (∗).
- Số trường hợp phải xét: ([𝑛/2] + 1) × (𝑛 + 1).
Cách 2:

Trương Phước Hải 7


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
- Từ (∗) suy ra 𝑦 = 𝑛 − 2𝑥. Do đó ta chỉ cần xét từng giá trị của 𝑥 và tìm 𝑦 tương ứng.
- Số trường hợp phải xét: [𝑛/2] + 1
Qua 2 ví dụ ta nhận thấy rằng nếu sử dụng một số phân tích về mặt toán học sẽ giúp làm giảm số trường
hợp dư thừa phải xét để tăng tính hiệu quả của thuật toán.

1.3.3. Biểu diễn thuật toán


Liệt kê các bước thực thi

Mô tả thuật toán dưới dạng các bước thi hành, mỗi bước chỉ ra các thao tác thi hành cụ thể để xử lý input
tìm ra output. Thuật toán sẽ thực thi lần lượt từng bước theo đúng trình tự mô tả.
Ví dụ 1.3.5:
Thuật toán giải và biện luận phương trình bậc 1: 𝑎𝑥 + 𝑏 = 0
Bước 1: Input 𝑎, 𝑏
Bước 2: Kiểm tra 𝑎 = 0?
- Nếu đúng thì đến bước 3.
- Ngược lại thì thông báo nghiệm –b/a. Đến bước 4.
Bước 3: Kiểm tra 𝑏 = 0?
- Nếu đúng thì thông báo phương trình vô số nghiệm. Đến bước 4.
- Ngược lại thông báo phương trình vô nghiệm. Đến bước 4.
Bước 4: Kết thúc

Sơ đồ thuật toán

Các kí hiệu sơ đồ thuật toán

Begin
End

Biểu diễn bắt đầu và kết thúc thuật toán

Input/Output Gán/Tính toán

Biểu diễn thao tác nhập/xuất dữ liệu Biểu diễn phép gán hoặc tính toán

Trương Phước Hải 8


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM

False True
Biểu thức logic

Biểu diễn thao tác kiểm tra logic (kiểm tra đúng/sai)

Ví dụ 1.3.6:
Sơ đồ thuật toán giải và biện luận phương trình bậc 1: 𝑎𝑥 + 𝑏 = 0

Begin

𝑎, 𝑏

False True
𝑎=0

False True
𝑏=0
−𝑏/𝑎

Thông báo PT VN Thông báo PT VSN

End

Bài tập áp dụng 1.3.3.

Thiết kế sơ đồ thuật toán giải các bài toán sau


1) Cho 2 số nguyên 𝑎, 𝑏. Hãy hoán vị giá trị của 𝑎 và 𝑏. Ví dụ: input 𝑎 = 3, 𝑏 = 5; output 𝑎 = 5, 𝑏 = 3.
2) Cho 4 số nguyên 𝑎, 𝑏, 𝑐, 𝑑. Tìm giá trị nhỏ nhất.
3) Cho 4 số nguyên 𝑎, 𝑏, 𝑐, 𝑑. Cho biết có bao nhiêu số dương trong 4 số này.
4) Cho 4 số nguyên 𝑎, 𝑏, 𝑐, 𝑑. Các số nguyên được đánh thứ tự tương ứng từ 1 đến 4. Hãy cho biết thứ
tự của số lớn nhất. Nếu có nhiều số lớn nhất thì tìm thứ tự nhỏ nhất.
5) Học lực của một học sinh được đánh giá dựa trên điểm trung bình học kỳ (TBHK) như sau:

Trương Phước Hải 9


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
- Xếp loại Giỏi: TBHK từ 8.0 trở lên
- Xếp loại Khá: không đạt Giỏi và TBHK từ 6.5 trở lên
- Xếp loại TB: không đạt Khá và TBHK từ 5.0 trở
- Xếp loại Yếu: không đạt TB và TBHK từ 3.5 trở lên
- Xếp loại Kém: các trường hợp còn lại
Cho biết xếp loại học lực của học sinh có điểm TBHK là 𝑥(𝑥 ≥ 0).
6) Có 5 đồng tiền vàng giống nhau và được đánh thứ tự từ 1 đến 5. Các đồng tiền có khối lượng lần lượt
là các số nguyên dương 𝑎, 𝑏, 𝑐, 𝑑, 𝑒. Trong đó có 1 đồng tiền giả có khối lượng khác với khối lượng
những đồng tiền thật còn lại. Thiết kế sơ đồ thuật toán xác định thứ tự của đồng tiền giả.

1.3.4. Các cấu trúc cơ bản của thuật toán


Định lý Bohn-Jacopini

Mọi quá trình tính toán đều có thể được biểu diễn dựa trên 3 cấu trúc cơ bản của thuật toán: cấu trúc tuần
tự, cấu trúc rẽ nhánh, cấu trúc lặp.

Các cấu trúc của thuật toán

Begin
Begin

Input
Input …

Xử lý 1
False True
ĐK rẽ nhánh

… …

Output
Output

End
End

Cấu trúc tuần tự Cấu rẽ nhánh

Trương Phước Hải 10


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM

Begin

Input

Khởi tạo lặp

… False True
Output ĐK lặp

Thao tác lặp


End
Cấu trúc lặp

Ví dụ 1.3.7:
Cho số nguyên dương 𝑛. Thiết kế sơ đồ thuật toán tính giá trị biểu thức
1 1
𝑆 = 1 + + ⋯+
2 𝑛
Input: 𝑛 ∈ ℤ+
Output: 𝑆 ∈ ℝ

Giải:
Do biểu thức có 𝑛 phân số, phân số thứ 𝑖(𝑖 = 1,2,3, … , 𝑛) có giá trị 1/𝑖. Giá trị của biểu thức 𝑆 được
tính bằng cách cộng dồn từng phân số vào kết quả nên quá trình tính toán được biểu diễn bằng cấu
trúc lặp 𝑛 lần như sau
- Khởi tạo lặp
𝑆=0
𝑖=1
- Điều kiện lặp
𝑖≤𝑛
- Thao tác lặp
1
𝑆=𝑆+
𝑖
𝑖 =𝑖+1

Trương Phước Hải 11


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Sơ đồ thuật toán

Begin

𝑆=0

𝑖=1

F T
𝑖≤𝑛

𝑆 𝑆 = 𝑆 + 1/𝑖

End 𝑖 =𝑖+1

Ví dụ 1.3.8:
Cho số nguyên dương 𝑛. Thiết kế sơ đồ thuật toán tính giá trị biểu thức
1 1
𝑆 = 1 − + ⋯ + (−1)𝑛+1
2 𝑛
Input: 𝑛 ∈ ℤ+
Output: 𝑆 ∈ ℛ

Giải:
Tương tự ví dụ 1.3.7, biểu thức có 𝑛 phân số, phân số thứ 𝑖(𝑖 = 1,2,3, … , 𝑛) có giá trị
1
(−1)𝑖+1
𝑖
Tuy nhiên thuật toán này không hiệu quả. Nhận xét (−1)𝑖+1 có giá trị 1 nếu 𝑖 lẻ, ngược lại có giá trị
−1 nếu 𝑖 chẵn. Ta có sơ đồ thuật toán hiệu quả hơn như sau

Trương Phước Hải 12


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM

Begin

𝑆=0

𝑖=1

F T
𝑖≤𝑛

𝑆 F T
𝑖 𝑚𝑜𝑑 2 = 0

𝑆 = 𝑆 + 1/𝑖 𝑆 = 𝑆 − 1/𝑖
End

𝑖 =𝑖+1

Trương Phước Hải 13


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM

BÀI TẬP THIẾT KẾ SƠ ĐỒ THUẬT TOÁN


1) Cho số nguyên dương 𝑛, tính giá trị của biểu thức:

𝑎) 𝑆 = 1 + √2 + ⋯ + √𝑛
1 2 𝑛−1
𝑏) 𝑆 = + + ⋯+
2 3 𝑛
1 1
𝑐) 𝑆 = 1 − + ⋯ + (−1)𝑛+1
2! 𝑛!

𝑑) 𝑆 = √2 + √2 + ⋯ + √2 (𝑛 dấu căn)

1 ∗ 3 ∗ … ∗ 𝑛, nếu 𝑛 lẻ
𝑒) 𝑆 = {
2 ∗ 4 ∗ … ∗ 𝑛, nếu 𝑛 chẵn
1
𝑓) 𝑆 = 1 +
1
2+
3 + ⋯+1
𝑛
2) Cho số nguyên 𝑘 và 2 số nguyên dương 𝑛, 𝑚. Tính giá trị biểu thức sau:
𝑎) 𝑆 = (1! + 2! + ⋯ + 𝑛!) mod 𝑚
𝑏) 𝑆 = (𝑘 𝑛 + 𝑘 𝑛−1 + ⋯ + 1) mod 𝑚
3) Cho số nguyên dương 𝑛 và số thực 𝑥. Tính giá trị các biểu thức:
𝑥2 𝑛+1
𝑥𝑛
𝑎) 𝑆 = 𝑥 − + … + (−1)
2! 𝑛!
𝑥 𝑛−2 𝑥 1
𝑏) 𝑆 = 𝑥 𝑛−1 + +⋯+ +
2! (𝑛 − 1)! 𝑛!
4) Cho số nguyên dương 𝑛, tìm số nguyên dương 𝑥 nhỏ nhất thỏa bất đẳng thức

1 + √2 + √3 + ⋯ + √𝑥 ≥ 𝑛
5) Cho 𝑛 điện trở giống nhau 𝑅1 = 𝑅2 = 𝑅3 = ⋯ = 𝑅𝑛 = 𝑘(Ω),
trong đó 𝑘 là một số thực dương. Người ta mắc các điện trở này
thành một mạch điện theo quy luật như hình vẽ.
Cho số nguyên 𝑛 và số thực dương 𝑘. Hãy tính tổng điện trở của
mạch điện được nối theo quy luật trên.

𝐹1 = 1
6) Dãy số nguyên {𝐹𝑖 } được định nghĩa như sau:{𝐹2 = 1
𝐹𝑛 = 𝐹𝑛−1 + 𝐹𝑛−2 , 𝑛 ≥ 3
Cho số nguyên dương 𝑛. Thiết kế thuật toán tìm giá trị của 𝐹𝑛 .
7) Dãy số nguyên {𝐹𝑛 } được định nghĩa như sau:
𝐹𝑛 = 𝑛(𝐹1 + 𝐹2 + ⋯ + 𝐹𝑛−1 )

Trương Phước Hải 14


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Biết rằng 𝐹1 = 1, hãy tính giá trị của 𝐹𝑛 .
8) Cho 3 số nguyên dương 𝑎, 𝑏, 𝑛. Tìm giá trị lớn nhất của biểu thức 𝑎𝑥 + 𝑏𝑦, trong đó (𝑥, 𝑦) là nghiệm
nguyên không âm của bất phương trình 𝑎𝑥 + 𝑏𝑦 ≤ 𝑛.
9) Cho số nguyên dương 𝑛. Thiết kế thuật toán thực hiện:
a) Đếm số chữ số của 𝑛.
b) Tìm số có các chữ số đảo ngược với các chữ số của 𝑛.
c) Tính tổng giá trị chênh lệch giữa 2 chữ số liền kề nhau của 𝑛.
d) Tìm giá trị chênh lệch lớn nhất giữa 2 chữ số liền kề nhau của 𝑛.
e) Tìm chữ số chẵn lớn nhất của 𝑛. Nếu 𝑛 không chứa chữ số chẵn thì output −1.
f) Số đối xứng là số khi viết các chữ số của nó theo thứ tự ngược lại thì giá trị không bị thay đối. Ví
dụ 11, 121, 1221, … là các số đối xứng. Kiểm tra 𝑛 có phải là số đối xứng?
g) Kiểm tra các chữ số của 𝑛 có thứ tự tăng dần từ trái sang phải hay không (số nằm bên trái ≤ số
nằm bên phải)?
h) Đếm số lượng ước số dương của 𝑛.
i) Kiểm tra 𝑛 có phải là số nguyên tố hay không?
j) Tìm chữ số lớn nhất và số lượng chữ số lớn nhất của 𝑛.
k) Phân tích 𝑛 thành tích các thừa số nguyên tố.
l) Đếm số chữ số 0 tận cùng của 𝑛!.
m) Tìm chỉ số của chữ số khác 0 nhỏ nhất của 𝑛. Chỉ số các chữ số được tính từ phải sang trái và chữ
số hàng đơn vị có chỉ số 0 (giả sử 𝑛 có 𝑘 chữ số có dạng ̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅).
𝑎𝑘−1 𝑎𝑘−2 … 𝑎1 𝑎0 Nếu có nhiều thứ tự
thỏa yêu cầu thì chỉ ra thứ tự nhỏ nhất.
n) Nếu tách 𝑛 thành các nhóm gồm các chữ số khác 0 nằm liên tiếp thì ta được bao nhiêu nhóm. Ví
dụ 𝑛 = 120304560780 thì ta tách được 4 nhóm 12, 3, 456, 78.
o) Tìm chiều dài lớn nhất của dãy gồm các chữ số khác 0 liên tiếp của 𝑛. Ví dụ 𝑛 = 10230456 thì
chiều dài lớn nhất tìm được là 3 gồm các chữ số 4,5,6.
p) Nếu tách 𝑛 thành các số tự nhiên gồm các chữ số khác 0 nằm liên tiếp thì ta được giá trị số tự
nhiên lớn nhất là bao nhiêu. Ví dụ 𝑛 = 980790063 thì tách được 3 số tự nhiên 98, 79 và 63. Số
lớn nhất là 98.
q) Một đường chạy là một dãy gồm các chữ số khác 0 nằm liên tiếp nhau thỏa chữ số bên trái luôn
≤ chữ số bên phải. Đếm số lượng đường chạy có trong 𝑛.
r) Tìm độ dài của đường chạy dài nhất trong 𝑛.
s) Tìm số tự nhiên 𝑚 được tạo bởi các chữ số trên đường chạy dài nhất của 𝑛. Nếu có nhiều kết quả
thì tìm 𝑚 có giá trị lớn nhất.
t) Xét số tự nhiên 𝑚 được tạo bởi tích các chữ số thuộc một dãy con gồm các chữ số nằm liên tiếp
trên 𝑛. Hãy tìm giá trị lớn nhất của 𝑚. Ví dụ 𝑛 = 28120890016131 thì 𝑚 = 8 × 9 = 72 đạt giá
trị lớn nhất.

Trương Phước Hải 15


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM

Trương Phước Hải 16


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM

1.4. Lập trình C/C++ (chuyển đổi sơ đồ thành chương trình)


1.4.1. Nguyên tắc chung
Mỗi câu lệnh được kết thúc bằng dấu chấm phẩy (;)
Các từ khóa, câu lệnh trong ngôn ngữ C/C++ phân biệt chữ hoa với chữ thường.
Tập tin chương trình được lưu với tên *.cpp
Cấu trúc 1 chương trình viết bằng C/C++

<khai báo thư viện>

<khai báo hằng>


<khai báo biến>

<định nghĩa hàm>

int main()
{
<Thân chương trình chính>;
return 0;
}

Trước khi viết chương trình trên môi trường Code::Blocks, ta cần phải tạo một project để chứa chương
trình.
Ví dụ một chương trình xuất ra màn hình thông điệp Welcome to C/C++ language!

Chương trình đầu tiên: Welcome.cpp


1 //**********************************************************
2 // Chương trình in ra màn hình thông điệp sau:
3 // Welcome to C/C++ language!
4 //**********************************************************
5
6 #include <iostream> //khai báo thư viện <iostream>
7
8 int main()
9 {
10 std::cout<<"Welcome to C/C++ language!";
11 return 0;
12 }

Output

Giải thích: to C/C++ language!


Welcome

Trương Phước Hải 17


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
- Các dòng từ 1 đến 4 là phần chú thích (comment). Các dòng chú thích bắt đầu bằng cặp kí tự (//) viết
liền nhau. Nội dung chú thích không ảnh hưởng đến kết quả thi hành của chương trình. Phần chú thích
giúp chèn các giải thích của lập trình viên vào chương trình.
- Dòng 6 có chức năng khai báo thư viện <iostream> cho chương trình. Mỗi câu lệnh của C/C++ đều
thuộc một thư viện nào đó nên việc khai báo thư viện là cần phải có trong một chương trình. Chẳng
hạn <iostream> là thư viện chứa các câu lệnh liên quan đến các thao tác nhập và xuất dữ liệu cho
chương trình.
- Dòng 8 chứa khai báo hàm main().
- Dòng 10 là câu lệnh in chuỗi thông điệp ra thiết bị xuất chuẩn (màn hình hoặc tập tin). Chuỗi thông
điệp cần in ra được đặt trong cặp dấu nháy kép ("") và ngay sau cặp kí tự <<. Cuối dòng lệnh này là
dấu chấm phẩy (;) cho biết kết thúc một câu lệnh.

Mỗi câu lệnh của C/C++ đều thuộc một thư viện nào đó nên việc khai báo thư viện là cần phải có trong
một chương trình. Một số thư viện thông dụng của C/C++ thuộc danh sách các thư viện chuẩn (Standard
Library)

STT Thư viện Ngôn ngữ Diễn giải

1 <iostream> C++ Chứa các lệnh nhập/xuất luồng chuẩn của C++

2 <stdio.h>/<cstdio> C/C++ Chứa các lệnh nhập/xuất luồng chuẩn

3 <math.h>/<cmath> C/C++ Chứa các lệnh tính toán số học

4 <string.h>/<cstring> C/C++ Chứa các lệnh xử lý chuỗi

1.4.2. Biến và kiểu dữ liệu


Chương trình đầu tiên chỉ thực hiện in ra màn hình thông điệp cố định mà lập trình viên chỉ định. Chương
trình máy tính có thể làm được nhiều hơn như thế: nhận dữ liệu đầu vào (input), thực hiện một số thao tác
xử lý trung gian và cho kết quả đầu ra (output). Như vậy, các chương trình máy tính có thể sẽ làm việc
với các loại dữ liệu khác nhau trong quá trình thi hành. Dữ liệu của chương trình được lưu trữ trong bộ
nhớ RAM và được chương trình truy xuất thông qua một đại lượng được gọi là biến.
Trong ngôn ngữ C/C++, mọi biến cần phải được khai báo
Biến là một vùng nhớ trên bộ nhớ RAM dùng trước khi sử dụng. Tùy vào giá trị mà biến lưu trữ, mỗi biến
Khái niệm

để lưu trữ giá trị trong chương trình. phải được xác định một kiểu dữ liệu tương ứng. Như vậy,
Các giá trị trong chương trình gồm có: input, kiểu dữ liệu xác định miền giá trị mà biến được phép lưu
output và các giá trị tính toán trung gian. trữ và kích thước vùng nhớ RAM dành để lưu giá trị cho
chương trình.
Cú pháp khai báo biến: <kiểu dữ liệu> <tên biến>;
Có 4 loại kiểu dữ liệu cơ sở: kiểu số nguyên, kiểu số thực, kiểu kí tự và kiểu luận lý (kiểu logic). Các kiểu
dữ liệu cơ sở thông dụng được cho trong bảng sau:

Trương Phước Hải 18


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Kích thước
Loại dữ liệu Kiểu dữ liệu Miền giá trị
(bits)
int 32 [−231 . . 231 − 1]

unsigned int 32 [0. . 232 − 1]


Kiểu số nguyên
long long 64 [−263 . . 263 − 1]

unsigned long long 64 [0. . 264 − 1]

float 32 [−1.8−38 . . 3.438 ]


Kiểu số thực
double 64 [−2.2−308 . . 1.8308 ]

char 8 [−128. .127]


Kiểu kí tự
unsigned char 8 [0. .255]

Kiểu luận lý bool 8 [false. . true]

Lưu ý
- Các kí tự được lưu trữ bên trong máy tính dưới dạng các giá trị nguyên chính là mã ASCII tương
ứng. Chẳng hạn 'A' có mã ASCII 65, 'b' có mã ASCII 98, …
- Các giá trị false, true của kiểu luận lý cũng được lưu trữ dưới dạng số nguyên với false tương
ứng 0 và true tương ứng với 1.

Ví dụ 1.4.1: Khai báo biến 𝑎 kiểu số nguyên có dấu kích thước 32 bits:
int a;
Ví dụ 1.4.2: Khai báo 3 biến số thực 𝑥, 𝑦, 𝑧:
double x, y, z;
Ví dụ 1.4.3: Khai báo biến 𝑎 kiểu nguyên không dấu kích thước 64 bits:
unsigned long long b; Ta có thể sử dụng định nghĩa kiểu typedef để làm gọn
câu lệnh khai báo:
Ví dụ 1.4.4: Khai báo biến 𝑎 sau khi định nghĩa kiểu
Mẹo

typedef unsigned long long ull;


ull:
Sau câu lệnh trên, ull được xem như là một kiểu dữ liệu
ull b; được định nghĩa thay cho unsigned long long.

1.4.3. Hằng số (constant)


Đôi khi lập trình viên cần sử dụng những đại lượng có giá trị không thay đổi trong suốt quá trình thi hành
chương trình chẳng hạn như giá trị số PI=3.14159, giá trị gia tốc trọng trường g=9.8m2/s hoặc sức

Trương Phước Hải 19


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
chứa tối đa của một phòng học MAX_OCCUPANCY=80. Các đại lượng này được gọi là hằng số hoặc gọi tắt
là hằng.
Hằng bắt buộc phải được chỉ định giá trị ngay tại thời điểm khai báo.
Cú pháp khai báo hằng: const <kiểu dữ liệu> <tên hằng> = <giá trị>;

Ví dụ 1.4.5: Khai báo hằng số thực pi:


const double PI = 3.1416;
Ví dụ 1.4.6: Khai báo hằng chuỗi:
const string msg = "Hello World!";

1.4.4. Câu lệnh gán (assignment)


Là câu lệnh cơ bản nhất trong lập trình được dùng để lưu trữ giá trị của chương trình vào bộ nhớ RAM
thông qua biến tương ứng.
Cú pháp lệnh gán: <biến> = <biểu thức>;
Chức năng: lưu trữ giá trị của <biểu thức> ở vế phải vào <biến> ở vế trái. Sau câu lệnh, <biến> sẽ
mang giá trị của <biểu thức>.
Có thể gán giá trị cho biến ở bước khai báo để khởi tạo giá trị ban đầu cho biến. Cú pháp lệnh khởi tạo:
<kiểu dữ liệu> <tên biến> = <biểu thức>;

Lưu ý
- Giá trị của biểu thức được gán cho biến phải là loại dữ liệu có cùng kiểu với biến.
- Giá trị của biểu thức được gán phải nằm trong miền giá trị giới hạn của biến.

Ví dụ 1.4.7: Gán cho biến 𝑎 kiểu int giá trị 10:


int a;
a = 10;
Ví dụ 1.4.8: Lưu giá trị của biến 𝑎 vào biến 𝑏:
int a = 8, b = a;
Ví dụ 1.4.9: Lưu giá trị của biến 𝑎 kiểu int vào biến 𝑏 kiểu double:
int a = 5;
C/C++ cung cấp một số câu lệnh toán học sau:
double b = a; - sqrt(x): tính căn bậc 2 của x.
Mẹo

- abs(x): tính giá trị tuyệt đối của x.


Ví dụ 1.4.10: Lưu giá trị √20 vào biến 𝑎 kiểu double: - sin(x): tính giá trị sin của số thực x.
- cos(x): tính giá trị cos của số thực x.
double a = sqrt(20);

Trương Phước Hải 20


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM

1.4.5. Các phép toán


Phép toán (operator) trong lập trình được sử dụng để kết hợp các toán hạng (operand) tạo thành một biểu
thức. Toán hạng trong lập trình có thể là một hằng, một biến hoặc một biểu thức.
Phép toán được phân loại thành phép toán 1 ngôi (unary operator) và phép toán 2 ngôi (binary operator).
Phép toán 1 ngôi chỉ tác động lên 1 toán hạng, phép toán 2 ngôi phải có sự tham gia của 2 toán hạng.

1.4.5.1. Phép toán số học

Phép toán số học là phép toán được áp dụng để thực hiện các thao tác tính toán số học đối với các giá trị
hoặc biến có kiểu dữ liệu là số nguyên hoặc số thực. Biểu thức có chứa các phép toán số học được gọi là
biểu thức số học.
Bảng dưới đây liệt kê các phép toán số học theo thứ tự độ ưu tiên từ cao xuống thấp:

Độ ưu tiên Phép toán Diễn giải Loại phép toán

1 - Phép đảo dấu 1 ngôi

2 * Phép nhân số học 2 ngôi

2 / Phép chia/phép chia lấy phần nguyên 2 ngôi

Phép chia lấy phần dư (chỉ áp dụng cho


2 % 2 ngôi
các kiểu số nguyên)

3 + Phép cộng số học 2 ngôi

3 - Phép trừ số học 2 ngôi

Ví dụ 1.4.11: Tính tổng của 𝑎 và 𝑏 và lưu vào 𝑐 (𝑎, 𝑏, 𝑐 có kiểu số):


c = a + b;
Ví dụ 1.4.12: Tăng giá trị của 𝑎 thêm 1 đơn vị (𝑎 có kiểu số):
a = a + 1;
Ví dụ 1.4.13: Gán 𝑑 là chữ số hàng đơn vị của số nguyên 𝑛:
d = n % 10;
Ví dụ 1.4.14: Loại chữ số hàng đơn vị của số nguyên 𝑛 ra khỏi 𝑛:
n = n/10;

Trương Phước Hải 21


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
1.4.5.2. Phép toán quan hệ

Phép toán quan hệ được dùng để so sánh hai giá trị hoặc biến. Biểu thức có chứa phép toán quan hệ được
gọi là biểu thức logic. Miền giá trị của biểu thức logic là false hoặc true.
Phép toán quan hệ thường được sử dụng để thiết lập điều kiện rẽ nhánh hoặc điều kiện lặp trong lập trình.

Phép toán Diễn giải

> Kiểm tra toán hạng bên trái lớn hơn toán hạng bên phải

< Kiểm tra toán hạng bên trái nhỏ hơn toán hạng bên phải

>= Kiểm tra toán hạng bên trái lớn hơn hoặc bằng toán hạng bên phải

<= Kiểm tra toán hạng bên trái nhỏ hơn hoặc bằng toán hạng bên phải

== Kiểm tra 2 toán hạng bằng nhau

!= Kiểm tra 2 toán hạng khác nhau

1.4.5.3. Phép toán logic

Phép toán logic được dùng để kết hợp các biểu thức logic. Có 3 toán tử logic: AND (phép và, kí hiệu &&),
OR (phép hoặc, kí hiệu ||), NOT (phép phủ định, kí hiệu !).
Xét 2 toán hạng 𝐴 và 𝐵 chỉ nhận giá trị false hoặc true, bảng chân trị của các phép toán logic được
cho trong bảng sau:

A B !A A & B A || B

false false true false false

false true true false true

true false false false true

true true false true true

Ví dụ 1.4.15: Biểu thức kiểm tra giá trị 𝑥 có thuộc đoạn [𝑎, 𝑏] hay không
(x >= a && x <= b)
Ví dụ 1.4.16: Biểu thức kiểm tra giá trị 𝑥 có nằm ngoài đoạn [𝑎, 𝑏] hay không
(x < a || x > b) hoặc !(x >= a && x <= b)
Ví dụ 1.4.17: Biểu thức kiểm tra 3 giá trị 𝑎, 𝑏, 𝑐 có bằng nhau không
(a == b && b == c)

Trương Phước Hải 22


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Ví dụ 1.4.18: Biểu thức kiểm tra năm 𝑦 có phải là năm nhuận hay không. Năm nhuận là năm thỏa 1 trong
2 điều kiện sau:
- Năm chia hết cho 4 và không chia hết cho 100
- Năm chia hết cho 400
((y % 4 == 0 && y % 100 != 0) || y % 400 == 0)

1.4.5.4. Phép gán phức hợp (compound)

STT Biểu thức Biểu thức tương đương

1 x += a x = x + a

2 x -= a x = x - a

3 x *= a x = x * a

4 x /= a x = x / a

5 x %= a x = x % a

1.4.5.5. Phép tăng/giảm giá trị của biến

STT Biểu thức Diễn giải

1 ++x Tăng giá trị của biến thêm 1 đơn vị trước khi thi hành câu lệnh.

2 --x Giảm giá trị của biến đi 1 đơn vị trước khi thi hành câu lệnh.

3 x++ Tăng giá trị của biến thêm 1 đơn vị sau khi thi hành câu lệnh.

4 x-- Giảm giá trị của biến đi 1 đơn vị sau khi thi hành câu lệnh.

Ví dụ 1.4.19:
a = 5, b = 10;
x = ++a, y = --b;
sau đoạn lệnh các biến sẽ có giá trị như sau: a = 6, b = 9, x = 6, y = 9
Ví dụ 1.4.20:
a = 5, b = 10;
c = a++ + b--;
sau đoạn lệnh các biến sẽ có giá trị như sau: a = 6, b = 9, c = 15.

Trương Phước Hải 23


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM

1.4.6. Chuyển đổi các cấu trúc thuật toán thành câu lệnh
1.4.6.1. Câu lệnh nhập/xuất

Lệnh nhập dữ liệu

Lệnh nhập dữ liệu thực hiện input dữ liệu cho chương trình từ thiết bị nhập chuẩn (bàn phím hoặc tập
tin).
Cú pháp lệnh nhập liệu:
- Ngôn ngữ C: scanf("<đặc tả dữ liệu nhập>", <danh sách biến>);
- Ngôn ngữ C++: cin>><danh sách biến>;
Chức năng: Đọc danh sách dữ liệu từ luồng nhập chuẩn (standard input: nhập từ bàn phím hoặc tập tin)
và lưu vào <danh sách biến> tương ứng. Khi sử dụng hàm nhập của ngôn C, ta cần phải thêm các kí
tự đặc tả dữ liệu nhập cho các biến tùy vào kiểu dữ liệu tương ứng của biến.
Một số kí tự đặc tả định dạng dữ liệu nhập (dành cho ngôn ngữ C) cho một số kiểu dữ liệu thường dùng

STT Kí tự đặc tả Kiểu dữ liệu

1 %d int

2 %u unsigned int

3 %lld long long

4 %llu unsigned long long

5 %lf double

Ví dụ 1.4.21:
Nhập 2 số nguyên từ bàn phím và lưu vào 2 biến tương ứng 𝑎, 𝑏

Ngôn ngữ C Ngôn ngữ C++

int a, b; int a, b;
scanf("%d%d", &a, &b); cin>>a>>b;

Ví dụ 1.4.22:
Nhập 1 số nguyên và một số thực từ bàn phím và lưu vào 2 biến tương ứng 𝑥, 𝑦

Ngôn ngữ C Ngôn ngữ C++

int x; int x;
double y; double y;

Trương Phước Hải 24


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
scanf("%d%lf", &x, &y); cin>>x>>y;

Lưu ý: Ta bắt buộc phải thêm kí tự & trước mỗi biến của hàm scanf.

Lệnh xuất dữ liệu

Lệnh xuất dữ liệu thực hiện output dữ liệu ra thiết bị xuất chuẩn (màn hình hoặc tập tin).
Cú pháp lệnh xuất:
- Ngôn ngữ C: printf("<đặc tả>/<thông điệp xuất>", <danh sách biến>);
- Ngôn ngữ C++: cout<<"<thông điệp xuất>"/<danh sách biến>;
Chức năng: Xuất giá trị của <danh sách biến> ra luồng xuất chuẩn (standard output: xuất ra màn hình
hoặc tập tin). <thông điệp xuất> cần phải được đặt trong cặp dấu "". Tương tự như hàm scanf, ta
cần phải thêm các kí tự đặc tả dữ liệu xuất cho các biến tùy vào kiểu dữ liệu tương ứng của biến. Các kí
tự đặc tả dữ liệu xuất của hàm printf tương tự như các kí tự đặc tả của hàm scanf.
Dữ liệu xuất ra luồng chuẩn trong C có thể được điều khiển bằng các hằng kí tự tương tự như các hằng kí
tự đặc tả định dạng dữ liệu nhập.

Ví dụ 1.4.23:
Xuất giá trị của 2 biến nguyên 𝑎, 𝑏 ra màn hình và cách nhau khoảng trắng

Ngôn ngữ C Ngôn ngữ C++

printf("%d %d", a, b); cout<<a<<" "<<b;

Ví dụ 1.4.24:
Xuất giá trị của 3 biến nguyên 𝑎, 𝑏, 𝑐 ra màn hình, mỗi biến nằm trên một dòng

Ngôn ngữ C Ngôn ngữ C++

printf("%d\n%d\n%d", a, b, c); cout<<a<<"\n"<<b<<"\n"<<c;

Ví dụ 1.4.25:
Xuất kết quả các phép tính: cộng, trừ, nhân và chia của số nguyên 𝑎 cho số nguyên 𝑏. Mỗi biểu thức
trên một dòng.

Ngôn ngữ C Ngôn ngữ C++

printf("%d + %d = %d\n", a,b,a+b); cout<<a<<" + "<<b<<" = "<<a+b<<"\n";

Trương Phước Hải 25


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
printf("%d - %d = %d\n", a,b,a-b); cout<<a<<" - "<<b<<" = "<<a-b<<"\n";
printf("%d * %d = %d\n", a,b,a*b); cout<<a<<" * "<<b<<" = "<<a*b<<"\n";
printf("%d / %d = %d\n", a,b,a/b); cout<<a<<" / "<<b<<" = "<<a/b<<"\n";

Ví dụ 1.4.26:
Xuất giá trị 2 biến số thực 𝑥, 𝑦 ra màn hình và cách nhau khoảng trắng, giá trị của mỗi biến được làm
tròn 3 chữ số thập phân

Ngôn ngữ C Ngôn ngữ C++

printf("%0.3lf %.3lf", x,y); cout<<setprecision(3)<<fixed<<x<<" "<<y;

Lưu ý
- Để sử dụng hàm setprecision(3) trong C++ ta cần phải khai báo thư viện <iomanip>.
- Câu lệnh printf("The number is:%[d.k]lf", x) xuất số thực 𝑥 ra màn hình và được làm
tròn 𝑘 chữ số thập phân với 𝑑 vị trí dành trước. Kết quả xuất được minh họa như hình.

The number is: 1234.56789

d
- Chuỗi <thông điệp xuất> có thể được chèn các kí tự điều khiển để định dạng kết quả xuất. Các
kí tự điều khiển được đặt trong chuỗi và bắt đầu bằng kí tự \. Một số kí tự điều khiển thông thường

STT Kí tự điều khiển Diễn giải


Chuyển con nháy xuống dòng mới để in giá trị tiếp theo.
1 \n
Tương tự thao tác nhấn Enter.
Dịch con nháy đến vị trí tab tiếp theo để in giá trị tiếp theo.
2 \t
Tương tự thao tác nhấn phím Tab.
Chuyển con nháy lui về 1 vị trí và xóa kí tự tại đó.
3 \b
Tương tự thao tác nhấn phím Backspace.
Chuyển con nháy trở về đầu dòng.
4 \r
Tương tự thao tác nhấn phím Home

5 \\ Xuất kí tự \ ra luồng xuất chuẩn.

6 \' Xuất kí tự ' ra luồng xuất chuẩn.

Trương Phước Hải 26


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM

7 \" Xuất kí tự " ra luồng xuất chuẩn.

1.4.6.2. Lệnh rẽ nhánh

Cấu trúc rẽ nhánh được sử dụng để đưa ra các quyết định thi hành trong lặp trình. Có 2 dạng rẽ nhánh
phổ dụng: rẽ nhánh đầy đủ và rẽ nhánh khuyết.

Rẽ nhánh đầy đủ

Sơ đồ thực thi
if (<ĐK rẽ nhánh>)
{
<Khối lệnh 1>;
false true }
ĐK rẽ nhánh else
{
Khối lệnh 2
<Khối lệnh 2>;
Khối lệnh 1
}

<Khối lệnh 1> được thực thi nếu <điều kiện rẽ nhánh> có giá trị true, ngược lại <Khối lệnh
2> được thực thi. Như vậy chỉ 1 trong 2 khối lệnh này được thực thi.

Ví dụ 1.4.27:

if (a % 2 == 0) else
{ {
a = a / 2; a = a*3 + 1;
++cnt; ++cnt;
} }

Lưu ý: Cặp dấu ngoặc được dùng để bao một khối lệnh. Nếu như khối lệnh chỉ có 1 câu lệnh thì ta có
thể bỏ chúng đi.

Ví dụ 1.4.28: Đoạn chương trình trên có thể được viết lại như sau
if (a % 2 == 0)
a = a / 2;
else a = a*3 + 1;
++cnt;

Trương Phước Hải 27


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Rẽ nhánh khuyết

if (<ĐK rẽ nhánh>)
true {
ĐK rẽ nhánh
<Khối lệnh>;
}
Khối lệnh
false

<Khối lệnh> được thực thi nếu <điều kiện rẽ nhánh> có giá trị true. Rẽ nhánh khuyết được sử
dụng khi không cần quan tâm trường hợp <điều kiện rẽ nhánh> có giá trị false.

Ví dụ 1.4.29:
Cho 4 số nguyên 𝑎, 𝑏, 𝑐, 𝑑. Các số nguyên được đánh thứ tự tương ứng từ 1 đến 4. Hãy cho biết thứ
tự của số lớn nhất. Nếu có nhiều số lớn nhất thì tìm thứ tự nhỏ nhất.
Đoạn chương trình thực hiện
int maxValue = a, pos = 1;
if (b > maxValue)
{
maxValue = b;
pos = 2;
}
if (c > maxValue)
{
maxValue = c;
pos = 3;
}
if (d > maxValue)
{
maxValue = d;
pos = 4;
}

Trương Phước Hải 28


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
1.4.6.3. Lệnh lặp

Khởi tạo lặp


<Khởi tạo lặp>;
while (<ĐK lặp>)
{
false true <Thao tác lặp>;
ĐK lặp }

Thao tác lặp

Ví dụ 1.4.30:
Cho số nguyên dương 𝑛. Tính tổng các chữ số của 𝑛

Đoạn chương trình thực hiện


int s = 0;
while (n > 0)
{
int d = n%10;
s = s + d;
n = n/10;
}

Ví dụ 1.4.31:
Cho số nguyên dương 𝑛. Tính giá trị biểu thức

𝑆 = √2 + √2 + ⋯ + √2 (𝑛 𝑑ấ𝑢 𝑐ă𝑛)

Đoạn chương trình thực hiện:


double S = 0;
int i = 1;
while (i <= n)
{
S = sqrt(S + 2);
++i;
}

Trương Phước Hải 29


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Ví dụ 1.4.32:
Phòng thí nghiệm (PTN) cần có 𝑛 cá thể virus để phục vụ nghiên cứu. Các virus sẽ tự nhân đôi số
lượng sau mỗi ngày nuôi cấy trong môi trường PTN. Ban đầu PTN sử dụng 𝑎 cá thể virus để nuôi
cấy. Lập trình tìm số ngày nuôi cấy tối thiểu để đạt đủ 𝑛 cá thể virus.

Phân tích:
Input: 𝑛, 𝑎 ∈ ℤ+ .
Output: numDays ∈ ℤ+ là số ngày nuôi cấy tối thiểu.
Thao tác lặp: Sau mỗi ngày nuôi cấy các cá thể virus nhân đôi số lượng, biểu thức biểu diễn:
𝑎 =𝑎∗2
numDays = numDays + 1
Khởi tạo lặp: Khi chưa bắt đầu nuôi cấy thì số ngày đếm được là 0, biểu thức biểu diễn: numDays = 0
Điều kiện lặp: Điều kiện còn tiếp tục nuôi cấy (khi số virus đang nuôi cấy chưa đủ 𝑛), biểu thức biểu diễn:
𝑎 < 𝑛.
Đoạn chương trình thực hiện:
int numDays = 0;
while (a < n)
{
a = a*2;
numDays = numDays + 1;
}

Cấu trúc lặp được chia thành 2 loại:


- Lặp không xác dịnh: không xác định được số lần lặp (ví dụ 1)
- Lặp xác định: xác định được số lần lặp (ví dụ 2, ví dụ 3)
Để tăng tính tiện dụng cho công việc lập trình, C/C++ cung cấp lệnh lặp for được dùng với ngữ nghĩa
xác định được số lần lặp của thuật toán.
Cú pháp
for (<khởi tạo đếm>; <điều kiện lặp>; <thay đổi đếm>)
{
<thao tác lặp>;
}

Đoạn chương trình ở ví dụ 2 có thể được viết lại như sau:


int S = 0;
for (int i = 1; i <= n; ++i)
S = sqrt(S + 2);

Trương Phước Hải 30


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Ví dụ 1.4.33:
Cho số nguyên dương 𝑛. Tính giá trị biểu thức

𝑆 = 1 + √2 + ⋯ + √𝑛
Đoạn chương trình thực hiện:
double S = 0;
for (int i = 1; i <= n; ++i)
S = S + sqrt(i);

Lưu ý: Trong ngôn ngữ C/C++, vòng lặp for thực chất là một cách “bố trí lại các câu lệnh” của vòng
lặp while. Về bản chất thì 2 vòng lặp này là như nhau.

1.4.7. Hàm (function)


Hàm (function) là một đơn vị của chương trình gồm một nhóm các câu lệnh thực thi một công việc cụ
thể. Hàm còn được gọi là chương trình con (subprogram hoặc subroutine) giải quyết một bài toán con
nào đó do đó hàm cũng nhận input, thực thi một số câu lệnh cụ thể để tạo ra output.
Hàm có thể được gọi thực thi trong các hàm khác hoặc trong chương trình chính (hàm main). Hàm có
tính tái sử dụng nhằm tránh việc viết lại những đoạn lệnh thực hiện những tác vụ tương tự nhau nhiều lần
và do đó giúp cho chương trình mang tính trong sáng, dễ sửa lỗi, dễ cải tiến.
Các ngôn ngữ lập trình luôn cung cấp các hàm cơ bản được xây dựng sẵn chứa trong các thư viện tích
hợp. Đồng thời cho phép lập trình viên có thể tự định nghĩa các hàm của riêng mình phục vụ các nhu cầu
đặc thù.
Một số hàm cơ bản thông dụng của ngôn ngữ C/C++:

STT Hàm Diễn giải

1 sqrt(<x>) Trả về giá trị căn bậc 2 của <x> (thuộc thư viện <cmath>)

2 abs(<x>) Trả về giá trị tuyệt đối của <x> (thuộc thư viện <cmath>)

3 max(<x>,<y>) Trả về giá trị lớn hơn trong 2 giá trị <x> và <y>

4 min(<x>,<y>) Trả về giá trị nhỏ hơn trong 2 giá trị <x> và <y>

5 swap(<x>,<y>) Hoán vị giá trị của 2 biến <x> và <y>

Trương Phước Hải 31


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
1.4.7.1. Phân loại hàm

Hàm trả về giá trị

Hàm trả về giá trị là được sử dụng khi bài toán con chỉ có đúng 1 output. Giá trị trả về của hàm chính là
giá trị output.
Cấu trúc hàm trả về giá trị:

<kiểu DL ouput> TênHàm(<kiểu DL> <d/s input>)


{
<Thân hàm>;
return <giá trị output>;
}

- <danh sách input> được gọi là tham số hình thức của hàm (formal parameter).
- Câu lệnh return trả giá trị output cho hàm và kết thúc hàm. Giá trị output của hàm có thể được
tính thông qua một biểu thức có cùng kiểu dữ liệu với kiểu dữ liệu của output. Các lệnh nằm sau lời
gọi thực thi lệnh return sẽ bị bỏ qua.
- Vì có trả về giá trị nên khi gọi thực thi hàm, ta có thể lưu trữ giá trị trả về của hàm vào một biến có
cùng kiểu với dữ liệu được trả về.
Cú pháp gọi thực thi hàm có giá trị trả về: <tên biến> = <TênHàm>(<d/s input>);

Ví dụ 1.4.34:
Cho số nguyên dương 𝑛. Xây dựng số nguyên dương 𝑚 có các chữ số có thứ tự đảo ngược với thứ tự
các chữ số của 𝑛
Phân tích:
Input: 𝑁𝑢𝑚𝑏𝑒𝑟 ∈ ℤ+ .
Output: reverseNum ∈ ℤ+ là số đảo ngược của 𝑛.
Vì bài toán chỉ có 1 output là reverseNum nên ta sử dụng hàm có giá trị trả về:
#include <iostream>
using namespace std;

int Reverse(int Number)


{
int reverseNum = 0;
while (Number > 0)
{
int lastDigit = Number % 10;
reverseNum = (reverseNum * 10) + lastDigit;
Number = Number / 10;
}
return reverseNum;

Trương Phước Hải 32


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
}

int main()
{
int n;
cin>>n;
int m = Reverse(n); //gọi thực thi hàm có giá trị trả về
cout<<m;
return 0;
}

Ví dụ 1.4.35:
Cho số nguyên 𝑛. Lập trình kiểm tra 𝑛 có phải là số nguyên tố và thông báo YES nếu 𝑛 là số nguyên
tố, ngược lại thông báo NO.
Phân tích:
Input: 𝑛 ∈ ℤ+ .
Output: True hoặc False tương ứng 𝑛 là số nguyên tố hoặc không là số nguyên tố.
Vì bài toán chỉ có 1 output là nên sử dụng hàm có giá trị trả về:
#include <iostream>
using namespace std;

bool IsPrime(int n)
{
if (n < 2)
return false;
int m = sqrt(n);
for (int i = 2; i <= m; ++i)
if (n % i == 0)
return false;

return true;
}

int main()
{
int n;
cin>>n;
if (IsPrime(n)) //gọi thực thi hàm có giá trị trả về
cout<<"YES";
else cout<<"NO";
return 0;
}

Trương Phước Hải 33


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Hàm không trả về giá trị

Hàm không trả về giá trị được sử dụng khi bài toán con không cho output hoặc cho nhiều output (từ 2
output trở lên).
Cấu trúc hàm không trả về giá trị
void TênHàm(<kiểu DL> <d/s input>, <kiểu DL> &<d/s output>)
{
<Thân hàm>;
}
- <d/s input> và <d/s output> được gọi là tham số hình thức của hàm.
- <d/s output> chỉ được khai báo hình thức khi số lượng <output> là cố định cho mọi trường hợp.
Trường hợp bài toán không có <output> hoặc có số lượng <output> không cố định thì <d/s
output> sẽ được lược bỏ đi.
- Trước mỗi <output> trong <d/s output> hình thức đều phải thêm dấu &.
- void là một kiểu dữ liệu đặc biệt cho biết hàm không trả về giá trị.
Cú pháp gọi thực thi hàm không trả về giá trị: <TênHàm>(<d/s input>, [<d/s output>]);

Ví dụ 1.4.36:
Cho số nguyên dương ℎ. Lập trình vẽ tam giác vuông cân có độ lớn cạnh góc vuông là ℎ bằng các kí
tự '*' có dạng như hình minh họa với ℎ = 4:
*
* *
* * *
* * * *
Phân tích:
Input: ℎ ∈ ℤ+
Output: không có.
Do bài toán không có output nên sử dụng hàm không trả về giá trị như sau:

#include <iostream>
using namespace std;

void DrawTriangle(int h)
{
for (int row = 1; row <= h; ++row)
{
for (int i = 1; i <= row; ++i)
cout<<"* ";
cout<<"\n";
}
}

Trương Phước Hải 34


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
int main()
{
int h;
cin>>h;
DrawTriangle(h); //gọi thực thi hàm không trả về giá trị
return 0;
}

Ví dụ 1.4.37:
Cho số nguyên dương 𝑛. Liệt kê tất cả ước dương của 𝑛.

Phân tích:
Input: 𝑛 ∈ ℤ+
Output: tất cả ước dương của 𝑛.
Do số lượng output của bài toán là không cố định nên sử dụng hàm không trả về giá trị như sau:

#include <iostream>
using namespace std;

void ListDivisors(int n)
{
for (int i = 1; i <= n; ++i)
if (n % i == 0)
cout<<i<<" ";
}

int main()
{
int n;
cin>>n;
ListDivisors(n);
return 0;
}

Ví dụ 1.4.38:
Cho số nguyên dương 𝑛. Đếm số chữ số và tính tổng các chữ số của 𝑛.

Phân tích
Bài toán có 2 output là số chữ số và tổng các chữ số của 𝑛 nên hàm xử lý là hàm không có giá trị trả
về (Solve).
#include <iostream>

Trương Phước Hải 35


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
using namespace std;

Phân tích
Bài toán Solve (đếm và tính
tổng các chữ số của 𝑛)
void Solve(int n, int &sum, int &cnt) - Input: 𝑛 ∈ ℤ+
{ - Output: 𝑠𝑢𝑚, 𝑐𝑛𝑡 ∈ ℤ+
sum = cnt = 0;
while (n > 0)
{
sum = sum + n % 10;
++cnt;
n = n / 10;
}
}

int main()
{
int n, s, k;
cin>>n; Gọi thực thi hàm:

Mẹo
Solve(n, s, k); - s nhận giá trị của sum
cout<<s<<" "<<k; - k nhận giá trị của cnt
return 0;
}

1.4.7.2. Phạm vi của biến

Phạm vi của biến cho biết tầm hoạt động của biến trong một chương trình, nghĩa là phần nào của chương
trình thì được hay không được phép truy xuất đến một biến nào đó. Phạm vi của biến được chia thành 2
dạng chính: biến cục bộ (local variable) và biến toàn cục (global variable).

Biến cục bộ

Biến cục bộ là biến được khai báo bên trong hàm hoặc bên trong một khối lệnh (bên trong cặp dấu ngoặc
giới hạn {}). Biến cục bộ có phạm vi hoạt động chỉ bên trong thân hàm hoặc bên trong khối lệnh được
khai báo. Do đó các hàm hoặc các khối lệnh khác nhau có thể đặt trùng tên biến cục bộ.
Biến cục bộ nếu không được khởi tạo giá trị thì sẽ mang giá trị bất kỳ (gọi là giá trị rác). Bộ nhớ dành cho
biến cục bộ sẽ tự giải phóng khi ra khỏi phạm vi của hàm hoặc khối lệnh được khai báo.
Tham số hình thức của hàm đóng vai trò tương tự biến cục bộ của hàm đó.
Xét chương trình trong ví dụ 1.4.34:
- Biến reverseNum là biến cục bộ của hàm Reverse nên có phạm vi hoạt động chỉ bên trong hàm
này.
- Biến lastDigit là biến cục bộ trong vòng lặp while (n > 0) nên có phạm vi hoạt động chỉ bên
trong vòng lặp này.
- Biến n, m là biến cục bộ của hàm main nên có phạm vi hoạt động chỉ bên trong hàm main.

Trương Phước Hải 36


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Biến toàn cục

Biến toàn cục là biến được khai báo ở ngoài tất cả hàm trong chương trình và có phạm vi hoạt động từ vị
trí khai báo đến cuối chương trình. Bộ nhớ dành cho biến toàn cục chỉ được giải phóng khi kết thúc
chương trình.
Biến toàn cục nếu không được khởi tạo thì sẽ được tự động khởi tạo giá trị 0.
Trường hợp biến toàn cục trùng tên với biến cục bộ hoặc tham số của hàm, thì các biến đóng vai trò cục
bộ sẽ được ưu tiên trong phạm vi cục bộ.

Ví dụ 1.4.39:
Cho số nguyên dương 𝑛. Tìm số đảo ngược của 𝑛. (xét lại ví dụ 1.4.34)

Chương trình trong ví dụ 1.4.34 có thể được viết lại như sau:
#include <iostream>
using namespace std;

int Number, reverseNum; //biến toàn cục

int Reverse(int Number)


{
int reverseNum = 0; //reverseNum là biến cục bộ của hàm Reverse
while (Number > 0)
{
int lastDigit = Number % 10;
reverseNum = (reverseNum * 10) + lastDigit;
Number = Number / 10;
}
return reverseNum;
}

int main()
{
cin>>Number; //Number là biến toàn cục
reverseNum = Reverse(Number); //reverseNum là biến toàn cục
cout<<reverseNum;
return 0;
}

- Biến Number, reverseNum toàn cục có phạm vi hoạt động trong toàn bộ chương trình. Tuy nhiên,
hàm Reverse cũng có tham số Number và biến cục bộ reverseNum trùng tên với 2 biến toàn cục.
Trong trường hợp này, Number và reverseNum cục bộ sẽ được ưu tiên bên trong phạm vi hàm
Reverse.

Trương Phước Hải 37


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Ví dụ 1.4.40:
Xét chương trình dưới đây và theo dõi giá trị của các biến cục bộ và toàn cục.
#include <iostream>
using namespace std;

int a = 5, b = 12, c;

int Subtract(int a, int b)


{
int c = a – b;
cout<<"Local value: "<<a<<" "<<b<<" "<<c<<"\n";
return c;
}

int main()
{
c = Subtract(b, a);
cout<<"Global value: "<<a<<" "<<b<<" "<<c<<"\n";
return 0;
}
Kết quả output như sau
Local value: 12 5 7
Global value: 5 12 7

Lưu ý: Nên hạn chế sử dụng biến toàn cục vì có khả năng không kiểm soát được các hàm truy xuất
giá trị của biến toàn cục.

Ví dụ 1.4.41:
Cho số nguyên dương 𝑛. Liệt kê các số nguyên tố không vượt quá 𝑛. Mỗi số trên một dòng.

Ví dụ này minh họa trường hợp sử dụng biến toàn cục gây lỗi lặp vô hạn
#include <iostream>
#include <cmath>

using namespace std;

int i, n;

bool IsPrime(int n)
{
if (n < 2) return false;
int m = sqrt(n);
for (i = 2; i <= m; ++i)

Trương Phước Hải 38


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
if (n % i == 0)
return false;
return true;
}

void PrimeList(int n)
{
for (i = 1; i <= n; ++i)
if (IsPrime(i))
cout<<i<<"\n";
}

int main()
{
cin>>n;
PrimeList(n);
return 0;
}

- Chương trình trên bị lỗi lặp vô hạn do xung đột giá trị biến đếm i của hai vòng lặp for trong hàm
IsPrime và hàm PrimeList. Lỗi trên được khắc phục bằng cách khai báo i là biến cục bộ của hai
hàm này.

Trương Phước Hải 39


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
BÀI TẬP LẬP TRÌNH
BÀI 1
Chuyển đổi tất cả bài tập trong phần thiết kế thuật toán thành chương trình.

BÀI 2
Viết chương trình nhập vào 3 số thực dương 𝑎, 𝑏, 𝑐. Cho biết 𝑎, 𝑏, 𝑐 có là 3 cạnh của 1 tam giác hay không.
Nếu có hãy xuất ra màn hình thông báo đó là tam giác loại gì trong các loại sau: đều, cân, vuông, vuông
cân, nhọn, tù.
Dữ liệu: Nhập vào 3 số thực 𝑎, 𝑏, 𝑐, (0 < 𝑎, 𝑏, 𝑐 ≤ 106 ).
Kết quả: Xuất ra thông báo KHONG PHAI TAM GIAC nếu 𝑎, 𝑏, 𝑐 không lập thành 3 cạnh của một tam
giác hoặc một trong các thông báo sau: TAM GIAC DEU, TAM GIAC CAN, TAM GIAC VUONG, TAM GIAC
VUONG CAN, TAM GIAC NHON, TAM GIAC TU.
Ví dụ:
INPUT OUTPUT
3 5 10 KHONG PHAI TAM GIAC
6 6 6 TAM GIAC DEU

BÀI 3
Viết chương trình nhập vào 2 số nguyên 𝑎, 𝑏 là số tuổi hiện tại của em và của anh. Cho biết sau bao nhiêu
năm nữa hay cách hiện tại bao nhiêu năm trước thì tuổi của anh gấp đôi tuổi của em.
Dữ liệu: Nhập vào 2 số nguyên 𝑎, 𝑏(0 < 𝑎 < 𝑏 ≤ 106 ).
Kết quả: Xuất ra thông báo sau x nam nua hoặc cach day x nam. Trong đó x là số năm cách năm
hiện tại mà tuổi anh gấp đôi tuổi em.
Ví dụ:
INPUT OUTPUT
5 8 cach day 2 nam
5 12 sau 2 nam nua

BÀI 4
Hình chữ nhật được gọi là che phủ hoàn toàn hình tròn nếu ta có thể đặt hình chữ nhật lên hình tròn trong
một mặt phẳng sao cho không có phần nào của hình tròn được nhìn thấy. Hãy kiểm tra hình chữ nhật có
thể che phủ hoàn toàn hình tròn hay không.
Dữ liệu: Nhập vào 3 số thực 𝑎, 𝑏, 𝑅(0 < 𝑎, 𝑏, 𝑅 ≤ 106 ).
Kết quả: Xuất ra thông báo YES hoặc NO tương ứng câu trả lời mảnh gỗ hình chữ nhật có kích thước
𝑎 × 𝑏 có thể che phủ được hoàn toàn một mảnh gỗ hình tròn bán kính 𝑅.
Ví dụ:

Trương Phước Hải 40


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
INPUT OUTPUT
4 5 3 NO
7 10 3 YES

BÀI 5
Một hình chữ nhật che phủ hoàn toàn được hình chữ nhật khác nếu ta có thể đặt hình này lên hình còn lại
sao cho không có phần nào của hình còn lại được nhìn thấy. Hãy kiểm tra hình chữ nhật thứ nhất có thể
che phủ hoàn toàn hình chữ nhật thứ hai hay không.
Dữ liệu: Nhập vào 4 số nguyên 𝑎1 , 𝑏1 , 𝑎2 , 𝑏2 (0 < 𝑎1 , 𝑏1 , 𝑎2 , 𝑏2 ≤ 106 ).
Kết quả: Xuất ra thông báo YES hoặc NO tương ứng câu trả lời hình chữ nhật kích thước là 𝑎1 × 𝑏1 có
che phủ hoàn toàn được hình chữ nhật kích thước 𝑎2 × 𝑏2 .
Ví dụ:
INPUT OUTPUT
4 5 3 6 NO
5 10 2 2 YES

BÀI 6
Một tiệm bánh bán 2 loại bánh hình tròn có chất lượng và độ dày như nhau nhưng khác nhau về bán kính.
Loại thứ nhất có bán kính 𝑟1 và giá tiền 𝑐1 ; loại thứ hai có bán kính 𝑟2 và giá tiền 𝑐2 . Cho biết mua bánh
nào thì có lợi hơn theo nghĩa: cùng số tiền nhưng mua được lượng bánh nhiều hơn.
Dữ liệu: Nhập vào 4 số nguyên 𝑟1 , 𝑐1 , 𝑟2 , 𝑐2 (0 < 𝑟1 , 𝑐1 , 𝑟2 , 𝑐2 ≤ 106 ).
Kết quả: Xuất ra số nguyên 1 hay 2 cho biết loại bánh có lợi hơn.
Ví dụ:
INPUT OUTPUT
10 10 50 20 2

BÀI 7
Một tiệm bánh bán 2 loại bánh: loại thứ nhất có giá 𝑎 đồng/cái, loại thứ hai có giá 𝑏 đồng/cái. Một người
sử dụng toàn bộ số tiền 𝑐 để mua 2 loại bánh trên (hoặc chỉ mua 1 loại bánh mua cả 2 loại bánh). Cho biết
tất cả những cách mua bánh với số tiền đúng bằng 𝑐 đồng.
Dữ liệu: Nhập vào 3 số nguyên 𝑎, 𝑏, 𝑐(0 < 𝑎, 𝑏 ≤ 106 ; 0 < 𝑐 ≤ 109 ).
Kết quả: Xuất ra tất cả cách mua 2 loại bánh để số tiền phải trả đúng bằng 𝑐, mỗi cách trên một dòng.
Với mỗi cách mua xuất theo định dạng 𝑥 𝑦(𝑥, 𝑦 ≥ 0) – tương ứng với số lượng bánh từng loại. Kết quả
xuất tăng dần theo 𝑥.
Ví dụ:
INPUT OUTPUT
2 3 10 2 2
5 0

Trương Phước Hải 41


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
BÀI 8
Phòng thí nghiệm cần 𝑛 tế bào để phục vụ nghiên cứu. Các tế bào sẽ nhân đôi số lượng sau mỗi ngày nuôi
cấy trong môi trường phù hợp. Ban đầu phòng thí nghiệm sử dụng 𝑎 tế bào để nuôi cấy. Hãy tìm số ngày
nuôi cấy tối thiểu để được đủ 𝑛 tế bào.
Dữ liệu: Nhập vào số nguyên 𝑎, 𝑛(1 ≤ 𝑎 ≤ 106 ; 1 ≤ 𝑛 ≤ 109 ).
Kết quả: Xuất ra số ngày nuôi cấy tối thiểu.
Ví dụ:
INPUT OUTPUT
3 20 3

BÀI 9
Một cây con có chiều cao ban đầu là 𝑎. Nếu được chăm sóc thích hợp, mỗi ngày chiều cao của cây tăng
thêm 𝑏%. Hãy tìm chiều cao của cây sau 𝑛 ngày chăm sóc.
Dữ liệu: Nhập vào số nguyên 𝑎(1 ≤ 𝑎 ≤ 103 ), số thực 𝑏(1 ≤ 𝑏 ≤ 100) và số nguyên 𝑛(1 ≤ 𝑛 ≤ 105 ).
Kết quả: Xuất ra số thực là chiều cao của cây sau 𝑛 ngày chăm sóc. Kết quả được làm tròn 3 chữ số thập
phân.
Ví dụ:
INPUT OUTPUT
5 10 10 12.969

BÀI 10
Cho số nguyên dương 𝑛. Hãy liệt kê 𝑛 số nguyên tố đầu tiên ra màn hình.
Dữ liệu: Nhập vào số nguyên 𝑛(1 ≤ 𝑛 ≤ 104 ).
Kết quả: Xuất ra các số nguyên tố theo thứ tự tăng dần, mỗi số nằm trên một dòng.
Ví dụ:
INPUT OUTPUT
5 2
3
5
7
11

Lưu ý:
Các bài tập sau đây có dữ liệu được nhập xuất từ file văn bản để có thể chấm tự động. Cách đặt tên file
bài làm (*.cpp), file input (*.inp) và output (*.out) được quy định trong mỗi bài. Trong đó 3 tập tin này ở
mỗi bài (*.cpp, *.inp, *.out) được đặt cùng tên nhau, chỉ khác nhau phần mở rộng.
Các câu lệnh dùng để nhập xuất file (trong thư viện <cstdio>)

Trương Phước Hải 42


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
...
#include <cstdio>
...

int main()
{
freopen("input.txt", "r", stdin); //mở file input.txt để đọc
freopen("output.txt", "w", stdout); //mở file output.txt để ghi
...
return 0;
}

Sau khi gọi câu lệnh freopen thì các thao tác đọc dữ liệu từ file hay ghi kết quả ra file được sử dụng
bằng các lệnh cin và cout như thông thường.

TÍNH CƯỚC CUỘC GỌI


Cước gọi điện thoại di động của một công ty viễn thông được chia thành block và phút như sau:
- Giá cước 𝑝 đồng /phút và 𝑡 đồng/block (6 giây).
- Dưới 6 giây thì được tính là 1 block (6 giây).
Yêu cầu: Hãy cho biết số tiền mà khách hàng phải trả cho công ty viễn thông khi gọi điện trong 𝑛 giây.
Dữ liệu: Vào từ tập tin văn bản MOBILE.INP chứa 3 số nguyên dương 𝑝, 𝑡, 𝑛(1 ≤ 𝑝, 𝑛 ≤ 109 ) – tương
với giá cước 1 phút, giá cước 1 block và thời gian gọi [điện (tính theo giây).
Kết quả: Ghi ra tập tin văn bản MOBILE.OUT một số nguyên là số tiền mà khách hàng phải trả.
Ví dụ:
MOBILE.INP MOBILE.OUT
100 12 200 348
Giải thích: 200 giây được chia thành 3 phút và 20 giây = 3 phút + 4 block. Do đó số tiền phải trả là 300 +
48 = 348.

GAME THỦ
Một game thủ đang tích cực cày một game mới. Nhiệm vụ của game thủ là tiêu diệt các quái vật có level
tăng dần từ level 1 trở đi. Mỗi lần tiêu diệt được quái vật ở level 𝑘 thì game thủ được 𝑘 điểm. Nghĩa là
tiêu diệt quái vật ở level 1 được 1 điểm, tiêu diệt quái vật ở level 2 được 2 điểm, … và level phải được
bắt đầu từ 1 trở đi. Game thủ hiện cần có 𝑛 điểm để mua 1 bảo vật. Cho biết game thủ cần phải tiêu diệt
xong quái vật ở level bao nhiêu.
Yêu cầu: Cho số nguyên dương 𝑛(𝑛 ≤ 109 ). Cho biết level mà game thủ cần phải đạt đến để đủ 𝑛 điểm
thưởng.
Dữ liệu: Vào từ tập tin văn bản GAMER.INP số nguyên 𝑛(1 ≤ 𝑛 < 109 ).
Kết quả: Ghi ra tập tin văn bản GAMER.OUT số nguyên tương ứng với level mà game thủ cần đạt.

Trương Phước Hải 43


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Ví dụ:
GAMER.INP GAMER.OUT
20 6

SƠN TƯỜNG
Tí đang trang trí bằng cách quét sơn lên một dãy gồm 𝑛 ô vuông nằm liên tiếp nhau như hình minh họa,
các ô được đánh số từ 1 đến 𝑛. Tí dự định sẽ sơn các ô vuông bằng 2 màu sơn đen và trắng như sau:
- Ban đầu tất cả ô vuông đều được sơn màu trắng.
- Tiếp theo những ô vuông có số thứ tự là ước của 𝑛 thì được sơn màu đen.
Tí nhận thấy rằng nếu làm như vậy, một số ô được quét 2 lần nước sơn, điều này gây lãng phí nên Tí cần
phải tính toán trước số lượng ô cần sơn từng màu cụ thể.

1 2 3 4 5 6 7 8
Yêu cầu: Cho số nguyên dương 𝑛. Hãy đếm số lượng ô được sơn trắng và số lượng ô được sơn đen.
Dữ liệu: Vào từ tập tin văn bản PAINTING.INP số nguyên 𝑛(1 ≤ 𝑛 ≤ 1014 ).
Kết quả: Ghi ra tập tin văn bản PAINTING.OUT 2 số nguyên 𝑥, 𝑦 tương ứng với số lượng ô cần được
sơn màu trắng và màu đen. Các số xuất trên một dòng và cách nhau 1 khoảng trắng.
Ví dụ:
PAINTING.INP PAINTING.OUT
8 4 4

MUA SỮA KHUYẾN MÃI


Công ty sữa đang có chương trình siêu khuyến mãi cho các khách hàng, khi mua 𝑛 hộp sữa sẽ được tặng
thêm 1 hộp. Mỗi hộp sữa có giá tiền 𝑎. Một khách hàng mang theo số tiền 𝐶 và muốn dùng toàn bộ để
mua sữa. Cho biết số hộp sữa nhiều nhất mà khách hàng có thể mua được.
Yêu cầu: Cho 3 số nguyên dương 𝑎, 𝑛, 𝐶. Tính số lượng hộp sữa mua được nhiều nhất.
Dữ liệu: Vào từ tập tin văn bản BUYMILK.INP chứa 3 số nguyên dương 𝑎, 𝑛, 𝐶(𝑎, 𝑛, 𝐶 ≤ 1014 ).
Kết quả: Ghi ra tập tin văn bản BUYMILK.OUT số hộp sữa mà khách hàng mua được.
Ví dụ:
BUYMILK.INP BUYMILK.OUT
5 10 100 22

DỊCH CHỮ SỐ
Một phép dịch phải số nguyên dương 𝑛 sẽ dịch chuyển các chữ số của 𝑛 sang phải một vị trí, chữ số hàng
đơn vị sẽ được chuyển lên hàng đầu tiên (hàng trái nhất). Chẳng hạn với 𝑛 = 2016 thì sau một phép dịch

Trương Phước Hải 44


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
phải ta nhận được giá trị 6201. Tiếp tục thực hiện thì ta lần lượt nhận được các giá trị tương ứng: 1620,
162, 216, 621. Trong đó 6201 là giá trị lớn nhất nhận được.
Yêu cầu: Cho số nguyên dương 𝑛. Tìm giá trị lớn nhất khi liên tục dịch phải 𝑛.
Dữ liệu: Vào từ tập tin văn bản RSHIFT.INP số nguyên dương 𝑛(𝑛 ≤ 1015 ).
Kết quả: Ghi ra tập tin văn bản RSHIFT.OUT giá trị lớn nhất nhận được khi thực hiện dịch phải số 𝑛.
Ví dụ:
RSHIFT.INP RSHIFT.OUT
2016 6201

SỐ HẠNH PHÚC
Số nguyên dương 𝑥 được gọi là hạnh phúc nếu 𝑥 có 2 × 𝑘 chữ số và tổng của 𝑘 chữ số đầu bằng tổng của
𝑘 chữ số cuối. Chẳng hạn số 135027 là số hạnh phúc vì 1 + 3 + 5 = 0 + 2 + 7.
Yêu cầu: Cho số nguyên dương 𝑛. Liệt kê tất cả số hạnh phúc không vượt quá 𝑛.
Dữ liệu: Vào từ tập tin văn bản HNUM.INP số nguyên dương 𝑛(𝑛 ≤ 107 ).
Kết quả: Ghi ra tập tin văn bản HNUM.OUT các số hạnh phúc có giá trị không vượt quá 𝑛, mỗi số trên
một dòng và theo thứ tự tăng dần. Nếu không tìm được số hạnh phúc nào thì xuất −1.
Ví dụ:
HNUM.INP HNUM.OUT
100 11
22
33
44
55
66
77
88
99

ĐẾM SỐ HÌNH VUÔNG


Cho một lưới hình chữ nhật kích thước 𝑛 × 𝑚 gồm các ô vuông đơn vị như hình. Ví dụ với lưới có kích
thước 𝑛 = 4 và 𝑚 = 3 như hình minh hoạ thì ta thấy có tổng cộng 20 hình vuông, trong đó có 12 hình
vuông kích thước 1 × 1, 6 hình vuông kích thước 2 × 2 và 2 hình vuông kích thước 3 × 3.

Yêu cầu: Cho 2 số nguyên dương 𝑛, 𝑚. Cho biết số hình vuông có trong lưới.
Dữ liệu: Vào từ tập tin văn bản CNTSQR.INP 2 số nguyên 𝑛, 𝑚(1 ≤ 𝑛, 𝑚 ≤ 106 ).

Trương Phước Hải 45


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Kết quả: Ghi ra tập tin văn bản CNTSQR.OUT tổng số hình vuông đếm được.
Ví dụ:
CNTSQR.INP CNTSQR.OUT
4 3 20

MÁY ATM
Hiện trong máy ATM chỉ còn các tờ bạc mệnh giá 5 đồng, 10 đồng và 20 đồng với số lượng tương ứng
𝑎, 𝑏, 𝑐 (1 ≤ 𝑎, 𝑏, 𝑐 ≤ 105 ) tờ. Một người cần rút một số tiền 𝑛 đồng (1 ≤ 𝑛 ≤ 105 ). Cho biết máy ATM
có bao nhiêu cách khác nhau để trả đủ 𝑛 đồng cho khách hàng bằng số tiền hiện có trong máy.
Yêu cầu: Cho 4 số nguyên dương 𝑎, 𝑏, 𝑐, 𝑛. Cho biết số cách trả tiền khác nhau.
Dữ liệu: Vào từ tập tin văn bản ATM.INP chứa 4 số nguyên dương 𝑎, 𝑏, 𝑐, 𝑛.
Kết quả: Ghi ra tập tin văn bản ATM.OUT số cách trả tiền tìm được.
Ví dụ:
ATM.INP ATM.OUT
5 3 2 50 6
Giải thích: có 6 cách trả tiền: (0,1,2), (0,3,1), (2,0,2), (2,2,1), (4,1,1), (4,3,0)

KHUYẾN MÃI
Công ty nước ngọt vừa đưa ra chương trình khuyến mãi vô cùng hấp dẫn cho các fan của loại nước uống
này. Nắp chai nước ngọt sau khi khui ra sẽ được dùng để tích lũy đổi chai mới cùng loại, với 𝑘 nắp chai
sẽ được đổi 1 chai nước ngọt cùng loại và nắp của chai này cũng có giá trị đổi tiếp tục. Một người sau
một thời gian đã tích lũy được 𝑛 nắp chai. Cho biết với số nắp chai hiện tại thì trong thời gian tới người
này có thể uống được bao nhiêu chai nước ngọt miễn phí.
Yêu cầu: Cho 2 số nguyên dương 𝑛, 𝑘. Tính số chai nước ngọt có thể nhận miễn phí.
Dữ liệu: Vào từ tập tin văn bản PROMOTION.INP chứa 2 số nguyên 𝑛, 𝑘(2 ≤ 𝑘 ≤ 109 ; 1 ≤ 𝑛 ≤ 109 ).
Kết quả: Ghi ra tập tin văn bản PROMOTION.OUT số lượng chai nước ngọt nhận miễn phí.
Ví dụ:
PROMOTION.INP PROMOTION.OUT
9 5 2
Giải thích: Với 9 nắp, người này lấy 5 nắp để đổi lấy 1 chai nước ngọt. Sau khi sử dụng xong chai vừa
đổi được thì nắp của chai này cộng với 4 nắp còn dư thì đổi được thêm 1 chai nữa.

TÍNH TIỀN ĐIỆN


Đơn giá điện được công ty điện lực quy định như sau:
- Cho kW từ 1 đến 50 có đơn giá 14đ/kW
- Cho kW từ 51 đến 100 có đơn giá 15đ/kW

Trương Phước Hải 46


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
- Cho kW từ 101 đến 200 có đơn giá 16đ/kW
- Cho kW từ 201 đến 300 có đơn giá 17đ/kW
- Cho kW từ 301 đến 400 có đơn giá 18đ/kW
- Cho kW từ 401 trở lên có đơn giá 20đ/kW
Yêu cầu: Cho số nguyên dương 𝑛 là số kW điện mà một gia đình đã tiêu thụ trong tháng. Hãy tính số tiền
điện mà chủ hộ phải trả cho công ty điện lực.
Dữ liệu: Vào từ tập tin văn bản BILL.INP số nguyên dương 𝑛(𝑛 ≤ 109 ).
Kết quả: Ghi ra tập tin văn bản BILL.OUT số tiền mà chủ hộ phải trả cho công ty điện lực.
Ví dụ:
BILL.INP BILL.OUT
70 1000

SỐ HỮU NGHỊ
Hai số tự nhiên khác nhau được gọi là hữu nghị nếu tổng các ước (không kể 1 và chính nó) của số này
bằng giá trị của số kia và ngược lại. Chẳng hạn 48 và 75 là cặp số hữu nghị vì 75 = 2 + 3 + 4 + 6 +
8 + 12 + 16 + 24 và 48 = 3 + 5 + 15 + 25.
Yêu cầu: Cho số nguyên dương 𝑛. Liệt kê tất cả cặp số hữu nghị không vượt quá 𝑛.
Dữ liệu: Vào từ tập tin văn bản FNUM.INP số nguyên dương 𝑛(48 ≤ 𝑛 ≤ 105 ).
Kết quả: Ghi ra tập tin văn bản FNUM.OUT tất cả cặp số hữu nghị dạng 𝑥, 𝑦(𝑥 < 𝑦 ≤ 𝑛). Mỗi cặp số
trên một dòng. Các cặp số được liệt kê tăng dần theo 𝑥. Nếu không tìm được cặp số hữu nghị nào thì xuất
−1.
Ví dụ:
FNUM.INP FNUM.OUT
100 48 75

Trương Phước Hải 47


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM

Chương 2. KIỂU MẢNG

2.1. Định nghĩa mảng


Mảng là một tập hữu hạn các phần tử có cùng kiểu dữ liệu và được lưu trữ liên tiếp trong bộ nhớ (RAM).
Mảng có thể được xem như là một tập các biến cùng kiểu dữ liệu với nhau. Mỗi phần tử của mảng được
xác định và truy xuất bởi chỉ số tương ứng trong mảng. Số phần tử của mảng được gọi là kích thước
mảng.
Mảng được sử dụng khi chương trình cần lưu trữ nhiều giá trị có cùng kiểu. Mảng giúp cho việc thao tác
trên một tập dữ liệu được tiện lợi hơn. Bên cạnh đó mảng còn được sử dụng để lưu trữ dữ liệu tra cứu
nhằm tăng hiệu quả xử lý của thuật toán.
Mảng được chia thành 2 loại: mảng một chiều và mảng nhiều chiều.

2.2. Mảng một chiều


Mảng một chiều được sử dụng khi chương trình cần lưu trữ một tập giá trị cùng kiểu có dạng dãy. Các
phần tử của mảng được đánh chỉ số bắt đầu từ 0, nghĩa là mảng có kích thước 𝑛 thì các phần tử được đánh
chỉ số từ 0 đến 𝑛 − 1.
Xét bảng dữ liệu về danh sách các lớp như sau:
STT Tên Lớp Phòng học Số HS
1 10A1 001 29
2 10A2 002 38
3 10A3 003 38
4 10A4 004 35
5 10A5 005 37
6 10B1 006 32
7 10B2 101 31
8 10B3 102 33
9 10B4 103 28
10 10B5 104 32

Cột dữ liệu về số lượng học sinh của từng lớp trong bảng trên tạo thành mảng một chiều được minh họa
như hình bên dưới, danh sách dữ liệu về số lượng học sinh được đặt tên Count.

Trương Phước Hải 48


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM

Tên mảng Phần tử mảng

Count 29 38 38 35 37 32 31 33 28 32
0 1 2 3 4 5 6 7 8 9

Giá trị phần tử Chỉ số phần tử

Mỗi ô trong dãy biểu diễn một phần tử của mảng. Phần tử thứ 0 có giá trị 29 cho biết số học sinh của lớp
thứ 0 là 29, tương tự phần tử thứ 1 có giá trị 38, …

2.2.1. Khai báo mảng


Cú pháp: <kiểu dữ liệu> <tên_mảng>[<kích thước mảng>];

Lưu ý: Mảng nên được khai báo toàn cục khi có kích thước lớn (cỡ 103 phần tử trở lên).

Ví dụ 2.2.1: Khai báo 2 mảng lưu giá trị 2 dãy số nguyên 𝑎0 , 𝑎1 , … , 𝑎𝑛−1 và 𝑏0 , 𝑏1 , … , 𝑏𝑚−1 với
(𝑛, 𝑚 ≤ 103 ).
int a[1000], b[1000];
Ví dụ 2.2.2: Khai báo mảng lưu trữ điểm trung bình học kì của các học sinh trong một lớp.
double GPA[100];
Ví dụ 2.2.3: Khai báo mảng số nguyên 𝑎 có kích thước 1000 và khởi tạo tất cả phần tử của mảng có giá
trị 0.
int a[1000] = {0};

Lưu ý: Khi khai báo và khởi tạo như sau int a[1000] = {10}; thì chỉ có phần tử a[0] có giá trị
10, các phần tử còn lại có giá trị 0.

2.2.2. Truy xuất phần tử mảng


Phần tử của mảng được truy xuất thông qua tên mảng và chỉ số của phần tử tương ứng.
Mỗi phần tử của mảng tương đương với một biến trong chương trình.
Cú pháp: <tên_mảng>[chỉ_số];

Trương Phước Hải 49


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Ví dụ 2.2.4:

a[1] = 10;

cout<<GPA[1];

++b[0];

cout<<b[0];

Lưu ý: chỉ số phần tử của mảng phải là một giá trị nguyên thuộc phạm vi từ 0 đến <size>-1, nghĩa
là 0  chỉ_số  <size>-1, với <size> là kích thước mảng.

2.2.3. Một số ví dụ minh họa


Ví dụ 2.2.5: Cho số nguyên 𝑛 và dãy số nguyên 𝑎0 , 𝑎1 , … , 𝑎𝑛−1 (𝑛 ≤ 106 ). Tính tổng các phần tử chẵn
của dãy.
Input: Dòng đầu chứa số nguyên 𝑛. Dòng tiếp theo chứa dãy 𝑎0 , 𝑎1 , … , 𝑎𝑛−1 .
Output: Một số nguyên là tổng tìm được.

#include <iostream>

using namespace std;


#define maxN 1000000 //định nghĩa hằng số

int a[maxN], n;

void ReadData()
{
cin>>n;
for (int i = 0; i < n; ++i)
cin>>a[i];
}

int Solve()
{
int s = 0;
for (int i = 0; i < n; ++i)
if (a[i] % 2 == 0)
s = s + a[i];
return s;
}

int main()
{

Trương Phước Hải 50


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
ReadData();
int res = Solve();
cout<<res;
return 0;
}

Lưu ý: Nên chia các công việc xử lý thành các hàm, mỗi hàm thực hiện một tác vụ độc lập của thuật
toán.

Ví dụ 2.2.6: Cho 2 số nguyên 𝑛, 𝑚 và 2 dãy số nguyên 𝑎0 , 𝑎1 , … , 𝑎𝑛−1 (𝑛 ≤ 106 ) và


𝑏0 , 𝑏1 , … , 𝑏𝑚−1 (𝑚 ≤ 106 ). Tính độ lệch giữa 2 phần tử lớn nhất của 2 dãy.
Input:
Dòng đầu chứa hai số nguyên 𝑛, 𝑚.
Dòng thứ hai chứa dãy 𝑎1 , 𝑎2 , … , 𝑎𝑛 .
Dòng thứ ba chứa dãy 𝑏1 , 𝑏2 , … , 𝑏𝑚 .
Output: Một số nguyên là kết quả tìm được.
#include <iostream>
#include <cmath>

using namespace std;


#define maxN 1000000

int a[maxN], b[maxN], n, m;

void ReadData()
{
cin>>n>>m;
for (int i = 0; i < n; ++i)
cin>>a[i];
for (int i = 0; i < m; ++i)
cin>>b[i];
}

int MaxVal(int a[], int n) //truyền mảng là tham số cho hàm


{
int res = a[0];
for (int i = 1; i < n; ++i)
if (res < a[i])
res = a[i];
return res;
}

int main()
{

Trương Phước Hải 51


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
ReadData();
cout<<abs(MaxVal(a, n) - MaxVal(b, m));
return 0;
}

Lưu ý
- Nếu sử dụng mảng là tham số cho hàm thì khi khai báo không cần chỉ định kích thước. Khi mảng
là tham số của hàm thì mảng có vai trò là tham biến.
- Sử dụng mảng là tham số cho hàm giúp hàm có tính tái sử dụng trong trường hợp thực hiện các
thao tác tương tự trên các mảng khác nhau.

2.3. Mảng hai chiều


Mảng 2 chiều (bảng) được sử dụng khi chương trình cần lưu trữ một tập dữ liệu có cùng kiểu và có dạng
bảng gồm các dòng và cột. Dòng và cột của mảng 2 chiều được đánh chỉ số là các số nguyên liên tiếp bắt
đầu từ 0 (zero-base index).
Mảng 2 chiều có thể được xem như là mảng 1 chiều mà mỗi phần tử là một mảng 1 chiều.
Hình ảnh minh họa mảng 2 chiều kích thước 𝑛 × 𝑚 (𝑛 dòng, 𝑚 cột).

0 1 … 𝑚−1

1 Phần tử
mảng

𝑛−1

Dòng được đánh số thứ tự liên tiếp từ trên xuống và bắt đầu từ 0, cột được đánh thứ tự liên tiếp từ trái
qua phải và bắt đầu từ 0.
Mỗi phần tử của bảng tương đương một biến trong chương trình và được xác định thông qua chỉ số dòng,
cột của nó trong bảng.

2.3.1. Khai báo mảng


Cú pháp:
<kiểu dữ liệu> <tên_mảng>[<kích thước dòng>][<kích thước cột>];
Ví dụ 2.3.1:

Trương Phước Hải 52


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Khai báo mảng 2 chiều kích thước 𝑛 × 𝑚(𝑛, 𝑚 ≤ 103 ) lưu trữ các số nguyên. Dòng được đánh thứ
tự từ 0 đến 𝑛 − 1, cột được đánh thứ tự từ 0 đến 𝑚 − 1.
#define maxN 1000
int a[maxN][maxN];

Lưu ý: Mảng nhiều chiều có cú pháp khai báo tổng quát như sau
<kiểu dữ liệu> <tên_mảng>[<size_1>][<size_2>]…[<size_k>];
Trong đó size_1, size_2, …, size_k là kích thước của 𝑘 chiều tương ứng.

2.3.2. Truy xuất phần tử mảng


Phần tử của mảng được truy xuất thông qua tên mảng cùng với cặp chỉ số dòng, cột của phần tử tương
ứng.
Cú pháp: <tên_mảng>[chỉ_số_dòng][chỉ_số_cột];

Ví dụ 2.3.2:

a[1][1] = 10;

cin>>a[i][j];

cout<<a[10][5];

2.3.3. Một số ví dụ minh họa


Ví dụ 2.3.3: Cho bảng số nguyên kích thước 𝑛 × 𝑚(𝑛, 𝑚 ≤ 103 ). Tìm vị trí của phần tử lớn nhất bảng.
Input: Dòng đầu chứa 2 số nguyên 𝑛, 𝑚. Dòng thứ 𝑖 trong 𝑛 dòng tiếp theo chứa dãy gồm 𝑚 số nguyên
𝑎𝑖1 , 𝑎𝑖2 , … , 𝑎𝑖𝑚 mô tả các phần tử trên dòng thứ 𝑖 của mảng.
Output: Hai số nguyên 𝑥, 𝑦 tương ứng với chỉ số dòng và chỉ số cột của phần tử lớn nhất bảng. Nếu
có nhiều kết quả thì tìm 𝑥, 𝑦 bất kỳ. Chỉ số của mảng được tính từ 1.
#include <iostream>

using namespace std;


#define maxN 1000

int a[maxN+1][maxN+1], n, m;
Tăng kích thước thêm 1 vì
void ReadData()
các chỉ số tính từ 1 trở đi

Trương Phước Hải 53


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
{
cin>>n>>m;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
cin>>a[i][j];
}

void Solve()
{
int x = 1, y = 1;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
if (a[i][j] > a[x][y])
x = i, y = j;
Câu lệnh kép
cout<<x<<" "<<y;
}

int main()
{
ReadData();
Solve();
return 0;
}

Ví dụ 2.3.4: Cho bảng vuông các số nguyên kích thước 𝑛 × 𝑛(𝑛 ≤ 103 ). Cho biết các phần tử của bảng
có đối xứng qua đường chéo chính hay không. Đường chéo chính của bảng là đường chéo bắt đầu từ ô
(1,1) đến ô (𝑛, 𝑛).
Input: Dòng đầu chứa số nguyên 𝑛. Mỗi dòng trong 𝑛 dòng tiếp theo chứa dãy gồm 𝑛 số nguyên mô
tả các phần tử trên bảng.
Output: Thông báo YES tương ứng với câu trả lời có, ngược lại thông báo NO.

#include <iostream>

using namespace std;


#define maxN 1000 Định nghĩa hằng số
maxN có giá trị 1000
int a[maxN][maxN], n, m;

void ReadData()
{
cin>>n;
for (int i = 0; i < n; ++i)
for (int j = 0; j < n; ++j)
cin>>a[i][j];
}

bool Solve()

Trương Phước Hải 54


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
{
for (int i = 0; i < n; ++i)
for (int j = 0; j < i; ++j)
if (a[i][j] != a[j][i])
return false;

return true;
}

int main()
{
ReadData();
if (Solve())
cout<<"YES";
else cout<<"NO";
return 0;
}

2.4. Mảng động với kiểu vector trong C++


Cách khai báo mảng như nội dung được trình bày ở trên được gọi là khai báo mảng tĩnh. Kích thước của
mảng phải được xác định cố định ngay khi khai báo. Trong quá trình sử dụng, kích thước mảng không
được thay đổi. Số phần tử của mảng không được vượt quá kích thước đã được chỉ định.
Ngôn ngữ C++ cung cấp kiểu dữ liệu vector<kiểu phần tử> thuộc thư viện <vector> có tính chất
và cách sử dụng tương tự mảng tĩnh nhưng cho phép thay đổi kích thước (khi thêm mới hoặc hủy phần
tử).
Ưu điểm của vector so với mảng tĩnh
- Không cần chỉ định kích thước khi khai báo.
- Không cần quản lý số phần tử.
- Không chứa các vùng nhớ dư thừa như mảng tĩnh nên tiết kiệm bộ nhớ và tiện lợi hơn trong quá trình
sử dụng.

2.4.1. Khai báo vector


Cú pháp
vector<kiểu phần tử> <tên_vector>([<kích thước>],[<giá trị khởi tạo>]);

Ví dụ 2.4.1: Khai báo vector 𝑣1 các số nguyên.


vector<int> v1;

Trương Phước Hải 55


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Ví dụ 2.4.2: Khai báo vector 𝑣2 các số nguyên có kích thước ban đầu là 100 và khởi tạo tất cả phần tử
của vector có giá trị 0.
vector<int> v2(100);

Ví dụ 2.4.3: Khai báo vector 𝑣3 các số nguyên có kích thước ban đầu là 100 và khởi tạo tất cả phần tử
của vector có giá trị -5.
vector<int> v3(100, -5);

Ví dụ 2.4.4: Khai báo vector 𝑣 các số nguyên long long có kích thước ban đầu là 105 và khởi tạo tất
cả phần tử của vector có giá trị 1.
vector<long long> v(100000, 1);

2.4.2. Một số phương thức hàm thông dụng trên vector

STT Têm hàm Diễn giải

1 size() Trả về kích thước hiện tại của vector

2 resize(<size>) Thay đổi kích thước của vector thành <size>

3 push_back(<v>) Thêm phần tử có giá trị <v> vào cuối vector

4 pop_back() Hủy phần tử cuối ra khỏi vector

5 clear() Hủy tất cả phần tử ra khỏi vector

6 begin() Trả về con trỏ đến địa chỉ phần tử đầu tiên của vector

7 end() Trả về con trỏ đến địa chỉ sau phần tử cuối của vector

begin() end()

2.4.3. Một số ví dụ minh họa


Ví dụ 2.4.5: Cho dãy số nguyên 𝑎0 , 𝑎1 , … , 𝑎𝑛−1 và số nguyên 𝑥. Đếm số phần tử trong dãy có giá trị 𝑥.
Dữ liệu: Nhập từ bàn phím gồm 2 dòng

Trương Phước Hải 56


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
- Dòng đầu chứa 2 số nguyên 𝑛, 𝑥(𝑛 ≤ 106 ; |𝑥| ≤ 106 ).
- Dòng tiếp theo chứa dãy 𝑎0 , 𝑎1 , … , 𝑎𝑛−1 (|𝑎𝑖 | ≤ 106 ).
Kết quả: Xuất ra màn hình số nguyên là số phần tử trong dãy có giá trị 𝑥.

#include <iostream>
using namespace std;
#define maxN 1000000 //định nghĩa hằng số maxN có giá trị 10^6

int a[maxN], n, x;

void ReadData()
{
cin>>n>>x;
for (int i = 0; i < n; ++i)
cin>>a[i];
}

int Solve()
{
int cnt = 0;
for (int i = 0; i < n; ++i)
if (a[i] == x)
++cnt;
return cnt;
}

int main()
{
ReadData();
cout<<Solve();
return 0;
}

Ví dụ 2.4.6: Cho dãy gồm 𝑛 số nguyên có giá trị tuyệt đối không vượt quá 1012 . Đếm số phần tử của dãy
là số Palindrome.
Dữ liệu: Nhập từ bàn phím gồm 2 dòng
- Dòng đầu tiên chứa số nguyên dương 𝑛(𝑛 ≤ 105 ).
- Dòng thứ hai chứa dãy 𝑎0 , 𝑎1 , … , 𝑎𝑛−1 (|𝑎𝑖 | ≤ 1012 ).
Kết quả: Xuất ra màn hình một số nguyên là số phần tử thỏa Palindrome.

#include <iostream>
#include <vector> //thư viện chứa kiểu vector
using namespace std;
typedef long long ll;

Trương Phước Hải 57


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
vector<ll> v;

void ReadData()
{
int n; ll x;
cin>>n;
for (int i = 0; i < n; ++i)
{
cin>>x;
v.push_back(x);
}
}

bool checkPALIND(ll n)
{
ll m1 = 0, m2 = n;
while (n > 0)
{
m1 = m1*10 + n%10;
n = n/10;
}
return (m1 == m2);
}

int Solve()
{
int cnt = 0;
for (int i = 0; i < v.size(); ++i)
if (checkPALIND(v[i])) ++cnt;
return cnt;
}

int main()
{
ReadData();
cout<<Solve();
return 0;
}

Ví dụ 2.4.7: Cho dãy gồm 𝑛(𝑛 ≤ 105 ) số nguyên có giá trị tuyệt đối không vượt quá 106 . Tìm số chính
phương lớn nhất và lớn nhì dãy.
Dữ liệu: Nhập từ bàn phím gồm 2 dòng
- Dòng đầu tiên chứa số nguyên dương 𝑛.
- Dòng thứ hai chứa dãy 𝑎0 , 𝑎1 , … , 𝑎𝑛−1 (|𝑎𝑖 | ≤ 106 ).
Kết quả: Ghi 2 số nguyên 𝑖, 𝑗(0 ≤ 𝑖, 𝑗 < 𝑛) tương ứng với vị trí của số chính phương lớn nhất và lớn
thứ nhì. Nếu không tìm được số chính phương ở vị trí nào thì ghi −1 ở vị trí tương ứng.

Trương Phước Hải 58


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM

#include <iostream>
#include <vector>
#include <cmath> //thư viện chứa hàm sqrt

using namespace std;

vector<int> a;

void ReadData()
{
int n;
cin>>n;
a.resize(n); //thay đổi kích thước của a từ 0 thành n
for (int i = 0; i < n; ++i)
cin>>a[i];
}

bool checkSQR(int n)
{
if (n < 0) return false;
int m = (int)sqrt(n); //ép kiểu: lấy phần nguyên của sqrt(n)
return (m*m == n);
}

//tìm vị trí p của số chính phương lớn nhất thỏa p != id


int MaxSQR(int id)
{
int p = -1;
for (int i = 0; i < a.size(); ++i)
{
if (i != id && checkSQR(a[i]))
if (p == -1 || a[i] > a[p])
p = i;
}
return p;
}

int main()
{
ReadData();
int p1 = MaxSQR(-1); //vị trí số chính phương lớn nhất
int p2 = MaxSQR(p1); //vị trí số chính phương lớn nhì
cout<<p1<<" "<<p2;
return 0;
}

Ví dụ 2.4.8: Cho dãy số nguyên 𝑎0 , 𝑎1 , … , 𝑎𝑛−1 . Tìm vị trí của phần tử dương chẵn lớn nhất dãy. Nếu dãy
không tồn tại số dương chẵn thì ghi −1.

Trương Phước Hải 59


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Dữ liệu: Vào từ tập tin văn bản input.txt gồm 2 dòng
- Dòng đầu chứa số nguyên dương 𝑛(𝑛 ≤ 106 ).
- Dòng tiếp theo chứa dãy 𝑎0 , 𝑎1 , … , 𝑎𝑛−1 (|𝑎𝑖 | ≤ 106 ).
Kết quả: Ghi ra tập tin văn bản output.txt vị trí của phần tử dương chẵn lớn nhất hoặc −1.

#include <iostream>
#include <vector>
#include <cstdio>

using namespace std;

vector<int> a;

void ReadData()
{
int n;
cin>>n;
a.resize(n);
for (int i = 0; i < n; ++i)
cin>>a[i];
}

int Solve()
{
int pos = -1;
for (int i = 0; i < a.size(); ++i)
{
if (a[i] > 0 && a[i] % 2 == 0)
if (pos == -1 || a[i] > a[pos])
pos = i;
}
return pos;
}

int main()
{
freopen("input.txt", "r", stdin); //mở file input.txt để đọc
freopen("output.txt", "w", stdout); //mở file output.txt để ghi
ReadData();
int p = Solve();
cout<<p;
return 0;
}

Ví dụ 2.4.9: Cho dãy gồm 𝑛(𝑛 ≤ 105 ) số nguyên có giá trị tuyệt đối không vượt quá 106 . Hãy tìm đường
chạy dài nhất của dãy.

Trương Phước Hải 60


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Dữ liệu: Nhập từ tập tin văn bản input.txt gồm 2 dòng
- Dòng đầu tiên chứa số nguyên dương 𝑛.
- Dòng thứ hai chứa dãy 𝑎0 , 𝑎1 , … , 𝑎𝑛−1 (|𝑎𝑖 | ≤ 106 ).
Kết quả: Ghi ra tập tin văn bản output.txt các phần tử thuộc đường chạy dài nhất. Nếu có nhiều kết
quả thì in đường chạy nằm trái nhất.

#include <iostream>
#include <vector>
#include <cstdio> //thư viện chứa hàm freopen

using namespace std;


#define maxN 100000

//mỗi vector lưu một đường chạy, khai báo mảng các vector
vector<int> a[maxN];
int m = 0; //m: số phần tử của mảng vector

void ReadData()
{
int n, x1 = (-1e6) - 10, x2; //1e6 = 10^6 = 1000000
cin>>n;
for (int i = 0; i < n; ++i)
{
cin>>x2;
if (x2 >= x1)
a[m].push_back(x2);
else a[++m].push_back(x2);
x1 = x2;
}
}

int MaxRuns()
{
int imax = 0;
for (int i = 1; i <= m; ++i)
{
if (a[i].size() > a[imax].size())
imax = i;
}
return imax;
}

void WriteANS(int id)


{
for (int i = 0; i < a[id].size(); ++i)
cout<<a[id][i]<<" ";
}

Trương Phước Hải 61


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
int main()
{
freopen("input.txt", "r", stdin);
freopen("output.txt", "w", stdout);
ReadData();
int id = MaxRuns();
WriteANS(id);
return 0;
}

Trương Phước Hải 62


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
BÀI TẬP CHƯƠNG 2
Lưu ý: các bài làm dưới đây được đặt tên theo định dạng MANGXX.cpp, trong đó XX là số hiệu bài làm.
Ví dụ: MANG01.cpp, MANG02.cpp, …

1) Cho dãy số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 và số nguyên 𝑥(|𝑥| ≤ 106 ). Hãy đếm số lượng phần tử của dãy là
ước của 𝑥.
Dữ liệu: Vào từ tập tin văn bản gồm 2 dòng
- Dòng chứa 2 số nguyên dương 𝑛, 𝑥(𝑛 ≤ 106 ; |𝑥| ≤ 106 ).
- Dòng tiếp theo chứa dãy 𝑎1 , 𝑎2 , … , 𝑎𝑛 (|𝑎𝑖 | ≤ 106 ).
Kết quả: Ghi ra tập tin văn bản số lượng phần tử của dãy là ước của 𝑥.
Ví dụ:
INPUT OUTPUT
7 20 4
5 3 -4 2 6 9 10

2) Cho dãy số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 . Tìm độ lệch nhỏ nhất giữa các phần tử nằm cạnh nhau.
Dữ liệu: Vào từ tập tin văn bản gồm 2 dòng
- Dòng đầu chứa số nguyên dương 𝑛(𝑛 ≤ 106 ).
- Dòng tiếp theo chứa dãy 𝑎1 , 𝑎2 , … , 𝑎𝑛 (|𝑎𝑖 | ≤ 106 ).
Kết quả: Ghi ra tập tin văn bản độ lệch nhỏ nhất tìm được.
Ví dụ:
INPUT OUTPUT
7 1
5 3 -4 2 6 9 10

3) Cho dãy số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 . Hãy đếm số lượng phần tử của dãy là số nguyên tố.
Dữ liệu: Vào từ tập tin văn bản gồm 2 dòng
- Dòng đầu chứa số nguyên dương 𝑛(𝑛 ≤ 104 ).
- Dòng tiếp theo chứa dãy 𝑎1 , 𝑎2 , … , 𝑎𝑛 (|𝑎𝑖 | ≤ 106 ).
Kết quả: Ghi ra tập tin văn bản một số nguyên là kết quả bài toán.
Ví dụ:
INPUT OUTPUT
7 3
5 3 -4 2 6 9 10
4) Cho dãy số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 . Tìm giá trị phân biệt nhỏ nhất và nhỏ thứ nhì dãy.

Trương Phước Hải 63


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Dữ liệu: Vào từ tập tin văn bản gồm 2 dòng
- Dòng đầu chứa số nguyên dương 𝑛(𝑛 ≤ 106 ).
- Dòng tiếp theo chứa dãy 𝑎1 , 𝑎2 , … , 𝑎𝑛 (1 ≤ 𝑎𝑖 ≤ 106 ).
Kết quả: Ghi ra tập tin văn bản giá trị phần tử nhỏ nhất và nhỏ nhì dãy trên cùng một dòng. Nếu không
tồn tại kết quả nào thì ghi −1 tại vị trí của kết quả tương ứng.
Ví dụ:
INPUT OUTPUT
7 2 5
5 2 2 7 10 8 2

5) Cho dãy số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 . Tìm vị trí của phần tử nhỏ nhất và nhỏ thứ nhì dãy (hoặc nhỏ nhất
đồng hạng) của dãy.
Dữ liệu: Vào từ tập tin văn bản gồm 2 dòng
- Dòng đầu chứa số nguyên dương 𝑛(𝑛 ≤ 106 ).
- Dòng tiếp theo chứa dãy 𝑎1 , 𝑎2 , … , 𝑎𝑛 (|𝑎𝑖 | ≤ 106 ).
Kết quả: Ghi ra tập tin văn bản vị trí của 2 phần tử cần tìm tương ứng. Nếu có nhiều kết quả thì ghi
các vị trí trái nhất.
Ví dụ:
INPUT OUTPUT
7 2 3
5 2 2 7 10 8 2

6) Cho dãy số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 . Tìm vị trí của số nguyên tố lớn nhất dãy. Nếu dãy không tồn tại
số nguyên tố thì ghi −1. Nếu có nhiều số nguyên tố lớn nhất thì in ra vị trí nhỏ nhất.
Dữ liệu: Vào từ tập tin văn bản gồm 2 dòng
- Dòng đầu chứa số nguyên dương 𝑛(𝑛 ≤ 104 ).
- Dòng tiếp theo chứa dãy 𝑎1 , 𝑎2 , … , 𝑎𝑛 (|𝑎𝑖 | ≤ 106 ).
Kết quả: Ghi ra tập tin văn bản vị trí của số nguyên tố lớn nhất dãy hoặc −1.
Ví dụ:
INPUT OUTPUT
7 4
5 2 2 7 10 8 2

7) Cho dãy số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 . Một đường chạy có độ dài 𝑘(𝑘 ≥ 1) là một dãy con dài nhất gồm
các phần tử liên tiếp thỏa 𝑎𝑖 ≤ 𝑎𝑖+1 ≤ ⋯ ≤ 𝑎𝑖+𝑘−1 . Hãy xuất ra tất cả đường chạy của dãy, mỗi đường
chạy trên một dòng.
Dữ liệu: Vào từ tập tin văn bản gồm 2 dòng

Trương Phước Hải 64


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
- Dòng đầu chứa số nguyên dương 𝑛(𝑛 ≤ 106 ).
- Dòng tiếp theo chứa dãy 𝑎1 , 𝑎2 , … , 𝑎𝑛 (|𝑎𝑖 | ≤ 106 ).
Kết quả: Ghi ra tập tin văn bản gồm nhiều dòng, mỗi dòng là một đường chạy tìm được.
Ví dụ:
INPUT OUTPUT
7 5
5 2 2 7 10 8 12 2 2 7 10
8 12

8) Cho dãy số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 . Một đường chạy có độ dài 𝑘(𝑘 ≥ 1) là một dãy con dài nhất gồm
các phần tử liên tiếp thỏa 𝑎𝑖 ≤ 𝑎𝑖+1 ≤ ⋯ ≤ 𝑎𝑖+𝑘−1 . Hãy tìm đường chạy dài nhất.
Dữ liệu: Vào từ tập tin văn bản gồm 2 dòng
- Dòng đầu chứa số nguyên dương 𝑛(𝑛 ≤ 106 ).
- Dòng tiếp theo chứa dãy 𝑎1 , 𝑎2 , … , 𝑎𝑛 (|𝑎𝑖 | ≤ 106 ).
Kết quả: Ghi ra tập tin văn bản một số nguyên là độ dài đường chạy dài nhất.
Ví dụ:
INPUT OUTPUT
7 4
5 2 2 7 10 8 12

9) Cho dãy số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 . Hãy tìm một dãy con dài nhất gồm các phần tử liên tiếp là số chính
phương. Số chính phương là số nguyên dương bằng bình phương của một số nguyên dương.
Dữ liệu: Vào từ tập tin văn bản gồm 2 dòng
- Dòng đầu chứa số nguyên dương 𝑛(𝑛 ≤ 106 ).
- Dòng tiếp theo chứa dãy 𝑎1 , 𝑎2 , … , 𝑎𝑛 (|𝑎𝑖 | ≤ 106 ).
Kết quả: Ghi ra tập tin văn bản một số nguyên là độ dài của dãy con tìm được.
Ví dụ:
INPUT OUTPUT
7 3
5 2 25 9 1 28 16

10) Cho dãy số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 . Một phép dịch trái sẽ dời phần tử nằm ở vị trí 𝑖 sang vị trí 𝑖 −
1(2 ≤ 𝑖 ≤ 𝑛) và phần tử nằm ở vị trí đầu (vị trí 1) sẽ được dời đến cuối dãy (vị trí 𝑛). Ví dụ dãy 1, 2, 3, 4, 5
sau 1 phép dịch trái sẽ trở thành 2, 3, 4, 5, 1; nếu dịch trái 1 lần nữa sẽ trở thành 3, 4, 5, 1, 2. Hãy cho biết
dãy kết quả sau khi thực hiện 𝑚 phép dịch trái đối với dãy 𝑎1 , 𝑎2 , … , 𝑎𝑛 .
Yêu cầu: Cho dãy số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 và số nguyên 𝑚. Hãy cho biết trạng thái của dãy khi thực
hiện 𝑚 phép dịch trái.

Trương Phước Hải 65


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Dữ liệu: Vào từ tập tin văn bản gồm 2 dòng
- Dòng đầu tiên chứa 2 số nguyên 𝑛, 𝑚(1 ≤ 𝑛 ≤ 106 ; 0 ≤ 𝑚 ≤ 109 ).
- Dòng thứ hai chứa dãy gồm 𝑛 số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 (|𝑎𝑖 | ≤ 106 )
Kết quả: Ghi ra tập tin văn bản dãy kết quả trên một dòng.
Ví dụ:
INPUT OUTPUT
7 5 28 16 5 2 25 9 1
5 2 25 9 1 28 16

11) Cho dãy số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 . Hãy chỉ ra một dãy con dài nhất gồm các phần tử nằm liên tiếp là
dãy đối xứng.
Dữ liệu: Vào từ tập tin văn bản gồm 2 dòng
- Dòng đầu chứa số nguyên dương 𝑛(𝑛 ≤ 100).
- Dòng tiếp theo chứa dãy 𝑎1 , 𝑎2 , … , 𝑎𝑛 (|𝑎𝑖 | ≤ 106 ).
Kết quả: Ghi ra tập tin văn bản 1 số nguyên là chiều dài của dãy con thỏa điều kiện.
Ví dụ:
INPUT OUTPUT
7 3
5 2 25 2 28 28 16

Trương Phước Hải 66


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM

Chương 3. ĐÁNH GIÁ THUẬT TOÁN

3.1. Lý do cần đánh giá thuật toán


Thuật toán cần được đánh giá để xác định tính khả thi của nó đối với độ lớn của dữ liệu
- Thời gian thực thi có chấp nhận được với dữ liệu cụ thể.
- Dung lượng bộ nhớ sử dụng có phù hợp.
- So sánh tính hiệu quả của các thuật toán khác nhau khi cùng giải quyết một bài toán.
- Làm cơ sở để cải tiến thuật toán thực thi hiệu quả hơn.

Các tiêu chí đánh giá thuật toán


- Tính đúng đắn: thuật toán phải luôn cho kết quả đúng.
- Tính đơn giản: có thể cài đặt được.
- Dung lượng bộ nhớ tiêu thụ: kích thước bộ nhớ mà thuật toán cần tiêu thục.
- Thời gian thực thi: thời gian mà máy tính xử lý hoàn toàn dữ liệu input để tìm ra output.

Thời gian thực thi là yếu tố quan trọng giúp đánh giá tính hiệu quả một thuật toán. Thời gian thực thi của
thuật toán phụ thuộc vào các yếu tố sau:
- Độ lớn dữ liệu (hay còn gọi là kích thước bài toán)
Ví dụ 3.1.1: Tìm kiếm phần tử có giá trị 𝑥 trong dãy số có 𝑛 phần tử. Với 𝑛 = 10 thì thời gian thực
thi sẽ nhanh hơn với 𝑛 = 106 . Vậy kích thước của bài toán là 𝑛.
Ví dụ 3.1.2: Tính giá trị của 𝑎𝑛 . Khi 𝑛 càng lớn thì thời gian thực hiện phép tính càng lớn. Vậy kích
thước của bài toán là 𝑛.
- Trạng thái dữ liệu
Ví dụ 3.1.3: Nếu ta cần sắp xếp tăng dần một dãy số có 𝑛 phần tử mà dãy này đã có thứ tự tăng dần
rồi thì thời gian thực hiện sẽ nhanh hơn so với sắp xếp dãy chưa có thứ tự.
- Tốc độ máy tính
- Ngôn ngữ lập trình
- Kỹ năng cài đặt
Các yếu tố trên ảnh hưởng một cách chủ quan đến tính hiệu quả của thuật toán. Do đó cần chọn một yếu
tố mang tính khách quan hơn đó là số thao tác sơ cấp mà thuật toán thực hiện. Các thao tác sơ cấp bao
gồm:
- Các phép toán số học
- Phép gán

Trương Phước Hải 67


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
- Phép so sánh
- Phép toán logic
- Thao tác nhập/xuất luồng chuẩn.

3.2. Độ phức tạp của thuật toán


Gọi 𝑇(𝑛) là hàm đo số thao tác sơ cấp của thuật toán khi giải quyết bài toán có kích thước 𝑛. Độ phức
tạp của thuật toán được đánh giá dựa trên tốc độ tăng của hàm 𝑇(𝑛) khi 𝑛 tăng. Kí hiệu 𝑇(𝑛) = 𝑂(𝑓(𝑛)),
trong đó 𝑓(𝑛) là tốc độ tăng của hàm 𝑇(𝑛) khi tăng giá trị 𝑛.
Ví dụ 3.2.1: Xét 2 hàm 𝑇1 (𝑛) = 100 × 𝑛 và 𝑇2 (𝑛) = 𝑛2 . Khi 𝑛 đủ lớn (𝑛 > 100) thì 𝑇1 (𝑛) < 𝑇2 (𝑛). Ta
thấy tốc độ tăng của 𝑇1 (𝑛) là 𝑛, tốc độ tăng của 𝑇2 (𝑛) là 𝑛2 . Vậy độ phức tạp của 𝑇1 (𝑛) = 𝑂(𝑛) và
𝑇2 (𝑛) = 𝑂(𝑛2 )
Xét thuật toán tổng quát giải bài toán có kích thước 𝑛 và hàm đo số thao tác sơ cấp của thuật toán là 𝑇(𝑛).
Ta có 𝑓(𝑛) được gọi là độ phức tạp của thuật toán 𝑇(𝑛) nếu tồn tại các hằng số dương 𝑐 và 𝑛0 sao cho
𝑇(𝑛) ≤ 𝑐 × 𝑓(𝑛), ∀𝑛 ≥ 𝑛0
Ví dụ 3.2.2: Xét thuật toán có 𝑇(𝑛) = (𝑛 + 1)2 . Ta chọn 𝑐 = 4 và 𝑛0 = 1 thì 𝑇(𝑛) ≤ 𝑐 × 𝑛2 , ∀𝑛 ≥ 𝑛0 .
Vậy độ phức tạp của thuật toán là 𝑂(𝑛2 ), hay ta nói số thao tác sơ cấp của thuật toán tăng theo 𝑛2 khi 𝑛
tăng.
Các trường hợp đánh giá thuật toán: Xấu nhất (trường hợp thuật toán thực thi nhiều thao tác sơ cấp nhất);
Tốt nhất (trường hợp thuật toán thực thi ít thao tác sơ cấp nhất); Trung bình (số thao tác sơ cấp tính trung
bình). Độ phức tạp của thuật toán được đánh giá trong trường hợp xấu nhất.
Các thuật toán có độ phức tạp 𝑂(𝑛3 ) trở xuống có thể cài đặt thực thi trên máy tính. Những thuật toán có
độ phức tạp cỡ 𝑂(2𝑛 ), 𝑂(𝑛!), 𝑂(𝑛𝑛 ) được gọi là thuật toán có độ phức tạp đa thức, những thuật toán này
rất khó để cài đặt thực thi trên máy tính.
Các hàm đánh giá độ phức tạp thông dụng theo kích thước bài toán 𝑛 (sắp theo thứ tự tăng dần về độ phức
tạp)

STT Hàm đánh giá Dạng hàm Tính khả thi

1 𝑐 Hàm hằng Khả thi

2 𝑙𝑜𝑔2 𝑛 Hàm logarit Khả thi

3 𝑛 Hàm tuyến tính Khả thi

4 𝑛 ∗ 𝑙𝑜𝑔2 𝑛 Khả thi

5 𝑛2 Hàm bậc 2 Khả thi

6 𝑛3 Hàm bậc 3 Khả thi

7 2𝑛 < 𝑛! < 𝑛𝑛 Đa thức Khó khả thi

Trương Phước Hải 68


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Phép toán logarit
Khái niệm: logarit (viết tắt là log) là phép toán nghịch đảo của phép tính lũy thừa. Cho 3 số thực dương
𝑎, 𝑀 và 𝑛 ≠ 1, ta có
𝑎𝑛 = 𝑀 ⟺ 𝑛 = log 𝑎 𝑀
log 𝑎 𝑀 đọc là logarit cơ số 𝑎 của 𝑀.
Bảng khảo sát tốc độ tăng của hàm logarit cơ số 2 của 𝑛 khi tăng giá trị 𝑛

STT 𝒏 𝐥𝐨𝐠 𝟐 𝒏

1 10 3.322

2 103 9.966

3 106 19.931

4 109 29.897

5 1018 59.795

Một số quy tắc tính logarit


Cho 3 số thực dương 𝑎, 𝑏, 𝑐 và 𝑎 ≠ 1, ta có
- log 𝑎 (𝑏 × 𝑐) = log 𝑎 𝑏 + log 𝑎 𝑐
- log 𝑎 (𝑏/𝑐) = log 𝑎 𝑏 − log 𝑎 𝑐
- log 𝑎 𝑏 𝑐 = 𝑐 × log 𝑎 𝑏
- log 𝑎 𝑏 × log 𝑏 𝑐 = log 𝑎 𝑐

3.3. Một số quy tắc xác định độ phức tạp


3.3.1. Quy tắc bỏ hệ số
𝑇(𝑛) = 𝑂(𝑐 × 𝑓(𝑛)) = 𝑂(𝑓(𝑛)), với 𝑐 là một hằng số.
Ví dụ: 𝑇(𝑛) = 10 × 𝑛2 = 𝑂(𝑛2 )

Ví dụ 3.3.1: Xét đoạn chương trình sau

int S = 0, P = 1;
for (int i = 1; i <= n; ++i)
{
S = S + i;

Trương Phước Hải 69


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
P = P*i;
}

Đoạn chương trình trên lặp 𝑛 lần, mỗi lần lặp thực hiện đúng 2 thao tác sơ cấp nên độ phức tạp 𝑇(𝑛) =
𝑂(2 × 𝑛) = 𝑂(𝑛).

Ví dụ 3.3.2: Xét đoạn chương trình đếm số lượng cặp phần tử khác nhau có tổng đúng bằng 𝑥.

int cnt = 0;
for (int i = 0; i < n; ++i)
{
for (int j = i+1; j < n; ++j)
if (a[i] + a[j] == x)
++cnt;
}

Số lần kiểm tra điều kiện if (a[i] + a[j] == x) bằng số lần lặp của 2 vòng lặp
𝑛(𝑛 − 1)
1 + 2 + ⋯ + (𝑛 − 1) =
2
Số lần thực thi câu lệnh ++cnt tùy thuộc vào biểu thức điều kiện (a[i] + a[j] == x). Trường hợp
tốt nhất là biểu thức điều kiện luôn sai, số lần thực thi câu lệnh là 0. Trường hợp xấu nhất là câu lệnh luôn
thực thi và bằng đúng số lần kiểm tra điều kiện. Vậy độ phức tạp của đoạn chương trình là
𝑛(𝑛 − 1)
𝑂( ) = 𝑂(𝑛(𝑛 − 1)) = 𝑂(𝑛2 − 𝑛)
2

Lưu ý
- Đối với các hàm số logarit ta có: log 2 𝑛 = log 2 10 × log10 𝑛. Áp dụng quy tắc bỏ hệ số ta có:
𝑂(log 2 𝑛) = 𝑂(log 2 10 × log10 𝑛) = 𝑂(log10 𝑛). Do vậy khi biểu diễn độ phức tạp thuật toán là
một hàm số logarit, người ta thường bỏ đi cơ số và sử dụng kí hiệu độ phức tạp 𝑂(log 𝑛) để thay
cho 𝑂(log 2 𝑛) hoặc 𝑂(log10 𝑛).

3.3.2. Quy tắc tổng


Nếu thuật toán phải thực thi 2 công đoạn có hàm đo số thao tác sơ cấp tương ứng là 𝑇1 (𝑛) = 𝑂(𝑓(𝑛)) và
𝑇2 (𝑛) = 𝑂(𝑔(𝑛)) thì số thao tác sơ cấp của thuật toán là

𝑇(𝑛) = 𝑇1 (𝑛) + 𝑇2 (𝑛) = 𝑂(𝑓(𝑛) + 𝑔(𝑛))


Ví dụ: 𝑇1 (𝑛) = 10 × 𝑛2 , 𝑇2 (𝑛) = 3.5 × 𝑛 ⟹ 𝑇(𝑛) = 𝑇1 (𝑛) + 𝑇2 (𝑛) = 𝑂(𝑛2 + 𝑛).

Trương Phước Hải 70


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Ví dụ 3.3.3: Xét đoạn chương trình sau

int a[maxN][maxN], S[maxN] = {0};


//công đoạn 1:
//- nhập giá trị cho mảng 2 chiều a và
//- tính tổng các phần tử trên mỗi dòng
for (int i = 0; i < n; ++i)
{
for (int j = 0; j < n; ++j)
{
scanf("%d", &a[i][j]);
S[i] = S[i] + a[i][j];
}
}

//công đoạn 2: tìm tổng lớn nhất của tất cả dòng


int maxS = S[0];
for (int i = 0; i < n; ++i)
maxS = max(maxS, S[i]);

Công đoạn 1: 𝑇1 (𝑛) = 𝑂(𝑛 × (2𝑛)) = 𝑂(2 × 𝑛2 ) = 𝑂(𝑛2 )


Công đoạn 2: 𝑇2 (𝑛) = 𝑂(𝑛)
Độ phức tạp của cả thuật toán: 𝑂(𝑛2 + 𝑛)

3.3.3. Quy tắc lấy max


Nếu thuật toán phải thực hiện 1 trong 2 công đoạn hoặc thực hiện cả 2 công đoạn có hàm đo số thao tác
sơ cấp tương ứng là 𝑇1 (𝑛) = 𝑂(𝑓(𝑛)) và 𝑇2 (𝑛) = 𝑂(𝑔(𝑛)) thì số thao tác sơ cấp của thuật toán là

𝑇(𝑛) = 𝑂(𝑓(𝑛) + 𝑔(𝑛)) = 𝑂(max{𝑓(𝑛), 𝑔(𝑛)})


Ví dụ: 𝑇1 (𝑛) = 10 × 𝑛2 , 𝑇2 (𝑛) = 3.5 × 𝑛 ⟹ 𝑇(𝑛) = 𝑂(𝑛2 + 𝑛) = 𝑂(𝑛2 ).

Ví dụ 3.3.4: Xét đoạn chương trình sau


int cnt = 0;
if (t == 1)
{
for (int i = 1; i <= n; ++i)
if (a[i] == x) ++cnt;
}
else
{
for (int i = 1; i <= n; ++i)
for (int j = i+1; j <= n; ++j)
if (abs(a[i] + a[j]) <= x)

Trương Phước Hải 71


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
++cnt;
}
Trường hợp t == 1: 𝑇1 (𝑛) = 𝑂(𝑛).
Ngược lại: 𝑇2 (𝑛) = 𝑂(𝑛2 − 𝑛) = 𝑂(𝑛2 ).
Độ phức tạp của đoạn chương trình: 𝑇(𝑛) = 𝑂(n2 ).

3.3.4. Quy tắc nhân


Nếu thuật toán có độ phức tạp 𝑇2 (𝑛) = 𝑂(𝑔(𝑛)) được lồng bên trong thuật toán có số thao tác 𝑇1 (𝑛) =
𝑂(𝑓(𝑛)) mà mỗi thao tác được tính theo 𝑂(𝑔(𝑛)) thì độ phức tạp của thuật toán là 𝑂(𝑓(𝑛) × 𝑔(𝑛)).

Ví dụ 3.3.5: Cho dãy 𝑎1 , 𝑎2 , … , 𝑎𝑛 . Xét đoạn chương trình đếm số lượng phần tử của dãy là số nguyên tố.
bool checkPrime(int n)
{
if (n < 2) return false;
int m = (int)sqrt(n);
for (int i = 2; i <= m; ++i)
if (n % i == 0)
return false;
return true;
}

int countPrime(int n)
{
int cnt = 0;
for (int i = 1; i <= n; ++i)
if (checkPrime(a[i]))
++cnt;
return cnt;
}

Độ phức tạp của hàm checkPrime(int n): 𝑇2 (𝑛) = √|𝑛|.

Độ phức tạp của hàm countPrime: 𝑇(𝑛) = 𝑂(𝑛 × 𝑇2 (|𝑎𝑖 |)) = 𝑂(𝑛 × √|𝑎𝑖 |).

3.4. Độ phức tạp một số bài toán thông dụng


Bài toán đếm số ước dương
Cho số nguyên dương 𝑛. Đếm số ước dương của 𝑛.
Thuật toán 1:
- Gọi 𝑎 là ước dương của 𝑛, suy ra 1 ≤ 𝑎 ≤ 𝑛.

Trương Phước Hải 72


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Đoạn chương trình thực hiện
int countDivisors(int n)
{
int cnt = 0;
for (int a = 1; a <= n; ++a)
if (n % a == 0)
++cnt;
return cnt;
}
Số lần lặp của thuật toán là 𝑛. Do đó độ phức tạp của thuật toán 1: 𝑂(𝑛).

Thuật toán 2:
- Nhận xét: nếu 𝑎 là ước dương của 𝑛 thì luôn tồn tại 𝑏 cũng là ước dương của 𝑛 sao cho 𝑎 × 𝑏 = 𝑛.
- Giả sử 𝑎 ≤ 𝑏 ⟹ 𝑎2 ≤ 𝑎 × 𝑏 = 𝑛 ⟹ 1 ≤ 𝑎 ≤ √𝑛.
Đoạn chương trình thực hiện
int countDivisors(int n)
{
int m = (int)sqrt(n), cnt = 0;
for (int a = 1; a <= m; ++a)
if (n % a == 0)
cnt = cnt + 2; //tìm được 2 ước là a và b = n/a

if (m*m == n) –-cnt;

return cnt;
}
Độ phức tạp của thuật toán 2 là 𝑂(√𝑛).

Bài toán tìm ước chung lớn nhất


Cho 2 số nguyên dương 𝑎, 𝑏. Tìm ước chung lớn nhất của 𝑎, 𝑏.
Thuật toán 1:
𝑎⋮𝑐
𝑐 = 𝑈𝐶𝐿𝑁(𝑎, 𝑏) ⟹ { ⟹ 1 ≤ 𝑐 ≤ min(𝑎, 𝑏)
𝑏⋮𝑐
Đoạn chương trình thực hiện
int GCD(int a, int b)
{
for (int i = min(a,b); i >= 1; --i)
if (a % i == 0 && b % i == 0)
return i;
}

Trương Phước Hải 73


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Trường hợp xấu nhất của thuật toán trên là khi vòng lặp thực hiện đúng min(𝑎, 𝑏) lần, khi đó 𝑎, 𝑏 nguyên
tố cùng nhau. Độ phức tạp của thuật toán 1: 𝑂(min(𝑎, 𝑏)).
Thuật toán 2:
𝑎, 𝑎=𝑏
𝑈𝐶𝐿𝑁(𝑎, 𝑏) = {𝑈𝐶𝐿𝑁(𝑎 − 𝑏, 𝑏), 𝑎 > 𝑏
𝑈𝐶𝐿𝑁(𝑎, 𝑏 − 𝑎), 𝑎 < 𝑏
Đoạn chương trình thực hiện
int GCD(int a, int b)
{
while (a != b)
{
if (a > b) a = a – b;
else b = b – a;
}
return a;
}
Trường hợp xấu nhất của thuật toán trên là khi một trong hai số có giá trị 1, số còn lại lớn hơn 1. Khi đó
số lần lặp tối đa của vòng lặp là max(𝑎, 𝑏). Vậy độ phức tạp của thuật toán 2 là 𝑂(max(𝑎, 𝑏)).
Thuật toán 3: Thuật toán Euclide
𝑎, 𝑎=𝑏
𝑈𝐶𝐿𝑁(𝑎, 𝑏) = {𝑈𝐶𝐿𝑁(𝑏, 𝑎 mod 𝑏), 𝑎 > 𝑏
𝑈𝐶𝐿𝑁(𝑎, 𝑏 mod 𝑎), 𝑎 < 𝑏
Đoạn chương trình thực hiện
int GCD(int a, int b)
{
while (b > 0)
{
int c = a % b;
a = b;
b = c;
}
return a;
}
Ta có 𝑐 = 𝑎 mod 𝑏 ⟹ 0 ≤ 𝑐 < min (𝑏, 𝑎/2). Trường hợp xấu nhất của thuật toán là khi 𝑐 đạt lớn nhất,
khi đó 𝑏 cũng đạt lớn nhất là 𝑎/2. Nghĩa là giá trị của 𝑎 giảm một nửa sau mỗi lần lặp. Do đó độ phức
tạp của thuật toán 3 là 𝑂(log 𝑛), trong đó 𝑛 = max(𝑎, 𝑏).
Kết luận: thuật toán hiệu quả nhất là thuật toán 3 (thuật toán Euclide).

Bài toán tìm đoạn con có tổng lớn nhất


Cho dãy số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 . Tìm một đoạn con sao cho tổng các phần tử nằm trên đoạn đó đạt lớn
nhất.

Trương Phước Hải 74


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Thuật toán 1:
- Một đoạn con là một dãy gồm các phần tử có vị trí 𝑖, 𝑖 + 1, … , 𝑗(𝑖 ≤ 𝑗).
- Xét tất cả các đoạn con có thể có của dãy và tính tổng các phần tử trong đoạn và tìm giá trị lớn nhất.
Đoạn chương trình thực hiện
int SumMax(int n)
{
int smax = -INF;
for (int i = 1; i <= n; ++i)
for (int j = i; j <= n; ++j)
{
int s = 0;
for (int k = i; k <= j; ++k)
s = s + a[k];
smax = max(smax, s);
}

return smax;
}
Thuật toán sử dụng 3 vòng lặp lồng nhau nên có độ phức tạp 𝑂(𝑛3 ).

Thuật toán 2: cải tiến thuật toán 1


- Vì vòng lặp thứ 3 sẽ tính lại tổng các phần tử ở đầu trái (𝑖) khi mở rộng đầu phải (𝑗) nên có nhiều thao
tác dư thừa.

Đoạn chương trình cải tiến


int SumMax(int n)
{
int smax = -INF;
for (int i = 1; i <= n; ++i)
{
int s = 0;
for (int j = i; j <= n; ++j)
{
s = s + a[j];
smax = max(smax, s);
}
}
return smax;
}
Thuật toán cải tiến sử dụng 2 vòng lặp lồng nhau nên có độ phức tạp 𝑂(𝑛2 ).
Thuật toán 3:

Trương Phước Hải 75


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
- Nhận xét: Tính tổng các phần tử thuộc đoạn con đến vị trí 𝑖, cập nhật kỉ lục nếu đoạn con có tổng lớn
hơn kỉ lục. Trường hợp các phần tử đến vị trí 𝑖 có tổng là âm thì đoạn con mới sẽ bắt đầu từ vị trí
𝑖 + 1.

Đoạn chương trình thực hiện


int SumMax(int n)
{
int smax = -INF, s = 0;
for (int i = 1; i <= n; ++i)
{
s = s + a[i];
smax = max(smax, s);
if (s < 0) s = 0;
}
return smax;
}
Độ phức tạp của thuật toán 3 là 𝑂(𝑛).

Trương Phước Hải 76


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM

Chương 4. SẮP XẾP VÀ TÌM KIẾM


Sắp xếp và tìm kiếm là các bài toán kinh điển trong tin học. Đây là những bài toán không thể thiếu trong
các hệ thống thông tin quản lý. Việc sắp xếp giúp bố trí lại tập dữ liệu, góp phần giúp quá trình khai thác
và tìm kiếm dữ liệu được thực hiện hiệu quả hơn. Chẳng hạn các email trong hộp thư được sắp theo thứ
tự thời gian nhận giúp việc quản lý và tìm kiếm được thực hiện dễ dàng, tiện lợi. Các tập tin và thư mục
trong máy tính luôn được sắp xếp theo một thuộc tính nào đó: tên, kích thước, ngày tạo, ngày chỉnh sửa,
… Các từ trong một từ điển luôn được sắp theo thứ tự alphabet giúp tra cứu tiện lợi và nhanh chóng.

4.1. Bài toán sắp xếp


4.1.1. Phát biểu bài toán
Cho dãy số 𝑎1 , 𝑎2 , … , 𝑎𝑛 . Hãy sắp xếp để tạo thành dãy có thứ tự không giảm, nghĩa là các phần tử của
dãy thỏa điều kiện 𝑎1 ≤ 𝑎2 ≤ ⋯ ≤ 𝑎𝑛 .
Có rất nhiều phương pháp sắp xếp khác nhau. Mỗi phương pháp được áp dụng trong một số trường hợp
cụ thể. Ta xem xét 2 phương pháp sắp xếp phổ biến sau.

4.1.2. Phương pháp đổi chỗ trực tiếp (interchange sort)


Ý tưởng: dãy có thứ tự 𝑎1 ≤ 𝑎2 ≤ ⋯ ≤ 𝑎𝑛 suy ra ∀𝑖 < 𝑗, 𝑎𝑖 ≤ 𝑎𝑗 .
Thuật toán: Xét tất cả cặp phần tử 𝑎𝑖 , 𝑎𝑗 (𝑖 < 𝑗). Nếu 𝑎𝑖 ≥ 𝑎𝑗 thì đổi chỗ cặp phần tử này.
Đoạn chương trình thực hiện

void InterchangeSort(int n)
{
for (int i = 1; i < n; ++i)
for (int j = i+1; j <= n; ++j)
if (a[i] >= a[j]) swap(a[i], a[j]);
}

Đánh giá thuật toán:


- Số lần kiểm tra điều kiện của câu lệnh if (a[i] >= a[j]) là 𝑛(𝑛 − 1)/2. Số lần thực hiện phép
đổi chỗ phụ thuộc vào điều kiện.
- Trường hợp tốt nhất: điều kiện a[i] >= a[j] luôn luôn đúng, số phép đổi chỗ là 0.
- Trường hợp xấu nhất: điều kiện a[i] >= a[j] luôn luôn sai, số phép đổi chỗ là 𝑛(𝑛 − 1)/2.
- Độ phức tạp của thuật toán 𝑂(𝑛2 ).

Trương Phước Hải 77


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
4.1.3. Phương pháp sắp xếp nhanh (quick sort)
Ý tưởng: phân hoạch dãy cần sắp thành 2 đoạn con
- Đoạn trái: gồm các phần tử có giá trị nhỏ hơn chốt.
- Đoạn phải: gồm các phần tử có giá trị lớn hơn chốt.
- Chốt là giá trị của một phần tử trong dãy được sử dụng làm tiêu chí phân hoạch dãy.

<𝑥 =𝑥 >𝑥

Hình 4.1.3.1. minh họa 𝑥 là chốt


Để phân hoạch dãy, ta sử dụng 2 con trỏ 𝑙, 𝑟 xuất phát từ 2 đầu của dãy (dãy gồm các phần tử nằm từ vị
trí 𝑙𝑜 đến vị trí ℎ𝑖), mỗi con trỏ sẽ phát hiện phần tử nằm sai đoạn để đổi chỗ cặp phần tử này.
𝑙 𝑟


𝑙𝑜 ℎ𝑖
Hình 4.1.3.2. Sử dụng con trỏ 𝑙 xuất phát từ 𝑙𝑜 và con trỏ 𝑟 xuất phát từ ℎ𝑖
Đoạn chương trình phân hoạch dãy thành 2 đoạn con theo chốt 𝑥

while (l <= r)
{
while (a[l] < x) ++l;
while (a[r] > x) –-r;
if (a[l] <= a[r])
{
swap(a[l], a[r]);
++l, --r;
}
}

Sau đoạn lệnh trên, dãy được phân hoạch thành 2 đoạn con (như hình minh họa 4.1.3.3):
- Đoạn trái: gồm các phần tử từ vị trí 𝑙𝑜 đến vị trí 𝑟 và có giá trị < 𝑥
- Đoạn phải: gồm các phần tử từ vị trí 𝑙 đến vị trí ℎ𝑖 và có giá trị > 𝑥
𝑟 𝑙


𝑙𝑜 Hình 4.1.3.3. Dãy sau khi phân hoạch ℎ𝑖

Các phần tử trong mỗi đoạn con này chưa đảm bảo có thứ tự, vì vậy ta tiếp tục phân hoạch từng đoạn con
này cho đến khi không thể phân hoạch được nữa (đoạn chỉ có 1 phần tử).

Trương Phước Hải 78


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Để ý rằng, giá trị chốt ảnh hưởng đến tính cân bằng về số lượng phần tử của mỗi đoạn con sau thao tác
phân hoạch. Nếu chốt là giá trị trung vị của dãy sẽ phân hoạch dãy thành 2 đoạn con số phần tử chênh
lệch nhau ít nhất (giá trị trung vị của dãy là giá trị của phần tử ở vị trí giữa dãy có thứ tự), trong trường
hợp tối ưu thì dãy được phân hoạch thành 2 đoạn con có số phần tử bằng nhau. Nếu chốt có giá trị lớn
nhất hoặc nhỏ nhất dãy sẽ phân hoạch dãy thành 2 đoạn con có số phần tử chênh lệch nhau nhiều nhất.
Mặt khác, ta nhận thấy rằng dãy sẽ nhanh phân hoạch thành các đoạn con có độ dài 1 hơn nếu thao tác
phân hoạch chia dãy thành 2 đoạn con cân bằng nhau về số phần tử. Xét dãy có 𝑛 phần tử:
- Trường hợp chốt được chọn luôn là giá trị lớn nhất hoặc nhỏ nhất: sau mỗi lần phân hoạch, dãy được
chia thành 2 dãy con có độ dài 1 và 𝑛 − 1. Như vậy sau 𝑛 lần phân hoạch, dãy được chia thành các
dãy con có độ dài 1.
- Trường hợp chốt được chọn luôn là giá trị trung vị: sau mỗi lần phân hoạch, dãy được phân hoạch
thành 2 dãy con có độ dài cỡ 𝑛/2. Do đó ở lần phân hoạch thứ 𝑘, dãy được chia thành các đoạn con
có cùng số phần tử cỡ 𝑛/2𝑘 . Thao tác phân hoạch dừng khi 𝑛 = 2𝑘 ⟹ 𝑘 = log 2 𝑛.
Như vậy nếu chốt là giá trị trung vị thì thuật toán sẽ đạt hiệu quả tối ưu. Tuy nhiên, việc chọn chốt là giá
trị trung vị lại khá tốn kém. Do đó người ta thường chọn chốt là một giá trị nằm ngẫu nhiên từ vị trí 𝑙𝑜
đến vị trí ℎ𝑖, khi đó thuật toán sẽ đạt hiệu quả gần tối ưu.

Cài đặt hàm sắp xếp nhanh

void quicksort(int lo, int hi)


{
int l = lo, r = hi, x = a[l + rand() % (r-l+1)];
while (l <= r)
{
while (a[l] < x) ++l;
while (a[r] > x) –-r;
if (l <= r)
swap(a[l++], a[r--]);
}
if (lo < r) quicksort(lo, r);
if (l < hi) quicksort(l, hi);
}

Đánh giá thuật toán:


- Độ phức tạp của thao tác phân hoạch dãy 𝑛 phần tử thành 2 dãy con là 𝑂(𝑛).
- Trường hợp xấu nhất (chốt luôn có giá trị lớn nhất hoặc nhỏ nhất): thuật toán thực hiện 𝑛 bước phân
hoạch. Độ phức tạp của thuật toán 𝑂(𝑛2 ).
- Trường hợp tối ưu (chốt luôn là giá trị trung vị): thuật toán thực hiện log 2 𝑛 bước phân hoạch. Độ
phức tạp của thuật toán 𝑂(𝑛 × log 𝑛).
- Trường hợp trung bình (chốt được chọn là phần tử ngẫu nhiên trong dãy cần phân hoạch): độ phức
tạp trung bình cỡ 𝑂(𝑛 × log 𝑛).

Trương Phước Hải 79


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
4.1.4. Sắp xếp trên nhiều khóa
Bài toán sắp xếp như trình bày ở trên chỉ xét trên dãy số nguyên, nghĩa là mỗi phần tử chỉ có 1 thuộc tính.
Sắp xếp trên nhiều khóa là bài toán sắp xếp một danh sách mà mỗi phần tử của danh sách gồm có nhiều
thuộc tính.
Trong thực tế, sắp xếp trên nhiều khóa là bài toán rất thông dụng. Chẳng hạn như sắp xếp một danh sách
học sinh, mỗi học sinh gồm có 2 thuộc tính là họ lót và tên, danh sách được sắp tăng dần theo tên,
nếu cùng tên thì sắp tăng dần theo họ lót. Ta xét bài toán sau:
Cho danh sách gồm 𝑛 phần tử 𝑎1 , 𝑎2 , … , 𝑎𝑛 , trong đó phần tử 𝑎𝑖 có 2 thuộc tính nguyên (𝑥, 𝑦). Hãy sắp
xếp danh sách có thứ tự tăng dần theo thuộc tính 𝑥, nếu có cùng thuộc tính 𝑥 thì sắp tăng dần theo thuộc
tính 𝑦.
Ví dụ: xét danh sách gồm 10 phần tử sau
i 1 2 3 4 5 6 7 8 9 10
x 3 4 8 6 3 4 12 8 3 5
y 9 7 3 4 2 2 1 6 7 10

Danh sách sau khi được sắp xếp có thứ tự tăng dần theo 𝑥, cùng 𝑥 thì sắp tăng dần theo 𝑦:
i 1 2 3 4 5 6 7 8 9 10
x 3 3 3 4 4 5 6 8 8 12
y 2 7 9 2 7 10 4 3 6 1

Ta thấy rằng việc sắp thứ tự danh sách phụ thuộc vào thao tác so sánh 2 phần tử để xác định thứ tự của
chúng (phần tử nhỏ hơn đứng trước). Với cặp phần tử (𝑎𝑖 , 𝑎𝑗 ), (𝑎𝑖 < 𝑎𝑗 ) nếu thỏa điều kiện sau:

(𝑎𝑖 . 𝑥 < 𝑎𝑗 . 𝑥) hoặc (𝑎𝑖 . 𝑥 = 𝑎𝑗 . 𝑥 và 𝑎𝑖 . 𝑦 < 𝑎𝑗 . 𝑦)


Do đó thao tác sắp xếp danh sách 2 khóa được thực hiện tương tự như trên danh sách 1 khóa, chỉ khác
nhau ở thao tác so sánh 2 phần tử.
Để khai báo kiểu dữ liệu cho phần tử có 2 thuộc tính thì ta có thể sử dụng kiểu dữ liệu tự định nghĩa
struct như sau:

struct pii
{
int x, y;
} a[maxN];

bool compare(pii a, pii b)


{
return (a.x < b.x || (a.x == b.x && a.y < b.y));
}

Trương Phước Hải 80


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
hoặc kiểu pair như sau:
typedef pair<int, int> pii;
pii a[maxN];

bool compare(pii a, pii b)


{
return (a.first < b.first ||
(a.first == b.first && a.second < b.second));
}

Đoạn chương trình minh họa sắp xếp danh sách 2 khóa bằng phương pháp đổi chỗ trực tiếp:
void InterchangeSort(int n)
{
for (int i = 1; i < n; ++i)
for (int j = i+1; j <= n; ++j)
if (compare(a[j], a[i])) swap(a[i], a[j]);
}

Đoạn chương trình minh họa sắp xếp danh sách 2 khóa bằng phương pháp quick sort:
void quicksort(int lo, int hi)
{
int l = lo, r = hi;
pii x = a[l + rand() % (r-l+1)];
while (l <= r)
{
while (compare(a[l], x)) ++l;
while (compare(x, a[r])) –-r;
if (l <= r)
swap(a[l++], a[r--]);
}
if (lo < r) quicksort(lo, r);
if (l < hi) quicksort(l, hi);
}

4.2. Bài toán tìm kiếm


4.2.1. Phát biểu bài toán
Cho dãy số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 và số nguyên 𝑥. Hãy cho biết có tồn tại phần tử có giá trị 𝑥 trong dãy.
Nếu có hãy chỉ ra 1 vị trí bất kỳ. Ngược lại xuất kết quả là −1.
Có hai phương pháp tìm kiếm thông dụng trên dãy: tìm kiếm tuần tự và tìm kiếm nhị phân.

Trương Phước Hải 81


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
4.2.2. Tìm kiếm tuần tự
Phương pháp tìm kiếm tuần tự được áp dụng để tìm kiếm phần tử trong dãy không có thứ tự.
Ý tưởng: duyệt lần lượt từng phần tử của dãy cho đến khi tìm thấy giá trị 𝑥 hoặc duyệt hết dãy và không
tìm thấy.
Cài đặt hàm tìm tuần tự
int SeqSearch(int n, int x)
{
for (int i = 1; i <= n; ++i)
if (a[i] == x)
return i;
return -1;
}
Đánh giá thuật toán: 𝑂(𝑛)

4.2.3. Tìm kiếm nhị phân


Điều kiện áp dụng: không gian tìm kiếm cần phải có thứ tự (tăng dần hoặc giảm dần) theo tiêu chí tìm
kiếm.
Một số ví dụ ứng dụng tìm kiếm nhị phân
- Tìm số báo danh trong một danh sách có thứ tự tăng dần theo số báo danh.
- Tìm họ và tên của một thí sinh trong danh sách có thứ tự tăng dần theo tên và họ.
- Tra nghĩa của một từ trong một từ điển (các từ trong từ điển được sắp thứ tự alphabet – tăng dần theo
chữ cái).

Ý tưởng: dựa vào tính có thứ tự của không gian tìm kiếm để loại bỏ đi một phần không gian không tồn tại
phần tử cần tìm kiếm. Phần không gian tìm kiếm bị loại bỏ bằng một nửa so với không gian tìm kiếm ban
đầu.
Thuật toán: tìm vị trí của phần tử có giá trị 𝑥 trong dãy 𝑎1 ≤ 𝑎2 ≤ ⋯ ≤ 𝑎𝑛 . Tổng quát, ta cần tìm vị trí
của 𝑥 trong dãy 𝑎𝑙𝑜 ≤ 𝑎𝑙𝑜+1 ≤ ⋯ ≤ 𝑎ℎ𝑖
- Bước 1: Nếu dãy không có phần tử (𝑙𝑜 > ℎ𝑖) thì dừng thất bại.
- Bước 2: Đặt 𝑚 = (𝑙𝑜 + ℎ𝑖)/2.
- Bước 3: Kiểm tra 𝑥 = 𝑎𝑚 .
Nếu đúng thì dừng thành công.
Ngược lại đến Bước 4.
- Bước 4: Kiểm tra 𝑥 < 𝑎𝑚 .
Nếu đúng thì đoạn [𝑚, ℎ𝑖] không chứa 𝑥. Vùng tìm kiếm là [𝑙𝑜, 𝑚 − 1]. Quay về Bước 1.

Trương Phước Hải 82


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Ngược lại đoạn [𝑙𝑜, 𝑚] không chứa 𝑥. Vùng tìm kiếm là [𝑚 + 1, ℎ𝑖]. Quay về Bước 1.

Ví dụ: tìm phần tử 𝑥 = 5 trong dãy 𝑎 = [2, 3,5,8,9,10,13,16,20].


- Lần thứ 1: so sánh 𝑥 = 5 với phần tử giữa đoạn [1. .9] (vị trí 5).

2 3 5 8 9 10 13 16 20

1 2 3 4 5 6 7 8 9
+ Do 𝑥 < 𝑎[5] nên 𝑥 không thể xuất hiện trong đoạn [5. .9]. Vùng tìm kiếm là [1. .4].

2 3 5 8 9 10 13 16 20

1 2 3 4 5 6 7 8 9

- Lần thứ 2: so sánh 𝑥 = 5 với phần tử giữa đoạn [1. .4] (vị trí 2).

2 3 5 8 9 10 13 16 20

1 2 3 4 5 6 7 8 9

+ Do 𝑥 > 𝑎[2] nên 𝑥 không thể xuất hiện trong đoạn [1. .2]. Vùng tìm kiếm là [3. .4].

2 3 5 8 9 10 13 16 20

1 2 3 4 5 6 7 8 9

- Lần thứ 3: so sách 𝑥 = 5 với phần tử giữa đoạn [3. .4] (vị trí 3).

2 3 5 8 9 10 13 16 20

1 2 3 4 5 6 7 8 9

+ Vì 𝑥 = 𝑎[3] nên thuật toán dừng thành công.


Cài đặt hàm tìm kiếm nhị phân
int BinSearch(int n, int x)
{
int lo = 1, hi = n;
while (lo <= hi)
{
int m = (lo + hi)/2;
if (x == a[m]) return m;
if (x < a[m]) hi = m-1;
else lo = m+1;
}
return -1;

Trương Phước Hải 83


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
}
- Thuật toán có độ phức tạp: 𝑂(log 𝑛).

Phương pháp tìm kiếm nhị phân còn được áp dụng để tìm giá trị lớn nhất hoặc nhỏ nhất trong đoạn giá
trị [𝑙𝑜. . ℎ𝑖] thỏa điều kiện nào đó. Một cách hình thức, bài toán có thể được mô hình hóa như sau: tìm giá
trị 𝑥 lớn nhất hoặc nhỏ nhất và 𝑥 ∈ [𝑙𝑜. . ℎ𝑖] thỏa hàm số 𝑓(𝑥). Điều kiện để có thể vận dụng phương pháp
tìm kiếm nhị phân (hay còn gọi là chặt nhị phân) cho bài toán này đó là ∀𝑥 < 𝑦: 𝑓(𝑥) ≤ 𝑓(𝑦) hoặc ∀𝑥 <
𝑦: 𝑓(𝑥) ≥ 𝑓(𝑦), nói cách khác hàm 𝑓(𝑥) là hàm đồng biến hoặc nghịch biến trên miền giá trị của 𝑥.

Ứng dụng: Cho số nguyên dương 𝑀(𝑀 ≤ 1018 ). Tìm số nguyên dương 𝑛 nhỏ nhất thỏa điều kiện
1 + 2 + ⋯ + 𝑛 ≥ 𝑀(∗)
Lời giải:
Bản chất của bài toán là tìm kiếm giá trị 𝑛 nhỏ nhất thỏa điều kiện bất phương trình (∗).

Cách 1: Tìm kiếm tuần tự


- Xét 𝑛 bắt đầu từ 1 trở đi và kiểm tra tổng nếu thỏa ≥ 𝑀 thì dừng thuật toán.

Đoạn chương trình minh họa


S = 0, n = 0;
while (S < M)
{
++n;
S = S + n;
}
cout<<n;
Phân tích độ phức tạp của thuật toán:
𝑛(𝑛 + 1)
𝑆 = 1 + 2 + ⋯+ 𝑛 = ≥ 𝑀 ⟹ 𝑛2 + 𝑛 ≥ 2 × 𝑀
2
Số lần lặp của thuật toán cỡ ⌈(−1 + √1 + 8 × 𝑀)/2 ⌉. Vậy thuật toán có độ phức tạp 𝑂(√8 × 𝑀).

Cách 2:
- Gọi 𝑆(𝑛) = 1 + 2 + ⋯ + 𝑛. Ta có ∀𝑛1 < 𝑛2 : 𝑆(𝑛1 ) < 𝑆(𝑛2 ). Vậy ta có thể áp dụng kỹ thuật chặt nhị
phân giá trị 𝑛 trong đoạn [1. . √8 × 𝑀].

Trương Phước Hải 84


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Đoạn chương trình minh họa
typedef long long ll;
ll Sum(ll n)
{
return (n*(n+1)/2);
}

ll Solve(ll M)
{
ll lo = 1, hi = sqrt(8*M), n;
while (lo <= hi)
{
ll x = (lo + hi)/2;
if (Sum(x) >= M)
{
hi = x-1;
n = x; //n là nghiệm ứng viên
}
else lo = x+1;
}
return n;
}
Thuật toán chặt nhị phân giá trị 𝑛 trong đoạn từ 1 đến √8 × 𝑀 nên có độ phức tạp 𝑂(log √8 × 𝑀).

Trương Phước Hải 85


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
BÀI TẬP CHƯƠNG 4
1) Cho dãy số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 và số nguyên 𝑥. Hãy chỉ ra 1 cặp phần tử khác nhau có tổng đúng
bằng 𝑥.
Dữ liệu: Vào từ tập tin văn bản CHAP0401.INP gồm 2 dòng
- Dòng đầu chứa 2 số nguyên dương 𝑛, 𝑥(𝑛 ≤ 106 ; |𝑥| ≤ 106 ).
- Dòng tiếp theo chứa dãy 𝑎1 , 𝑎2 , … , 𝑎𝑛 (|𝑎𝑖 | ≤ 106 ).
Kết quả: Ghi ra tập tin văn bản CHAP0401.OUT 2 số nguyên 𝑖, 𝑗(𝑖 < 𝑗) thỏa 𝑎𝑖 + 𝑎𝑗 = 𝑥. Nếu có
nhiều kết quả thì in cặp 𝑖, 𝑗 sao cho 𝑖 + 𝑗 nhỏ nhất, nếu có nhiều cặp có 𝑖 + 𝑗 cùng giá trị thì chọn cặp
có 𝑖 nhỏ nhất. Nếu không tìm được cặp phần tử nào thỏa thì ghi NO SOLUTION.
Ví dụ:
CHAP0401.INP CHAP0401.OUT
10 9 1 3
7 3 2 4 6 5 13 1 20 3

2) Cho dãy số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 và số nguyên 𝑥. Hãy đếm số cặp phần tử 𝑖, 𝑗 (1 ≤ 𝑖 < 𝑗 ≤ 𝑛) sao
cho 𝑎𝑖 + 𝑎𝑗 ≤ 𝑥.
Dữ liệu: Vào từ tập tin văn bản CHAP0402.INP gồm 2 dòng
- Dòng đầu chứa 2 số nguyên dương 𝑛, 𝑥(𝑛 ≤ 106 ; |𝑥| ≤ 2 × 106 ).
- Dòng tiếp theo chứa dãy 𝑎1 , 𝑎2 , … , 𝑎𝑛 (|𝑎𝑖 | ≤ 106 ).
Kết quả: Ghi ra tập tin văn bản CHAP0402.OUT một số nguyên là số cặp (𝑖, 𝑗) sao cho 𝑎𝑖 + 𝑎𝑗 ≤ 𝑥.
Ví dụ:
CHAP0402.INP CHAP0402.OUT
6 9 8
4 6 13 5 1 3

3) Cho 2 tập họp được biểu diễn bằng dãy các số nguyên có giá trị phân biệt nhau đôi một 𝑎1 , 𝑎2 , … , 𝑎𝑛
và 𝑏1 , 𝑏2 , … , 𝑏𝑚 . Hãy tìm phần hội, giao và hiệu của tập {𝑎𝑖 } với tập {𝑏𝑗 }.
Dữ liệu: Vào từ tập tin văn bản CHAP0403.INP gồm 3 dòng
- Dòng đầu tiên chứa 2 số nguyên dương 𝑛, 𝑚(1 ≤ 𝑛, 𝑚 ≤ 105 ).
- Dòng thứ hai chứa dãy 𝑎1 , 𝑎2 , … , 𝑎𝑛 (|𝑎𝑖 | ≤ 105 ).
- Dòng thứ ba chứa dãy 𝑏1 , 𝑏2 , … , 𝑏𝑛 (|𝑏𝑗 | ≤ 105 ).
Kết quả: Ghi ra tập tin văn bản CHAP0403.OUT trên 3 dòng
- Dòng thứ nhất ghi kết quả của phép hội.
- Dòng thứ hai ghi kết quả của phép giao.

Trương Phước Hải 86


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
- Dòng thứ ba ghi kết quả của phép hiệu.
- Các giá trị được liệt kê theo thứ tự tăng dần.
Ví dụ:
CHAP0403.INP CHAP0403.OUT
6 5 1 2 3 4 6 7 8 10
8 2 4 7 1 10 2 4 8
3 4 2 8 6 1 7 10

4) Cho 2 dãy số nguyên {𝑎𝑖 } và {𝑏𝑗 }. Hãy trộn các phần tử của 2 dãy để tạo thành một dãy mới có thứ
tự tăng dần.
Dữ liệu: Vào từ tập tin văn bản CHAP0404.INP gồm 3 dòng
- Dòng đầu tiên chứa 2 số nguyên 𝑛, 𝑚(1 ≤ 𝑛, 𝑚 ≤ 105 ).
- Dòng thứ hai chứa dãy 𝑎1 , 𝑎2 , … , 𝑎𝑛 (|𝑎𝑖 | ≤ 106 ).
- Dòng thứ ba chứa dãy 𝑏1 , 𝑏2 , … , 𝑏𝑚 (|𝑏𝑗 | ≤ 106 ).
Kết quả: Ghi ra tập tin văn bản CHAP0404.OUT gồm 𝑛 + 𝑚 số nguyên trên một dòng là dãy sau
khi ghép.
Ví dụ:
CHAP0404.INP CHAP0404.OUT
6 5 1 2 3 4 6 7 8 10
8 2 4 7 1 10 2 4 8
3 4 2 8 6 1 7 10

5) Cho 2 dãy số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 và 𝑏1 , 𝑏2 , … , 𝑏𝑚 . Tìm các giá trị xuất hiện ở cả 2 dãy.
Dữ liệu: Vào từ tập tin văn bản CHAP0405.INP gồm 3 dòng
- Dòng đầu tiên chứa 2 số nguyên 𝑛, 𝑚(1 ≤ 𝑛, 𝑚 ≤ 105 ).
- Dòng thứ hai chứa dãy 𝑎1 , 𝑎2 , … , 𝑎𝑛 (|𝑎𝑖 | ≤ 109 ).
- Dòng thứ ba chứa dãy 𝑏1 , 𝑏2 , … , 𝑏𝑚 (|𝑏𝑗 | ≤ 109 ).
Kết quả: Ghi ra tập tin văn bản CHAP0405.OUT trên một dòng gồm các giá trị chung của 2 dãy. Các
giá trị được ghi theo thứ tự tăng dần.
Ví dụ:
CHAP0405.INP CHAP0405.OUT
6 5 2 4 8
8 2 4 7 1 10
3 4 2 8 6

6) Cho 2 dãy số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 và 𝑏1 , 𝑏2 , … , 𝑏𝑚 . Đếm số cặp phần tử 𝑎𝑖 , 𝑏𝑗 (1 ≤ 𝑖 ≤ 𝑛; 1 ≤ 𝑗 ≤


𝑚) sao cho 𝑎𝑖 < 𝑏𝑗 .

Trương Phước Hải 87


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Dữ liệu: Vào từ tập tin văn bản CHAP0406.INP gồm 3 dòng
- Dòng đầu tiên chứa 2 số nguyên 𝑛, 𝑚(1 ≤ 𝑛, 𝑚 ≤ 105 ).
- Dòng thứ hai chứa dãy 𝑎1 , 𝑎2 , … , 𝑎𝑛 (|𝑎𝑖 | ≤ 109 ).
- Dòng thứ ba chứa dãy 𝑏1 , 𝑏2 , … , 𝑏𝑚 (|𝑏𝑗 | ≤ 109 ).
Kết quả: Ghi ra tập tin văn bản CHAP0406.OUT một số nguyên là kết quả tìm được.
Ví dụ:
CHAP0406.INP CHAP0406.OUT
3 4 8
4 10 2
3 4 8 6

7) Cho 2 dãy số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 và 𝑏1 , 𝑏2 , … , 𝑏𝑚 . Đếm số cặp phần tử 𝑎𝑖 , 𝑏𝑗 (1 ≤ 𝑖 ≤ 𝑛; 1 ≤ 𝑗 ≤


𝑚) sao cho 𝑎𝑖 ≠ 𝑏𝑗 .
Dữ liệu: Vào từ tập tin văn bản CHAP0407.INP gồm 3 dòng
- Dòng đầu tiên chứa 2 số nguyên 𝑛, 𝑚(1 ≤ 𝑛, 𝑚 ≤ 105 ).
- Dòng thứ hai chứa dãy 𝑎1 , 𝑎2 , … , 𝑎𝑛 (|𝑎𝑖 | ≤ 109 ).
- Dòng thứ ba chứa dãy 𝑏1 , 𝑏2 , … , 𝑏𝑚 (|𝑏𝑗 | ≤ 109 ).
Kết quả: Ghi ra tập tin văn bản CHAP0407.OUT một số nguyên là kết quả tìm được.
Ví dụ:
CHAP0407.INP CHAP0407.OUT
3 4 11
4 10 2
3 4 8 6

8) Cho 2 dãy số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 và 𝑏1 , 𝑏2 , … , 𝑏𝑚 . Đếm số cặp phần tử 𝑎𝑖 , 𝑏𝑗 (1 ≤ 𝑖 ≤ 𝑛; 1 ≤ 𝑗 ≤


𝑚) sao cho 𝑎𝑖 + 𝑏𝑗 < 𝑠, với 𝑠 là một số nguyên cho trước.
Dữ liệu: Vào từ tập tin văn bản CHAP0408.INP gồm 3 dòng
- Dòng đầu tiên chứa 3 số nguyên 𝑛, 𝑚, 𝑠(1 ≤ 𝑛, 𝑚 ≤ 105 ; |𝑠| ≤ 109 ).
- Dòng thứ hai chứa dãy 𝑎1 , 𝑎2 , … , 𝑎𝑛 (|𝑎𝑖 | ≤ 109 ).
- Dòng thứ ba chứa dãy 𝑏1 , 𝑏2 , … , 𝑏𝑚 (|𝑏𝑗 | ≤ 109 ).
Kết quả: Ghi ra tập tin văn bản CHAP0408.OUT một số nguyên là kết quả tìm được.
Ví dụ:
CHAP0408.INP CHAP0408.OUT
3 4 9 5
4 10 2
3 4 8 6

Trương Phước Hải 88


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM

9) Cho 2 dãy số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 và 𝑏1 , 𝑏2 , … , 𝑏𝑚 . Tìm giá trị lớn nhất của 𝑎𝑖 +


𝑏𝑗 (1 ≤ 𝑖 ≤ 𝑛; 1 ≤ 𝑗 ≤ 𝑚) sao cho 𝑎𝑖 + 𝑏𝑗 < 𝑠, với 𝑠 là một số nguyên cho trước.
Dữ liệu: Vào từ tập tin văn bản CHAP0409.INP gồm 3 dòng
- Dòng đầu tiên chứa 3 số nguyên 𝑛, 𝑚, 𝑠(1 ≤ 𝑛, 𝑚 ≤ 105 ; |𝑠| ≤ 109 ).
- Dòng thứ hai chứa dãy 𝑎1 , 𝑎2 , … , 𝑎𝑛 (|𝑎𝑖 | ≤ 109 ).
- Dòng thứ ba chứa dãy 𝑏1 , 𝑏2 , … , 𝑏𝑚 (|𝑏𝑗 | ≤ 109 ).
Kết quả: Ghi ra tập tin văn bản CHAP0409.OUT một số nguyên là kết quả tìm được.
Ví dụ:
CHAP0409.INP CHAP0409.OUT
3 4 9 8
4 10 2
3 4 8 6

10) Cho 2 dãy số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 và 𝑏1 , 𝑏2 , … , 𝑏𝑚 . Đếm số cặp phần tử 𝑎𝑖 , 𝑏𝑗 (1 ≤ 𝑖 ≤ 𝑛; 1 ≤ 𝑗 ≤


𝑚) khác nhau sao cho 𝑎𝑖 + 𝑏𝑗 = 𝑠, với 𝑠 là một số nguyên cho trước. Hai cặp được gọi là khác nhau nếu
tồn tại phần tử trong cặp này không xuất hiện trong cặp còn lại.
Dữ liệu: Vào từ tập tin văn bản CHAP0410.INP gồm 3 dòng
- Dòng đầu tiên chứa 3 số nguyên 𝑛, 𝑚, 𝑠(1 ≤ 𝑛, 𝑚 ≤ 105 ; |𝑠| ≤ 109 ).
- Dòng thứ hai chứa dãy 𝑎1 , 𝑎2 , … , 𝑎𝑛 (|𝑎𝑖 | ≤ 109 ).
- Dòng thứ ba chứa dãy 𝑏1 , 𝑏2 , … , 𝑏𝑚 (|𝑏𝑗 | ≤ 109 ).
Kết quả: Ghi ra tập tin văn bản CHAP0410.OUT một số nguyên là kết quả tìm được.
Ví dụ:
CHAP0410.INP CHAP0410.OUT
3 4 9 3
4 10 2
3 7 5 -1

11) Cho 2 dãy số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 và 𝑏1 , 𝑏2 , … , 𝑏𝑚 . Tìm giá trị nhỏ nhất của |𝑎𝑖 − 𝑏𝑗 |(1 ≤ 𝑖 ≤
𝑛; 1 ≤ 𝑗 ≤ 𝑚).
Dữ liệu: Vào từ tập tin văn bản CHAP0411.INP gồm 3 dòng
- Dòng đầu tiên chứa 2 số nguyên 𝑛, 𝑚(1 ≤ 𝑛, 𝑚 ≤ 105 ).
- Dòng thứ hai chứa dãy 𝑎1 , 𝑎2 , … , 𝑎𝑛 (|𝑎𝑖 | ≤ 109 ).
- Dòng thứ ba chứa dãy 𝑏1 , 𝑏2 , … , 𝑏𝑚 (|𝑏𝑗 | ≤ 109 ).
Kết quả: Ghi ra tập tin văn bản CHAP0411.OUT một số nguyên là kết quả tìm được.

Trương Phước Hải 89


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Ví dụ:
CHAP0411.INP CHAP0411.OUT
3 4 9 1
4 10 2
3 7 5 -1

12) Cho 2 dãy số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 và 𝑏1 , 𝑏2 , … , 𝑏𝑚 . Tìm giá trị nhỏ nhất của |𝑎𝑖 + 𝑏𝑗 |
(1 ≤ 𝑖 ≤ 𝑛; 1 ≤ 𝑗 ≤ 𝑚).
Dữ liệu: Vào từ tập tin văn bản CHAP0412.INP gồm 3 dòng
- Dòng đầu tiên chứa 2 số nguyên 𝑛, 𝑚(1 ≤ 𝑛, 𝑚 ≤ 105 ).
- Dòng thứ hai chứa dãy 𝑎1 , 𝑎2 , … , 𝑎𝑛 (|𝑎𝑖 | ≤ 109 ).
- Dòng thứ ba chứa dãy 𝑏1 , 𝑏2 , … , 𝑏𝑚 (|𝑏𝑗 | ≤ 109 ).
Kết quả: Ghi ra tập tin văn bản CHAP0412.OUT một số nguyên là kết quả tìm được.
Ví dụ:
CHAP0412.INP CHAP0412.OUT
3 4 1
4 10 2
3 7 5 -1

13) Cho mảng 2 chiều kích thước 𝑛 × 𝑚. Hãy tìm vị trí 𝑖, 𝑗(0 ≤ 𝑖 < 𝑛; 0 ≤ 𝑗 < 𝑚) sao cho tổng tất cả
phần tử nằm trên dòng 𝑖 và cột 𝑗 là lớn nhất. Nếu có nhiều kết quả thì ghi vị trí 𝑖, 𝑗 sao cho 𝑖 + 𝑗 là nhỏ
nhất.
Dữ liệu: Vào từ tập tin văn bản CHAP413.INP
- Dòng đầu chứa 2 số nguyên dương 𝑛, 𝑚(1 ≤ 𝑛, 𝑚 ≤ 103 ).
- Mỗi dòng trong 𝑛 dòng tiếp theo chứa dãy gồm 𝑚 số nguyên mô tả 1 dòng tương ứng của mảng.
Kết quả: Ghi ra tập tin văn bản CHAP0413.OUT 2 số nguyên 𝑖, 𝑗 tìm được.
Ví dụ:
CHAP0413.INP CHAP0413.OUT
3 4 1 3
5 2 -4 10
-3 1 8 -6
2 -1 8 3

Trương Phước Hải 90


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM

Chương 5. MỘT SỐ KỸ THUẬT CƠ BẢN

5.1. Kỹ thuật mảng thống kê


Kỹ thuật mảng thống kê đếm tần số xuất hiện của các giá trị trong một dãy, từ đó giúp các thao tác liên
quan đến việc thống kê giá trị của dãy được thực hiện một cách hiệu quả.
Xét bài toán sau: Cho dãy gồm 𝑛 số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 (0 ≤ 𝑎𝑖 ≤ 𝑚𝑎𝑥𝑉; 1 ≤ 𝑛 ≤ 106 ). Hãy thực hiện
các yêu cầu
- Đếm số giá trị phân biệt của dãy.
- Cho biết giá trị có tần số xuất hiện nhiều nhất. Nếu có nhiều giá trị có cùng tần số lớn nhất thì cho
biết giá trị nhỏ nhất trong số đó.
- Sắp xếp dãy có thứ tự tăng dần

Kỹ thuật mảng thống kê lưu tần số xuất hiện của giá trị 𝑥 tại vị trí 𝑥 trong mảng thống kê cnt, nói cách
khác cnt[x] cho biết tần số xuất hiện của giá trị 𝑥 trong dãy. Mảng thống kê cnt được xây dựng như
sau
cnt[maxV+1] = {0};

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


++cnt[a[i]];
Độ phức tạp của thao tác: 𝑂(𝑛).
Như vậy với mảng thống kê cnt[0], cnt[1], …, cnt[maxV] ta có thể thực hiện các yêu cầu trên như
sau

Đếm số giá trị phân biệt của dãy


int ans = 0;
for (int v = 0; v <= maxV; ++v)
if (cnt[v] > 0) ++ans;
Độ phức tạp của thao tác: 𝑂(𝑚𝑎𝑥𝑉).

Tìm giá trị xuất hiện nhiều nhất


int maxfrq = 0, val;
for (int v = 0; v <= maxV; ++v)
if (cnt[v] > maxfrq)
{
maxfrq = cnt[v];
val = v;
}

Trương Phước Hải 91


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Độ phức tạp của thao tác: 𝑂(𝑚𝑎𝑥𝑉).

Sắp xếp dãy (phương pháp distributing sort)


for (int v = 0; v <= maxV; ++v)
for (int i = 0; i < cnt[v]; ++i)
cout<<v<<" ";
Độ phức tạp của thao tác: 𝑂(𝑛).

Phương pháp mảng thống kê thực hiện các yêu cầu với độ phức tạp tuyến tính. Kích thước của mảng cnt
chính là độ lớn miền giá trị của 𝑥 (giá trị phần tử dãy). Do đó nếu miền giá trị của các phần tử trong dãy
quá lớn thì không thể áp dụng phương pháp này. Phương pháp mảng thống kê chỉ hiệu quả và khả thi khi
độ lớn miền giá trị của các phần tử trong dãy cỡ 107 .

5.2. Kỹ thuật 2 con trỏ


Kỹ thuật 2 con trỏ được áp dụng cho các bài toán xử lý trên dãy số nguyên. Kỹ thuật này sử dụng 2 biến
𝑖 và 𝑗 để quản lý một đoạn con gồm các phần tử nằm liên tiếp từ vị trí 𝑖 đến vị trí 𝑗 trong dãy hoặc xử lý
2 phần tử ở 2 vị trí 𝑖 và 𝑗 trong dãy. Con trỏ 𝑖 và 𝑗 có thể di chuyển trên cùng một dãy hoặc trên 2 dãy
khác nhau. Kỹ thuật 2 con trỏ có những dạng thường gặp sau:

Hai con trỏ cùng chiều


Trong kỹ thuật này, 2 con trỏ 𝑖 và 𝑗 xuất phát từ đầu dãy và di chuyển về cuối dãy. Trong đó con trỏ 𝑗 di
chuyển nhanh hơn và đi trước, con trỏ 𝑖 di chuyển chậm hơn và đi sau.

Bài toán 1: Cho dãy số nguyên gồm 𝑛 phần tử thỏa điều kiện 𝑎1 ≤ 𝑎2 ≤ ⋯ ≤ 𝑎𝑛 . Hãy loại các giá trị
trùng lắp ra khỏi dãy.

Cách 1: sử dụng kỹ thuật vét thông thường, độ phức tạp 𝑂(𝑛2 )


int a[maxN], n;

void RemoveDup1()
{
if (n == 0) return 0;
for (int i = 1; i < n;)
{
if ( a[i] == a[i-1])
{
for (int j = i+1; j < n; j++)
a[j-1] = a[j];
n--;

Trương Phước Hải 92


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
}
else i++;
}
}

Cách 2: Sử dụng kỹ thuật 2 con trỏ, độ phức tạp 𝑂(𝑛)


int a[maxN], n;

void RemoveDup2()
{
if (n == 0) return 0;
int i = 0;
for (int j = 1; j < n; j++)
{
if (a[i] != a[j])
a[++i] = a[j];
}
n = i+1;
}

Bài toán 2: Cho 2 dãy số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 và 𝑏1 , 𝑏2 , … , 𝑏𝑚 . Hãy kiểm tra dãy {𝑎𝑖 } có phải là một dãy
con gồm các phần tử không liên tiếp nhau của dãy {𝑏𝑗 }?

Thuật toán: Sử dụng kỹ thuật 2 con trỏ, độ phức tạp 𝑂(𝑚)


int a[maxN], b[maxN], n, m;

bool CheckSub()
{
for (int j = 0, i = 0; j < m; j++)
if (a[i] == b[j])
{
i++;
if (i == n) return true;
}
return false;
}

Bài toán 3: Cho dãy số nguyên dương 𝑎1 , 𝑎2 , … , 𝑎𝑛 và số nguyên dương 𝑆. Hãy chỉ ra một dãy con gồm
các phần tử liên tiếp sao cho tổng của chúng đúng bằng 𝑆.

Nhận xét: một dãy con liên tiếp là dãy gồm các phần tử nằm từ vị trí 𝑖 đến vị trí 𝑗(1 ≤ 𝑖 ≤ 𝑗 ≤ 𝑛).

Cách 1: Sử dụng phương pháp vét cạn. Xét tất cả cặp vị trí (𝑖 ≤ 𝑗) và kiểm tra tổng 𝑎𝑖 + 𝑎𝑖+1 + ⋯ + 𝑎𝑗
có bằng 𝑆. Độ phức tạp 𝑂(𝑛2 )
int a[maxN], n;

Trương Phước Hải 93


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
void FindSub1(ll S, int &lo, int &hi)
{
for (int i = 1; i <= n; ++i)
{
ll t = 0;
for (int j = i; j <= n; ++j)
{
t = t + a[j];
if (t == S)
{
lo = i, hi = j;
return;
}
}
}
}

Cách 2: Sử dụng kỹ thuật 2 con trỏ. Độ phức tạp 𝑂(𝑛)


- Xét cặp vị trí (𝑖 ≤ 𝑗), đặt 𝑡 = 𝑎𝑖 + 𝑎𝑖+1 + ⋯ + 𝑎𝑗 .
- Giả sử 𝑡 < 𝑆 và 𝑡 + 𝑎𝑗+1 > 𝑆, khi đó ta tăng con trỏ 𝑖 đến khi 𝑎𝑖 + 𝑎𝑖+1 + ⋯ + 𝑎𝑗 + 𝑎𝑗+1 < 𝑆
- Lặp lại thao tác trên cho đến khi tìm được dãy con có tổng đúng bằng 𝑆 hoặc báo cáo không tìm thấy.
int a[maxN], n;

void FindSub2(ll S, int &lo, int &hi)


{
int i = 1, j = 0;
lo = hi = -1;
ll t = 0;
while (i <= n && j <= n)
{
if (t + a[j+1] <= S)
t = t + a[++j];
else t = t – a[i++];
if (t == S)
{
lo = i, hi = j;
return;
}
}
}

Trương Phước Hải 94


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
Hai con trỏ ngược chiều
Trong kỹ thuật này, con trỏ 𝑖 xuất phát từ đầu dãy và di chuyển về cuối dãy, con trỏ 𝑗 xuất phát từ cuối
dãy và di chuyển theo chiều ngược lại với con trỏ 𝑖.

Bài toán 1: Cho chuỗi kí tự, đảo ngược thứ tự các kí tự của chuỗi

Thuật toán: sử dụng kỹ thuật 2 con trỏ, độ phức tạp 𝑂(𝑛)


void Reverse(string s)
{
int i = 0, j = s.size() - 1;
while (i < j)
{
swap(s[i], s[j]);
i++, j--;
}
}

Bài toán 2: Cho chuỗi 𝑠 chỉ gồm các kí tự chữ cái in thường. Hãy kiểm tra xem 𝑠 có phải là chuỗi
Palindrome hay không? Chuỗi rỗng được xem là chuỗi Palindrome

Thuật toán: Sử dụng kỹ thuật 2 con trỏ, độ phức tạp 𝑂(𝑛)


bool isPalindrome(string s)
{
int len = s.size();
if (len == 0) return true;
int i = 0, j = len-1;
while (i < j)
{
if (s[i] != s[j])
return false;
i++, j--;
}
return true;
}

Bài toán 3: Cho dãy gồm 𝑛 số nguyên thỏa 𝑎1 ≤ 𝑎2 ≤ ⋯ ≤ 𝑎𝑛 và số nguyên 𝑥. Hãy tìm vị trí 2 phần tử
khác nhau của dãy có tổng đúng bằng 𝑥.

Thuật toán: Sử dụng kỹ thuật 2 con trỏ, độ phức tạp 𝑂(𝑛)


int a[maxN], n;

void TwoSum(int x, int &i, int &j)


{
i = 0, j = n-1;

Trương Phước Hải 95


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
while (i < j)
{
if (a[i] + a[j] == x)
return;
if (a[i] + a[j] < x) i++;
else j--;
}
if (i >= n) i = -1;
if (j < 0) j = -1;
}

Bài toán 4: Cho dãy gồm 𝑛 số nguyên 𝑎1 , 𝑎2 , … , 𝑎𝑛 và số nguyên 𝑘. Hãy thực hiện xoay dãy 𝑘 bước, mỗi
bước đưa phần tử ở cuối dãy về đầu dãy. Ví dụ dãy 𝑛 = 7 phần tử {1,2,3,4,5,6,7} và 𝑘 = 3, dãy biến đổi
1 2 3
qua 3 bước như sau {1,2,3,4,5,6,7} → {7,1,2,3,4,5,6} → {6,7,1,2,3,4,5} → {5,6,7,1,2,3,4}.

Cách 1: Sử dụng phương pháp vét cạn, độ phức tạp 𝑂(𝑛 ∗ 𝑘)


int a[maxN], n;

void rotate1(int k)
{
for (int i = 1; i <= k; i++)
{
int x = a[n-1];
for (int j = n-1; j > 0; j--)
a[j] = a[j-1];
a[0] = x;
}
}

Cách 2: Sử dụng mảng phụ, độ phức tạp 𝑂(𝑛)

Ý tưởng: khi thực hiện một phép xoay phải dãy thì mỗi phần tử của dãy đều bị dịch tới 1 vị trí, do đó khi
thực hiện 𝑘 phép xoay phải thì phần tử thứ 𝑖 của dãy bị dịch tới vị trí 𝑖 + 𝑘.
int a[maxN], n;

void rotate2(int k)
{
vector<int> b(n);
for (int i = 0; i < n; i++) b[(i+k) % n] = a[i];

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


}

Cách 3: Sử dụng kỹ thuật đảo mảng, độ phức tạp 𝑂(𝑛)

Trương Phước Hải 96


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
void reverse(int l, int r)
{
int i = l, j = r;
while (i < j)
swap(a[i++], a[j--]);
}

void rotate3(int k)
{
k = k % n;
reverse(a, 0, n-k-1);
reverse(a, n-k, n-1);
reverse(a, 0, n-1);
}

5.3. Kỹ thuật mảng hằng


Một trong những ứng dụng hiệu quả của mảng hằng là hạn chế các điều kiện rẽ nhánh làm tinh gọn
chương trình. Xét một số bài toán sau
Bài toán 1: Cho số nguyên 𝑛(1 ≤ 𝑛 ≤ 12). Viết hàm in ra tên tiếng Anh (viết tắt) của tháng 𝑛.
So sánh 2 đoạn chương trình minh họa dưới để thấy sự khác biệt giữa việc sử dụng câu lệnh rẽ nhánh
(đoạn chương trình 1) và sử dụng mảng hằng (đoạn chương trình 2).
Đoạn chương trình 1: Sử dụng câu lệnh rẽ nhánh
void PrintMonth(int n)
{
if (n == 1) cout<<"Jan";
else if (n == 2) cout<<"Feb";
else if (n == 3) cout<<"Mar";
else if (n == 4) cout<<"Apr";
else if (n == 5) cout<<"May";
else if (n == 6) cout<<"Jun";
else if (n == 7) cout<<"Jul";
else if (n == 8) cout<<"Aug";
else if (n == 9) cout<<"Sep";
else if (n == 10) cout<<"Oct";
else if (n == 11) cout<<"Nov";
else cout<<"Dec";
}

Đoạn chương trình 2: Sử dụng mảng hằng


//khai báo mảng hằng month
string month[13] = {"", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
void PrintMonth(int n)

Trương Phước Hải 97


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
{
cout<<month[n];
}

Bài toán 2: Cho bảng số nguyên 𝐴 kích thước 𝑚 × 𝑛(1 ≤ 𝑚, 𝑛 ≤ 103 ). Hãy đếm số ô trong bảng thỏa
tính chất giá trị tại ô đó lớn hơn giá trị tất cả ô chung cạnh và chung đỉnh xung quanh.
Đoạn chương trình 1: Sử dụng câu lệnh rẽ nhánh
int Count(int m, int n)
{
int ans = 0;
for (int i = 1; i <= m; ++i)
for (int j = 1; j <= n; ++j)
{
if (A[i][j] > A[i][j-1] && A[i][j] > A[i-1][j-1] &&
A[i][j] > A[i-1][j] && A[i][j] > A[i-1][j+1] &&
A[i][j] > A[i][j+1] && A[i][j] > A[i+1][j+1] &&
A[i][j] > A[i+1][j] && A[i][j] > A[i+1][j-1]) ++ans;
}
return ans;
}

Đoạn chương trình 2: Sử dụng mảng hằng


int dx[8] = {0, -1, -1, -1, 0, 1, 1, 1}; //khai báo mảng hằng dx
int dy[8] = {-1, -1, 0, 1, 1, 1, 0, -1}; //khai báo mảng hằng dy
int Count(int m, int n)
{
int ans = 0;
for (int i = 1; i <= m; ++i)
for (int j = 1; j <= n; ++j)
{
int cnt = 0;
for (int k = 0; k < 8; ++k)
if (A[i][j] > A[i + dx[k]][j + dy[k]]) ++cnt;
if (cnt == 8) ++ans;
}
return ans;
}

Nhận xét: Kỹ thuật mảng hằng giúp rút gọn các điều kiện được xét trong câu lệnh rẽ nhánh hoặc tránh
việc xét quá nhiều câu lệnh rẽ nhánh. Điều này giúp cho chương trình giảm thiểu các lỗi khi xét các điều
kiện phức hợp và trong một số trường hợp có thể giúp chương trình chạy nhanh hơn.

Trương Phước Hải 98


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
5.4. Kỹ thuật xử lý số nguyên lớn
Các kiểu dữ liệu số nguyên trong đa số các ngôn ngữ lập trình hiện tại cho phép thực hiện tính toán với
miền giá trị lên đến 263 − 1. Tuy nhiên một số bài toán lập trình cần thực hiện các thao tác tính toán trên
các số nguyên với số lượng chữ số lên đến hàng ngàn chữ số hoặc hơn. Các số nguyên như vậy được gọi
là các số nguyên lớn.

Ngôn ngữ C/C++ không hỗ trợ xử lý tính toán trên các số nguyên có miền giá trị lớn hơn 263 − 1. Do đó,
ta cần phải cài đặt các thao tác tính toán cơ bản trên các số nguyên lớn để phục vụ giải các bài toán liên
quan.

Có nhiều cách để biểu diễn một số nguyên lớn, ta có thể coi số nguyên lớn như là một dãy các chữ số, do
đó có thể sử dụng xâu kí tự, mảng một chiều hoặc danh sách liêt kết để biểu diễn một số nguyên lớn. Tài
liệu này sử dụng mảng một chiều để biểu diễn một số nguyên lớn vì tính đơn giản trong cài đặt.

Nhận xét rằng các phép toán cộng, trừ, nhân trên số nguyên đều thực hiện từ hàng đơn vị trở đi. Do đó,
để thuận tiện cho việc cài đặt, số nguyên lớn được biểu diễn thành mảng với các chữ số có thứ tự ngược
lại với giá trị thực sự của nó. Chẳng hạn số nguyên 3000 được biểu diễn như sau:

0 0 0 3
0 1 2 3
Khai báo số nguyên lớn

typedef vector<int> bigint;

Các phép toán trên số nguyên lớn được cài đặt theo cách tính toán bằng tay thông thường. Dưới đây ta
xét một số phép toán cơ bản đối với số nguyên lớn không dấu.

Phép cộng 2 số nguyên lớn


bigint operator + (bigint a, bigint b)
{
bigint res;
int i = 0, j = 0, cr = 0;
while (i < a.size() || j < b.size()) {
if (i < a.size())
cr = cr + a[i++];
if (j < b.size())

Trương Phước Hải 99


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
cr = cr + b[j++];

res.push_back(cr%10);
cr = cr / 10;
}
if (cr > 0) res.push_back(cr);
return res;
}

Phép trừ 2 số nguyên lớn


//điều kiện a >= b
bigint operator - (bigint a, bigint b)
{
bigint res;
int i = 0, j = 0, cr = 0;
while (i < a.size() || j < b.size())
{
int c = a[i++] - cr;
if (j < b.size()) c = c - b[j++];
if (c < 0)
{
c = c + 10;
cr = 1;
}
else cr = 0;
res.push_back(c);
}
while (res.back() == 0) res.pop_back(); //loại bỏ chữ số 0 vô nghĩa
return res;
}

Phép nhân nguyên số lớn với số nguyên có 1 chữ số


bigint operator * (bigint a, int b)
{
bigint res;
int cr = 0;
for (int i = 0; i < a.size(); ++i)
{
cr = cr + a[i]*b;
res.push_back(cr % 10);
cr = cr / 10;
}
if (cr > 0) res.push_back(cr);
return res;

Trương Phước Hải 100


Tài liệu chuyên Tin – PTNK ĐHQG TPHCM
}

Phép nhân 2 số nguyên lớn


//hàm cho kết quả của a*10^n
bigint mul10(bigint a, int n)
{
bigint res(a.size()+n, 0);
for (int i = 0, j = n; i < a.size(); ++i, ++j)
res[j] = a[i];
return res;
}

bigint operator * (bigint a, bigint b)


{
bigint res;
for (int i = 0; i < b.size(); ++i)
{
//tái sử dụng phép nhân với số nguyên có 1 chữ số
bigint c = a*b[i];
c = mul10(c, i);
res = res + c;
}
return res;
}

Trương Phước Hải 101

You might also like