You are on page 1of 47

Së GIÁO DỤC VÀ ĐÀO TẠO NGHỆ AN

TRƯỜNG THPT NGUYỄN DUY TRINH


---***---

SÁNG KIẾN KINH NGHIỆM

“GIÚP HỌC SINH RÈN LUYỆN VÀ NÂNG CAO


KỸ NĂNG LẬP TRÌNH QUA VIỆC LỰA CHỌN
THUẬT TOÁN TỐI ƯU PHÙ HỢP VỚI DỮ LIỆU
BÀI TOÁN”
MÔN: TIN HỌC

GIÁO VIÊN: NGUYỄN THỊ TÚ ANH


TỔ: TOÁN TIN
NĂM HỌC: 2020 – 2021
ĐT: 0942797783

NGHI LỘC, THÁNG 3/2021


1
MỤC LỤC
I. ĐẶT VẤN ĐỀ ................................................................................................................. 3
1. Lý do chọn đề tài ............................................................................................................. 3
2. Mục đích nghiên cứu của SKKN..................................................................................... 3
3. Nhiệm vụ nghiên cứu của SKKN .................................................................................... 4
4. Đối tượng nghiên cứu của SKKN ................................................................................... 4
6. Phương pháp thực hiện .................................................................................................... 4
7. Đóng góp của SKKN ....................................................................................................... 4
II. NỘI DUNG .................................................................................................................... 5
1. Cơ sở lí luận của đề tài .................................................................................................... 5
2. Thực trạng của vấn đề trước khi áp dụng SKKN ............................................................ 5
2.1. Đặc điểm tình hình ................................................................................................... 5
2.2. Thực trạng trước khi nghiên cứu .............................................................................. 6
3. Các giải pháp giải quyết vấn đề ....................................................................................... 6
3.1. Cơ sở lý thuyết.......................................................................................................... 7
3.1.1. Độ phức tạp thuật toán ...................................................................................... 7
3.1.1.1. Tính hiệu quả của thuật toán ...................................................................... 7
3.1.1.2. Tại sao cần thuật toán có tính hiệu quả? .................................................... 7
3.1.1.3. Đánh giá thời gian thực hiện thuật toán ..................................................... 8
3.1.1.4. Các quy tắc đánh giá thời gian thực hiện thuật toán .................................. 9
3.1.1.5. Ước lượng độ phức tạp thuật toán tương ứng với độ lớn dữ liệu............. 10
3.1.2. Lựa chọn thuật toán ......................................................................................... 11
3.2. Lựa chọn thuật toán tối ưu phù hợp với dữ liệu bài toán ....................................... 11
3.2.1. Dạng 1: Các bài toán liên quan đến số học ..................................................... 12
3.2.2. Dạng 2: Sử dụng thuật toán sắp xếp ............................................................... 20
3.2.2.1. Bài toán sắp xếp........................................................................................ 20
3.2.2.2. Bài tập ví dụ.............................................................................................. 23
3.2.3. Dạng 3: Sử dụng thuật toán tìm kiếm............................................................. 32
3.2.3.1. Bài toán tìm kiếm ..................................................................................... 32
3.2.3.2. Bài tập ví dụ.............................................................................................. 34
3.2.4. Bài tập luyện tập .............................................................................................. 40
4. Tính mới của SKKN ...................................................................................................... 43
5. Hiệu quả của SKKN ...................................................................................................... 44
6. Những hướng phát triển của đề tài ................................................................................ 44
III. KẾT LUẬN VÀ KIẾN NGHỊ .................................................................................. 45
1. Kết luận.......................................................................................................................... 45
2. Kiến nghị ....................................................................................................................... 45
TÀI LIỆU THAM KHẢO............................................................................................... 46

2
I. ĐẶT VẤN ĐỀ
1. Lý do chọn đề tài
Khi giải các bài toán Tin học người lập trình luôn mong muốn viết chương
trình với thuật toán tối ưu để có thể giải với dữ liệu lớn, thời gian thực hiện nhanh,
bộ nhớ hạn chế…Tuy nhiên, bài toán Tin học thường đa dạng, phong phú nên để có
thể tìm được thuật toán tối ưu phù hợp dữ liệu bài toán là việc không hề dễ dàng.
Đây là vấn đề nhiều giáo viên gặp khó khăn trong việc giảng dạy học sinh học lập
trình, cũng như công tác ôn thi học sinh giỏi để đạt kết quả cao.
Ở trường phổ thông hàng năm thường diễn ra các cuộc thi học sinh giỏi các
môn học trong đó có môn Tin học. Bồi dưỡng học sinh giỏi là một nhiệm vụ rất cần
thiết và quan trọng đối với mỗi giáo viên. Do đó, việc nghiên cứu, tìm tòi, tích lũy
kiến thức là công việc thường nhật của mỗi giáo viên nhằm nâng cao trình độ chuyên
môn nghiệp vụ, tích lũy kinh nghiệm cho bản thân.Trong những năm gần đây mức
độ yêu cầu của đề thi học sinh giỏi ngày càng nâng cao, môn Tin học cũng vậy. Các
bài toán trong đề thi thường yêu cầu giải quyết dữ liệu vào lớn trong khi đòi hỏi thời
gian thực hiện nhanh (thường không quá 1 giây). Những bài toán này thường khó
đối với học sinh, chương trình của các em thường không thực hiện được hết các test
yêu cầu, đặc biệt là đối với học sinh không chuyên. Bởi vì vừa giải quyết vấn đề dữ
liệu, vừa phải giải quyết vấn đề thời gian. Do đó giáo viên bồi dưỡng thường phải
giúp học sinh rèn luyện kĩ năng lập trình. Nghĩa là không chỉ dừng lại ở việc hướng
dẫn học sinh giải được bài toán, mà còn phải giúp học sinh rèn luyện được thói quen
tư duy, cải tiến thuật toán để chương trình tối ưu nhất có thể. Trong lập trình tin học
đã có rất nhiều phương pháp giải các bài toán đã được nêu ra. Tuy nhiên rất ít tài
liệu trình bày cụ thể về cách lựa chọn thuật toán thế nào sao cho phù hợp với dữ liệu
bài toán để đạt độ tối ưu, đảm bảo giải quyết được yêu cầu của bài toán đặt ra.
Qua quá trình giảng dạy, học tập, tìm tòi và đặc biệt là tham gia bồi dưỡng học
sinh giỏi nhiều năm qua, tôi đã tích lũy được một số kinh nghiệm về vấn đề này. Do
đó, tôi quyết định viết sáng kiến kinh nghiệm: “Giúp học sinh rèn luyện và nâng
cao kĩ năng lập trình qua việc lựa chọn thuật toán tối ưu phù hợp với dữ liệu bài
toán”
2. Mục đích nghiên cứu của SKKN
- SKKN nêu ra các định hướng giúp học sinh có thể lựa chọn thuật toán tối ưu
phù hợp với dữ liệu bài toán trong một số dạng bài toán quen thuộc trên ngôn ngữ
lập trình C++ .
- Từ đó bồi dưỡng học sinh năng lực giải quyết vấn đề trong giải toán Tin học,
đồng thời rèn luyện và nâng cao kĩ năng lập trình cho các em. Đặc biệt là học sinh
tham gia dự thi học sinh giỏi cấp tỉnh THCS, THPT hoặc thi vào các trường chuyên.

3
3. Nhiệm vụ nghiên cứu của SKKN
- SKKN phân tích các thuật toán trong các dạng toán quen thuộc, so sánh độ
phức tạp thuật toán và định hướng lựa chọn thuật toán tối ưu trong các trường hợp
dữ liệu cụ thể nhằm giải bài toán hiệu quả nhất.
- Minh họa bằng các ví dụ cụ thể, liên hệ các đề thi vào trường chuyên, đề thi
học sinh giỏi tỉnh thời gian qua.
4. Đối tượng nghiên cứu của SKKN
- Độ phức tạp thuật toán và giải pháp lựa chọn thuật toán tối ưu trong các dạng
bài toán quen thuộc trên ngôn ngữ lập trình C++.
- Phương pháp bồi dưỡng năng lực giải quyết vấn đề cho học sinh
5. Phạm vi nghiên cứu của SKKN
- Chương trình Tin học THCS, THPT để bồi dưỡng học sinh giỏi Tin học và
thi vào trường chuyên THPT.
- Cách giải quyết vấn đề của học sinh.
6. Phương pháp thực hiện
- Nghiên cứu tài liệu
- Thực hiện bồi dưỡng học sinh giỏi
- Trao đổi chuyên môn với bạn bè, đồng nghiệp để giải quyết vấn đề
- Thực nghiệm sư phạm.
7. Đóng góp của SKKN
- Sáng kiến trình bày được một số kinh nghiệm giúp học sinh rèn luyện và nâng
cao kĩ năng lập trình qua việc lựa chọn thuật toán tối ưu phù hợp với dữ liệu bài toán
yêu cầu của các dạng bài toán quen thuộc, thường có trong các đề thi học sinh giỏi
Tin học.
- Là tài liệu tham khảo để bồi dưỡng học sinh giỏi Tin học, học sinh thi trường
chuyên có hiệu quả.
- Giúp bồi dưỡng học sinh năng lực giải quyết vấn đề trong giải toán Tin học,
rèn luyện và nâng cao kỹ năng lập trình cho học sinh.

4
II. NỘI DUNG
1. Cơ sở lí luận của đề tài
Nghị quyết hội nghị Trung ương VIII khóa XI đã nêu: “Đối với giáo dục phổ
thông tập trung phát triển trí tuệ, thể chất, hình thành phẩm chất, năng lực công dân,
phát hiện và bồi dưỡng năng khiếu, định hướng nghề nghiệp cho học sinh. Nâng cao
chất lượng giáo dục toàn diện, chú trọng giáo dục lý tưởng truyền thống đạo đức, lối
sống, ngoại ngữ, tin học, năng lực và kỹ năng thực hành, vận dụng kiến thức vào
thực tiễn, phát triển khả năng sáng tạo và tự học, khuyến khích học tập suốt đời".
Toàn ngành đang ra sức phấn đấu xây dựng chương trình sách giáo khoa mới, đổi
mới phương pháp dạy học theo định hướng hình thành và phát triển năng lực.
Mục tiêu của môn Tin học trong chương trình giáo dục phổ thông là giúp học
sinh hình thành và phát triển năng lực Tin học, đổi mới phương pháp dạy học nhằm
góp phần thực hiện tốt mục tiêu của ngành giáo dục trong giai đoạn mới. Môn Tin
học là môn học đặc thù có nhiều kiến thức khó. Đặc biệt là phần học lập trình ở lớp
11. Đây cũng là phần kiến thức đề thi học sinh giỏi tỉnh môn Tin học. Trong một số
năm gần đây do sự phát triển nhanh chóng của khoa học kỹ thuật, tốc độ xử lí của
máy tính ngày càng cao. Các đề thi trong các cuộc thi lập trình cũng ngày càng đòi
hỏi cao hơn về thời gian thực hiện, về độ lớn dữ liệu…Nên gây rất nhiều khó khăn
trong việc ôn luyện cho thầy và trò. Một trong những vấn đề luôn đặt ra là làm thế
nào để lựa chọn được thuật toán tối ưu đảm bảo đáp ứng toàn bộ yêu cầu dữ liệu vào
của bài toán. Do đó đòi hỏi giáo viên cần có giải pháp để giúp học sinh giải quyết
vấn đề này nhằm nâng cao chất lượng công tác bồi dưỡng học sinh giỏi bộ môn Tin
học hiện nay.
2. Thực trạng của vấn đề trước khi áp dụng SKKN
2.1. Đặc điểm tình hình
* Thuận lợi:
- Với sự phát triển nhanh của ngành công nghệ thông tin, máy tính ngày càng
có tốc độ xử lý cao đáp ứng được yêu cầu xử lý các bài toán có dữ liệu lớn trong thời
gian thực hiện ngắn.
- Học sinh và giáo viên có thể dễ dàng tìm hiểu nguồn tài liệu để học tập tham
khảo.
* Khó khăn:
- Những kiến thức trong chương trình Tin học phổ thông còn hạn chế hoặc
không đủ đáp ứng cho việc giải một số bài toán trong các kỳ thi học sinh giỏi Tỉnh
khi có yêu cầu dữ liệu lớn cùng thời gian thực hiện ngắn.
- Các tài liệu tổng hợp các cách để giải quyết các bài toán yêu cầu cao như vậy
chưa có nhiều để học sinh tham khảo, ôn luyện.

5
2.2. Thực trạng trước khi nghiên cứu
Với sự phát triển nhanh về tốc độ của máy tính hiện nay thì đề thi học sinh giỏi
Tin học cũng đòi hỏi ngày càng nâng cao hơn. Đặc biệt về mặt thời gian thực hiện
và độ lớn của dữ liệu đầu vào. Đa số giáo viên gặp khó khăn trong việc hướng dẫn
học sinh giải bài toán thế nào để đạt được trọn vẹn yêu cầu của bài toán. Những năm
trước đây khi bồi dưỡng học sinh thi học sinh giỏi tỉnh tôi cũng đã chú trọng nhiều
hơn đến cải tiến chương trình tối ưu, tuy nhiên hiệu quả mang lại chưa cao.
Nguyên nhân chủ yếu của thực trạng này là giáo viên chưa có phương pháp
giảng dạy phù hợp với vấn đề này. Cụ thể giáo viên thường giúp học sinh cải tiến,
làm mịn dần thuật toán của từng bài cụ thể. Chưa phân loại thành các dạng bài, định
hướng học sinh đánh giá thuật toán và cải thiện thuật toán nhưng chưa ước lượng
với mỗi mức dữ liệu thì cần phải xây dựng thuật toán có độ phức tạp tương ứng như
thế nào để có thể đáp ứng được…Chính vì vậy, học sinh thường cố gắng tinh chỉnh,
tìm cách giải tối ưu nhưng lại không chắc chắn rằng bài giải của mình đã đáp ứng
trọn vẹn dữ liệu yêu cầu của bài toán hay chưa. Cho nên học sinh sẽ gặp khó khăn
trong việc vận dụng vào các bài toán tương tự, cũng như không linh hoạt trong các
bài toán mới. Kết quả là học sinh vẫn giải được các bài toán nhưng không đạt điểm
số cao nên thường chỉ đạt giải khuyến khích trở xuống.
Vì vậy, tôi vẫn luôn duy trì hướng dẫn, uốn nắn các em biết cách giải bài, rồi
rèn luyện kĩ năng tinh chỉnh, làm mịn dần thuật toán, kết hợp biết cách nhận biết độ
lớn dữ liệu bài toán và lựa chọn thuật toán phù hợp để đạt hiệu quả nhất. Trong quá
trình giảng dạy tôi còn cho học sinh kiểm chứng qua đánh giá của phần mềm Themis
để dễ dàng so sánh và ghi nhớ. Từ đó đạt hiệu quả cao hơn.
3. Các giải pháp giải quyết vấn đề
Có những bài toán nhiều khi có vẻ đơn giản, đa số học sinh có thể giải được
nhưng khi thực hiện với dữ liệu lớn thì không đáp ứng được thời gian yêu cầu hoặc
không thể thực hiện được tất cả các test yêu cầu. Lúc này đòi hỏi phải tìm được thuật
toán tối ưu nhất.
Tối ưu hoá là một đòi hỏi thường xuyên trong quá trình giải quyết các bài toán
tin học. Tối ưu hoá thuật toán là một công việc yêu cầu tư duy thuật toán rất cao,
cùng với khả năng sử dụng thuần thục các cấu trúc dữ liệu. Vì vậy, việc tìm ra thuật
toán tối ưu là không dễ chút nào. Tối ưu hoá thường được tiến hành theo 2 góc độ
đó là tối ưu theo không gian có nghĩa là tối ưu không gian lưu trữ (bộ nhớ), và tối
ưu theo thời gian có nghĩa là giảm độ phức tạp thuật toán, giảm các bước xử lý trong
chương trình…Tuy nhiên không phải lúc nào ta cũng có thể đạt được đồng thời cả 2
điều đó, trong nhiều trường hợp tối ưu về thời gian sẽ làm tăng không gian lưu trữ,
và ngược lại. Bài toán trong Tin học thì đa dạng, phong phú. Đa số giáo viên gặp
khó khăn trong việc hướng dẫn học sinh thiết kế thuật toán hoặc lựa chọn thuật toán
nào để có thể giảm độ phức tạp của thuật toán, đồng thời phù hợp với dữ liệu và yêu
cầu của đề bài. Đa số khi chấm bài, dùng test chấm mới biết được chương trình đáp
6
ứng được bao nhiêu test so với yêu cầu của đề ra. Điều này chỉ thực hiện được khi
ôn luyện cho các em, còn khi các em đi thi thì không tự ước lượng được bài làm đạt
kết quả thế nào. Có một cách để ước lượng được chương trình giải được có thể đáp
ứng được yêu cầu của đề hay không (nghĩa là đáp ứng được khoảng dữ liệu vào bao
nhiêu), đó là đánh giá độ phức tạp của thuật toán trong chương trình. Như vậy để có
thể tối ưu hóa thuật toán trước tiên chúng ta phải hiểu rõ về độ phức tạp của thuật
toán và cách đánh giá ước lượng thuật toán so với dữ liệu đầu vào yêu cầu.
3.1. Cơ sở lý thuyết
3.1.1. Độ phức tạp thuật toán
3.1.1.1. Tính hiệu quả của thuật toán
Khi giải một bài toán, chúng ta cần chọn trong số các thuật toán một thuật
toán mà chúng ta cho là “tốt” nhất. Vậy dựa trên cơ sở nào để đánh giá thuật toán
này “tốt” hơn thuật toán kia? Thông thường ta dựa trên hai tiêu chuẩn sau:
1. Thuật toán đơn giản, dễ hiểu, dễ cài đặt (dễ viết chương trình).
2. Thuật toán hiệu quả: Chúng ta thường đặc biệt quan tâm đến thời gian thực
hiện của thuật toán (gọi là độ phức tạp tính toán), bên cạnh đó chúng ta cũng quan
tâm tới dung lượng không gian nhớ cần thiết để lưu giữ các dữ liệu vào, ra và các
kết quả trung gian trong quá trình tính toán.
Khi viết chương trình chỉ để sử dụng một số ít lần thì tiêu chuẩn (1) là quan
trọng, nhưng nếu viết chương trình để sử dụng nhiều lần, cho nhiều người sử dụng
thì tiêu chuẩn (2) lại quan trọng hơn. Trong trường hợp này, dù thuật toán có thể
phải cài đặt phức tạp, nhưng ta vẫn sẽ lựa chọn để nhận được chương trình chạy
nhanh hơn, hiệu quả hơn.
3.1.1.2. Tại sao cần thuật toán có tính hiệu quả?
Kĩ thuật máy tính tiến bộ rất nhanh, ngày nay các máy tính lớn có thể đạt tốc
độ tính toán hàng nghìn tỉ phép tính trong một giây. Vậy có cần phải tìm thuật toán
hiệu quả hay không? Chúng ta xem ví dụ bài toán kiểm tra tính nguyên tố của
một số nguyên dương n (n ≥ 2).
bool is_prime(int n)
{
for(int k=2;k<=n-1;k++)
if (n % k ==0) then return false;
return true;
}

7
Dễ dàng nhận thấy rằng, nếu n là một số nguyên tố chúng ta phải mất n- 2
phép toán chia lấy dư (%). Giả sử một siêu máy tính có thể tính được trăm nghìn tỉ
(1014) phép % trong một giây, như vậy để kiểm tra một số khoảng 25 chữ số mất
khoảng ~ 3170 năm .

Trong khi đó, nếu ta có nhận xét việc thử k từ 2 đến n - 1 là không cần thiết
mà chỉ cần thử k từ 2 đến √𝑛 , ta có:
bool is_prime(int n)
{
for(int k=2;k<=sqrt(n);k++)
if (n % k ==0) then return false;
return true;
}
Như vậy để kiểm tra một số khoảng 25 chữ số mất khoảng
3.1.1.3. Đánh giá thời gian thực hiện thuật toán
Có hai cách tiếp cận để đánh giá thời gian thực hiện của một thuật toán. Cách
thứ nhất bằng thực nghiệm, chúng ta viết chương trình và cho chạy chương trình với
các dữ liệu vào khác nhau trên một máy tính. Cách thứ hai bằng phương pháp lí
thuyết, chúng ta coi thời gian thực hiện thuật toán như hàm số của cỡ dữ liệu vào
(cỡ của dữ liệu vào là một tham số đặc trưng cho dữ liệu vào, nó có ảnh hưởng quyết
định đến thời gian thực hiện chương trình. Ví dụ đối với bài toán kiểm tra số nguyên

14
tố thì cỡ của dữ liệu vào là số cần kiểm tra; hay với bài toán sắp xếp dãy số, cỡ của
dữ liệu vào là số phần tử của dãy). Thông thường cỡ của dữ liệu vào là một số nguyên

10 x
dương n , ta sử dụng hàm số T(n) trong đó n là cỡ của dữ liệu vào để biểu diễn thời
gian thực hiện của một thuật toán.
Xét ví dụ bài toán kiểm tra tính nguyên tố của một số nguyên dương n (cỡ dữ
liệu vào là n ), nếu n là một số chẵn (n>2) chỉ cần một lần thử chia 2 để kết luận n
không phải là số nguyên tố. Nếu n (n>3) không chia hết cho 2 nhưng lại chia hết
cho 3 thì cần 2 lần thử (chia 2 và chia 3) để kết luận n không nguyên tố. Còn nếu n
là một số nguyên tố thì thuật toán phải thực hiện nhiều lần thử nhất.
Trong tài liệu này, chúng ta hiểu hàm số T(n) là thời gian nhiều nhất cần thiết
để thực hiện thuật toán với mọi bộ dữ liệu đầu vào cỡ n .
Sử dụng kí hiệu toán học ô lớn để mô tả độ lớn của hàm. Giả sử n là một số
nguyên dương, T(n) và f(n) là hai hàm thực không âm. Ta viết T(n)= O(f(n)) nếu và
chỉ nếu tồn tại các hằng số dương c và n0 , sao cho T(n)≤ c x f(n), mọi n ≥ n0.
Nếu một thuật toán có thời gian thực hiện T(n)= O(f(n)) chúng ta nói rằng
thuật toán có thời gian thực hiện cấp f(n).
8
Ví dụ: Giả sử T(n) = n2 + 2n, ta có n2 + 2n ≤ 3n2 với mọi n ≥ 1.
Vậy T(n) = O(n2) trong trường hợp này ta nói thuật toán có thời gian thực hiện
cấp n2.
3.1.1.4. Các quy tắc đánh giá thời gian thực hiện thuật toán
Để đánh giá thời gian thực hiện thuật toán được trình bày bằng ngôn ngữ
C++, ta cần biết cách đánh giá thời gian thực hiện các câu lệnh của C++
Trước tiên, chúng ta hãy xem xét các câu lệnh chính trong C++. Các câu lệnh
trong C++ được định nghĩa như sau:
1. Các phép gán, đọc, viết là các câu lệnh (được gọi là lệnh đơn).
2. Nếu S1, S2, ..., Sm là câu lệnh thì
{ S1; S2; …; Sm; }
là câu lệnh (được gọi là lệnh hợp thành hay khối lệnh).
3. Nếu S1 và S2 là các câu lệnh và E là biểu thức lôgic thì
If (E) S1; else S2;
là câu lệnh (được gọi là lệnh rẽ nhánh hay lệnh If).
4. Nếu S là câu lệnh và E là biểu thức lôgic thì
While (E) S;
là câu lệnh (được gọi là lệnh lặp điều kiện trước hay lệnh While).
5. Nếu S1, S2,…,Sm là các câu lệnh và E là biểu thức lôgic thì
Do
S1; S2; …; Sm;
While (E);
là câu lệnh (được gọi là lệnh lặp điều kiện sau hay lệnh Do .. While)
6. Nếu S là lệnh, E1 và E2 là các biểu thức cùng một kiểu thứ tự đếm được
Thì For (i=E1; i<= E2;i++) S;
là câu lệnh (được gọi là lệnh lặp với số lần xác định hay lệnh For).
Để đánh giá, chúng ta phân tích chương trình xuất phát từ các lệnh đơn, rồi
đánh giá các lệnh phức tạp hơn, cuối cùng đánh giá được thời gian thực hiện của
chương trình, cụ thể:
1. Thời gian thực hiện các lệnh đơn: gán, đọc, viết là O(1)
2. Lệnh hợp thành: giả sử thời gian thực hiện của S1, S2,…,Sm tương ứng là

9
O((f1(n)), O((f2(n)), …, O((fm(n)). Khi đó thời gian thực hiện của lệnh hợp
thành là O(max(f1(n), f2(n), …, fm(n))).
3. Lệnh If: giả sử thời gian thực hiện của S1, S2 tương ứng là
Khi đó thời gian thực hiện của lệnh If là: O((f1(n)), O((f2(n)). Khi đó thời gian
thực hiện của lệnh If là O(max(f1(n), f2(n)))
4. Lệnh lặp While: giả sử thời gian thực hiện lệnh S (thân của lệnh While) là
O(f(n)) và g(n) là số lần lặp tối đa thực hiện lệnh S. Khi đó thời gian thực hiện
lệnh While là O(f(n)g(n)).
5. Lệnh lặp Repeat: giả sử thời gian thực hiện khối lệnh { S1; S2;…; Sm; }
là O(f(n)) và g(n) là số lần lặp tối đa. Khi đó thời gian thực hiện lệnh Do..While
là O(f(n)g(n)).
6. Lệnh lặp For: giả sử thời gian thực hiện lệnh S là O(f(n)) và g(n) là số lần
lặp tối đa. Khi đó thời gian thực hiện lệnh For là O(f(n)g(n)).
3.1.1.5. Ước lượng độ phức tạp thuật toán tương ứng với độ lớn dữ liệu
Độ phức tạp thuật toán là một hàm phụ thuộc đầu vào. Tuy nhiên trong những
ứng dụng thực tiễn, chúng ta không cần biết chính xác hàm này mà chỉ cần biết một
ước lượng đủ tốt của chúng. Việc ước lượng rất quan trọng trong việc giải quyết bài
toán lập trình thi đấu, đòi hỏi nhanh nhạy và có quyết định chính xác ngay từ đầu.
Độ phức tạp của thuật toán có thể có những giá trị là O(1), O(log n), O(n), O(nlogn),
O(n2), O(n3)… Và thời gian thực hiện được so sánh như sau:
O(1)< O(log n) < O(n) < O(nlogn) < O(n2) < O(n3)…
Sau đây là bảng ước lượng (*) giới hạn dữ liệu vào tương ứng với độ phức tạp
của thuật toán đảm bảo thực hiện trong thời gian tối đa 1 giây.
Độ phức tạp O(1) O(logn) O(n) O(nlogn) O(n2) O(n3)
Ước lượng n Không ảnh hưởng 108 4.107 104 500
tối đa thời gian, thường
chỉ phụ thuộc giới
hạn kiểu dữ liệu
Với bảng ước lượng1 này giáo viên có thể hướng dẫn học sinh xác định độ
phức tạp của thuật toán phải đạt được để đảm bảo thời gian thực hiện. Từ đó yêu cầu
học sinh phải suy luận, tư duy để đạt được yêu cầu của bài toán. Hơn nữa, sau khi
hoàn thành chương trình học sinh dù chưa chấm bằng test vẫn có thể dự đoán được
kết quả hoàn thành bao nhiêu so với giới hạn yêu cầu của bài toán.

1
Theo https://vnoi.info/wiki/translate/topcoder/Computational-Complexity-Section-1.md
10
3.1.2. Lựa chọn thuật toán
Các bước để giải một bài toán trên máy tính:
Bước 1: Xác định bài toán
Bước 2: Lựa chọn hoặc thiết kế thuật toán
Bước 3: Viết chương trình
Bước 4: Kiểm thử
Bước 5: Viết tài liệu
Để có chương trình đúng được thực hiện ở bước 3, 4, 5 thì người lập trình
phải làm tốt ở bước 1, 2.
Bước 1: Xác định bài toán
Là xác định Input, Output của bài toán. Khi xác định Input của bài toán ta
quan tâm đến độ lớn của dữ liệu vào vì nó ảnh hưởng nhiều đến việc lựa chọn hoặc
thiết kế thuật toán phù hợp ở bước 2.
Bước 2: Lựa chọn hoặc thiết kế thuật toán
- Trường hợp 1: Trong trường hợp bài toán chưa có cách giải thì ta thiết kế
thuật toán. Khi thiết kế thuật toán ngoài yêu cầu đảm bảo tính đúng đắn, còn phải
phân tích dữ liệu vào để ước lượng độ phức tạp thuật toán cho phép phù hợp, có thể
đảm bảo giải quyết yêu cầu của bài toán.
Ví dụ: Dữ liệu vào yêu cầu cỡ 108 thì độ phức tạp của chương trình cho phép
tối đa là O(n)
- Trường hợp 2: Trong trường hợp bài toán có nhiều cách giải thì lựa chọn
thuật toán tối ưu hơn về thời gian thực hiện, bộ nhớ, độ phức tạp…
3.2. Lựa chọn thuật toán tối ưu phù hợp với dữ liệu bài toán
Trong giới hạn sáng kiến này tôi muốn trình bày kinh nghiệm về việc hướng
dẫn học sinh lựa chọn một thuật toán của những dạng bài toán đã có nhiều thuật toán
để giải. Sao cho sự lựa chọn đó phù hợp với độ lớn dữ liệu vào của bài toán để đảm
bảo giải quyết bài toán tối ưu, hiệu quả nhất. Từ đó giúp học sinh rèn luyện và nâng
cao kĩ năng lập trình.
Khi hướng dẫn học sinh làm bài toán mới tôi thường yêu cầu học sinh làm theo
các trình tự sau:
Bước 1: Xác định bài toán (xác định Input, Output)
Bước 2: Xác định đặc điểm dữ liệu (như số lượng phần tử, kiểu dữ liệu của
phần tử, độ lớn của phần tử...).
Bước 3: So sánh giữa các thuật toán có thể áp dụng cho bài toán (gồm đánh giá độ
phức tạp, thời gian thực hiện, khả năng đảm bảo yêu cầu phù hợp với dữ liệu của bài toán).
11
Bước 4: Đánh giá và lựa chọn thuật toán hiệu quả nhất.
Bước 5: Cài đặt thuật toán đã lựa chọn bằng ngôn ngữ C++.
3.2.1. Dạng 1: Các bài toán liên quan đến số học
Trong nhiều bài toán tin học nếu biết suy luận dựa theo các định lí, công thức,
kết quả… của toán học có thể cho ta cách giải rất tối ưu. Những cách giải đó có thể
đáp ứng với dữ liệu lớn. Chúng ta xét các ví dụ sau:
Ví dụ 1:
TỔNG BÌNH PHƯƠNG2
Lại là tính tổng! Cô giáo yêu cầu Thắng phải hoàn thành một bài toán về tính
tổng, nhưng do nghỉ phòng chống dịch Covid-19 thời gian dài, Thắng quên kiến thức
về lĩnh vực này nên đành nhờ đến tài năng của các bạn với bài toán như sau:
Cho số nguyên dương N (N ≤ 105).
Yêu cầu: Tính tổng S(N) = 12 + 22 +…+ N2
Dữ liệu vào: Từ tệp văn bản TONGBP.INP gồm:
 Dòng đầu chứa số nguyên dương T là số lượng test (T ≤ 105)
 T dòng tiếp theo, mỗi dòng là một số nguyên dương N.
Kết quả: Ghi ra tệp văn bản TONGBP.OUT gồm T dòng, với mỗi dòng là giá trị tổng
S(N) tương ứng.
Ví dụ:
TONGBP.INP TONGBP.OUT
2 14
3 1015
14
Giải thích: T = 2 (có 2 test):
Test 1: N = 3, S(3) = 12 + 22 + 32 = 14
Test 2: N = 14, S(14) = 12 + 22 + … + 142 = 1015
Giới hạn: - 80% số test với 1 ≤ N, T ≤ 103
- 20% số test với 103 < N, T ≤ 105
Bước 1: Xác định bài toán
Input: số nguyên dương T là số lượng test (T ≤ 105),số nguyên dương N (N ≤ 105).
Output: T dòng, với mỗi dòng là giá trị tổng S(N) tương ứng.

2
Đề thi vào lớp 10 Chuyên Phan Bội Châu –Năm học 2020-2021
12
Bước 2: Đặc điểm dữ liệu
Dữ liệu vào yêu cầu tính tổng S(N) với N ≤ 105, hơn nữa lại có T ≤ 105 test
Kiểu dữ liệu T, N nguyên dương
Bước 3: Thuật toán có thể giải bài toán
Cách 1: -Tính lần lượt mỗi test, với mỗi test tính tổng bình phương các số từ 1
đến n. Độ phức tạp sẽ là O(n2)
Cách 2: Theo tính chất dãy số trong toán học thì:
12 + 22 +…+ N2 = N *(N+1) * (2*N +1) /6
Vận dụng tính chất này để tính mỗi Ti ta đã đưa độ phức tạp từ O(n2) xuống
O(n) với n là số lượng test
Bước 4: Đánh giá, lựa chọn thuật toán
Với cách 1, độ phức tạp là O(n2). Đối chiếu theo bảng ước lượng (*) đã nêu với
thời gian thực hiện tối đa 1 giây thì đáp ứng được giới hạn với 1 ≤ N, T ≤ 103 chiếm
80% số test theo yêu cầu của đề.
Với cách 2, độ phức tạp O(n) đáp ứng được thời gian thực hiện tối đa 1 giây
cho cả 20% số test còn lại với 103 < N, T ≤ 105
Kết luận: Lựa chọn cách 2 để giải được 100% số test theo yêu cầu của đề bài.
Bước 5: Cài đặt chương trình
Code tham khảo
#include <bits/stdc++.h>
using namespace std;
int main()
{
freopen ("tongbp.inp" , "r" , stdin);
freopen ("tongbp.out" , "w" , stdout);
ios_base::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
long long n,t;
cin >> n;
for (long long i = 0 ; i < n ; i++)
{

13
cin >> t;
cout << (t * (t + 1) * (2 * t + 1)) / 6 << '\n';
}
return 0;
}
Kết luận: Đối với những bài toán dạng số học thế này, nếu dữ liệu nhỏ (cỡ
≤10 ) thì có thể giải theo cách thông thường. Trường hợp dữ liệu lớn hơn thường đòi
4

hỏi người giải phải linh hoạt vận dụng các suy luận, công thức…trong toán học để
giải thì mới đáp ứng được toàn bộ yêu cầu của bài toán.
Ví dụ 2:
Thuật toán: Kiểm tra tính nguyên tố của một số nguyên dương (N>0)
Đây là thuật toán cơ bản, quen thuộc và thường được sử dụng để giải các bài
toán trong các đề thi Tin học. Tuy nhiên, đa số học sinh chưa biết cách vận dụng linh
hoạt thuật toán này sao cho phù hợp với đề ra.
Thực tế có rất nhiều cách để có thể thực hiện kiểm tra tính nguyên tố của một
số nguyên dương. Tuy nhiên, có 2 cách giải tối ưu thường được sử dụng là:
Cách 1: Kiểm tra nguyên tố theo cách thông thường
Nhận xét thấy nếu 𝑛 có một ước nguyên dương 𝑖 thì nó sẽ có ước nguyên
dương 𝑛/𝑖. Từ đó ta chỉ cần kiểm tra xem nếu n không chia hết cho các phần tử từ
2 đến √𝑛 thì 𝑛 là số nguyên tố.
Ta có hàm kiểm tra nguyên tố như sau:
bool check(long long x)
{if (x==1) return false;
for(long long i=2;i<=sqrt(x);i++)
if (x%i==0) return false;
return true;
}
Thuật toán này có độ phức tạp là O(√𝑛), nếu chỉ kiểm tra một số nguyên tố thì
có thể thực hiện với n cỡ 1016 trong 1 giây.
Tuy nhiên, nếu bài toán yêu cầu liệt kê các số nguyên tố nhỏ hơn 𝑛, khi đó ta
có thể sử dụng hàm kiểm tra nguyên tố ở trong cách 1 như sau:
for(long long i=1; i<=n; i++)
if (check(i)) cout<<i<<endl;

14
Ta thấy độ phức tạp lúc này là O(n√𝑛), với 𝑛 cỡ 107 trường hợp trên đã có cỡ
1010 lệnh thực thi. Do đó việc sử dụng thuật toán ở trên sẽ vượt quá tốc độ xử lý của
máy tính hiện nay vì tốc độ máy tính hiện tại chỉ có thể thực hiện tối đa 108 phép
toán trên một giây.
Theo bảng ước lượng đã nêu thì với dữ liệu vào 𝑛 cỡ 107 thì độ phức tạp phải
là O(n). Vì vậy trong trường hợp này ta cần sử dụng thuật toán khác đó là sàng
nguyên tố Eratosthenes
Như vậy thuật toán kiểm tra nguyên tố theo cách này có:
Ưu điểm:
- Code đơn giản, dễ hiểu, độ phức tạp nhỏ O(√𝑛), thích hợp với bài toán chỉ
dùng để kiểm tra ít số.
Nhược điểm:
- Hạn chế đối với bài toán liệt kê các số nguyên tố với số lượng phần tử lớn
Cách 2: Kiểm tra nguyên tố bằng sàng nguyên tố Eratosthenes
Sàng nguyên tố Eratosthenes là một thuật toán giúp nhanh chóng liệt kê các số
nguyên tố. Đây là một thuật toán tìm số nguyên tố tối ưu khi muốn tìm tất cả các số
nguyên tố nhỏ hơn một số N cho trước (N >=2)
Ý tưởng của thuật toán sàng nguyên tố Eratosthenes
Dựa theo lý thuyết về số nguyên tố: Một số nguyên tố là số chỉ có 2 ước là 1
và chính nó. Do vậy, nếu ta xác định được số x là số nguyên tố, ta có thể kết luận
mọi số chia hết cho x đều không phải số nguyên tố. Do đó ta đã loại bỏ được rất
nhiều số mà không cần kiểm tra.
Ví dụ:
Số 2 là số nguyên tố => các số 4, 6, 8, 10…không phải số nguyên tố.
Số 3 là số nguyên tố => các số 9, 15, 21…không phải số nguyên tố (Do 6, 12,
18 đã bị loại ở số 2)
Thuật toán sàng nguyên tố Eratosthenes
1. Tạo mảng đánh dấu cho tất cả các phần tử từ 2 đến N và mặc định tất cả đều
là số nguyên tố
2. Xét số đầu tiên tìm được là số nguyên tố – giả sử x, đánh dấu tất cả các ước
của x: 2x, 3x, 4x,… trong đoạn [x, N] không phải số nguyên tố.
3. Tìm số tiếp theo được đánh dấu là số nguyên tố trong [x, N]. Nếu không còn
số nào, thoát chương trình. Nếu còn, gán nó bằng x và lặp lại bước 2.
4. Khi kết thúc giải thuật, các số không bị đánh dấu là các số nguyên tố
Ta có hàm kiểm tra nguyên tố dùng sàng nguyên tố Eratosthenes như sau:
15
bool isPrime[nmax];
void sieve()
{
memset(isPrime, true, sizeof(isPrime));
isPrime[1] = false;
for (long long i = 2; i*i <= nmax; i++)
if (isPrime[i])
for (long long j = i*i; j<= nmax; j+=i)
isPrime[j] = false;
}
Độ phức tạp của thuật toán:
Số lần lặp của vòng lặp trong là:
- Khi i=2, vòng lặp trong lặp N/2 lần.
- Khi i=3, vòng lặp trong lặp N/3 lần.
- Khi i=5, vòng lặp trong lặp N/5 lần.
- …
Độ phức tạp tổng: N(1/2+1/3+1/5+…)=O(NlogN)
Thuật toán sàng nguyên tố cho ta mảng chứa các số nguyên tố, khi kiểm tra số
nguyên bất kỳ có phải nguyên tố không chỉ việc so sánh với các phần tử trong mảng
đã tìm được. Như vậy thuật toán sàng nguyên tố có:
Ưu điểm:
- Mảng nguyên tố đã được tạo sẵn nên việc kiểm tra nhanh
- Độ phức tạp là O(nlogn)
Nhược điểm:
- Vì sử dụng mảng để chứa các số nguyên tố nên giới hạn dữ liệu chỉ cỡ 107
phần tử.
Kết luận:
Ta có bảng so sánh như sau:

16
So sánh Kiểm tra nguyên tố Sàng nguyên tố
Độ phức tạp O(√𝑛) O(nlogn)
Đặc điểm Cài đặt đơn giản, dễ sử Cài đặt phức tạp hơn
dụng
Trường hợp nên sử dụng - Kiểm tra tính nguyên tố - Kiểm tra và liệt kê
(thời gian thực hiện ≤1s) của ít số. Hoặc kiểm tra nhiều số nguyên tố
số lượng phần tử khoảng -Kiểm tra số lượng phần
n≤ 105 tử khoảng n≤ 107
Xét một số ví dụ để so sánh việc sử dụng 2 thuật toán trên:
Bài 1: - Số lượng số nguyên tố3
Yêu cầu: Cho dãy số a1, a2,..., an. Hãy đếm số nguyên tố có mặt trong dãy đã cho.
Dữ liệu:
- Dòng đầu tiên ghi số nguyên không âm n (0 < n ≤ 103).
- Dòng thứ 2 ghi n số nguyên không âm kiểu 64 – bit.
Kết quả: in ra số lượng số nguyên tố có mặt trong dãy.
Ví dụ
input
10
1 2 3 4 5 6 7 9 11 13
output
6
Bước 1: Xác định bài toán
- Input: dãy số a1, a2,..., an (0 < n ≤ 103).
- Output: Số lượng số nguyên tố trong dãy a1, a2,..., an
Bước 2: Đặc điểm dữ liệu:
- Dữ liệu vào yêu cầu thực hiện đếm số nguyên tố với dãy số nguyên không âm
có giá trị ai ≤ 108 và n≤103 trong thời gian ≤ 1 giây.
Bước 3: Các cách giải có thể lựa chọn:
- Cách 1: Với mỗi ai kiểm tra tính nguyên tố của nó bằng thuật toán kiểm tra
nguyên tố thông thường. Độ phức tạp tính được là O(𝑛√𝑛)
3
Bài N1001A trên http://laptrinhphothong.vn/
17
- Cách 2: Sử dụng sàng nguyên tố và kiểm tra từng ai có thuộc mảng đã sàng
hay không. Độ phức tạp tính được O(max(nlogn,n) = O(nlogn)
Bước 4: Đánh giá, lựa chọn:
- Đối chiếu với bảng ước lượng (*) thì với độ phức tạp của cách 1 và cách 2
đều đáp ứng được thời gian thực hiện ≤ 1 giây. Như vậy ta có thể sử dụng cách nào
cũng được. Tuy nhiên ta nên dùng thuật toán 1 vì tính đơn giản của nó.
Bước 5: Viết chương trình:
Code tham khảo:
#include <bits/stdc++.h>
using namespace std;
#define nmax 1009
int a[nmax];
bool check(long long x)
{if (x==1) return false;
for(long long i=2;i<=sqrt(x);i++)
if (x%i==0) return false;
return true;
}
int main()
{
int n, dem = 0;
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
if (check(a[i])) dem++;
}
cout << dem;
return 0;
}

18
Bài 2: Số nguyên tố trong đoạn4
Yêu cầu: Đếm số lượng số nguyên tố trong đoạn [a; b].
Dữ liệu: Một dòng ghi hai số nguyên a, b với 0 < a, b ≤ 107.
Kết quả: Một dòng là số lượng số nguyên tố trong đoạn [a; b].
Ví dụ
input
1 10
output
4
Bước 1: Xác định bài toán
- Input: hai số nguyên a, b với 0 < a, b ≤ 107
- Output: số lượng số nguyên tố trong đoạn [a; b].
Bước 2: Đặc điểm dữ liệu:
- Dữ liệu vào a, b ≤ 107, trường hợp lớn nhất là phải xét 107 số, với mỗi số
ai≤107
Bước 3: Các cách giải có thể lựa chọn
Với ai≤107 ta có thể sử dụng 2 cách giải
- Cách 1: Với mỗi ai kiểm tra tính nguyên tố của nó bằng thuật toán kiểm tra
nguyên tố thông thường. Độ phức tạp tính được là O(𝑛√𝑛)
- Cách 2: Sử dụng sàng nguyên tố và kiểm tra từng ai có thuộc mảng đã sàng
hay không. Độ phức tạp tính được O(max(nlogn,n) = O(nlogn)
Bước 4: Đánh giá, lựa chọn
- Đối chiếu với bảng ước lượng (*) thì với độ phức tạp của cách 1 (trường hợp
xấu nhất là cỡ 1010) nên không thể thực hiện hết các test của đề bài trong thời gian
yêu cầu. Cách 2 đáp ứng được thời gian thực hiện với mọi test. Vì vậy lựa chọn cách
2 để giải bài toán.
Bước 5: Viết chương trình
Code tham khảo
#include <bits/stdc++.h>
#define nmax 10000009
using namespace std;

4
Bài N1002B trên http://laptrinhphothong.vn/

19
bool isPrime[nmax];
void sieve()
{
memset(isPrime, true, sizeof(isPrime));
isPrime[1] = false;
for (long long i = 2; i*i <= nmax; i++)
if (isPrime[i])
for (long long j = i*i; j<= nmax; j+=i)
isPrime[j] = false;
}
int main()
{
sieve();
long long a, b;
cin >> a >> b ;
int dem = 0;
for (long long i = a; i <=b ; i++)
{
if (isPrime[i]) dem++;
}
cout << dem;
return 0;
}
3.2.2. Dạng 2: Sử dụng thuật toán sắp xếp
3.2.2.1. Bài toán sắp xếp
Cho dãy a gồm n số nguyên a1, a2, ..., an. Hãy sắp xếp dãy a thành dãy không giảm.
Có nhiều thuật toán sắp xếp, tuy nhiên có 3 thuật toán sắp xếp thường được sử
dụng giảng dạy ôn luyện học sinh giỏi cấp tỉnh đó là : sắp xếp nổi bọt, sắp xếp nhanh
và sắp xếp đếm phân phối.
* Thuật toán sắp xếp nổi bọt (Bubble Sort)
Ý tưởng:
- So sánh cặp số a1 với a2, nếu số sau nhỏ hơn số trước thì đổi chỗ hai số đó cho
20
nhau. Làm tương tự với các cặp số a1 với a3, ..., a1 với an. Khi đó phần tử nhỏ nhất sẽ
đưa lên đầu dãy (tức a1).
- Thực hiện lặp lại với các cặp số: a2 với a3; a2 với a4; ...; a2 với an; ...; an-1 với an.
Đoạn chương trình thực hiện thuật toán:
for (i = 1; i < n - 1; i++)
for (j = i + 1; j < n; j++)
if (a[i] > a[j])
swap(a[i],a[j];//Đổi chỗ a[i] với a[j]
Nhận xét:
Ưu điểm:
- Code đơn giản, dễ hiểu.
- Sắp xếp được với phần tử có miền giá trị lớn |ai| ≤ 1018
Nhược điểm:
- Độ phức tạp của thuật toán lớn O(n2).
Chạy không đủ nhanh (quá 1 giây) với dữ liệu lớn, chỉ áp dụng với số phần
-
tử n ≤ 104.
* Thuật toán sắp xếp nhanh (Quick Sort)
Ý tưởng:
Chọn một phần tử làm chốt (ở đây ta chọn phần tử ở vị trí giữa). Từ trái sang
tìm phần tử có vị trí i lớn hơn hoặc bằng phần tử chốt, từ phải sang tìm phần tử có
vị trí j bé hơn hoặc bằng phần tử chốt. Nếu i <= j thì đổi chỗ hai phần tử. Làm cho
đến khi i > j. Lúc này sẽ chia ra được 2 nhóm cần sắp xếp. Làm tương tự như vậy
với mỗi nhóm cho đến khi đã sắp xếp hết dãy.
Hàm thực hiện:
void qsort(a[], l, r)
{
x = a[(l + r) / 2];
i = l;
j = r;
do
{
while (a[i] < x) i++;
while (a[j] > x) j--;
21
if (i <= j)
{
swap(a[i],a[j]
i++;
j--;
}
}
while (i <= j)
if (l < j) qsort(a, l, j);
if (i < r) qsort(a, i, r);
}
Trong C++ đây là hàm có sẵn nên khi sử dụng chỉ cần gọi hàm sort(a,a+n) để
sắp xếp dãy từ a0,… ,an là được.
Nhận xét:
Ưu điểm:
- Thời gian thực hiện nhanh. Độ phức tạp O(nlogn). Áp dụng với số phần tử n ≤ 106.
- Sắp xếp được các phần tử với miền giá trị lớn |ai| ≤ 1018
Nhược điểm:
- Chạy chậm (quá 1 giây) với số phần tử n > 106.
-Không ổn định, tùy thuộc vào cách chia thành 2 phần, nếu chia không tốt độ
phức tạp trong trường hợp xấu nhất có thể là O(n2) (trường hợp này hiếm khi xảy
ra).
*Thuật toán sắp xếp bằng phương pháp đếm phân phối (Distribution Counting)
Ý tưởng:
- Khởi tạo giá trị ban đầu cho mảng dem.
- Dùng mảng dem để đếm số lần xuất hiện của số a[i] trong dãy.
-Duyệt i từ giá trị nhỏ nhất (gtmin) của các a[i] đến giá trị lớn nhất (gtmax)
của các a]i]. Duyệt j từ 1 đến dem[i] rồi in ra i.
Đoạn chương trình mô phỏng:
//khoi tao gia tri mang dem
for (i = 0; i <= max; i++)
dem[i] = 0;

22
//so lan xuat hien cua a[i]
for (i = 0; i < n; i++)
dem[a[i]]++;
//in day so
for (i = gtmin; i<gtmax; i++)
for (j=1; j<= dem[i]; j++)
cout << i << " ";
Nhận xét:
Ưu điểm:
- Code đơn giản.
- Độ phức tạp O(max(n,gtmax)) phụ thuộc miền giá trị.
- Áp dụng được với dãy có số phần tử lớn n ≤ 108.
Nhược điểm:
- Phải biết miền giá trị của số nguyên.
-Miền giá trị > 107 sẽ không thể tạo được mảng dem để lưu trữ (nghĩa là chỉ
áp dụng được với miền giá trị |ai| ≤ 107).
Kết luận:
So sánh Sắp xếp nổi bọt Sắp xếp nhanh Sắp xếp đếm phân phối
Độ phức tạp O(n2) O(nlogn) trường O(max(n,gtmax))
hợp xấu nhất có phụ thuộc miền giá trị
thể là O(n2)
Đặc điểm Code đơn giản, Thời gian thực Code đơn giản
dễ hiểu hiện nhanh, trong
thư viện C++ có
sẵn hàm sort
Trường hợp nên số phần tử n < 104 số phần tử n ≤ 106. số phần tử lớn n ≤ 108
sử dụng (thời miền giá trị lớn miền giá trị lớn chỉ áp dụng được với
gian thực hiện |a | ≤ 1018 |ai| ≤ 1018 miền giá trị |ai| ≤ 107
i
≤1s)
3.2.2.2. Bài tập ví dụ
Bài 1: Cho một dãy số nguyên a1, a2, ..., an. Hãy đếm số lượng giá trị khác nhau
trong dãy và đưa ra số lần lặp của giá trị xuất hiện nhiều nhất.
Dữ liệu vào: File BAI1.INP gồm 2 dòng:
23
+ Dòng đầu số nguyên dương N (N <= 106)
+ Dòng thứ hai gồm dãy số nguyên a1, a2, ..., an.( |ai| <= 109)
Dữ liệu ra: File BAI1.OUT gồm 2 dòng:
+ Dòng đầu tiên ghi số lượng giá trị khác nhau trong dãy
+ Dòng thứ 2 ghi số lần lặp của giá trị xuất hiện nhiều nhất.
Ví dụ:
BAI1.INP BAI1.OUT
8 5
67174668 3
Giới hạn: - 60% số test với 1 ≤ N ≤ 104
- 40% số test với 104 < N ≤ 106
Bước 1: Xác định bài toán
- Input: dãy số nguyên a1, a2, ..., an.( |ai| <= 109, N <= 106)
- Output: số lượng giá trị khác nhau trong dãy và số lần lặp của giá trị xuất
hiện nhiều nhất.
Bước 2: Đặc điểm dữ liệu
Dữ liệu vào gồm dãy số nguyên a1, a2, ..., an.( |ai| <= 109) với N <= 106
Bước 3: Các cách giải có thể lựa chọn
Ý tưởng:
B1: Sắp xếp dãy số tăng dần. (các số bằng nhau đứng liên tiếp nhau)
B2: Khởi tạo d:=1 (số phần tử khác nhau), dd:=1 (số phần tử liên tiếp bằng
nhau) và dmax:=1 (số lần lặp của giá trị xuất hiện nhiều nhất). Duyệt i từ phần tử
thứ 2 đến n, nếu 2 số đứng cạnh nhau mà bằng nhau thì tăng dd, ngược lại khác nhau
thì tăng d, đồng thời tìm dmax.
Cách 1: nếu sử dụng thuật toán sắp xếp nổi bọt thì chương trình lúc đó sẽ có
độ phức tạp là O(max(n2,n)) = O(n2)( O(n2) của đoạn chương trình sắp xếp B1, O(n)
của đoạn chương trình B2).
Cách 2: Nếu sử dụng thuật toán sắp xếp nhanh thì chương trình lúc đó sẽ có
độ phức tạp là O(max(=nlogn,n)) = O(nlogn) )( O(nlogn) của đoạn chương trình sắp
xếp B1, O(n) của đoạn chương trình B2).
Bước 4: Đánh giá, lựa chọn
Như vậy theo bảng ước lượng (*) :
Cách 1: sẽ thỏa mãn khoảng 60% test của bài toán (1 ≤ N ≤ 103)
24
Cách 2: sẽ thỏa mãn cả 100% test của bài toán với |ai| <= 109, N <= 106
Vậy lựa chọn thuật toán sắp xếp nhanh để áp dụng cho bài toán này.
Trường hợp nếu dữ liệu vào là |ai| <= 106, N <= 107 thì sử dụng thuật toán
đếm phân phối là phù hợp nhất.
Bước 5: Viết chương trình
Code tham khảo:
#include <bits/stdc++.h>
using namespace std;
long long n, d, dmax, dd, i, a[1000001] = {};
main(){
freopen ("bai1.inp" , "r" , stdin);
freopen ("bai1.out" , "w" , stdout);
cin >> n;
for (i = 0; i < n ; i++){
cin >> a[i];
}
sort(a, a + n); //sắp xếp theo thứ tự từ bé đến lớn
dd = 1; d = 1; dmax = 1;
for (i = 1 ; i < n ; i++){
if (a[i] == a[i - 1]){
dd++;
}
else{
if (dd > dmax){
dmax = dd;
}
d++; dd= 1;
}
}
cout << d << '\n' << dmax;
return 0;}

25
Bài 2: TRỒNG CÂY5
Bác Bình có một khu vườn rất nhiều cây, mỗi cây có một chiều cao khác nhau.
Bác Bình muốn chọn tất cả các cây đặc biệt trong vườn để trồng thành một hàng cây
mới (cây đặc biệt là cây có chiều cao là một số nguyên tố). Hơn nữa để cây tiếp xúc
tốt với ánh sáng bác có ý tưởng bố trí vị trí các cây đặc biệt theo quy tắc thực hiện
lần lượt như sau:
- B1: Chọn cây cao nhất đặt làm mốc;
- B2: Cây cao tiếp theo đặt ở bên trái của mốc;
- B3: Cây cao tiếp theo nữa ở bên phải của
mốc;
Thực hiện lặp đi lặp lại B2, B3 cho đến khi
hết số cây đặc biệt. Lần 4 Lần 2 Lần 1 Lần 3 Lần 5
Yêu cầu: Cho trước số lượng và chiều cao
của các cây trong vườn, hãy giúp bác Bình trồng hàng các cây đặc biệt theo đúng ý
tưởng của bác.
Dữ liệu vào: Từ tệp văn bản TRONGCAY.INP gồm hai dòng:
 Dòng thứ nhất chứa số nguyên N (1 ≤ N ≤ 106) là số lượng cây trong vườn.
 Dòng thứ hai chứa N số nguyên ai (1 ≤ ai ≤ 107, 1 ≤ i ≤ N) là chiều cao của
cây thứ i.
Kết quả: Ghi ra tệp văn bản TRONGCAY.OUT gồm các số trên cùng một dòng là
chiều cao của các cây đặc biệt trong hàng cây mới. Nếu không có cây đặc biệt nào
thì ghi ra tệp giá trị ˗1.
Các số cách nhau ít nhất một dấu cách trống.
Ví dụ:
TRONGCAY.INP TRONGCAY.OUT TRONGCAY.INP TRONGCAY.OUT
8 5 23 53 17 2 3 ˗1
2 55 23 16 53 5 25 18 9
20 17
Giới hạn: - 75% số test với 1 ≤ N ≤ 104.
- 25% số test với 104 < N ≤ 106.
Bước 1: Xác định bài toán
- Input: Dãy N số nguyên ai (1 ≤ ai ≤ 107, 1 ≤ i ≤ N, 1 ≤ N ≤ 106 ) là chiều cao
của cây thứ i

5
Đề thi vào lớp 10 chuyên Phan Bội Châu năm học 2020-2021
26
- Output: chiều cao của các cây đặc biệt trong hàng cây mới
Bước 2: Đặc điểm dữ liệu:
Dữ liệu vào N ≤ 106 và ai ≤ 107
Bước 3: Các cách giải có thể lựa chọn
Ý tưởng:
+ Kiểm tra chiều cao của mỗi cây có phải là số nguyên tố không. Nếu là nguyên
tố thì lưu vào mảng c
+ Sắp xếp mảng c theo giá trị tăng dần
+ Lấy các giá trị trên mảng a ghi vào tệp kết quả
- Nếu số phần tử của a là chẵn:
Lần 1: Ghi các giá trị ở chỉ số lẻ: a1, a3, …, aN-1
Lần 2: Ghi các giá trị ở chỉ số chẵn theo chỉ số giảm dần: aN, aN-2, …, a2
- Nếu số phần tử của a là lẻ :
Lần 1: Ghi các giá trị ở chỉ số chẵn a2, a4,…, aN-1
Lần 2: Ghi các giá trị ở chỉ số lẻ theo chỉ số giảm dần: aN, aN-2, …, a1
Các cách giải có thể lựa chọn
Nếu sử dụng hàm kiểm tra nguyên tố thông thường thì trường hợp xấu nhất có
thể đạt tới 106.√107 ≈ 109 lệnh thực thi nên không đáp ứng về mặt thời gian. Đối
chiếu bảng so sánh 2 thuật toán kiểm tra nguyên tố thì ta áp dụng thuật toán sàng
nguyên tố Eratosthenes là phù hợp. Độ phức tạp của sàng nguyên tố là O(nlogn)
Tiếp theo cần sắp xếp mảng c.
Cách 1: Nếu sử dụng thuật toán sắp xếp nổi bọt thì độ phức tạp là O(n2)
 Độ phức tạp của toàn chương trình là O(max(nlogn, n2)) = O(n2)
Cách 2: Nếu sử dụng thuật toán sắp xếp nhanh thì độ phức tạp là O(nlogn)
 Độ phức tạp của toàn chương trình là O(max(nlogn, nlogn)) = O(nlogn)
Sử dụng thuật toán sắp xếp đếm phân phối trong trường hợp này là khá phức
tạp so với ý tưởng nêu trên, do đó không nên sử dụng thuật toán này.
Bước 4: Đánh giá, lựa chọn thuật toán
Như vậy theo bảng ước lượng (*) :
- Cách 1: độ phức tạp O(n2) => chỉ đáp ứng được 75% số test với 1 ≤ N ≤ 104.
- Cách 2: độ phức tạp O(nlogn) => đáp ứng 100% số test của bài toán với N ≤ 106
Vậy sử dụng thuật toán sắp xếp nhanh để cài đặt chương trình

27
Bước 5: Cài đặt chương trình
Code tham khảo
#include <bits/stdc++.h>
#define LL long long
#define TASK "TRONGCAY"
const int maxn=1e6+2;
const int q=1e7+2;
using namespace std;
int c[maxn], n; bool b[q];
///--------------------------
void sangnt()
{
memset(b,true,sizeof(b));
b[1]=false;
for(int i=2;i*i<=q;++i) if(b[i])
for(int j=i*i;j<=q;j+=i) b[j]=false;
}
///--------------------------
void solve()
{
sangnt();
cin>>n; int x,e,i=0;
for(int j=1;j<=n;++j)
{
cin>>x;
if(b[x]) { ++i; c[i]=x;}
}
if (i==0) cout<<-1;
sort(c+1,c+i+1);
if(i%2==0)
{

28
for(int j=1;j<=i;j+=2) cout<<c[j]<<" ";
for(int j=i;j>1;j-=2) cout<<c[j]<<" ";
}
else
{
for(int j=2;j<=i;j+=2) cout<<c[j]<<" ";
for(int j=i;j>=1;j-=2) cout<<c[j]<<" ";
}
}
///--------------------------
int main()
{
#ifndef ONLINE_JUDGE
freopen(TASK".inp", "r", stdin);
freopen(TASK".out", "w", stdout);
#endif // ONLINE_JUDGE
solve();
}
Bài 3: Bán hàng
Tại một điểm bán hàng tự động, mỗi loại hàng được gán tương ứng với một số
nguyên dương gọi là mã hàng, hai loại hàng khác nhau có mã hàng khác nhau. Mỗi
lần khách mua hàng, máy chỉ bán một loại hàng với số lượng là 1 sản phẩm và ghi
vào nhật kí của máy mã loại hàng đã bán. Sau khi kết thúc một đợt bán hàng, nhật
kí bán hàng của máy là một dãy số nguyên dương. Người quản lí cần thống kê xem
loại hàng nào đã được máy bán nhiều nhất, số lượng hàng loại đó đã bán là bao
nhiêu? Bạn hãy viết chương trình giúp người quản lý tìm loại hàng đó.
Dữ liệu vào: Vào từ file văn bản DEMHANG.INP:
- Dòng đầu tiên ghi số nguyên dương N (N≤107) là số lượng hàng đã bán.
-N dòng tiếp theo mỗi dòng ghi một số nguyên dương là mã loại hàng đã bán
trong nhật kí của máy. Giá trị các số nguyên dương không vượt quá 106.
Kết quả ra: Đưa ra file văn bản DEMHANG.OUT chỉ một dòng duy nhất ghi
mã loại hàng đã bán nhiều nhất và số lượng hàng loại đó mà máy đã bán, hai giá trị
này cách nhau một ký tự trống. Nếu như có nhiều loại hàng có cùng số lượng bán
nhiều nhất thì in ra mã loại hàng có giá trị bé nhất.
29
Ví dụ:
DEMHANG.INP DEMHANG.OUT
11 2 4
12232452676
Giới hạn: - 60% số test với 1 ≤ N ≤ 104.
- 40% số test với 104 < N ≤ 107.
Bước 1: Xác định bài toán
- Input: Dãy N số nguyên ai (1 ≤ ai ≤ 106, 1 ≤ i ≤ N, 1 ≤ N ≤ 107 ) là mã loại
hàng đã bán thứ i
- Output: mã loại hàng đã bán nhiều nhất và số lượng hàng loại đó mà máy đã
bán
Bước 2: Đặc điểm dữ liệu:
Dữ liệu vào N ≤ 107 và ai ≤ 106
Bước 3: Các cách giải có thể lựa chọn
Ý tưởng:
+ Sắp xếp dãy a1….aN
+ Với mỗi ai (loại mã hàng) đếm xem xuất hiện bao nhiêu lần trong dãy (đã
được bán mấy lần)
+ Đưa ra mã loại hàng đã bán nhiều nhất và số lượng hàng loại đó mà máy đã
bán
Các cách giải có thể lựa chọn
Cách 1: Nếu sử dụng thuật toán sắp xếp nổi bọt thì độ phức tạp là O(n2), độ
phức tạp của đoạn chương trình đếm số lần xuất hiện của mỗi ai có thể là O(n2)
 Độ phức tạp của toàn chương trình có thể là O(max(n2, n2) = O(n2)
Cách 2: Nếu sử dụng thuật toán sắp xếp nhanh thì độ phức tạp là O(nlogn), độ
phức tạp của đoạn chương trình đếm số lần xuất hiện của mỗi ai có thể là O(n2)
 Độ phức tạp của toàn chương trình là O(max(nlogn, n2)) = O(n2)
Cách 3: Nếu sử dụng thuật toán sắp xếp đếm phân phối thì độ phức tạp cỡ O(n)
(vì ai ≤ 106), và vì mảng đếm đã đếm các giá trị nên không phải đếm giá trị mỗi ai
nữa.
 Độ phức tạp của toàn chương trình là O(n)
Bước 4: Đánh giá, lựa chọn thuật toán
Như vậy theo bảng ước lượng (*) :
30
- Cách 1 và cách 2: độ phức tạp O(n 2 ) => chỉ đáp ứng được 60% số test
với N ≤ 10 4 .
- Cách 3: độ phức tạp O(n) => đáp ứng 100% số test của bài toán với N ≤ 107
Đây là bài toán đặc trưng cho dạng bài sử dụng thuật toán sắp xếp đếm phân
phối.
Vậy lựa chọn thuật toán đếm phân phối để cài đặt chương trình.
Bước 5: Cài đặt chương trình
Code tham khảo:
#include <bits/stdc++.h>
using namespace std;
const long long maxn = 10000001,maxdem=1000001;
long long a[maxn] = {},dem[maxdem] = {};
long long n,i,j,mmax,res;
main(){
freopen ("demhang.inp" , "r" , stdin);
freopen ("demhang.out" , "w" , stdout);
cin >> n;
for (i = 1; i <= n ; i++){
cin >> a[i];
dem[a[i]]++;
}
//xử lí
mmax = 0; res = 0;
for (i = 1 ; i < maxdem ; i++){
if (dem[i] > mmax ){
mmax = dem[i];
res = i;
}
}
cout << res << ' ' << mmax;
return 0;
}
31
3.2.3. Dạng 3: Sử dụng thuật toán tìm kiếm
3.2.3.1. Bài toán tìm kiếm
Trong thực tế, có rất nhiều bài toán, nhưng hầu như tất cả chúng đều quy về
một bài toán duy nhất, đó chính là bài toán tìm kiếm. Từ những nhu cầu thực tế đó,
bài toán tìm kiếm dẫn đến chúng ta phải tạo ra thuật toán tìm kiếm để giải quyết nó.
Vậy thì thuật toán tìm kiếm là gì? Thuật toán tìm kiếm là thuật toán giúp ta tìm ra
trong một tập dữ liệu đã cho một hoặc nhiều phần tử thỏa mãn yêu cầu tìm kiếm.Tùy
theo cấu trúc dữ liệu mà chúng ta sẽ có những thuật toán tìm kiếm khác nhau phù
hợp cho mỗi cấu trúc đó.
Ta xét bài toán tìm kiếm dạng đơn giản sau:
Cho dãy A gồm N số nguyên A1, A2, …AN và số nguyên x. Cần biết có hay không chỉ
số i (1≤ i ≤ N) mà Ai = x. Nếu có hãy cho biết chỉ số đó.
Trong giới hạn đề tài, chúng ta sẽ tìm hiểu 2 thuật toán tìm kiếm phổ biến
nhất là: tìm kiếm tuần tự và tìm kiếm nhị phân.
Tìm kiếm tuần tự
Tìm kiếm tuần tự (sequential search) là thuật toán tìm kiếm bằng cách duyệt
qua tất cả các phần tử của danh sách cho đến khi gặp phần tử cần tìm hoặc là đã hết
danh sách. Do cách tìm kiếm duyệt từ đầu đến cuối này, độ phức tạp thời gian của
thuật toán này sẽ là O(n).
Chúng ta có một mảng A có n phần tử bắt đầu từ vị trí 0. Để tìm kiếm phần tử
x trong mảng A này, ta làm như sau:
1. Gán i = 0.
2. So sánh giá trị của A[i] và x:
 Nếu A[i] == x thì dừng và trả về giá trị của i (vị trí của x trong mảng A).
 Nếu A[i] != x thì sang bước 3.
3. Gán i = i + 1:
 Nếu i == n (tức hết mảng) thì dừng lại và trả kết quả là -1 (không tìm thấy x).
 Nếu i < n thì quay lại bước 2.
Hàm tìm kiếm tuần tự được viết như sau:
int sequentialsearch(int A[], int n, int x)
{ for(int i=0;i<n;i++)
if (A[i]==x)
return i; //đưa ra vị trí i khi tìm thấy
return -1; //duyệt hết mảng, không tìm thấy x

32
Nhận xét: thuật toán tìm kiếm tuần tự đơn giản, dễ cài đặt, có độ phức tạp O(n)
Tìm kiếm nhị phân
Tìm kiếm nhị phân (binary search) hay còn một số tên gọi khác nữa như tìm
kiếm nửa khoảng (half-interval search), tìm kiếm logarit (logarithmic search), chặt
nhị phân (binary chop) là thuật toán tìm kiếm dựa trên việc chia đôi khoảng đang xét
sau mỗi lần lặp, sau đó xét tiếp trong nửa khoảng có khả năng chứa giá trị cần tìm,
cứ như vậy cho đến khi không chia đôi khoảng được nữa. Thuật toán tìm kiếm nhị
phân chỉ áp dụng được cho danh sách đã có thứ tự hay đã được sắp xếp.
Do cách tìm kiếm chia đôi khoảng này, sau mỗi lần lặp, khoảng đang xét lại
được chia đôi, và tiếp tục khoảng tiếp lại chia đôi khoảng đã được chia trước đó. Do
đó, độ phức tạp thời gian của thuật toán này sẽ là O(log(n)), tốt hơn rất rất nhiều so
với tìm kiếm tuần tự.
Cho một mảng A có n phần tử bắt đầu từ vị trí 0, mảng A được sắp xếp tăng
dần. Để tìm phần tử có giá trị x trong mảng A chúng ta sẽ cài đặt thuật toán tìm kiếm
nhị phân như sau:
1. Gán left = 0, right = n – 1.
2. Gán mid = (left + right) / 2 (lấy phần nguyên, đây là phần tử chính giữa của
khoảng hiện tại)
 Nếu như A[mid] == x:
 Dừng lại và trả về giá trị của mid (chính là vị trí của x trong mảng A).
 Nếu như A[mid] > x (có thể x nằm trong nửa khoảng trước):
 right = mid – 1 // giới hạn khoảng tìm kiếm lại là nửa khoảng trước
 Nếu như A[mid] < x (có thể x nằm trong nửa khoảng sau):
 left = mid + 1 // giới hạn khoảng tìm kiếm lại là nửa khoảng sau
3. Nếu left <= right:
 Đúng thì quay lại bước 2 (còn chia đôi được).
 Sai thì dừng và trả về kết quả -1 (không tìm thấy x)
int BinarySearch(int A[], int n, int x)
{ int left = 0;
int right = n - 1;
int mid;
while (left <= right)
{
mid = (left + right) / 2;

33
if (A[mid] == x)
return mid; // tìm thấy x, trả về mid là vị trí
của x trong mảng A
if (A[mid] > x)
right = mid - 1; // Giới hạn khoảng tìm kiếm lại
là nửa khoảng trước
else if (A[mid] < x)
left = mid + 1; // Giới hạn khoảng tìm kiếm lại
là nửa khoảng sau }
return -1; // không tìm thấy x
}
Nhận xét: thuật toán tìm kiếm nhị phân được sử dụng khi dãy số đã sắp xếp lúc
đó độ phức tạp là O(log(n)). Trong trường hợp dãy chưa sắp xếp, thì phải sắp xếp
rồi mới sử dụng được tìm kiếm nhị phân. Lúc này độ phức tạp phụ thuộc vào độ
phức tạp của thuật toán sắp xếp đã sử dụng trước tìm kiếm.
Kết luận:
So sánh Tìm kiếm tuần tự Tìm kiếm nhị phân
Độ phức tạp O(n) O(logn)
Đặc điểm Code đơn giản, dễ hiểu Code phức tạp hơn
Trường hợp nên sử dụng - Dãy chưa sắp xếp - Dãy đã sắp xếp
(thời gian thực hiện 1s) - tìm kiếm số lượng phần - Nếu dãy chưa sắp xếp
tử khoảng n≤ 107 (giới thì phụ thuộc thuật toán
hạn của mảng) sắp xếp sử dụng.
3.2.3.2. Bài tập ví dụ
Bài 1: Tìm kiếm nhị phân6
Cho dãy số a(n) nguyên dương. Tìm phần tử trong dãy a(n) có giá trị bằng x
Dữ liệu: Dòng đầu ghi số nguyên dương n (n≤105)
Dòng 2 ghi n số nguyên dương ai (ai ≤1018)
Dòng 3 ghi số nguyên dương T (T ≤105)
T dòng kế tiếp, mỗi dòng ghi số nguyên dương x

6
Bài SB02B trên http://laptrinhphothong.vn/
34
Kết quả: ghi ra T dòng, dòng thứ i ghi Y nếu trong dãy a(n) tồn tại phần tử x,
nếu không tồn tại ghi N
Giới hạn: - 60% số test với 1 ≤ N ≤ 103.
- 40% số test với 103 < N ≤ 105.
Bước 1: Xác định bài toán
- Input: dãy số a(n) nguyên dương (n≤105, ai ≤1018), T test (T ≤105), mỗi test
một số nguyên dương x
- Output: ghi ra T dòng, dòng thứ i ghi Y nếu trong dãy a(n) tồn tại phần tử x,
nếu không tồn tại ghi N
Bước 2: Đặc điểm dữ liệu
Dữ liệu vào gồm dãy số a(n) nguyên dương (n≤105, ai ≤1018) và T test (T≤105),
mỗi test là một số nguyên dương x
Bước 3: Các cách giải có thể lựa chọn
Đây là bài toán tìm kiếm đơn giản, đa số học sinh sẽ dùng cách tìm kiếm tuần
tự. Tuy nhiên vì dữ liệu khá lớn (n≤105 ,T ≤105) nên ta xét các cách giải sau:
- Cách 1: sử dụng thuật toán tìm kiếm tuần tự thì mỗi T độ phức tạp là O(n)
=> Độ phức tạp toàn bộ chương trình là O(T*n)
- Cách 2: - sắp xếp dãy tăng dần
- Tìm kiếm nhị phân cho mỗi T với T ≤105
Vì dãy số a(n) nguyên dương (n≤105, ai ≤1018), nên ta chọn thuật toán sắp xếp
nhanh là phù hợp. Độ phức tạp sẽ là O(nlogn) với n≤105
Tìm kiếm nhị phân cho mỗi T với T ≤105 có độ phức tạp là O(Tlogn)
 Độ phức tạp của chương trình là O(max(nlogn,Tlogn)) ≈O(nlogn) (vì n ≈ T)
Bước 4: Đánh giá, lựa chọn thuật toán
Như vậy theo bảng ước lượng (*) :
Nếu sử dụng cách 1 (tìm kiếm tuần tự cho mỗi T) thì chỉ đáp ứng được 60%
số test với 1 ≤ N ≤ 103, T≤105
Nếu sử dụng cách 2 (sắp xếp dãy an tăng dần theo sắp xếp nhanh và dùng tìm
kiếm nhị phân cho mỗi T) thì đáp ứng 100% test với N ≤ 105, T≤105
Vậy lựa chọn cách 2 để cài đặt chương trình
Bước 5: Viết chương trình
Code tham khảo

35
#include<bits/stdc++.h>
using namespace std;
const long long maxn=1e5+7;
long long n,x,t;
long long a[maxn];
int nhiphan(long long B[], long long m, long long y)
{
int left = 0;
int right = m - 1;
int mid;
while (left <= right)
{
mid = (left + right) / 2;
if (B[mid] == y)
return mid; // tìm thấy x, trả về mid là vị trí
của x trong mảng A
if (B[mid] > y)
right = mid - 1; // Giới hạn khoảng tìm kiếm lại
là nửa khoảng trước
else if (B[mid] < y)
left = mid + 1; // Giới hạn khoảng tìm kiếm lại
là nửa khoảng sau
}
return -1; // không tìm thấy x
}
int main()
{cin>>n;
for(int i=0;i<n;i++)
cin>>a[i];
sort(a,a+n);
cin>>t;
for(int j=1;j<=t;j++)
36
{cin>>x;
if (nhiphan(a,n,x)==-1)
cout<<"N"<<"\n";
else cout<<"Y"<<"\n";
}
return 0;
}

Bài 2: SEQ7
Cho dãy số gồm n số nguyên a1, a2, …, an và 2 số nguyên không âm L, R (L ≤ R).
Yêu cầu: Đếm số cặp (i, j) thỏa mãn điều kiện: i ≤ j và L ≤ |ai+…+aj| ≤ R .
Dữ liệu vào: Từ file văn bản SEQ.INP gồm:
- Dòng đầu tiên chứa 3 số nguyên n, L, R (n ≤ 105 ; 0 ≤ L ≤ R ≤ 109)
- Dòng thứ hai chứa n số nguyên dương a1, a2,…, an (ai ≤ 109)
Kết quả: Ghi ra file văn bản SEQ.OUT gồm một số nguyên duy nhất là số lượng
cặp (i, j) đếm được.
Ví dụ:
SEQ.INP SEQ.OUT
301 4
1 -1 2
Hạn chế: - Có 50% số test ứng với 0 < n ≤ 103
- Có 50% số test ứng với 103 < n ≤ 105
Bước 1: Xác định bài toán
- Input: n số nguyên dương a1, a2,…, an (ai ≤ 109, n ≤ 105), 2 số nguyên L, R
(0 ≤ L ≤ R ≤ 109)
- Output: số cặp (i, j) thỏa mãn điều kiện: i ≤ j và L ≤ |ai+…+aj| ≤ R
Bước 2: Đặc điểm dữ liệu
Dữ liệu vào gồm dãy số a(n) nguyên dương (ai ≤ 109, n ≤ 105) và 2 số nguyên
L, R (0 ≤ L ≤ R ≤ 109)
Bước 3: Các cách giải có thể lựa chọn

7
Đề thi học sinh giỏi Tỉnh năm học 2016-2017
37
Ta thấy dữ liệu vào L ≤ R ≤ 109 thì không thể sử dụng tìm kiếm tuần tự được.
Ta nghĩ đến cách tìm kiếm nhị phân, nhưng dãy này không thể sắp xếp vì sẽ thay
đổi vị trí các phần tử. Bài này vận dụng thuật toán tìm kiếm nhị phân linh hoạt như
sau:
Trước hết tính tổng các số từ 1 đến n với s[i] = a[1]+a[2]+…+a[n]
Nhận xét tại vị trí j cần tìm vị trí i (i <= j) sao cho
l <= s[j]-s[i-1] <= r
=> s[j]-l <= s[i-1] <= s[j]-r
Ta dùng BIT để đếm số lượng các số trước đó thỏa điều kiện trên.
Nhận xét là nó bị kẹp nên ta dùng phần bù : chỉ việc lấy số lượng các số >=
s[j]-l trừ đi số lượng các số > s[j]-r thì ta sẽ được phần cần tìm
Vậy tạo một BIT để đếm, nhưng số lớn (109 và hơn thế nữa) nên ta cần nén số
lại, những số ta dùng hay nói cách khác là cập nhật và lấy giá trị là s[i-1], s[i]-l và
s[i]-r+1. Ta cho vào một mảng rồi sort lại, đẩy vào một mảng mới tương ứng mỗi
giá trị sẽ có một thứ tự 1 2 3… trên BIT. Cuối cùng dùng chặt nhị phân để tìm kết
quả
Bước 4: Đánh giá, lựa chọn thuật toán
Như vậy theo bảng ước lượng (*) :
Nếu sử dụng cách 1 (Dùng mảng tính tổng rồi sử dụng tìm kiếm tuần tự ) thì
chỉ đáp ứng được 60% số test với N ≤ 103
Nếu sử dụng cách 2 (theo ý tưởng như trên) thì đáp ứng 100% test với N ≤ 105
Vậy lựa chọn cách 2 để cài đặt chương trình
Bước 5: Viết chương trình
Code tham khảo
#include<bits/stdc++.h>
#define N 100005
using namespace std;
int n,L,R;
long long s[N];
int main()
{
freopen("SEQ.inp","r",stdin);
freopen("SEQ.out","w",stdout);

38
ios::sync_with_stdio(0);
cin>>n>>L>>R;
for (int i=1; i <= n; i++)
{
int a;
cin>>a;
s[i]=s[i-1]+a;
}
if (1LL*n*n <= 50000000)
{
long long res=0;
for (int i=1; i <= n; i++)
{
if (abs(s[i]) >= L && abs(s[i]) <= R)
res++;
for (int j=1; j < i; j++)
{
long long a=abs(s[i]-s[j]);
if (a >= L && a <= R)
res++;
}
}
cout<<res;
return 0;
}
s[++n]=0;
sort(s+1,s+1+n);
int l=1, r=1;
long long res=0;
for (int i=2; i <= n; i++)
{

39
while (l <= i && abs(s[i]-s[l]) >= L)
l++;
if (l > 1 && abs(s[i]-s[l]) < L)
l--;
while (r <= i && abs(s[i]-s[r]) > R)
r++;
if (abs(s[i]-s[l]) >= L && abs(s[i]-s[r]) <= R)
res+=l-r+1;
}
cout<<res;
return 0;
}
Tóm lại, khi giải một bài toán ngoài việc giải đúng yêu cầu của bài toán thì với
những bài toán có dữ liệu vào lớn còn cần phải lựa chọn thuật toán tối ưu nhất thích
hợp đáp ứng dữ liệu vào. Trên đây chúng ta đã tìm hiểu một số trường hợp cụ thể.
Tuy nhiên, trong quá trình giải người lập trình phải rất linh hoạt mới đáp ứng được
yêu cầu của bài toán. Trong quá trình giảng dạy, giáo viên nên cho học sinh luyện
tập nhiều bài toán có dữ liệu lớn, với nhiều dạng khác nhau để học sinh rèn luyện
được khả năng tư duy linh hoạt và kỹ năng vận dụng sáng tạo theo bài toán cụ thể.
Sau đây là một số bài tập giúp học sinh luyện tập, vận dung cho các dạng đã nêu.
3.2.4. Bài tập luyện tập
Bài 1: CHIA HẾT
Lợi rất hào hứng với các bài toán số học, lần này Lợi muốn thử năng lực lập
trình của các bạn với bài toán như sau:
Cho bốn số nguyên dương a, b, x, y (a ≤ b; a, b, x, y ≤ 109).
Yêu cầu: Đếm số lượng số nguyên dương thuộc đoạn [a ; b] chia hết cho x hoặc chia
hết cho y.
Dữ liệu vào: Từ tệp văn bản CHIAHET.INP gồm một dòng duy nhất chứa bốn số
nguyên dương a, b, x, y và các số cách nhau ít nhất một dấu cách trống.
Kết quả: Ghi ra tệp văn bản CHIAHET.OUT số lượng số nguyên dương đếm được.
Ví dụ:
CHIAHET.INP CHIAHET.OUT
2 15 3 5 7

40
Giải thích:
a = 2, b = 15, x = 3, y = 5 các số nguyên dương thuộc đoạn [2 ; 15] chia hết cho
3 hoặc 5 là 3, 5, 6, 9, 10, 12, 15 nên số lượng là 7.
Giới hạn: - 40% số test với a ≤ b ≤ 102
- 40% số test với a ≤ b ≤ 106
- 20% số test với a ≤ b ≤ 109
Bài 2: TỔNG CHẴN
Trên giá sách của thư viện trường THPT chuyên Phan Bội Châu có N quyển sách
được đánh số thứ tự 1, 2, ... , N (2 < N < 106). Mỗi quyển sách có số lượng trang tương
ứng là a1, a2, ... , aN (ai < 104, 1 < i < N).
Yêu cầu: Tính số lượng tất cả các cách để có thể lấy 2 quyển sách trong số N quyển
sách, sao cho tổng số lượng trang sách trong N - 2 quyển sách còn lại trên giá là một số
chẵn.
Dữ liệu vào: Từ tệp văn bản TONGCHAN.INP gồm hai dòng:
 Dòng thứ nhất chứa số nguyên dương N.
 Dòng thứ hai chứa N số nguyên dương a1, a2, ..., aN, các số cách nhau ít nhất một
dấu cách trống.
Kết quả: Ghi ra tệp văn bản TONGCHAN.OUT gồm một dòng duy nhất chứa một
số nguyên là số cách có thể chọn.
Ví dụ:
TONGCHAN.INP TONGCHAN.OUT
5 4
36 58 27 64 75
Giải thích:
Có 4 cách chọn là:
Cách 1: Lấy quyển 1 và quyển 2 thì tổng số trang sách của các quyển còn lại:
27+64+75 = 166 là số chẵn
Cách 2: Lấy quyển 1 và quyển 4 thì tổng số trang sách của các quyển còn lại:
58+27+75 = 160 là số chẵn
Cách 3: Lấy quyển 2 và quyển 4 thì tổng số trang sách của các quyển còn lại:
36+27+75 = 138 là số chẵn
Cách 4: Lấy quyển 3 và quyển 5 thì tổng số trang sách của các quyển còn lại:
36+58+64 = 158 là số chẵn
41
Giới hạn: - 60% số test với N ≤ 104
- 40% số test với 104 < N ≤ 106
Bài 3: TÌM SỐ
Hãy tìm số nguyên dương nhỏ nhất có chữ số hàng đơn vị là d (1≤d≤9) sao cho
nếu chuyển chữ số hàng đơn vị lên trước chữ số đầu tiên thì ta được một số mới gấp
k lần số cũ (1≤k≤9)
Dữ liệu: Vào từ file văn bản NUMBER.INP
Gồm nhiều dòng, mỗi dòng chứa hai số nguyên dương d, k cách nhau ít nhất
một dấu cách.
Kết quả: Ghi ra file văn bản NUMBER.OUT
Gồm nhiều dòng, mỗi dòng ghi một số nguyên tìm được ứng với d,k ở dòng
tương ứng của file dữ liệu. Ghi -1 nếu không có số thỏa mãn
Ví dụ:
NUMBER.INP NUMBER.OUT
8 4 205128
Ghi chú: Có 50% số test kết quả không vượt quá 106
Bài 4: Đếm từ
Từ là một dãy gồm các chữ cái thường 'a'..'z' đứng liền nhau không chứa dấu cách.
Cho xâu s gồm các chữ cái thường 'a' .. 'z' và dấu cách. Mỗi từ trong xâu s dài
không quá 10 kí tự.
Yêu cầu: Đếm số lượng từ khác nhau nhận được từ xâu s.
Dữ liệu vào: Đọc từ tệp văn bản DEMTU.INP gồm một xâu s có độ dài không vượt
quá 106 (có ít nhất 1 từ).
Dữ liệu ra: Ghi ra tệp văn bản DEMTU.OUT gồm duy nhất một số là kết quả cần
tìm.
Ví dụ:
DEMTU.INP DEMTU.OUT
roi nhu bong bong 3
Bài 5: Dãy con
Cho dãy gồm N số a1, a2, ..., an. Hãy tìm dãy con liên tiếp dài nhất có tổng bằng 0.
Dữ liệu vào: Từ tệp DAYCON.INP gồm:
+ Dòng đầu tiên chứa số nguyên dương N (N≤105).
42
+ Dòng thứ 2 chứa N số a1, a2, ..., an (|ai|≤109).
Dữ liệu ra: Ghi ra file văn bản DAYCON.OUT gồm hai dòng:
+ Dòng đầu tiên ghi độ dài dãy con liên tiếp dài nhất có tổng bằng 0.
+ Dòng thứ 2 ghi các giá trị của dãy con thỏa mãn (nếu nhiều dãy con thỏa
mãn thì ghi ra dãy con đầu tiên, nếu không có dãy thỏa mãn ghi NO).
Ví dụ:
DAYCON.INP DAYCON.OUT
10 5
3 0 2 1 -2 0 3 -2 1 -2 1 -2 0 3 -2
Bài 6: Taxi
Trong dịp ngỉ hè các bạn học sinh lớp 12 dự định tổ chức dã ngoại đến biển
sầm sơn và sẽ đi bằng taxi. Các bạn được chia thành n nhóm, nhóm thứ i gồm si bạn
(1 ≤ si ≤ 4) và mỗi chiếc taxi chở tối đa 4 hành khách. Vậy lớp 12 cần thuê ít nhất
bao nhiêu chiếc taxi để chở các nhóm đi, với điều kiện là các bạn trong nhóm phải
ngồi chung taxi (một taxi có thể trở một nhóm trở lên).
Dữ liệu vào: Từ tệp TAXI.INP gồm:
- Dòng đầu chứa số nguyên dương n (1 ≤ n ≤ 105) (số lượng các nhóm học sinh).
-Dòng số 2 chứa dãy số nguyên s1, s2, ..., sn (1 ≤ si ≤ 4). Các số nguyên cách
nhau bởi dấu cách với các si là số học sinh trong nhóm thứ i.
Dữ liệu ra: Ghi ra file văn bản TAXI.OUT là một số nguyên duy nhất là số lượng
tối thiểu xe taxi cần thiết để trở tất cả học sinh đến nơi.
Ví dụ:
TAXI.INP TAXI.OUT
5 4
12433
Tất cả file code của bài tập ví dụ và bài tập luyện tập thực hiện trong sáng
kiến kinh nghiệm tôi đã lưu vào drive https://bit.ly/3d1VY5W
4. Tính mới của SKKN
- SKKN đã đưa ra định hướng để giúp học sinh có thể lựa chọn thuật toán phù hợp
với dữ liệu bài toán để giải quyết bài toán hiệu quả nhất có thể. SKKN đã phân loại
thành các dạng thường gặp, giúp người đọc dễ hiểu và dễ liên hệ với các bài toán khác.
- Với bảng ước lượng độ phức tạp thuật toán tương ứng dữ liệu bài toán giúp
học sinh có thể có căn cứ để giải bài toán phù hợp.
43
- Hệ thống các bài toán trong ví dụ có thể không phải là những bài toán xa lạ, nhưng
trong SKKN đã đưa ra được hướng giải quyết theo hướng lựa chọn thuật toán phù hợp.
5. Hiệu quả của SKKN
Qua các năm giảng dạy và tham gia bồi dưỡng học sinh giỏi Tỉnh, tôi đã tích
lũy được những kinh nghiệm trên. Hai năm 2019-2020 và 2020-2021 tôi đã áp
dụng các giải pháp trên kết hợp sử dụng ngôn ngữ C++ để cài đặt vào thực hiện
giảng dạy và thấy khá hiệu quả. Từ những ví dụ nên trên, với mỗi bài đều được phân
tích, so sánh để làm rõ định hướng đã lựa chọn giúp học sinh dễ hiểu, dễ nhớ, dễ liên
hệ tương tự. Từ đó giúp học sinh rèn luyện và nâng cao được kĩ năng tư duy lập
trình, đáp ứng được yêu cầu ngày càng cao của các cuộc thi lập trình Tin học hiện
nay. Sau khi tôi áp dụng vào giảng dạy, hầu như các em đều tự làm được những bài
tương tự, thậm chí phức tạp hơn và luôn tìm cách để bài giải trở nên tốt nhất. Từ đó
các em tự khai thác khả năng tư duy, sáng tạo của bản thân mình và đam mê Tin học
hơn. Kết quả bồi dưỡng học sinh giỏi năm học 2020-2021 đạt giải ba với số điểm
khá cao.
Như vậy việc giúp học sinh biết cách lựa chọn thuật toán tối ưu phù hợp với dữ
liệu của bài toán đem lại hiệu quả khá tốt, tránh được học sinh bị mất điểm đáng tiếc
ở phần dữ liệu lớn. Từ đó góp phần nâng cao chất lượng bồi dưỡng học sinh giỏi.
6. Những hướng phát triển của đề tài
- SKKN có thể thêm các giải pháp khác để tạo thành chuyên đề giải pháp tối
ưu hóa thuật toán để rèn luyện và nâng cao kĩ năng lập trình cho học sinh, làm tài
liệu bồi dưỡng học sinh giỏi Tin học THPT

44
III. KẾT LUẬN VÀ KIẾN NGHỊ
1. Kết luận
Từ thực tế giảng dạy và bồi dưỡng học sinh giỏi, tôi nhận thấy: bài toán Tin
học đa dạng và phong phú. Người lập trình phải rất linh hoạt trong giải và cài đặt
chương trình. Đặc biệt với những bài toán đòi hỏi xử lý với dữ liệu lớn trong thời
gian hạn chế. Với mỗi dạng toán cần có phương pháp giải phù hợp, với mỗi bài toán
cần tìm giải thuật tối ưu cùng với việc xây dựng cấu trúc dữ liệu lưu trữ hợp lý. Qua
các năm giảng dạy, cùng với sự tìm tòi các tài liệu, các bài viết trên sách tham khảo
và mạng Internet cùng với cấu trúc đề ra và kiến thức cơ bản qua các kì thi chọn học
sinh giỏi Tỉnh, tôi thấy với các giải pháp đã nêu có thể giúp học sinh có thêm kinh
nghiệm để giải nhiều bài toán tương tự. Với mỗi giải pháp chúng ta có thể đưa ra rất
nhiều ví dụ để học sinh có thể vận dụng, tuy nhiên trong phạm vi của một sáng kiến
kinh nghiệm, tôi chỉ đưa ra một số ví dụ để trình bày. Vì kinh nghiệm chưa
nhiều, không thể tránh khỏi các hạn chế, rất mong muốn nhận được những góp ý từ
các bạn đồng nghiệp, Hội đồng khoa học các cấp và bạn bè chia sẻ, bổ sung để đề
tài có thể hoàn thiện hơn.
2. Kiến nghị
Đối với Sở giáo dục và đào tạo: nên tổ chức nhiều đợt tập huấn nâng cao chuyên
môn cho giáo viên Tin học về tiếp cận nhưng ngôn ngữ lập trình mới như C++,
Python.. để đáp ứng của chương trình giáo dục phổ thông mới.
Đối với nhà trường:
- Cần đẩy mạnh hơn nữa cuộc vận động: “Mỗi thầy cô giáo là một tấm gương
tự học và sáng tạo”.
- Ngoài ra cần có chương trình giáo dục cho học sinh nhận thức được tầm quan
trọng của bộ môn Tin học. Từ đó tạo hứng thú và đam mê cho học sinh tham gia các
kì thi học sinh giỏi.
Đối với giáo viên:
- Luôn trăn trở và tìm mọi giải pháp để bồi dưỡng năng lực giải các bài toán
cho học sinh, đặc biệt là năng lực tu duy, sáng tạo trong học lập trình, giúp học sinh
yêu học bộ môn hơn.
- Thường xuyên trao đổi với học sinh và đồng nghiệp để tìm hiểu thêm nhiều
vấn đề nghiên cứu để đáp ứng nhu cầu đổi mới dạy học.
Đối với học sinh:
- Đổi mới cách học sao cho hiệu quả nhất; nên tăng cường phương pháp tự học;
tự nghiên cứu theo chuyên đề hoặc dự án theo sự hướng dẫn của giáo viên.

45
TÀI LIỆU THAM KHẢO
1. Tài liệu chuyên Tin học - Quyển 1 - Hồ Sĩ Đàm - Nhà xuất bản Giáo dục
2. Sáng tạo trong thuật toán và lập trình -Nguyễn Xuân Huy - NXB Thông tin
truyền thông.
3. Một số vấn đề chọn lọc trong môn Tin học -Nguyễn Xuân My – NXB Giáo dục
4. Cấu trúc dữ liệu và giải thuật – Trần Hạnh Nhi, Dương Anh Đức- NXB ĐH
quốc gia TPHCM
4. Tạp chí Tin học nhà trường – Hội Tin học Việt Nam
5. Nguồn đề học sinh giỏi Tin học Tỉnh Nghệ An các năm qua.
6. http://laptrinhphothong.vn/
7. https://vnoi.info/

46
NHẬN XÉT CỦA HỘI ĐỒNG
………………………………………………………………………………………
………………………………………………………………………………………
………………………………………………………………………………………
………………………………………………………………………………………
………………………………………………………………………………………
………………………………………………………………………………………
………………………………………………………………………………………
………………………………………………………………………………………
………………………………………………………………………………………
………………………………………………………………………………………
………………………………………………………………………………………
………………………………………………………………………………………
………………………………………………………………………………………
………………………………………………………………………………………
………………………………………………………………………………………
………………………………………………………………………………………
……………………………………………………………………………………….
CHỦ TỊCH HỘI ĐỒNG

47

You might also like