Siêu đối xứng (2022TPB02) (Tin học trẻ TP Đà Nẵng 2021-2022 – Bảng B)
Đề bài
Bài 2. Siêu đối xứng (2.0 điểm) – Hội thi Tin học trẻ cấp thành phố Đà Nẵng, năm học 2021-2022, đề thi Bảng B – cấp THCS.
Một số nguyên dương được gọi là siêu đối xứng nếu tất cả các chữ số của nó giống nhau. Chẳng hạn số $777$ hoặc $4444$ là các số nguyên dương siêu đối xứng.
Nhập từ bàn phím một số nguyên dương $x$. Hãy tìm và in ra màn hình số nguyên dương $y$ nhỏ nhất sao cho tổng $x + y$ là một số nguyên dương siêu đối xứng.
Giới hạn:
- Có $50\%$ test ứng với $x \le 10^6$;
- $30\%$ test ứng với $10^6 \lt x \le 10^9$;
- $20\%$ test ứng với $10^9 \le x \le 10^{16}$.
4510Giải thích: $45 + 10 = 55$.
Lời mở đầu
Bài này mình được một bạn học viên hỏi nên sẵn tiện mình viết bài giải chi tiết để nhiều bạn được tham khảo luôn. Hy vọng mang đến được giá trị cho các bạn 😇.
Trước khi vào bài, các bạn nhớ giúp mình chuyện giới hạn thời gian: với các hệ thống chấm bài giới hạn 1 giây, toàn bài chỉ nên lặp cỡ $(3 \div 5) \cdot 10^7$ lần là an toàn, tệ nhất $10^8$ lần (lúc này hên xui). Ngưỡng này là cho C/C++; Python, Java, C# chạy chậm hơn nên ngưỡng phải thấp hơn. Đề cho 2 giây thì nhân ngưỡng lên 2, 100 ms thì chia 10, cứ thế mà tính.
Đề bài thì quá rõ ràng rồi nên mình cũng không có gì giải thích thêm nha các bạn: với $x = 45$, số siêu đối xứng nhỏ nhất lớn hơn $45$ là $55$, nên $y = 55 - 45 = 10$.
Tư tưởng chung: tìm z rồi lấy z − x
Khá dễ để nhìn ra tư tưởng xử lý: hãy tìm ra số $z$ đầu tiên (cũng đồng nghĩa là nhỏ nhất) là số siêu đối xứng sao cho $z \gt x$, rồi từ đó $y = z - x$ – giá trị $y$ này bảo đảm là nhỏ nhất.
Ta thử đưa ra nhiều test để nhìn thấy rõ cách làm:
| x | z nhỏ nhất (siêu đối xứng, z > x) | y = z − x |
|---|---|---|
| 78 | 88 | 10 |
| 99 | 111 | 12 |
| 123 | 222 | 99 |
| 9031 | 9999 | 968 |
| 9998 | 9999 | 1 |
| 9999 | 11111 | 1112 |
Mấu chốt quyết định là tìm ra $z$ nhỏ nhất là số siêu đối xứng thoả $z \gt x$, đáp án chính là $z - x$. Tuy nhìn ra cách giải nhưng mình đã thấy nhiều bạn chọn cách cài không khéo nên rất phức tạp, nhiều lỗi khó kiểm soát, chưa kể còn bị quá thời gian nếu chọn hướng không tối ưu.
Bấm vào nút bên dưới để nộp bài và được hệ thống CLUE OJ chấm tự động.
Nộp bài trên CLUE OJLộ trình lời giải, bấm vào từng phần để nhảy tới:
Cách 1Vét cạn: tăng z lên từng đơn vị – O(Z − X)
Hướng nhẹ đầu nhất mà nhiều bạn chắc chắn nhìn ra: cứ lần lượt tăng $z$ lên 1 đơn vị, mỗi lần kiểm tra $z$ có phải số siêu đối xứng không; tới khi thoả thì in $z - x$. Đây là cách vét cạn dễ cài đặt. Bạn nào chưa nghĩ ra cách tối ưu thì mình luôn khuyên cứ cài cách vét cạn này trước, để dùng nó làm công cụ kiểm tra tính chính xác cho cách tối ưu về sau (cuối bài mình có làm mẫu luôn).
Source code để các bạn tham khảo nếu làm theo cách vét cạn:
#include <iostream>
using namespace std;
bool isSupersymmetryNumber(long long x){
int num = x % 10;
x /= 10;
while(x){
if(x % 10 != num){
return false;
}
x /= 10;
}
return true;
}
int main(){
long long x;
cin >> x;
long long z = x;
while(!isSupersymmetryNumber(++z));
cout << z - x;
return 0;
}
def isSupersymmetryNumber(x):
num = x % 10
x //= 10
while x:
if x % 10 != num:
return False
x //= 10
return True
x = int(input())
z = x + 1
while not isSupersymmetryNumber(z):
z += 1
print(z - x)
Giải thích một số chỗ trong code:
- Đề cho $x$ tối đa tới $10^{16}$ nên phải chọn kiểu số nguyên 8 byte
long longđể tránh tràn số. while(x)⇔while(x != 0): trong lập trình khi gọi một biến ra so sánh mà không nói rõ so sánh gì thì nghĩa là kiểm tra nó có khác 0 không. Tương tựwhile(!x)⇔while(x == 0). Số 0 đại diện chofalse, nên với biến kiểuboolthìwhile(x)⇔while(x == true), cònwhile(!x)⇔while(x == false).- Hàm
isSupersymmetryNumberkiểm tra $x$ có phải số siêu đối xứng không: lấynum = x % 10(chữ số cuối) làm mốc,x /= 10bỏ chữ số đó, rồi liên tục% 10và/ 10lấy từng chữ số ra so vớinum; chỉ cần gặp 1 chữ số khácnumlàreturn falsengay. Vòng lặp kết thúc mà hàm vẫn chạy xuống dưới nghĩa là mọi chữ số đều giống nhau ⇒return true. while(!isSupersymmetryNumber(++z));: $z$ ban đầu chép từ $x$; mỗi vòng tăng $z$ lên 1 trước rồi mới kiểm tra (nhờ vậy không bao giờ xét $z = x$). Chưa thoả thì lặp tiếp, thoả thì dừng và in $z - x$.
Em bấm Tiếp để chạy tay code C++ của cách vét cạn (chọn $x$ sao cho đáp án nhỏ để dễ theo dõi):
Đánh giá độ phức tạp của cách làm này:
- Không gian: $O(1)$, chỉ dùng các biến đơn lẻ.
- Thời gian: vòng
whiletrongmainlặp $Z - X$ lần ($Z$ là số siêu đối xứng nhỏ nhất $\gt X$), mỗi lần gọi hàm kiểm tra tách tối đa 16 chữ số ⇒ $O(16 \cdot (Z - X))$.
Cách 2Dựng thẳng z bằng xâu – O(độ dài x)
Ta không cần cực như thế, cứ liên tục tăng $z$ lên từng đơn vị. Ví dụ $x = 11111$ là số siêu đối xứng thì $z = 22222$ là số siêu đối xứng tiếp theo, quá rõ để nhìn ra, cần gì phải lặp tăng dần?
Dễ nhất cho bài này là dùng chuỗi (string): đọc luôn $x$ ở dạng string, ví dụ x = "102". Số $z$ siêu đối xứng nhỏ nhất $\gt x$ trước tiên sẽ có độ dài (số chữ số) bằng độ dài $x$ – vì nếu $z$ ít chữ số hơn thì chắc chắn $z \lt x$ rồi.
- Khởi đầu cho cả chuỗi $z$ là ký tự đầu tiên
x[0]:x = "102"thìz = "111". So sánhz > x: thoả ⇒ đổi cả hai sang số rồi trừ: $111 - 102 = 9$. - Với
x = "135":z = "111"không lớn hơn $x$. Lúc này biến $z$ thành số siêu đối xứng tiếp theo bằng cách cho cả chuỗi là ký tựx[0] + 1:z = "222", chắc chắn $z \gt x$ ⇒ $222 - 135 = 87$. - Với
x = "999":z = "999"không lớn hơn $x$, màx[0]đã là'9'rồi, không tăng được nữa. Lúc này $z$ phải dài hơn $x$ 1 chữ số và toàn chữ số'1':z = "1111"⇒ $1111 - 999 = 112$.
string của C++ có sẵn các toán tử so sánh, chỉ cần gọi if(z > x). Nó so từ trái qua phải từng cặp ký tự cùng vị trí theo mã ASCII, cặp đầu tiên khác nhau quyết định chuỗi nào lớn hơn. Với 2 số có cùng số chữ số, đó cũng chính là cách so sánh số: so hàng cao nhất trước (hàng trăm, rồi hàng chục, rồi hàng đơn vị), khác nhau ở đâu thì kết luận ngay ở đó. Vì $z$ và $x$ cùng độ dài nên so chuỗi = so số.Chuẩn hoá lại quy trình: tạo z cùng độ dài với x, toàn ký tự x[0]. Nếu z > x ⇒ in $z - x$. Ngược lại, nếu z[0] < '9' thì đổi toàn bộ z sang ký tự z[0] + 1 rồi in $z - x$. Còn nếu z[0] == '9' thì $z$ là chuỗi dài hơn 1 ký tự, toàn chữ số '1', rồi in $z - x$.
Minh hoạ dựng z
Em nhập $x$ rồi bấm Tiếp để xem $z$ được dựng và so sánh với $x$ từng ký tự như thế nào:
Source code để các bạn tham khảo với hướng làm tối ưu này:
#include <iostream>
#include <iomanip>
#include <string>
#include <algorithm>
using namespace std;
int main(){
string x;
cin >> x;
string z(x.length(), x[0]);
if(z > x){
cout << stoll(z) - stoll(x);
}
else{
if(z[0] < '9'){
fill(z.begin(), z.end(), z[0] + 1);
cout << stoll(z) - stoll(x);
}
else{
cout << setfill('1') << setw(z.length()) << 2;
}
}
return 0;
}
x = input().strip()
z = x[0] * len(x)
if z > x:
print(int(z) - int(x))
else:
if z[0] < '9':
z = chr(ord(z[0]) + 1) * len(x)
print(int(z) - int(x))
else:
print('1' * (len(x) - 1) + '2')
Bấm Tiếp để chạy tay từng dòng code C++:
Giải thích một số chỗ trong code:
string z(x.length(), x[0]);dùng hàm khởi tạo có tham số của classstringđể tạo sẵn chuỗi có độ dàix.length(), toàn bộ là ký tựx[0]. Bạn nào không biết cú pháp này sẽ phải code dài hơn, biết nó rất tiện.stoll(z)chuyển chuỗizsang số nguyên kiểulong long, tương tựstoll(x); kết quả là số nên trừ nhau được. Tương tự có hàmstoitrả về kiểuint.fill(z.begin(), z.end(), z[0] + 1);thay toàn bộ ký tự từ đầu tới cuối chuỗizbằng ký tựz[0] + 1. (Mình thêm#include <algorithm>cho hàmfillđể code biên dịch được trên mọi trình biên dịch.)- Trường hợp cuối:
zkhông lớn hơnxmàz[0] == '9'nghĩa làxtoàn chữ số 9. Để ý quy luật: $x = 9$ ⇒ $11 - 9 = 2$; $x = 99$ ⇒ $12$; $x = 999$ ⇒ $112$; $x = 9999$ ⇒ $1112$. Đáp án luôn là (độ dài x − 1) chữ số 1 rồi một chữ số 2. Nên khỏi phải tạo lạizrồi đổi sang số trừ nhau, in luôn:cout << setfill('1') << setw(z.length()) << 2;nghĩa là in số 2 và đệm ký tự'1'đằng trước sao cho tổng độ dài (tính cả số 2) đúng bằngz.length().
Đánh giá độ phức tạp của cách làm này:
- Không gian: chỉ có 2 chuỗi tối đa 17 ký tự ⇒ $O(\text{length}_x)$, trong bài này coi như $O(1)$.
- Thời gian: tạo
z, so sánh chuỗi, đổi sang số,fill, in các ký tự'1'... đều là $O(\text{length}_x)$; theo quy tắc cộng Big-O ⇒ $O(\text{length}_x)$. Với $x \le 10^{16}$ (tối đa 17 chữ số) thì có thể coi là hằng số, nhưng cứ để $O(\text{length}_x)$ để nếu đề nâng $x$ lên tới $10^{1000}$ hay $10^{100000}$ thì lại khác.
Dùng vét cạn để kiểm tra cách tối ưu
Sẵn mình cho các bạn xem luôn cách dùng code vét cạn (cách 1) làm công cụ kiểm tra tính chính xác cho cách tối ưu (cách 2): sinh ngẫu nhiên $x$, chạy cả 2 hàm và so kết quả.
#include <iostream>
#include <ctime>
#include <string>
#include <algorithm>
using namespace std;
bool isSupersymmetryNumber(long long x){
int num = x % 10;
x /= 10;
while(x){
if(x % 10 != num){
return false;
}
x /= 10;
}
return true;
}
long long solve1(long long x){
long long z = x;
while(!isSupersymmetryNumber(++z));
return z - x;
}
long long solve2(long long a){
string x = to_string(a);
string z(x.length(), x[0]);
if(z > x){
return stoll(z) - stoll(x);
}
if(z[0] < '9'){
fill(z.begin(), z.end(), z[0] + 1);
return stoll(z) - stoll(x);
}
fill(z.begin(), z.end(), '1');
z.back() = '2';
return stoll(z);
}
int main(){
srand(time(0));
while(1){
long long x = 1 + rand() % 1000000;
long long res1 = solve1(x);
long long res2 = solve2(x);
if(res1 == res2){
cout << "OK\n";
}
else{
cout << "FAIL\n";
cout << "x = " << x << endl;
cout << "res1 = " << res1 << endl;
cout << "res2 = " << res2;
break;
}
}
return 0;
}
Chạy chương trình lên, treo 10 phút mà nhìn lại toàn thấy chữ OK là yên tâm code ổn. Còn thấy chữ FAIL to đùng xuất hiện thì chương trình dừng ngay, in ra $x$, res1, res2 – ta biết bị sai ở trường hợp $x$ đó và kiểm tra lại bài làm với nó.
- Không biết học gì trước, học gì sau, học lan man nên kiến thức bị hổng.
- Bí bài không ai gỡ, ngồi cả buổi vẫn không ra rồi bỏ cuộc.
- Code chạy được nhưng không đủ điểm, không biết sai ở đâu, chậm ở đâu.
- Không có đề thi thử và người chấm, nên không biết mình đang ở mức nào.
- Dễ nản giữa chừng khi không có ai theo sát.
- Lộ trình 111 buổi video sắp sẵn từ dòng code đầu tiên đến đề HSG thật của các tỉnh.
- Thầy trực tiếp gỡ bí, chữa bài cho từng em qua nhóm Zalo riêng.
- Hệ thống chấm bài tự động 24/7 và thi thử HSG hằng tuần để biết chính xác mình đang ở đâu.
- Đóng học phí một lần, thầy kèm lâu dài: từ thi chuyên Tin đến HSG quốc gia và cả khi lên đại học.
Nhận xét
Đăng nhận xét