Số Siêu May Mắn
Đề bài
Số siêu may mắn (input file: stdin, output file: stdout, time limit: 1000 ms, memory limit: 1256000 kB)
Số may mắn theo quan niệm của người Mỹ là số mà các chữ số của nó chỉ gồm số 4 và 7. Ví dụ: 47, 744, 4 là số may mắn còn 5, 17, 467 thì không phải. Số siêu may mắn là số may mắn có số lượng chữ số 4 và 7 bằng nhau. Ví dụ: 47, 7744, 474477 là số siêu may mắn còn 4, 744, 467 thì không phải.
Task: Hãy lập trình tìm số siêu may mắn nhỏ nhất lớn hơn số nguyên dương $n$ cho trước.
Input:
- Dòng đầu tiên chứa số nguyên dương $T$ là số testcase thỏa $1 \le T \le 100$.
- $T$ dòng tiếp theo chứa số nguyên dương $n$ thỏa $1 \le n \le 10^9$, $n$ không chứa chữ số 0 đứng trước.
Output:
- Ứng với mỗi testcase in ra số siêu may mắn nhỏ nhất lớn hơn hoặc bằng $n$, mỗi testcase in trên một dòng.
2
4500
474747
47Lời mở đầu
Nay mình đi comment dạo thấy bài này cũng được nên đăng để anh em thử sức, 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 😇.
Bạn nào muốn thử nộp bài để hệ thống chấm xem có đúng hay không thì link bài đây nhé:
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 OJMình giải thích ví dụ mẫu để bạn nào chưa hiểu đề sẽ hiểu rõ:
- $n = 4500$: các số siêu may mắn có 4 chữ số theo thứ tự tăng dần là 4477, 4747, 4774, 7447, 7474, 7744. Số 4477 nhỏ hơn 4500 nên bị loại, số đầu tiên $\ge 4500$ là 4747.
- $n = 47$: bản thân 47 đã là số siêu may mắn (một chữ số 4, một chữ số 7) nên in luôn 47.
Lộ trình lời giải, bấm vào từng phần để nhảy tới:
Cách 1Làm theo đề: tăng n từng đơn vị – chậm
Cách nghĩ đầu tiên ai cũng nghĩ ra: viết hàm kiểm tra một số $x$ có phải số siêu may mắn không (tách từng chữ số, gặp chữ số khác 4 và 7 là loại ngay, cuối cùng so số lượng chữ số 4 và chữ số 7). Sau đó cứ cho $n$ tăng dần lên từng đơn vị cho tới khi hàm kiểm tra trả về đúng thì in ra.
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 sieuMayMan(long long x){
int c4 = 0, c7 = 0;
while(x > 0){
int d = x % 10;
if(d == 4) c4++;
else if(d == 7) c7++;
else return false;
x /= 10;
}
return c4 == c7;
}
int main(){
int T;
cin >> T;
while(T--){
long long n;
cin >> n;
while(!sieuMayMan(n)) n++;
cout << n << "\n";
}
return 0;
}
def sieu_may_man(x):
c4 = c7 = 0
while x > 0:
d = x % 10
if d == 4:
c4 += 1
elif d == 7:
c7 += 1
else:
return False
x //= 10
return c4 == c7
T = int(input())
for _ in range(T):
n = int(input())
while not sieu_may_man(n):
n += 1
print(n)
Đánh giá độ phức tạp của cách làm này:
- Không gian: $O(1)$.
- Thời gian: mỗi lần kiểm tra mất $O(\log_{10} n)$, nhưng số lần phải tăng $n$ có thể rất lớn. Ví dụ $n = 777777778$: số siêu may mắn có 8 chữ số lớn nhất là 77774444 < $n$, nên đáp án phải nhảy lên 10 chữ số là 4444477777, tức là phải tăng khoảng $3.67 \cdot 10^9$ lần!
int (khoảng $2.1 \cdot 10^9$). Cả 2 cách đều phải lưu bằng long long.Cách 2Sinh hết số siêu may mắn rồi tìm kiếm nhị phân – O(350 + T log 350)
Có bạn bình luận dưới bài là "dùng IDE online liệt kê hết các số $\le 10^9$ thoả mãn rồi tạo một mảng chứa hết các số đó, việc còn lại là duyệt mảng". Thật ra đó chính là ý tưởng đúng 👍, chỉ có điều mình không cần chép tay ra mảng, mà để chương trình tự sinh ngay lúc chạy. Lý do là các số siêu may mắn rất ít:
- Số siêu may mắn có số chữ số 4 bằng số chữ số 7 nên độ dài luôn chẵn: $2, 4, 6, 8, 10, \ldots$
- Với độ dài $2k$, ta chỉ cần chọn $k$ vị trí để đặt chữ số 4 (còn lại là 7), nên có $C_{2k}^{k}$ số.
- Vì $n \le 10^9$ và 4444477777 (10 chữ số) đã lớn hơn $10^9$, nên đáp án luôn có không quá 10 chữ số.
| Độ dài | 2 | 4 | 6 | 8 | 10 | Tổng |
|---|---|---|---|---|---|---|
| Số lượng $C_{2k}^{k}$ | 2 | 6 | 20 | 70 | 252 | 350 |
Chỉ có 350 số thôi các bạn! Vậy mình sinh hết 350 số này vào mảng a theo thứ tự tăng dần, rồi với mỗi $n$ chỉ cần tìm phần tử đầu tiên của a mà $\ge n$ – đó chính là lower_bound (tìm kiếm nhị phân).
44..477..7 ($k$ chữ số 4 rồi $k$ chữ số 7), rồi gọi next_permutation liên tục để đi qua tất cả các hoán vị khác nhau của xâu theo thứ tự từ điển tăng dần. Các xâu cùng độ dài thì so sánh từ điển cũng chính là so sánh giá trị số, và số ngắn hơn luôn nhỏ hơn số dài hơn ⇒ duyệt độ dài $2, 4, \ldots, 10$ và đẩy lần lượt vào mảng thì mảng a tự động đã được sắp xếp, không cần sort.Hàm next_permutation biến xâu hiện tại thành hoán vị kế tiếp qua 3 bước:
- Từ phải sang trái, tìm vị trí $i$ đầu tiên mà $s_i \lt s_{i+1}$ (ở đây tức là một chữ số 4 đứng ngay trước một chữ số 7). Không có vị trí nào như vậy ⇒ xâu đã là hoán vị lớn nhất (dạng
77..744..4), hàm trả vềfalsevà vòng lặp dừng. - Tìm vị trí $j \gt i$ xa nhất bên phải mà $s_j \gt s_i$ (một chữ số 7), đổi chỗ $s_i$ và $s_j$.
- Đảo ngược đoạn phía sau vị trí $i$ để đoạn đó nhỏ nhất có thể.
Minh hoạ sinh hoán vị kế tiếp
Các bạn chọn độ dài rồi bấm Tiếp để xem từng bước next_permutation sinh ra các số siêu may mắn theo thứ tự tăng dần nhé:
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(){
vector<long long> a;
for(int len = 2; len <= 10; len += 2){
string s = string(len / 2, '4') + string(len / 2, '7');
do{
a.push_back(stoll(s));
}while(next_permutation(s.begin(), s.end()));
}
int T;
cin >> T;
while(T--){
long long n;
cin >> n;
int k = lower_bound(a.begin(), a.end(), n) - a.begin();
cout << a[k] << "\n";
}
return 0;
}
import sys
from bisect import bisect_left
from itertools import combinations
a = []
for length in range(2, 11, 2):
# chọn length/2 vị trí đặt chữ số 4, các vị trí còn lại là 7
for pos in combinations(range(length), length // 2):
s = ['7'] * length
for p in pos:
s[p] = '4'
a.append(int(''.join(s)))
a.sort()
data = sys.stdin.read().split()
T = int(data[0])
out = []
for i in range(1, T + 1):
n = int(data[i])
k = bisect_left(a, n)
out.append(str(a[k]))
print('\n'.join(out))
next_permutation, nên mình dùng itertools.combinations để chọn $k$ vị trí đặt chữ số 4 (đúng với công thức $C_{2k}^{k}$ ở trên), sau đó sort lại cho chắc rồi dùng bisect_left – chính là lower_bound của Python.Khung chạy tay bên dưới chạy đúng từng dòng code C++ ở trên. Phần sinh mảng có khoảng 700 bước (350 số), các bạn có thể bấm ⏭ để nhảy tới cuối rồi bấm ◀ Lùi để xem phần trả lời từng testcase nhé:
Đánh giá độ phức tạp của cách làm này:
- Không gian: mảng
achỉ có 350 phần tử – coi như $O(1)$. - Thời gian: sinh 350 số mất cỡ $350 \times 10$ phép tính; mỗi testcase tìm kiếm nhị phân mất $O(\log 350) \approx 9$ phép so sánh. Tổng cộng $O(350 \cdot 10 + T \log 350)$ – chạy tức thì.
lower_bound luôn tìm thấy và a[k] không bao giờ vượt ra ngoài mảng.- 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