You are on page 1of 10

Phần III.

Lập trình cấu trúc

Lập trình cấu trúc (structured programming) là một mô hình lập trình nhằm viết
ra các chương trình rõ ràng, chất lượng, giảm thời gian phát triển cũng như bảo
trì chương trình.
Trong khi các ngôn ngữ lập trình bậc thấp chỉ cho phép cấu trúc lệnh tuần tự và
lệnh nhảy, các ngôn ngữ lập trình bậc cao bổ sung thêm các cấu trúc lệnh chọn,
lệnh lặp, khái niệm khối, chương trình con và đệ quy. Những thành tố này được
sử dụng rộng rãi và thay thế dần cho lệnh nhảy (yếu tố dễ làm chương trình mất
kiểm soát).
Hầu hết các nhà khoa học máy tính đều thừa nhận rằng việc học và áp dụng các
khái niệm lập trình cấu trúc là rất hữu ích, tuy vậy vẫn còn nhiều tranh cãi xung
quanh việc chấm dứt hay duy trì sự tồn tại của lệnh nhảy. C++ hỗ trợ đầy đủ các
thành phần của lập trình cấu trúc và cũng duy trì lệnh nhảy, bởi trong vài trường
hợp, việc cố gắng thay thế lệnh nhảy một cách cứng nhắc sẽ làm chương trình
phức tạp thêm đáng kể.
Trong phần này, ngoài các khái niệm của lập trình cấu trúc, ta cũng sẽ làm quen
với khái niệm bài toán trong tin học, thuật toán và phương pháp tinh chế từng
bước trong lập trình chuyển giao thuật toán cho máy tính.

73
Phần III
Lập trình cấu trúc

Chương 8. Các cấu trúc điều khiển


Chúng ta mới làm quen với một số câu lệnh C++ đơn giản như câu lệnh khai báo
và khởi tạo biến, câu lệnh gán, các biểu thức, lệnh nhập xuất dữ liệu kiểu cơ sở.
Chúng luôn kết thúc bằng dấu chấm phẩy “;” và được thực hiện theo thứ tự xuất
hiện của chúng trong chương trình.
Tuy nhiên chương trình không chỉ giới hạn trong cấu trúc đó. Trong quá trình thực
hiện, chương trình có thể quyết định thực hiện hay không thực hiện một đoạn mã
hoặc thực hiện lặp đi lặp lại một đoạn mã nhiều lần…
Ta sẽ giới thiệu lần lượt các cấu trúc điều khiển của C++. Các cấu trúc điều khiển
này dễ học hơn nhiều so với ngữ pháp tự nhiên. Nhưng từ việc hiểu đúng tới lúc
dùng được thành thạo lại cần một quá trình luyện tập bền bỉ và rút kinh nghiệm
nghiêm túc. Ta sẽ thấy có rất nhiều ví dụ trong chương này không chỉ để hiểu hoạt
động của cấu trúc điều khiển, mà để ôn lại các khái niệm đã trình bày trong chương
trước cũng như giới thiệu một số hàm chuẩn hay dùng nhằm hỗ trợ việc lập trình
hiệu quả hơn.
8.1. Cấu trúc tuần tự
Khi các câu lệnh được viết liên tiếp tạo thành một khối lệnh, chúng sẽ được thực
hiện tuần tự theo đúng thứ tự ấy.
8.1.1. Giải phương trình bậc nhất
Ví dụ đầu tiên được chọn là chương trình giải phương trình bậc nhất:
𝑎𝑥 + 𝑏 = 0 (𝑎 ≠ 0)
𝑏
Ta biết rằng phương trình này có duy nhất một nghiệm 𝑥 = − . Chương trình có
𝑎
nhiệm vụ cho nhập vào hai số thực 𝑎, 𝑏 và in ra nghiệm 𝑥 của phương trình.

 LinearEqn.cpp ✓  Giải phương trình bậc nhất


1 | #include <iostream> Cho a, b = 3 2
2 | using namespace std; Nghiem x = -0.666667
3 |
4 | int main()
5 | {
6 | double a, b;
7 | cout << "Cho a, b = ";
8 | cin >> a >> b;
9 | cout << "Nghiem x = " << -b / a;
10 | }
Cấu trúc tuần tự thể hiện trong khối lệnh của hàm main:
Dòng 6 khai báo hai biến số thực double tên 𝑎 và 𝑏.
Dòng 7 là lệnh in ra màn hình dòng chữ “Cho a, b = ”.

Lê Minh Hoàng
74
Chương 8
Các cấu trúc điều khiển

Dòng 8 là lệnh đọc dữ liệu, chờ người dùng nhập vào hai giá trị số thực, sau đó giá
trị thứ nhất được gán cho biến 𝑎, giá trị thứ hai được gán cho biến 𝑏
Dòng 9 là lệnh in ra màn hình dòng chữ “Nghiem x = ”, tiếp theo là giá trị của biểu
thức −𝑏/𝑎 ứng với hai giá trị 𝑎, 𝑏 vừa nhập vào.
8.1.2. Tính diện tích hình tròn
Ví dụ tiếp theo là chương trình tính diện tích hình tròn biết đường kính của nó. Ta
𝑑2
biết rằng hình tròn đường kính 𝑑 có diện tích bằng 𝜋 × . Giá trị số 𝜋 có thể viết
4
xấp xỉ trong hệ thập phân, tuy nhiên làm như vậy dễ nhầm lẫn.
Thư viện cmath cung cấp rất nhiều hàm, trong đó có các hàm lượng giác và hàm
ngược của chúng:
Tên hàm Tham số Giá trị kết quả Tên hàm toán học
cos(α) 𝛼 ∈ (−∞; +∞) ∈ [−1; +1] Cosine of α
sin(α) 𝛼 ∈ (−∞; +∞) ∈ [−1; +1] Sine of α
tan(α) 𝛼 ∈ (−∞; +∞) ∈ [−∞; +∞] Tangent of α
acos(t) 𝑡 ∈ [−1; +1] ∈ [0; 𝜋] Arccosine of t
asin(t) 𝑡 ∈ [−1; +1] ∈ [−𝜋/2; +𝜋/2] Arcsine of t
atan(t) 𝑡 ∈ [−∞; +∞] ∈ [−𝜋/2; +𝜋/2] Arctangent of t
atan2(x, y) 𝑥, 𝑦 ∈ [−∞; +∞] ∈ [−𝜋/2; +𝜋/2]
1
Trong bảng trên, 𝛼 là số thực tương ứng với số đo cot 𝛼 =
tan 𝛼
góc theo đơn vị radian (rad) * (180° = 𝜋 (rad)).
C++ không cung cấp hàm cot(𝛼) để tính cotangent,
tuy nhiên ta có thể tính qua công thức: sin 𝛼 tan 𝛼
𝛼
cot(𝛼) = 1/ tan(𝛼)
1 cos 𝛼
Cách viết miền giá trị [−∞; +∞] có nghĩa là tham
số hoặc kết quả hàm có thể bằng INFINITY và -
INFINITY (hai giá trị đặc biệt trong các kiểu số
thực)
Các hàm acos, asin atan là hàm ngược tương ứng với các hàm cos, sin và tan:
acos(𝑡) = 𝛼 ⟹ cos(𝛼) = 𝑡
asin(𝑡) = 𝛼 ⟹ sin(𝛼) = 𝑡
atan(𝑡) = 𝛼 ⟹ tan(𝛼) = 𝑡
Hàm atan2(𝑥, 𝑦) trả về số đo góc (có hướng) tạo bởi tia Ο𝑥 với tia OM trong đó M
là điểm có tọa độ (𝑥, 𝑦). Ví dụ: atan2(0. ,1. ) = atan(𝐼𝑁𝐹𝐼𝑁𝐼𝑇𝑌) = 𝜋/2.

*Trên đường tròn đơn vị (bán kính 1), lấy một cung tròn độ dài 1, góc ở tâm tương ứng với cung
đó có độ lớn đúng 1 radian.

Ngôn ngữ lập trình C++


75
Phần III
Lập trình cấu trúc

Các hàm lượng giác và hàm ngược của chúng có nhiều phiên bản, đều trả về kết
quả kiểu số thực. Kiểu này chính là kiểu của tham số nếu tham số thuộc kiểu số
thực, và là kiểu double nếu tham số thuộc kiểu số nguyên. Trình dịch sẽ lựa chọn
phiên bản phù hợp với tham số của hàm.
Bằng các hàm ngược của hàm lượng giác, ta có thể tính hằng số 𝜋 bằng nhiều cách:
𝜋 = atan(1) × 4 = acos(−1) = atan(INFINITY) × 2 = atan2(0,1) × 2
Ta chọn cách viết đầu tiên để khai báo hằng số 𝜋:
const long double Pi = atan(1.L) * 4;

 CircleArea.cpp ✓  Tính diện tích hình tròn


1 | #include <iostream> Cho duong kinh: 2
2 | #include <cmath> Dien tich: 3.14159
3 | using namespace std;
4 | const long double Pi = atan(1.L) * 4;
5 |
6 | int main ()
7 | {
8 | long double d;
9 | cout << "Cho duong kinh: ";
10 | cin >> d;
11 | cout << "Dien tich: " << d * d / 4 * Pi;
12 | }
Biến 𝑑 kiểu long double được khai báo trong khối lệnh của hàm main, trong khi
hằng Pi được khai báo bên ngoài. Một khai báo không nằm trong bất kỳ hàm nào
gọi là khai báo toàn cục (global). Những định danh (tên) khai báo toàn cục có thể
dùng ở bất cứ đâu trong chương trình, còn những định danh khai báo trong khối
lệnh chỉ được dùng nội bộ trong khối lệnh đó mà thôi. Trong chương trình đơn
giản này chỉ có một hàm, có thể khai báo hằng Pi trong khối lệnh hàm main hoặc
khai báo biến 𝑑 là biến toàn cục đều được.
Lý do cách tính hằng 𝜋 = atan(1) × 4 được chọn mà không dùng acos(−1) khá
đặc biệt. Thực ra trong các bộ đồng xử lý toán học chuyên tính toán số thực, cách
viết acos(−1) không những đúng mà còn ngắn gọn hơn. Tuy nhiên khi tính toán
và kể cả gán giá trị số thực, người ta luôn đề phòng một sai số nhất định do không
biết trình dịch và bộ xử lý làm gì bên trong. Nếu giá trị −1 vì một lý do nào đó bị
giảm đi dù chỉ một lượng rất nhỏ, nó sẽ nằm ngoài khoảng phạm vi chấp nhận cho
tham số ([−1; +1]), khiến cho phép tính trở thành hành vi không xác định (thông
thường hàm acos() sẽ trả về NAN nếu tham số nằm ngoài khoảng [−1; +1]).
Khi in một giá trị biểu thức số thực, mặc định luồng xuất chuẩn sẽ in ra tối đa 6 chữ
số. Muốn in ra khuôn dạng số thập phân với 𝑘 chữ số sau dấu chấm thập phân, ta
dùng lệnh:
std::cout << std::fixed << std::setprecision(k);

Lê Minh Hoàng
76
Chương 8
Các cấu trúc điều khiển

Lệnh này đẩy ra std::cout hai đối tượng: std::fixed để thiết lập in số thực theo
khuôn dạng thập phân thông thường, tiếp theo là đẩy ra std::cout đối tượng
std::setprecision(𝑘) để thiết lập in số thực với 𝑘 chữ số sau dấu chấm thập phân.
Sau khi đã thiết lập, tất cả giá trị số thực đẩy ra std::cout sẽ được in ra dưới khuôn
dạng đó. Chú ý nạp thư viện iomanip để dùng được std::setprecision(. ).

 CircleArea2.cpp ✓  Tính diện tích hình tròn


1 | #include <iostream> Cho duong kinh: 2
2 | #include <cmath> Dien tich: 3.1415926535897932
3 | #include <iomanip>
4 | using namespace std;
5 | const long double Pi = atan(1.L) * 4;
6 |
7 | int main ()
8 | {
9 | long double d;
10 | cout << "Cho duong kinh: ";
11 | cin >> d;
12 | cout << fixed << setprecision(16);
13 | cout << "Dien tich: " << d * d / 4 * Pi;
14 | }
8.1.3. Bài toán và thuật toán
Ta tạm dừng trình bày về C++ để làm rõ một số vấn đề của các bài toán tin học.
Xét một ví dụ về ba cách phát biểu vấn đề:
Bài toán A: Một người đi siêu thị mua một vài sản phẩm giống nhau. Khi thanh toán,
mỗi túi của siêu thị chỉ gói được tối đa một số sản phẩm nhất định. Hỏi nhân viên
bán hàng cần dùng ít nhất bao nhiêu túi để gói hàng cho khách.
Bài toán B: Cho 𝑛 sản phẩm, mỗi túi có thể chứa tối đa 𝑘 sản phẩm. Cho 𝑛 và 𝑘 (0 ≤
𝑛 ≤ 109 ; 1 ≤ 𝑘 ≤ 109 ), hãy cho biết số túi ít nhất cần dùng.
Bài toán C: Một khách hàng đi siêu thị mua 100 sản phẩm giống nhau. Mỗi túi của
siêu thị có thể đựng được tối đa 6 sản phẩm. Hỏi để gói hết các sản phẩm cho khách
thì nhân viên bán hàng cần ít nhất bao nhiêu túi.
Mặc dù cả ba vấn đề trên có gì đó rất chung, sự khác biệt nằm ở chỗ:
(A) là bài toán tổng quát (abstract problem), vấn đề mà các nhân viên bán hàng,
các ông chủ siêu thị phải giải quyết khá thường xuyên; Bài toán tổng quát thường
cho bởi những thông tin khá mơ hồ và hình thức, yêu cầu về lời giải không cụ thể
và rõ ràng.
(B) là mô hình (model) toán học của bài toán tổng quát, mô hình này xác định rõ
ràng phải giải quyết vấn đề gì?, với giả thiết nào đã cho và lời giải cần phải đạt
những yêu cầu gì? Mô hình toán học cho phép tìm ra giải pháp cho bài toán tổng
quát. Việc tìm kiếm giải pháp có thể bắt đầu từ việc hiểu cách thức con người giải
quyết vấn đề tổng quát, cũng có thể do sự khác biệt (cả về ưu điểm và nhược điểm)

Ngôn ngữ lập trình C++


77
Phần III
Lập trình cấu trúc

của máy tính mà cách thức giải quyết vấn đề trên mô hình toán học hoàn toàn khác
biệt so với cách thông thường của con người. Giải pháp này sẽ được chuyển giao
ra thành chương trình cho máy tính thực hiện.
(C) là một trường hợp cụ thể (instance) của bài toán tổng quát trong thực tế mà
máy tính sẽ tuân theo chính xác giải pháp (đã được lập trình) để đưa ra kết quả
với trường hợp cụ thể đó.
Cần hiểu rõ: máy tính không giải được bài toán tổng quát, máy tính chỉ giải một
trường hợp cụ thể của bài toán tổng quát xác định qua dữ liệu cụ thể. Lời giải
của trường hợp cụ thể này đạt yêu cầu hay không đạt yêu cầu, tốt hay không tốt…
là phụ thuộc vào giải pháp được lập trình sẵn, giải pháp đó (gọi là thuật toán) được
xây dựng trên mô hình của bài toán và việc này phải do con người thực hiện.
Những bài toán tin học được phát biểu theo dạng “hỗn hợp” giữa vấn đề thực tế và
mô hình toán học. Không có liên hệ thực tế, bài toán trở nên khô khan và vô bổ.
Không có mô hình toán học, các ràng buộc dữ liệu và yêu cầu chất lượng lời giải
trở nên mập mờ dẫn tới khó tìm thuật toán cũng như viết chương trình. Lưu ý là
các ràng buộc dữ liệu cũng như các yêu cầu chất lượng lời giải đôi khi phải được
ngầm hiểu qua bài toán thực tế: Chẳng hạn tuổi của một người luôn phải là số
không âm, hay số quả trứng trong một giỏ chắc chắn phải là số nguyên và cũng
không bao giờ là số âm được.
Đây không phải là cuốn sách về thuật toán nên ta chỉ trình bày sơ lược các khái
niệm như vậy. Một số ví dụ trong tài liệu này đôi khi sử dụng thẳng mô hình toán
học cho ngắn gọn, như bài toán giải phương trình bậc nhất hay tính diện tích hình
tròn, bởi ứng dụng thực tế của nó đã được môn toán trình bày từ bậc phổ thông.
Một số ví dụ khác dựa trên bài toán thực tế, tuy nhiên mô hình có thể không hoàn
toàn khớp với bài toán đó. Một số ràng buộc có thể bị lược bỏ nếu chúng không
quá quan trọng, cũng có thể sửa đổi cho rõ ràng hơn, cũng có thể tổng quát lên để
tìm giải pháp phổ dụng hơn. Như ví dụ ở mô hình (B), không có khách hàng nào
vào siêu thị mua tới 1 tỉ món hàng (109 ), và cũng không có túi nào của siêu thị dùng
để chứa tới 1 tỉ sản phẩm cả. Nhưng mô hình ấy lại có thể chuyển đổi để xử lý một
bài toán khác: Chẳng hạn người ta muốn phân 𝑛 vi khuẩn vào một số ít nhất các
nhóm để làm thí nghiệm, mỗi nhóm chứa không quá 𝑘 vi khuẩn…, khi ấy số lượng
vi khuẩn lên tới 109 lại là sự kiện rất bình thường.
Ngay cả những ràng buộc hoàn toàn phi thực tế cũng có thể được đưa vào mô hình,
mục đích là để bắt buộc người lập trình phải chọn cách thức khai báo phù hợp và
phải tìm một giải pháp tốt. Làm quen với các bài toán tin học, bạn sẽ được giả định
có những ngôi nhà 1 tỉ tỉ tầng, những thực thể nhanh hơn tốc độ ánh sáng, hay hai

Lê Minh Hoàng
78
Chương 8
Các cấu trúc điều khiển

người chơi một trò chơi với các đống sỏi lên tới 1 tỉ viên… Thay vì thắc mắc, hãy
tìm giải pháp cho nó.
Ta bắt đầu với ví dụ ngay ở đầu mục này:
Một người đi siêu thị mua 𝑛 sản phẩm giống nhau (0 ≤ 𝑛 ≤ 109 ), mỗi túi của siêu
thị có thể gói được tối đa 𝑘 sản phẩm (1 ≤ 𝑘 ≤ 109 ). Hỏi khi thanh toán, nhân viên
bán hàng của siêu thị cần dùng ít nhất bao nhiêu túi để gói hàng cho khách.
Yêu cầu: Viết chương trình nhập vào hai số nguyên 𝑛 (số sản phẩm được mua) và
𝑘 (số sản phẩm tối đa có thể đưa vào một túi), cho biết số túi ít nhất cần dùng để
gói hàng.
Thuật toán để giải quyết dựa trên chính hành vi thực tế của nhân viên siêu thị: Lấy
túi cho đầy sản phẩm vào, nếu túi đã đầy và còn sản phẩm chưa được gói thì lấy
một túi mới... Như vậy số túi chứa đầy đủ 𝑘 sản phẩm là ⌊𝑛/𝑘⌋, sau khi dùng hết số
lượng túi này thì số sản phẩm còn lại là 𝑛 % 𝑘, nếu con số này khác 0 ta cần thêm
đúng 1 túi nữa. Công thức để tính số túi có thể viết trong C++ là:
n / k + (n % k != 0 ? 1 : 0)
Công thức khác dựa vào bản chất của phép ngầm chuyển đổi kiểu từ bool sang int
(false: 0, true: 1) là:
n / k + (n % k != 0)
hoặc:
n / k + bool(n % k)
Tuy nhiên cách viết gọn nhất có thể là:
(n + k - 1) / k
Công thức trên có thể lý giải như sau: Trước khi cho sản phẩm cuối cùng vào túi,
𝑛−1
ta có đúng ⌊ ⌋ túi đầy, dù sản phẩm cuối cùng phải cho vào túi mới hay đưa vào
𝑘
𝑛−1 𝑛+𝑘−1
túi chưa đầy thì ta vẫn cần thêm 1 túi nữa. Kết quả là ⌊ + 1⌋ = ⌊ ⌋. Công
𝑘 𝑘
thức này cũng đúng kể cả khi 𝑛 = 0.
Với ràng buộc dữ liệu 𝑛, 𝑘 ≤ 109 , kiểu số nguyên int là đủ để khai báo hai biến này
cũng như để thực hiện các phép tính cần thiết.

 WRAP.cpp ✓ 
1 | #include <iostream> Cho n, k = 100 6
2 | using namespace std; So tui = 17
3 |
4 | int main()
5 | {
6 | int n, k;
7 | cout << "Cho n, k = ";
8 | cin >> n >> k;
9 | cout << "So tui = " << (n + k - 1) / k;
10 | }

Ngôn ngữ lập trình C++


79
Phần III
Lập trình cấu trúc

Có một cách tính khác: Số túi cần tìm sẽ là số nguyên 𝑞 nhỏ nhất thỏa mãn 𝑞 × 𝑘 ≥
𝑛, hay nói cách khác 𝑞 là số nguyên nhỏ nhất không nhỏ hơn 𝑛/𝑘. Trong toán học
ký hiệu giá trị này là ⌈𝑛/𝑘⌉, còn trong C++ ta có thể viết là ceil(double(𝑛)/𝑘) để
tính phép chia số thực 𝑛/𝑘 rồi làm tròn lên. Tuy nhiên ta không nên sử dụng các
phép tính số thực chừng nào vẫn còn có thể dùng các phép tính số nguyên để thay
thế nhằm tránh sai số không đáng có.
Hàm ceil(𝑥): Trả về số nguyên nhỏ nhất ≥ 𝑥 (trần của 𝑥, ký hiệu ⌈𝑥⌉).
Hàm floor(𝑥): Trả về số nguyên lớn nhất ≤ 𝑥 (sàn của 𝑥, ký hiệu ⌊𝑥⌋).
Hai hàm này nằm trong thư viện cmath. Kết quả có kiểu trùng với kiểu của 𝑥 nếu
𝑥 thuộc kiểu số thực, kết quả có kiểu double nếu 𝑥 thuộc kiểu số nguyên. (Chú ý là
mặc dù kết quả nguyên nhưng hai hàm này trả về kiểu số thực). Ví dụ:
ceil(3.2) = 4.0
ceil(−3.2) = −3.0
floor(3.6) = 3.0
floor(−3.6) = −4.0
Ta xét một bài toán tin học khác:
Một người nông dân cần quây một khu đất hình chữ nhật để trồng rau. Diện tích
khu đất đó không được nhỏ hơn 𝑛 mét vuông (𝑛 là số nguyên dương không quá
1018 ) và khi quây xong, người nông dân phải dựng rào bao quanh khu đất. Vì chiều
ngang của mỗi đoạn tường rào mua sẵn dài đúng 1 mét nên để đỡ mất công, anh
ta muốn độ dài cạnh của khu đất cũng phải là số nguyên và để giảm chi phí, anh ta
muốn chu vi khu đất nhỏ nhất có thể.
Yêu cầu: Viết chương trình cho nhập vào số 𝑛 và đưa ra độ dài hai cạnh của khu
đất theo phương án tối ưu tìm được.
Bỏ qua những điều phi thực tế như diện tích khu đất trồng rau có thể lên tới 1 tỉ tỉ
mét vuông hay để rào khu đất đó lại cần tới hàng tỉ đoạn tường rào. Ta có thể mô
hình hóa bài toán này như sau:
Dữ liệu được cho (đầu vào – input): Số nguyên dương 𝑛 ≤ 1018
Yêu cầu: Tìm hai số nguyên dương 𝑎, 𝑏 thỏa mãn:
𝑎×𝑏 ≥𝑛
{
𝑎 + 𝑏 → min; (𝑎 + 𝑏 nhỏ nhất có thể)
Kết quả cần đưa ra (đầu ra – output): Hai số 𝑎, 𝑏 tìm được chính là số đo hai cạnh
của hình chữ nhật tính bằng mét.
Việc xây dựng thuật toán lại là vấn đề hoàn toàn con người, dựa vào các kỹ năng
toán học và logic. Không giảm tính tổng quát, giả sử 𝑎 ≤ 𝑏. Với bất kỳ phương án

Lê Minh Hoàng
80
Chương 8
Các cấu trúc điều khiển

nào mà 𝑎 và 𝑏 chênh lệch nhau nhiều hơn 1 đơn vị, ta có thể tăng 𝑎 lên 1 và giảm
𝑏 đi 1, điều này làm cho chu vi không đổi nhưng diện tích được tăng lên:
(𝑎 + 1) × (𝑏 − 1) = 𝑎𝑏 + ⏟
𝑏 − 𝑎 − 1 > 𝑎𝑏
>0

Suy ra tồn tại phương án thỏa mãn yêu cầu mà 𝑎 và 𝑏 chênh lệch nhau không quá
1, ta sẽ tìm một phương án như vậy.
Nhận xét thứ hai là 𝑏 ≥ ⌈√𝑛⌉, thật vậy, nếu 𝑏 < ⌈√𝑛⌉ thì do 𝑏 là số nguyên, ta có
𝑏 < √𝑛 và vì thế 𝑎 × 𝑏 ≤ 𝑏 2 < 𝑛, trái với ràng buộc.
Ta cũng suy ra luôn được 𝑎 ≤ ⌈√𝑛⌉, bởi khi 𝑏 ≥ ⌈√𝑛⌉, chọn 𝑎 = ⌈√𝑛⌉ cũng đủ để
𝑎 × 𝑏 ≥ 𝑛 rồi, tăng 𝑎 lên nữa sẽ không tối ưu về chu vi. Vậy thì:
𝑎 ≤ ⌈√𝑛⌉ ≤ 𝑏
Bây giờ ta chỉ ra rằng chắc chắn 𝑏 = ⌈√𝑛⌉:
Nếu 𝑎 = ⌈√𝑛⌉, khi đó chỉ cần chọn 𝑏 = ⌈√𝑛⌉ cũng đủ để 𝑎 × 𝑏 ≥ 𝑛, tăng 𝑏 lên
nữa sẽ không tối ưu về chu vi.
Nếu 𝑎 < ⌈√𝑛⌉, do tính nguyên của cả 𝑎 và 𝑏, ta suy ra 𝑏 = ⌈√𝑛⌉ bởi nếu không
𝑎 kém 𝑏 nhiều hơn 1 đơn vị.
Vậy thì thuật toán có thể tóm tắt lại: Tính 𝑏 = ⌈√𝑛⌉, sau đó tính 𝑎 là số nguyên
𝑛
dương nhỏ nhất thỏa mãn 𝑎 × 𝑏 ≥ 𝑛 theo công thức 𝑎 = ⌈ ⌉, công thức này ta đã
𝑏
giới thiệu ở ví dụ trước về cách viết trong C++: 𝑎 = (𝑛 + 𝑏 − 1)/𝑏;
Trong chương trình này ta sẽ phải thực hiện phép khai căn bậc 2. Thư viện cmath
hỗ trợ hàm sqrt(𝑥) để tính căn bậc 2 của 𝑥, kết quả hàm cùng kiểu với 𝑥 nếu 𝑥
thuộc kiểu số thực, kết quả hàm thuộc kiểu double nếu 𝑥 thuộc kiểu số nguyên. Vì
ta cần tính sqrt(𝑛) với 𝑛 có thể cần tới 18 chữ số có nghĩa*, biến 𝑛 được khai báo
kiểu long long để đọc dữ liệu, và được ép kiểu thành long double khi tính căn để
tránh sai số tính toán (một cách khác là dùng hàm sqrtl(𝑛)).

*Giả thiết cho 𝑛 ≤ 1018 , kể cả khi 𝑛 = 1018 có 19 chữ số nhưng giá trị này được biểu diễn dưới
dạng 1e18 trong kiểu số thực, chỉ cần 1 chữ số có nghĩa là đủ.

Ngôn ngữ lập trình C++


81
Phần III
Lập trình cấu trúc

RectLand .cpp ✓ 
1 | #include <iostream> Cho dien tich toi thieu: 123
2 | #include <cmath> Kich thuoc: 11 * 12
3 | using namespace std;
4 |
5 | int main()
6 | {
7 | long long n;
8 | cout << "Cho dien tich toi thieu: ";
9 | cin >> n;
10 | int a, b;
11 | b = ceil(sqrt((long double)n));
12 | a = (n + b - 1) / b;
13 | cout << "Kich thuoc: " << a << " * " << b;
14 | }
8.2. Cấu trúc ghép
Khi các câu lệnh được viết liên tiếp tạo thành một khối lệnh và được đặt vào giữa
cặp ngoặc nhọn ({… }), chúng được coi như một lệnh duy nhất được gọi là lệnh
ghép.
Công dụng của lệnh ghép là ở một số nơi trong chương trình chỉ được viết một
lệnh duy nhất, nếu chúng ta muốn thực hiện nhiều lệnh, cần phải “gói” chúng vào
thành một lệnh ghép.
Lệnh ghép không cần kết thúc bởi dấu chấm phẩy “;” (dù có thừa cũng không sao).
Phần nằm giữa hai dấu ngoặc nhọn là khối lệnh của lệnh ghép. Những định danh
(hằng, biến, …) khai báo bên trong một khối lệnh chỉ được dùng bên trong khối
lệnh đó mà thôi (ta sẽ trình bày rõ hơn về các khối lệnh và phạm vi nhìn thấy của
định danh trong một chương riêng)
1 | #include <iostream> #include <iostream>
2 |
3 | int main() int main()
4 | { {
5 | int a = 0; int a = 0;
6 | { //Bắt đầu lệnh ghép { //Bắt đầu lệnh ghép
7 | int b = a + 1; //OK int b = a + 1; //OK
8 | std::cout << b; //OK } //Kết thúc lệnh ghép
9 | } //Kết thúc lệnh ghép std::cout << b; //Lỗi
10 | } }
Hai chương trình trên đều có biến 𝑎 được khai báo và khởi tạo ở dòng 5, nằm trong
khối lệnh của hàm main, nên trong bất kỳ nơi nào của hàm main nó cũng được
nhìn thấy. Điều này làm cho lệnh ở dòng 7 của cả hai chương trình hoàn toàn hợp
lệ: Khai báo biến 𝑏 trong khối lệnh của lệnh ghép và khởi tạo nó bằng 𝑎 + 1.
Dòng 8 của chương trình bên trái nằm trong khối lệnh của lệnh ghép, nó nhìn thấy
biến 𝑏 và như thế lệnh: std::cout << b; hoàn toàn hợp lệ. Tuy nhiên dòng 9 của
chương trình bên phải nằm ngoài khối lệnh của lệnh ghép, nó sẽ không nhìn thấy
biến 𝑏 và báo lỗi biên dịch.

Lê Minh Hoàng
82

You might also like