Một số lỗi thường gặp trong lập trình
đã đăng vào 18, Tháng 11, 2024, 18:00
Một số lỗi thường gặp trong lập trình
Bài viết này tổng hợp một số lỗi thường gặp trong lập trình thi đấu mà chắc hẳn bạn đã gặp phải ít nhất một lần và phải tốn hàng giờ để tìm ra nguyên nhân hoặc không hiểu lý do thật sự đằng sau là gì.
Bài viết được dịch từ blog trên codeforces của tác giả YouKn0wWho. Bài viết đầy đủ của tác giả có thể tìm thấy tại blog này.
Các code ví dụ trong bài viết này được viết bằng C++, ngôn ngữ được sử dụng phổ biến nhất trong CP.
Giờ thì hãy cùng đi vào bài viết nhé.
Một số lỗi thường gặp
🔍 Lỗi thường gặp 1
Hãy xem đoạn code dưới đây:
Code
#include <bits/stdc++.h> using namespace std; int main() { int a = 1000'000'000, b = 1000'000'000; long long product = a * b; cout << product << '\n'; return 0; }
Kết quả mong muốn là ~10^{18}~. Nhưng khi chạy đoạn code trên thì bạn lại nhận được một kết quả khác?
Lý do
Kể cả khi bạn đã khai báo biến
product
làlong long
, nhưng có vẻ như vẫn bị tràn số? Thực tế, đầu tiên trình biên dịch sẽ nhâna
vớib
sau đó mới gán kết quả của phép nhân cho biếnproduct
mà cảa
vàb
đều mang kiểuint
do đó kết quả sẽ mang kiểuint
, gây tràn số đối với kết quả ~10^{18}~.Cách khắc phục
Typecast - Ép kiểu!
Code
#include <bits/stdc++.h> using namespace std; int main() { int a = 1000'000'000, b = 1000'000'000; long long product = (long long) a * b; cout << product << '\n'; return 0; }
🔍 Lỗi thường gặp 2
Hãy xem đoạn code dưới đây:
Code
#include <bits/stdc++.h> using namespace std; int get_size(vector<int> a) { return (int) a.size(); } int main() { int n = 1000000; vector<int> a(n, 0); long long sum = 0; for (int i = 1; i <= n; i++) { sum += get_size(a); } cout << sum << '\n'; return 0; }
Hãy chạy thử đoạn code này ở máy của bạn. Ở đây ta chỉ duyệt từ ~1~ đến ~n~ nên độ phức tạp mong muốn là ~\mathcal{O}(n)~. Nhưng dường như không phải như vậy?
Lý do
- Thật ra đoạn code trên có độ phức tạp ~\mathcal{O}(n^2)~.
- Lý do là vì ta đang truyền
vector
theo kiểu tham trị - có nghĩa là mỗi lần ta gọi hàmget_size
thì một bản sao củaa
sẽ được tạo ra sau đó truyền vào hàmget_size
.
Cách khắc phục
Truyền theo kiểu tham chiếu - ta chỉ truyền địa chỉ của
vector
vào hàmCode
#include <bits/stdc++.h> using namespace std; int get_size(vector<int> &a) { return (int) a.size(); } int main() { int n = 1000000; vector<int> a(n, 0); long long sum = 0; for (int i = 1; i <= n; i++) { sum += get_size(a); } cout << sum << '\n'; return 0; }
Xem thêm: Difference Between Call by Value and Call by Reference.
Edit: Nếu bạn không muốn nội dung của mảng
a
bị thay đổi khi truyền tham chiếu thì có thể thêm từ khóaconst
:const vector<int> &a
.
🔍 Lỗi thường gặp 3
Hãy xem đoạn code dưới đây:
Code
#include <bits/stdc++.h> using namespace std; const int N = 100000; int main() { int test_cases = 100000; while (test_cases--) { int n; cin >> n; vector<int> a(N, 0); int sum = 0; for (int i = 0; i < n; i++) { sum += a[i]; } cout << sum << '\n'; } // dữ liệu đảm bảo tổng các giá trị n <= 100000 return 0; }
Chú ý rằng
dữ liệu đảm bảo tổng các giá trị n <= 100000
. Vậy có tổng cộng bao nhiêu thao tác đoạn code trên thực hiện trong trường hợp xấu nhất?Trả lời
Trong trường hợp xấu nhất là khoảng ~10^{10}~ vì ta khai báo
vector
với kích thước ~10^5~ mỗi lần lặp.Cách khắc phục
Khai báo vector với kích thước đủ dùng trong mỗi lần lặp.
Code
#include <bits/stdc++.h> using namespace std; const int N = 100000; int main() { int test_cases = 100000; while (test_cases--) { int n; cin >> n; vector<int> a(n, 0); int sum = 0; for (int i = 0; i < n; i++) { sum += a[i]; } cout << sum << '\n'; } // dữ liệu đảm bảo tổng các giá trị n <= 100000 return 0; }
🔍 Lỗi thường gặp 4
Hãy tìm lỗi trong đoạn code dưới đây?
Code
#include <bits/stdc++.h> using namespace std; int main() { int n = 5; int a[n]; memset(a, 1, sizeof a); for (int i = 0; i < n; i++) { cout << a[i] << ' '; } cout << '\n'; return 0; }
Đầu ra mong muốn của đoạn code trên là
1 1 1 1 1
. Nhưng khi chạy thì đoạn code trên lại cho ra kết quả16843009 16843009 16843009 16843009 16843009
Lý do
- Hàm
memset
thực ra được dùng cho kiểuchar
hoặc1 byte
. Xem thêm: Memset in C++. - Có thể bạn đã từng gặp dòng code
memset(a, -1, sizeof a)
hoặcmemset(a, 0, sizeof a)
. Thật ra là vì nó chỉ tình cờ đúng với trường hợp giá trị là ~0~ và ~-1~. Xem thêm tại comment này để hiểu lý do. - Dùng
memset
để set giá trị các phần tử trong mảng thành ~0~ hoặc ~-1~ sẽ nhanh hơn việc lặp qua mảng và set từng phần tử. Nhưng hãy cẩn thận khi sử dụng hàm này với những giá trị khác.
- Hàm
🔍 Lỗi thường gặp 5
Đừng dùng
endl
!. Nếu code của bạn cần in một số lượng lớn các dòng thì việc sử dụngendl
sẽ chậm hơn'\n'
rất nhiều.Lý do
Xem thêm: std::endl vs \n in C++
🔍 Lỗi thường gặp 6
Đừng dùng hàm
pow()
cho việc tính toán trên các số nguyên.Lý do
- Đừng dùng hàm
pow()
trừ khi bạn bắt buộc phải dùng bởi vì đôi khi sai số của kết quả đầu ra là khá lớn. Ví dụ, dễ thấy kết quả của ~\log_2(1 << 30)~ sẽ là ~30~. Nhưng đôi khi kết quả lại là ~29.9999999999999~ và khi được chuyển thành kiểuint
, kết quả trả về là29
. - Tương tự, hàm
pow()
nhận các đối số có kiểudouble
, ví dụ đối vớipow(5, 2)
thì kết quả mong muốn là ~5^2 = 25.00~. Nhưng đôi khi kết quả lại là ~24.9999999999999~ và khi chuyển thành kiểuint
, kết quả cuối cùng là ~24~. Bạn có thể dùnground(pow(a, n))
trong trường hợp này để tránh việc sai số hoặc có thể duyệt trâu khin
không quá lớn.
- Đừng dùng hàm
🔍 Lỗi thường gặp 7
Hãy chạy thử đoạn code dưới đây:
Code
#include <bits/stdc++.h> using namespace std; int main() { vector<int> v; cout << v.size() - 1 << '\n';; return 0; }
Kết quả mong muốn là ~-1~. Nhưng khi chạy lại cho ra kết quả
18446744073709551615
!Lý do
Xem thêm tại đây,
v.size()
sẽ trả về kết quả mang kiểu dữ liệusize_type
.Cách khắc phục
Ép kiểu dữ liệu!
Code
#include <bits/stdc++.h> using namespace std; int main() { vector<int> v; cout << (int) v.size() - 1 << '\n';; return 0; }
Đó là lý do mà đoạn code dưới đây sẽ chạy gần như vô tận
Code
#include <bits/stdc++.h> using namespace std; int main() { vector<int> v; for (int i = 0; i < v.size() - 1; i++) { // do something } return 0; }
🔍 Lỗi thường gặp 8
- Dùng
cerr
là một lựa chọn tốt để debug code của bạn vì nó không ảnh hưởng đến dòng xuất chuẩn (standard output). Nhưng hãy nhớ comment hoặc xóa các dòngcerr
trước khi nộp code của bạn lên các hệ thống OJ vì có thể sẽ bịTLE
. - Xem thêm: Debugging in C++.
🔍 Lỗi thường gặp 9
Hãy xem đoạn code dưới đây:
Code
#include <bits/stdc++.h> using namespace std; int main() { ios_base::sync_with_stdio(0); cin.tie(0); for (int i = 1; i <= 3; i++) { cout << i << '\n'; printf("%d\n", i + 10); } return 0; }
Đầu ra mong muốn:
output
1 11 2 12 3 13
Nhưng thực tế kết quả lại không như mong muốn. Tùy vào máy mà sẽ cho kết quả khác nhau. Một kết quả đầu ra ví dụ:
output
1 2 3 11 12 13
Về cơ bản,
cout
vàprintf
dường như đang hoạt động độc lập với nhau.Lý do
Đó là do sự đồng bộ hóa đã bị vô hiệu hóa khi sử dụng dòng
ios_base::sync_with_stdio(0)
. Tìm hiểu thêm: std::ios_base::sync_with_stdio.Cách khắc phục
Không nên pha trộn các luồng xuất giữa C-style và C++-style sau khi đã dùng
ios_base::sync_with_stdio(0)
.
🔍 Lỗi thường gặp 10
Hãy xem đoạn code dưới đây:
Code
#include <bits/stdc++.h> using namespace std; int main() { int a = 1, b = 3; // chúng ta muốn tính b & 3, sau đó cộng với a int result = a + b & 3; cout << result << '\n'; return 0; }
Kết quả đầu ra mong muốn là
4
, nhưng đoạn code trên lại cho ra kết quả là0
!.Lý do
- Độ ưu tiên của toán tử
+
cao hơn toán tử&
. Vì vậy toán tử+
sẽ được thực hiện trước, do đóa + b = 4
sẽ được thực hiện đầu tiên, sau đó mới đến toán tử&
, nên4 & 3
sẽ được thực thi sau cùng và cho kết quả là0
. Xem thêm độ ưu tiên của các toán tử trong C++: C++ Operator Precedence.
Cách khắc phục
Dùng cặp ngoặc tròn trong các trường hợp bạn không chắc chắn về độ ưu tiên của các toán tử.
Code
#include <bits/stdc++.h> using namespace std; int main() { int a = 1, b = 3; int result = a + (b & 3); cout << result << '\n'; return 0; }
Một trường hợp tương tự khác khá kinh điển đó là
1 << n - 1
không phải ~2^n - 1~ mà là ~2^{n - 1}~ vì toán tử-
có độ ưu tiên cao hơn<<
.
- Độ ưu tiên của toán tử
🔍 Lỗi thường gặp 11
Hãy xem tính đúng đắn của đoạn code dưới đây
Code
#include <bits/stdc++.h> using namespace std; const int INF = (int) 1e6; int main() { int minimum = INF; for (int i = 1; i <= 10; i++) { int x; cin >> x; // giá trị của x có thể lên đến 10000 minimum = min(minimum, x * x); } cout << minimum << '\n'; return 0; }
Nếu không đúng thì vấn đề nằm ở đâu?
Lời giải
- Ở đây giá trị ~x~ có thể lên đến ~10^4~ do đó giá trị của ~x * x~ có thể lên đến ~10^8~. Nhưng trong đoạn code trên biến
minimum
được khởi tạo giá trị ~10^6~ là không đủ lớn. - Do đó luôn chắc chắn rằng code của bạn khởi tạo giá trị các biến là đủ lớn (hoặc đủ nhỏ).
Edit: thông thường tác giả dùng giá trị
0x3f3f3f3f
cho kiểuint
hoặc0x3f3f3f3f3f3f3f3f
cho kiểulong long
. Các giá trị này thường là đủ lớn đối với giới hạn đề bài (~10^9~ đối vớiint
hoặc ~10^{18}~ đối vớilong long
) và không tràn số khi cộngINF + INF
.- Ở đây giá trị ~x~ có thể lên đến ~10^4~ do đó giá trị của ~x * x~ có thể lên đến ~10^8~. Nhưng trong đoạn code trên biến
🔍 Lỗi thường gặp 12
Đoạn code dưới đây tính số lần xuất hiện lớn nhất trong mảng.
Code
#include <bits/stdc++.h> using namespace std; unordered_map<int, int> mp; int main() { int n = 300000; for (int i = 1; i <= n; i++) { int x; cin >> x; mp[x]++; } int max_occurrence = 0; for (auto [val, cnt] : mp) { max_occurrence = max(max_occurrence, cnt); } cout << max_occurrence << '\n'; return 0; }
Có thể thấy đoạn code này chạy khá nhanh, nhưng với một số input đặc biệt sẽ bị TLE.
Lý do
- Lý do là vì chúng ta sử dụng
unordered_map
! Trong trường hợp xấu nhất đoạn code trên có độ phức tạp ~\mathcal{O}(n^2)~, bởi vìunordered_map
là mộthash map
nên có thể tốn hơn ~\mathcal{O}(1)~ thao tác để thêm hoặc truy cập phần tử trong trường hợp input không được sinh ngẫu nhiên. - Tham khảo thêm: Blowing up unordered_map, and how to stop getting hacked on it.
- Lý do là vì chúng ta sử dụng
🔍 Lỗi thường gặp 13
Hãy thử xóa một phần tử trong
multiset
Code
#include <bits/stdc++.h> using namespace std; int main() { multiset<int> se({1, 1, 2, 2, 2, 3, 4, 5, 5}); // xóa một phần tử 2 ra khỏi multiset se.erase(2); // in các phần tử sau khi xóa để kiểm tra for (auto val : se) { cout << val << ' '; } cout << '\n'; return 0; }
Khi chạy đoạn code trên thì output nhận được là
1 1 3 4 5 5
thay vì1 1 2 2 3 4 5 5
. Có vẻ như ta đã vô tình xóa tất cả các phần tử2
trongmultiset
thay vì một phần tử.Lý do
Lý do là vì nếu truyền một phần tử vào hàm
erase()
củamultiset
thì tất cả các phần tử này trongmultiset
sẽ bị xóa. Ngược lại nếu bạn truyền mộtiterator
thì chỉ phần tử được trỏ đến mới bị xóa. Xem thêm: std::multiset::erase.Cách khắc phục
#include <bits/stdc++.h> using namespace std; int main() { multiset<int> se({1, 1, 2, 2, 2, 3, 4, 5, 5}); // hàm find() sẽ trả về iterator trỏ đến vị trí xuất hiện đầu tiên của phần tử 2 se.erase(se.find(2)); for (auto val: se) { cout << val << ' '; } cout << '\n'; // 1 1 2 2 3 4 5 5 return 0; }
🔍 Lỗi thường gặp 14
Hãy chạy thử đoạn code dưới đây:
Code
#include <bits/stdc++.h> using namespace std; int main() { map<int, int> mp; // thêm các số từ 1 đến 5 vào map for (int i = 1; i <= 5; i++) { mp[i]++; } int cnt = 0; // kiểm tra xem có bao nhiêu số từ 1 đến 10 tồn tại trong map for (int i = 1; i <= 10; i++) { if (mp[i]) { ++cnt; } } // in kích thước của map cout << (int) mp.size() << '\n'; return 0; }
Hãy thử đoán xem output có phải là
5
hay không?Giải thích
- Thật ra output của đoạn code trên sẽ là
10
. Lý do là vì câu lệnh
if (mp[i])
sẽ thêm các phần tửi
vào map nếu phần tửi
chưa có trong map. Lỗi này đôi khi có thể dẫn đến MLE (tràn bộ nhớ).Cách khắc phục
Dùng hàm
find()
hoặccount()
để kiểm tả một phần tử có trong map hay không trước khi dùng toán tử[]
Code
#include <bits/stdc++.h> using namespace std; int main() { map<int, int> mp; // thêm các số từ 1 đến 5 vào map for (int i = 1; i <= 5; i++) { mp[i]++; } int cnt = 0; // kiểm tra có bao nhiêu số từ 1 đến 10 tồn tại trong map for (int i = 1; i <= 10; i++) { if (mp.find(i) != mp.end()) { ++cnt; } // hoặc dùng if (mp.count(i)) {} } // in kích thước của map cout << (int) mp.size() << '\n'; return 0; }
- Thật ra output của đoạn code trên sẽ là
🔍 Lỗi thường gặp 15
Đoạn code dưới đây tính tổng các số tự nhiên dùng set:
Code
#include <bits/stdc++.h> using namespace std; int main() { int n = 100000; set<int> se; for (int i = 1; i <= n; i++) { se.insert(i); } // tính tổng các số tự nhiên từ 1 đến n long long sum = 0; for (int i = 1; i <= n; i++) { // tìm iterator trỏ đến phần tử nhỏ nhất mà >= i // trong trường hợp này chính là i auto it = lower_bound(se.begin(), se.end(), i); sum += *it; // *it = i } cout << sum << '\n'; return 0; }
Dễ thấy đoạn code này có độ phức tạp là ~\mathcal{O}(n \times \log n)~ nhưng lại không thể chạy trong giới hạn ~1~ giây với ~n = 10^5~?
Lý do
- Thật ra độ phức tạp của đoạn code trên là ~\mathcal{O}(n^2)~.
- Nhưng hàm
lower_bound
là ~\mathcal{O}(\log n)~ thì tại sao lại là ~\mathcal{O}(n^2)~? Thật ra hai hàm
std::lower_bound
vàstd::set::lower_bound
là hai hàm hoàn toàn khác nhau. Xem thêm tại đây. Tương tự vớiupper_bound
.Cách khắc phục
Dùng
set.lower_bound(value)
với độ phức tạp là ~\mathcal{O}(\log n)~Code
#include <bits/stdc++.h> using namespace std; int main() { int n = 100000; set<int> se; for (int i = 1; i <= n; i++) { se.insert(i); } // tính tổng các số tự nhiên từ 1 đến n long long sum = 0; for (int i = 1; i <= n; i++) { // tìm iterator trỏ đến phần tử nhỏ nhất mà >= i // trong trường hợp này chính là i auto it = se.lower_bound(i); sum += *it; // *it = i } cout << sum << '\n'; return 0; }
🔍 Lỗi thường gặp 16
Hãy xem đoạn code dưới đây:
Code
#include <bits/stdc++.h> using namespace std; int main() { int n = 1000000; string s = ""; for (int i = 0; i < n; i++) { s = s + 'a'; // nối thêm `a` vào chuỗi } cout << s << '\n'; return 0; }
Hãy thử đoán độ phức tạp của đoạn code trên
Lời giải
- Mặc dù chỉ duyệt
n
lần nhưng độ phức tạp của đoạn code trên là ~\mathcal{O}(n^2)~. Lý do là vì khi gán
s = s + 'a'
thì ở vế bên phải truy cập toàn bộ chuỗis
(với kích thước ~\mathcal{O}(n)~).Cách khắc phục
Dùng cú pháp
s += 'a'
để không phải truy cập toàn bộ chuỗi mỗi lần gánCode
#include <bits/stdc++.h> using namespace std; int main() { int n = 1000000; string s = ""; for (int i = 0; i < n; i++) { s += 'a'; // nối thêm `a` vào chuỗi } cout << s << '\n'; return 0; }
- Mặc dù chỉ duyệt
🔍 Lỗi thường gặp 17
Hãy xem đoạn code đơn giản dưới đây:
Code
#include <bits/stdc++.h> using namespace std; int main() { int test_cases; cin >> test_cases; // nhập vào một mảng n phần tử // in ra YES nếu kích thước mảng là 1 ngược lại in ra NO while (test_cases--) { int n; // kích thươc mảng cin >> n; if (n == 1) { cout << "YES\n"; continue; } vector<int> a(n); for (int i = 0; i < n; i++) { cin >> a[i]; } cout << "NO\n"; } return 0; }
Nhiệm vụ của bạn là tìm bug trong đoạn code trên
Bug
Trong trường hợp ~n = 1~ thì đoạn code in ra
YES
và tiếp tục vòng lặp mới ngay cả khi chưa nhập phần tử của mảng. Điều này dẫn đến việc sẽ bịWA
trong một số trường hợp.Test ví dụ
2 1 1 2 1 3
Cách khắc phục
Đọc hết toàn bộ dữ liệu của một testcase trước khi bạn muốn
continue
khi kiểm tra các trường hợp cơ bản.Code
#include <bits/stdc++.h> using namespace std; int main() { int test_cases; cin >> test_cases; // nhập vào một mảng n phần tử // in ra YES nếu kích thước mảng là 1 ngược lại in ra NO while (test_cases--) { int n; // kích thươc mảng cin >> n; vector<int> a(n); for (int i = 0; i < n; i++) { cin >> a[i]; } if (n == 1) { cout << "YES\n"; continue; } cout << "NO\n"; } return 0; }
🔍 Lỗi thường gặp 18
Đoạn code dưới đây đếm số phần tử phân biệt trong mảng:
Code
#include <bits/stdc++.h> using namespace std; const int MAX_ELEMENT = (int) 1e5; int cnt[MAX_ELEMENT + 1]; int main() { int test_cases; cin >> test_cases; while (test_cases--) { int n; cin >> n; for (int i = 0; i < n; i++) { int x; cin >> x; cnt[x]++; } int unique_elements = 0; for (int i = 0; i <= MAX_ELEMENT; i++) { if (cnt[i] > 0) { unique_elements++; } } cout << unique_elements << '\n'; } return 0; }
Hãy thử tìm bug trong đoạn code trên
Bug
Trong bài này có nhiều testcase nhưng đoạn code trên lại không reset lại mảng
cnt
. Do đó dữ liệu của các testcase trước vẫn còn lưu trong mảngcnt
dẫn đến WA.Test ví dụ
2 1 1 1 2
Cách khắc phục
Reset lại mảng
cnt
sau mỗi testcaseCode
#include <bits/stdc++.h> using namespace std; const int MAX_ELEMENT = (int) 1e5; int cnt[MAX_ELEMENT + 1]; int main() { int test_cases; cin >> test_cases; while (test_cases--) { int n; cin >> n; for (int i = 0; i < n; i++) { int x; cin >> x; cnt[x]++; } int unique_elements = 0; for (int i = 0; i <= MAX_ELEMENT; i++) { if (cnt[i] > 0) { unique_elements++; } } cout << unique_elements << '\n'; // reset lại mảng cnt for (int i = 0; i <= MAX_ELEMENT; i++) { cnt[i] = 0; } } return 0; }
🔍 Lỗi thường gặp 19
Có khá nhiều lỗi dễ mắc phải khi tính toán liên quan đến phép toán module.
Ví dụ
#include <bits/stdc++.h> using namespace std; const int mod = 1e9 + 7; int main() { int a = 1e9, b = 1e9, c = 1e9; int sum = (a + b + c) % mod; cout << sum << '\n'; int product = a * b % mod; cout << product << '\n'; int random = 1LL * a * (b * c % mod) % mod; cout << random << '\n'; a = 1e8; int sub = (a - b) % mod; cout << sub << '\n'; return 0; }
Hãy cùng đi tìm bug nào
Bug
#include <bits/stdc++.h> using namespace std; const int mod = 1e9 + 7; int main() { // 0 <= a, b, c < mod int a = 1e9, b = 1e9, c = 1e9; int sum = (a + b + c) % mod; // tràn số vì (a + b + c) > INT_MAX (2147483647) // cách tốt hơn: ((a + b) % mod + c) % mod cout << sum << '\n'; int product = a * b % mod; // tràn số vì a * b > INT_MAX // cách tốt hơn: 1LL * a * b % mod cout << product << '\n'; int random = 1LL * a * (b * c % mod) % mod; // tràn số ở phần (b * c % mod) // cách đúng: 1LL * a * (1LL * b * c % mod) % mod // cách tốt hơn: 1LL * a * b % mod * c % mod cout << random << '\n'; a = 1e8; int sub = (a - b) % mod; // không tràn số nhưng khi a < b thì kết quả sẽ ra số âm // cách tốt hơn: (a - b + mod) % mod cout << sub << '\n'; // Hãy đảm bảo sau mỗi phép toán // thì tất cả các biến mang giá trị trong đoạn [0, mod - 1] return 0; }
🔍 Lỗi thường gặp 20
Hãy xem đoạn code dưới đây:
Code
#include <bits/stdc++.h> using namespace std; int main() { long long val = (long long) 1e12; vector<long long> v({val, val, val}); // tính tổng các phần tử long long sum = accumulate(v.begin(), v.end(), 0); cout << sum << '\n'; return 0; }
Khi chạy đoạn code trên in ra ~2112827392~ thay vì ~3 \cdot 10^{12}~?
Lý do
Giá trị khởi tạo trong hàm
accumulate()
(giá trị ~0~) mang kiểuint
nên kết quả trả về làint
gây tràn số. Xem thêm: std::accumulate.Cách khắc phục
Truyền giá trị khởi tạo mang kiểu
long long
(0LL)
Code
#include <bits/stdc++.h> using namespace std; int main() { long long val = (long long) 1e18; vector<long long> v({val, val, val}); // tính tổng các phần tử long long sum = accumulate(v.begin(), v.end(), 0LL); cout << sum << '\n'; return 0; }
Với double
#include <bits/stdc++.h> using namespace std; int main() { vector<double> v({1.23, 2.05, 3.003}); // tính tổng các phần tử double sum = accumulate(v.begin(), v.end(), 0); // cách đúng: 0.0 cout << sum << '\n'; // kết quả in ra là 6 thay vì 6.283 return 0; }
🔍 Lỗi thường gặp 21
- Dùng
sqrt()
vàcbrt()
cho các số nguyên một cách trực tiếp có thể gây ra các vấn đề về sai số và cho kết quả sai. Một trong những cách tốt nhất để tính căn bậc hai của một số:
Code
long long x = (long long) 1e18 - 1; long long k = sqrtl(x); while (k * k < x) ++k; while (k * k > x) --k; cout << k << '\n';
Edit: Cho trước một số ~s~. Tìm giá trị ~n~ lớn nhất sao cho tổng ~n~ số nguyên đầu tiên không vượt quá ~s~.
Code
#include <bits/stdc++.h> using namespace std; int main() { int s; cin >> s; // tìm giá trị n lớn nhất sao cho n * (n + 1) / 2 <= s // <=> n * (n + 1) <= 2 * s // lấy xấp xỉ n = sqrt(2 * s) int n = (int) sqrt(2 * s); while (n * (n + 1) < 2 * s) ++n; while (n * (n + 1) > 2 * s) --n; cout << n << '\n'; return 0; }
🔍 Lỗi thường gặp 22
Hãy xem đoạn code dưới đây:
Code
#include <bits/stdc++.h> using namespace std; int main() { int n = 100000; multiset<int> se; // thêm n phần tử 1 vào multiset for (int i = 1; i <= n; i++) { se.insert(1); } long long tot = 0; // lặp n lần và đếm xem có bao nhiêu phần tử 1 trong multiset for (int i = 1; i <= n; i++) { tot += se.count(1); } cout << tot << '\n'; return 0; }
Hãy thử đoán độ phức tạp của đoạn code trên
Lời giải
- Độ phức tạp của đoạn code trên là ~\mathcal{O}(n^2)~.
- Hàm
std::multiset::count
có độ phức tạp là ~\mathcal{O}(\log N + K)~ với ~N~ và ~K~ lần lượt là kích thước và số lần xuất hiện của phần tử trongmultiset
. Trong trường hợp này phần tử ~1~ xuất hiện tổng cộng ~n~ lần. Do đó hãy cẩn thận khi dùng hàmstd::multiset::count
. - Nếu muốn kiểm tra một phần tử có tồn tại trong
multiset
hay không thì ta có thể dùng hàmfind
thay vì hàmcount
.
🔍 Các lỗi khác
- Tránh việc dùng hàm
insert
hoặcerase
(ví dụset.erase
,vector.insert
) khi đang duyệt qua các phần tử dùng vòng lặp ví dụ nhưfor (auto val : container)
. Bởi vì đối với các container động thì việc thêm và xóa có thể thay đổi cấu trúc của container. Do đó việc duyệt song song với việc thêm và xóa có thể dẫn đến một số hành vi không mong muốn. - Dùng
setprecision
cho việc in các số thực, nếu không có thể bịWA
do lỗi sai số. Xem thêm tại đây. - Không nên dùng hàm
rand()
cho việc sinh số ngẫu nhiên. Ngoài ra giá trị lớn nhất có thể từ hàmrand()
làRAND_MAX
, ở một số hệ thống hoặc máy chấm thìRAND_MAX
~= 2^{15} - 1 = 32767~, khá nhỏ so vớiINT_MAX
. Cách thay thế tốt hơn có thể xem tại bài viết này. - Chỉ khai báo các biến khi bạn cần dùng đến thay vì khai báo
int a, b, c, d, e, f, x, y, z;
và hằng hà các biến khác ở đầu code. Việc này giúp bạn tránh được các lỗi phát sinh khi sử dụng cùng một biến ở hai hoặc nhiều nơi khác nhau. - Hạn chế việc lạm dụng
long long
vìlong long
có thể chậm hơn đến ~2~ lần so vớiint
. Do đó chỉ nên dùnglong long
khi cần thiết. - Trong trường hợp bạn muốn đếm số bit ~1~ của một số kiểu
long long
thì hãy dùng hàm__builtin_popcountll
thay vì__builtin_popcount
. - Trong C++,
comparator
nên trả vềfalse
khi các đối số là bằng nhau. Nếu không code của bạn có thể sẽ bịRuntime Error
. Xem thêm bài viết này. - Hầu hết những trường hợp gặp lỗi
Runtime Error
là do khai báo mảng với kích thước không đủ lớn. Do đó hãy kiểm tra kỹ giới hạn của đề bài trước khi bắt tay vào code. long long x = 1 << 40
vẫn sẽ bị tràn số vì1
mang kiểuint
nên kết quả sẽ mang kiểuint
, mà ~2^{40}~ lớn hơn nhiều so với giới hạn của kiểuint
. Giải pháp là1LL << 40
(ép kiểu1
thànhlong long
) xem thêm: What is 1LL or 2LL in C and C++?.- Khi so sánh hai số thực, thay vì dùng
if (a == b)
thì hãy dùngif (abs(a - b) < EPS)
vớiEPS = 1e-9
hoặc các giá trị đủ nhỏ khác để tránh bị sai số. - Nếu bạn muốn làm tròn lên (ceiling) kết quả của
a / b
trong đóa
vàb
là các số nguyên dương, hãy dùng(a + b - 1) / b
thay vìceil(1.0 * a / b)
. Với làm tròn xuống (floor) thì chỉ cầna / b
là đủ vì kết quả sẽ được tự động làm tròn xuống số nguyên gần nhất. - Trong trường hợp bạn muốn tính ~\lfloor \log_2 n\rfloor~ (tương đương việc tìm index của bit ~1~ lớn nhất trong ~n~) của một số nguyên dương ~n~, hãy dùng
__lg(n)
. - Đừng dùng hash tràn số (mod ~2^{64}~), xem thêm Anti-hash test. Và trong một số contest, nếu chỉ hash với một cơ số nguyên tố và một số mod thì có thể bạn sẽ
WA
khi gặpanti-hash test
. Xem thêm On the mathematics behind rolling hashes and anti-hash tests. Khi đó hãy dùng ít nhất hai cơ số nguyên tố và hai số mod.