Tính tổng (OLP2B)
Đề bài
Tính tổng (OLP MT&TN 2021 CT)
Giới hạn thời gian: 2 giây. Giới hạn bộ nhớ: 512 MB (theo trang LQDOJ).
Trang https://lqdoj.edu.vn/ đang là một trang học lập trình của nhiều học sinh, đó cũng là trang mà Hồng luyện tập để chuẩn bị cho kỳ thi Tin học trẻ. Hàng ngày, mẹ giao nhiệm vụ dạy em Phúc luyện tập về phép cộng, để có nhiều thời gian code, Hồng đã nghĩ ra loại bài tập cho em Phúc như sau: Chọn một số nguyên dương $n$, em Phúc cần phải cộng liên tiếp các số $n$ cho tới khi tất cả các chữ số của tổng nhận được đều bằng nhau và trả lời số lượng số đã cộng. Ví dụ, nếu $n = 37$ thì em Phúc cần cộng ba số $37$ để nhận được tổng là $111$ có các chữ số giống nhau.
Nhằm lựa chọn số nguyên dương $n$ để em Phúc không phải cộng quá nhiều và kiểm tra câu trả lời của em Phúc, Hồng muốn xây dựng một chương trình tính số lượng số cần cộng với một số nguyên dương $n$.
Input:
- Vào từ thiết bị vào chuẩn gồm một dòng chứa một số nguyên dương $n$ $(n \lt 10^3)$.
Output:
- Ghi ra thiết bị ra chuẩn một dòng chứa một số nguyên là số lượng số cần cộng. Trường hợp số $n$ đã gồm các chữ số giống nhau chỉ cần ghi số 1, trường hợp không tồn tại tổng có các chữ số giống nhau ghi -1.
Scoring:
- Subtask 1 ($40\%$ số điểm): số lần cộng không vượt quá $10^6$;
- Subtask 2 ($60\%$ số điểm): không có ràng buộc nào thêm.
373111100-1Lời mở đầu
Bài này mình được một em học viên hỏi và mình đã hướng dẫn chi tiết cách thức giải quyết cho em ấy. Tin rằng sẽ có giá trị với nhiều bạn nên mình viết lại thành bài giải đầy đủ để các bạn tham khảo nhen. Hy vọng mang đến được giá trị cho các bạn 😇.
Giải thích ví dụ cho bạn nào chưa hiểu đề:
- $n = 37$: $37$ (chưa được), $37 + 37 = 74$ (chưa được), $37 + 37 + 37 = 111$ có các chữ số giống nhau ⇒ cộng 3 số.
- $n = 11$: bản thân $11$ đã có các chữ số giống nhau ⇒ in 1.
- $n = 100$: tổng luôn là $100, 200, 300, \ldots$, luôn tận cùng bằng $00$ mà chữ số đầu khác $0$ ⇒ không bao giờ được ⇒ in -1.
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 1Làm theo đề: cộng dồn từng lần – Subtask 1
Khi mình hỏi em học viên có lấy được subtask 1 không, em trình bày: cho vào vòng for chạy $10^6$ lần, mỗi lần cộng thêm $n$ vào tổng rồi kiểm tra các chữ số có giống nhau không; hết $10^6$ lần mà vẫn chưa được thì in ra -1. Đúng là như vậy được với subtask 1, vì đề đã đảm bảo số lần cộng không vượt quá $10^6$.
Kiểm tra các chữ số giống nhau thì lấy chữ số cuối s % 10 làm mốc, rồi bóc dần từng chữ số (s % 10, s /= 10) xem có chữ số nào khác mốc không. Tổng lớn nhất khoảng $10^6 \cdot 999 \approx 10^9$ nên dùng long long cho chắc.
Source code để các bạn tham khảo nếu làm theo hướng này:
#include <bits/stdc++.h>
using namespace std;
bool sameDigits(long long s){
int last = s % 10;
while(s > 0){
if(s % 10 != last){
return false;
}
s /= 10;
}
return true;
}
int main(){
long long n, s = 0;
cin >> n;
for(int i = 1; i <= 1000000; ++i){
s += n;
if(sameDigits(s)){
cout << i;
return 0;
}
}
cout << -1;
return 0;
}
def sameDigits(s):
last = s % 10
while s > 0:
if s % 10 != last:
return False
s //= 10
return True
n = int(input())
s = 0
ans = -1
for i in range(1, 1000001):
s += n
if sameDigits(s):
ans = i
break
print(ans)
Đánh giá độ phức tạp của cách làm này: gọi $K$ là số lần cộng (tối đa $10^6$), mỗi lần kiểm tra chữ số mất khoảng 10 phép ⇒ $O(K \cdot \log_{10}(K \cdot n))$.
Cách 2Sinh số có các chữ số giống nhau + modulo + chia số lớn
Đổi góc nhìn: sinh ra kết quả trước
Đề bài cung cấp khá nhiều dữ kiện cho ta thấy hướng giải quyết, nhưng nếu chỉ nhìn đúng như đề mô tả thì ta sẽ đi cộng liên tiếp số $n$ đến khi nào ra kết quả. Hãy để ý: tổng sau cùng là một số có tất cả các chữ số bằng nhau, và tổng của $k$ số $n$ chính là $k \cdot n$.
Ý tưởng là tạo ra các số $x$ thoả mãn toàn bộ các chữ số giống nhau và thử xem $x$ có chia hết cho $n$ không. Nếu có thì đáp án là $x / n$. Đề cần số lần cộng ít nhất, tức $x / n$ nhỏ nhất, mà $n$ cố định ⇒ $x$ phải nhỏ nhất có thể. Vậy ta tạo các số $x$ theo giá trị tăng dần, gặp số $x$ đầu tiên chia hết cho $n$ thì tính $x / n$ ra đáp án và dừng luôn. Các giá trị $x$ tăng dần là:
Cứ thế đến vô tận… Nói vô tận cũng không đúng, vì đề nói nếu không có cách nào thì in ra -1. Mà làm sao biết được liệu có đáp án không? Theo tư duy của mình: thời gian có hạn nên cho lặp qua khoảng 1 triệu giá trị $x$ theo chiều tăng dần. Sau 1 triệu giá trị mà vẫn chưa tìm được $x$ chia hết cho $n$ thì kết luận luôn -1 😄, chứ sinh tiếp đến bao giờ.
Vấn đề: số $x$ quá lớn
Việc sinh ra số $x$ không khó, nhưng cần ngẫm chỗ này: kiểu số nguyên có sẵn lớn nhất 8 byte chỉ lưu được khoảng 19–20 chữ số. Trong khi đó mỗi độ dài có 9 số, nếu sinh 1 triệu số thì độ dài có thể lên tới $10^6 / 9 \approx 111\,111$ chữ số, làm gì có kiểu nào chứa nổi đúng không?
Hãy để ý: $x$ cực kỳ lớn nhưng $n$ rất nhỏ, chưa tới một ngàn. Kiểm tra $x$ chia hết cho $n$ tức là kiểm tra x % n == 0, nên ta chỉ cần biết $x \bmod n$ chứ không cần biết $x$. Ta dựa vào các tính chất modulo:
$(a - b) \bmod c = ((a \bmod c) - (b \bmod c)) \bmod c$
$(a \cdot b) \bmod c = ((a \bmod c) \cdot (b \bmod c)) \bmod c$
Lưu ý là không có phép chia nhé, chỉ có cộng, trừ, nhân thôi.
Tưởng tượng khi $x = 11$ thì $11 = 1 \cdot 10 + 1$; khi $x = 555$ thì $555 = 55 \cdot 10 + 5$. Vậy với mỗi chữ số $d$, ta lưu trạng thái $a_d$ = (số gồm $L$ chữ số $d$) $\bmod n$. Giả sử đang ở $88$, khi sang $888$ chỉ cần lấy lại kết quả của $88$ đã $\bmod n$, nhân $10$, cộng $8$, rồi $\bmod n$:
Vì $a_d \lt n \lt 1000$ nên $a_d \cdot 10 + d$ chưa tới $10^4$, dùng int thoải mái.
Tính kết quả $x / n$
$x$ có thể là số rất lớn (vài trăm chữ số), nên:
- Code Python thì khoẻ, cứ tính trực tiếp
x // nvì Python hỗ trợ số lớn sẵn. Các ngôn ngữ như C#, Java cũng có classBigIntegergọi ra xài thôi. - Code C++ thì phải tự xử lý số lớn (big int) cho phép chia. Lúc làm bài mình đi copy nguyên source big int có sẵn về dùng 😄, còn nếu trong phòng thi thì phải tự ngồi code phép chia, y như phép chia hồi tiểu học á. Big int cũng phổ biến nên nhiều bạn còn học thuộc code luôn, để đụng là múc.
Ở bài này số chia $n$ nhỏ (dưới 1000) nên phép chia "số lớn cho số nhỏ" rất gọn: đi từ trái sang phải, hạ từng chữ số của $x$ xuống phần dư r = r * 10 + digit, ghi chữ số thương r / n (bỏ các số 0 ở đầu), rồi giữ lại dư r %= n. Trong code bên dưới mình viết luôn phần chia này thay cho cả thư viện big int để các bạn dễ đọc.
Minh hoạ sinh số và cập nhật số dư
Em nhập $n$ rồi bấm Tiếp: bảng có 9 dòng ứng với 9 chữ số $d$, cột cuối là $a_d = x \bmod n$. Dòng cam là số $x$ vừa sinh, dòng xanh là số $x$ chia hết cho $n$. (Khi số chữ số $L \ge 4$, mỗi bước gom cả 9 số cùng độ dài cho gọn.)
Source code để các bạn tham khảo nếu làm theo hướng này:
#include <bits/stdc++.h>
using namespace std;
int main(){
int n;
cin >> n;
int a[10] = {0}; // a[d] = (số gồm L chữ số d) % n
int len = 0, digit = 0;
// sinh tối đa 9 * 111111 (khoảng 1 triệu) số x tăng dần
for(int L = 1; L <= 111111 && digit == 0; ++L){
for(int d = 1; d <= 9; ++d){
a[d] = (a[d] * 10 + d) % n;
if(a[d] == 0){
len = L;
digit = d;
break;
}
}
}
if(digit == 0){
cout << -1;
return 0;
}
// chia số lớn x (digit lặp len lần) cho n như phép chia tiểu học
string q = "";
int r = 0;
for(int k = 0; k < len; ++k){
r = r * 10 + digit;
if(q != "" || r / n != 0){
q += char('0' + r / n);
}
r %= n;
}
cout << q;
return 0;
}
n = int(input())
a = [0] * 10 # a[d] = (số gồm L chữ số d) % n
length = 0
digit = 0
L = 1
# sinh tối đa 9 * 111111 (khoảng 1 triệu) số x tăng dần
while L <= 111111 and digit == 0:
for d in range(1, 10):
a[d] = (a[d] * 10 + d) % n
if a[d] == 0:
length = L
digit = d
break
L += 1
if digit == 0:
print(-1)
else:
x = int(str(digit) * length) # Python hỗ trợ số lớn sẵn
print(x // n)
Một vài ghi chú về code:
- Vòng
forngoài sinh độ dàiL, vòng trong sinh chữ sốdtừ 1 đến 9, nên các số $x$ được xét đúng theo thứ tự tăng dần $1, 2, \ldots, 9, 11, \ldots, 99, 111, \ldots$ Điều kiệndigit == 0ở vòng ngoài giúp dừng ngay khi đã tìm thấy. - Với $L \le 111\,111$, ta xét tối đa $9 \cdot 111\,111 \approx 10^6$ số $x$, đúng như ngưỡng 1 triệu đã nói.
- Phép chia ở cuối bỏ qua chữ số 0 ở đầu thương: vì $x \ge n$ nên thương luôn có ít nhất một chữ số khác 0,
qkhông bao giờ rỗng. - Bản gốc của mình lúc nộp bài dùng nguyên một thư viện big int (cộng, trừ, nhân, chia đầy đủ) copy từ VNOI, phần của mình chỉ có hàm
mainvới đúng ý tưởng trên. Ở đây mình thay thư viện đó bằng đoạn chia số lớn cho số nhỏ cho gọn, kết quả y hệt. - Bản Python:
int(str(digit) * length)dựng $x$ rồi lấyx // n. Python từ bản 3.11 giới hạn chuyển chuỗi thành số tối đa 4300 chữ số, ở bài này $x$ dài nhất chỉ 982 chữ số nên không sao.
Đánh giá độ phức tạp của cách làm này (gọi $L^*$ là độ dài của $x$ tìm được):
- Không gian: mảng 10 phần tử cộng chuỗi thương dài tối đa $L^*$ ⇒ $O(L^*)$.
- Thời gian: sinh số mất $O(9 \cdot L^*)$, chia số lớn mất $O(L^*)$. Với $n \lt 1000$ thì $L^* \le 982$, còn trường hợp -1 lặp đủ khoảng $10^6$ lần, vẫn rất nhanh.
- 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