Tuyển sinh 🎓 Mình nhận đào tạo lập trình giải thuật thi đấu: thi vào lớp 10 chuyên tin · thi học sinh giỏi tin học cấp tỉnh/thành · thi vào đội tuyển quốc gia · Olympic tin học sinh viên / ICPC · phỏng vấn thuật toán. Bấm vào đây để xem thông tin khóa học → Xem khóa học

Số siêu nguyên tố (SPRIME)

Số siêu nguyên tố (SPRIME)
Nguồn: LuyenCode Độ khó: Trung bình Số nguyên tố Hàng đợi

Đề bài

Số siêu nguyên tố là số nguyên tố mà khi bỏ một số tuỳ ý các chữ số bên phải của nó thì phần còn lại vẫn tạo thành một số nguyên tố.

Ví dụ: $7331$ là một số siêu nguyên tố có $4$ chữ số vì $733$, $73$, $7$ cũng là các số nguyên tố.

Hiếu tình cờ nhặt được số nguyên dương $n$, và tự hỏi có bao nhiêu số siêu nguyên tố có $n$ chữ số. Bạn hãy giúp Hiếu giải đáp câu hỏi trên nhé!

Dữ liệu vào: số nguyên dương $n$ ($1 \le n \le 10$).

Dữ liệu ra: in ra các số siêu nguyên tố có $n$ chữ số trên cùng $1$ dòng, cách nhau $1$ kí tự trắng.

Ví dụ – input
1
Ví dụ – output
2 3 5 7

Lờ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 chia sẻ đề bài để các bạn thử sức, kèm theo đó là toàn bộ tin nhắn của mình với em ấy để bạn nào không giải quyết được thì có thể tham khảo nhen. Hi vọng mang đến được giá trị cho các bạn 😇. Bài này các bạn có thể xem đề và nộp tại đây nha:

Dành cho tất cả mọi ngườiNộp bài trên LuyenCode

Link này công khai, bạn nào muốn thử nộp bài để hệ thống chấm xem có đúng hay không thì dùng link này nhé. Chưa có tài khoản thì tạo tài khoản rồi đăng nhập nộp bài thử hen.

Nộp bài trên LuyenCode

Lộ trình lời giải, bấm vào từng phần để nhảy tới:

Vấn đềVì sao xét hết các số có $n$ chữ số bị TLE?

Đầu tiên em ấy cho mình xem code bài làm, đi theo đúng như định nghĩa đề bài: viết hàm kiểm tra số $X$ bất kỳ có phải số nguyên tố không, sau đó duyệt qua toàn bộ các số có $n$ chữ số (ví dụ $n = 4$ thì xét từ 1000 đến 9999). Với mỗi số thì loại bỏ dần từng chữ số bên phải và kiểm tra phần còn lại có còn là số nguyên tố không, nếu tất cả đều thoả thì đó là số siêu nguyên tố. Ví dụ 1234 thì xét 1234, 123, 12, 1.

HV
“Em làm đúng như đề: duyệt hết các số có N chữ số, bỏ dần chữ số bên phải rồi kiểm tra nguyên tố, mà sao hệ thống báo TLE vậy anh? Anh hướng dẫn em cách làm với ạ.”

Nếu làm như cách trên của em thì với $n$ tối đa là 10 như đề bài cho, nghĩa là lặp từ 1 tỷ đến 10 tỷ → lặp 9 tỷ lần. Mình còn chưa tính chi phí với mỗi số phải đi loại bỏ dần bên phải, rồi mỗi số đó còn phải kiểm tra nguyên tố. Chỉ nói riêng việc lặp 9 tỷ lần: máy tính phổ thông 1 giây lặp được khoảng 30 đến 50 triệu lần, tức $(3 \text{ đến } 5) \cdot 10^7$. Cho 1 giây được $5 \cdot 10^7$ đi thì $\frac{9 \cdot 10^9}{5 \cdot 10^7} \approx 200$ giây. Trong khi bài này chỉ cho 20 ms 🤣 tức là $\frac{1}{50}$ giây! Nội việc lặp 9 tỷ lần đã cần ít nhất 200 giây, chưa kể còn đống bước xử lý sau đó, không TLE mới lạ 😝

Ngưỡng cho 20 ms
$\frac{5 \cdot 10^7}{50} = 10^6$. Nghĩa là toàn bài từ đầu (input) đến hết bước xử lý ra kết quả, tính trong trường hợp xấu nhất ($n = 10$), mà tổng số lần lặp tầm 1 triệu đổ lại thì yên tâm chạy trong 20 ms; quá ngưỡng đó thì 20 ms không chịu nổi đâu.

Bài này không thể nào xét hết toàn bộ các số có $n$ chữ số như thế. Em hãy nghĩ ra một cách khác sao cho số lượng số cần xét giảm đi thật nhiều dựa trên định nghĩa của số này nhé. Phải nói là giảm đi rất rất nhiều, không cần xét hết mà chỉ một số rất ít thôi 😁

Ý tưởngKiểm tra từ số nhỏ, sinh dần từ trái sang phải

Dưới đây là hướng giải mình nói ra luôn sau khi em ấy vẫn chưa giải quyết được.

Giả sử cần liệt kê tất cả các số siêu nguyên tố có 5 chữ số. Số có 5 chữ số sẽ có dạng $\overline{abcde}$, thì nó phải thoả tất cả:

abcde2nguyên tố ✓
abcde23nguyên tố ✓
abcde235không ✗ – dừng!
abcde2357khỏi xét
abcde23579khỏi xét

Thì chắc chắn ta sẽ cần một hàm kiểm tra số $X$ bất kỳ có là số nguyên tố không. Vấn đề là $X$ càng lớn thì hàm đó càng phải chạy nhiều và lâu.

Quay lại ví dụ số 23579. Em có nghĩ là cần phải kiểm tra cả số 23579 ngay từ đầu không? Một cách làm thông minh hơn là kiểm tra các số thật nhỏ trước, nếu thoả thì mới đi kiểm tra số lớn hơn, còn không thoả thì khỏi kiểm tra luôn. Với 23579: kiểm tra 2 thoả → kiểm tra tiếp 23 thoả → kiểm tra tiếp 235 không thoả → dừng, khỏi mất công kiểm tra 2357, 23579 làm gì. Hay với số 45793: ngay từ đầu 4 đã không phải số nguyên tố thì dừng luôn, khỏi xét 45, 457, 4579, 45793.

Sinh dần từng tầng

  1. Đầu tiên tạo danh sách có 4 số: 2, 3, 5, 7 – đây chính là 4 số siêu nguyên tố có 1 chữ số.
  2. Từ 4 số này sinh ra các số siêu nguyên tố có 2 chữ số bằng cách ghép thêm 1 chữ số vào bên phải. Ta không cần xét chữ số chẵn (0, 2, 4, 6, 8) vì khi đó số mới chia hết cho 2, chắc chắn không phải số nguyên tố. Nên ví dụ với số 2, không cần xét 20, 22, 24, 26, 28 mà chỉ xét với các chữ số lẻ 1, 3, 5, 7, 9: 21, 23, 25, 27, 29 – thằng nào là nguyên tố thì giữ lại.
  3. Tương tự với 3 xét 31, 33, 35, 37, 39; với 5 xét 51, 53, 55, 57, 59; với 7 xét 71, 73, 75, 77, 79.
  4. Trong 21, 23, 25, 27, 29 thì 23, 29 thoả → giữ lại để sinh tầng 3 chữ số: từ 23 sinh 231, 233, 235, 237, 239; từ 29 sinh 291, 293, 295, 297, 299...
  5. Cứ thế sinh liên tục đến khi được các số có đủ $n$ chữ số thì dừng, in ra toàn bộ các số đó.
Vì sao cách này đúng?
Mọi số siêu nguyên tố $n$ chữ số, khi bỏ chữ số cuối, đều là số siêu nguyên tố $n - 1$ chữ số. Nên chỉ cần sinh từ các số siêu nguyên tố của tầng trước là không bỏ sót số nào; và mỗi số sinh ra đều đã được kiểm tra đủ mọi tiền tố nên không thừa.
Số chữ số12345678910
Số lượng số siêu nguyên tố49141615128500

Bảng trên cho thấy số lượng cần xét cực kỳ ít: mỗi tầng chỉ có vài chục số, tầng 9 thì không còn số nào (5 số 8 chữ số ghép thêm chữ số nào cũng không ra số nguyên tố) nên tầng 10 cũng rỗng luôn.

Minh hoạCây sinh số siêu nguyên tố

Em nhập $n$ (từ 1 đến 6) rồi bấm Tiếp để xem mỗi số ở tầng trước được ghép thêm 1, 3, 5, 7, 9 như thế nào, số nào là nguyên tố thì được giữ lại cho tầng sau:

Ví dụ nhanh:

CodeHai hàng đợi q, q2 – sinh theo tầng

Source code giải quyết theo tư tưởng mình đã trình bày ở trên và cũng đã nộp thành công trên web, các bạn xem để hiểu rõ hơn. Hàng đợi q chứa các số siêu nguyên tố của tầng hiện tại, còn q2 dùng để chứa các số của tầng kế tiếp; xử lý xong một tầng thì gán q = q2 và làm rỗng q2, biến cnt đếm số chữ số hiện có:

#include <bits/stdc++.h>
using namespace std;
bool isPrime(long long x){
    for(long long i = 2; i * i <= x; i++){
        if(x % i == 0){
            return false;
        }
    }
    return x >= 2;
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    long long n;
    cin >> n;
    queue<long long> q, q2;
    q.push(2LL);
    q.push(3LL);
    q.push(5LL);
    q.push(7LL);
    long long cnt = 1;
    while(cnt < n){
        while(!q.empty()){
            long long x = q.front();
            q.pop();
            if(isPrime(x * 10 + 1)){
                q2.push(x * 10 + 1);
            }
            if(isPrime(x * 10 + 3)){
                q2.push(x * 10 + 3);
            }
            if(isPrime(x * 10 + 5)){
                q2.push(x * 10 + 5);
            }
            if(isPrime(x * 10 + 7)){
                q2.push(x * 10 + 7);
            }
            if(isPrime(x * 10 + 9)){
                q2.push(x * 10 + 9);
            }
        }
        q = q2;
        queue<long long> q_empty;
        swap(q2, q_empty);
        cnt++;
    }
    while(!q.empty()){
        cout << q.front() << " ";
        q.pop();
    }
    return 0;
}
from collections import deque
import sys
input = sys.stdin.readline

def is_prime(x):
    if x < 2:
        return False
    for i in range(2, int(x**0.5) + 1):
        if x % i == 0:
            return False
    return True

n = int(input())
q = deque([2, 3, 5, 7])
q2 = deque()
cnt = 1
while cnt < n:
    while q:
        x = q.popleft()
        for d in [1, 3, 5, 7, 9]:
            new_num = x * 10 + d
            if is_prime(new_num):
                q2.append(new_num)
    q = q2
    q2 = deque()
    cnt += 1
for num in q:
    print(num, end=' ')
C++▶ Chạy tay từng dòng codeSinh dần các số siêu nguyên tố theo tầng bằng 2 hàng đợi q, q2.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3bool isPrime(long long x){
4 for(long long i = 2; i * i <= x; i++){
5 if(x % i == 0){
6 return false;
7 }
8 }
9 return x >= 2;
10}
11int main(){
12 ios::sync_with_stdio(0);
13 cin.tie(0);
14 long long n;
15 cin >> n;
16 queue<long long> q, q2;
17 q.push(2LL);
18 q.push(3LL);
19 q.push(5LL);
20 q.push(7LL);
21 long long cnt = 1;
22 while(cnt < n){
23 while(!q.empty()){
24 long long x = q.front();
25 q.pop();
26 if(isPrime(x * 10 + 1)){
27 q2.push(x * 10 + 1);
28 }
29 if(isPrime(x * 10 + 3)){
30 q2.push(x * 10 + 3);
31 }
32 if(isPrime(x * 10 + 5)){
33 q2.push(x * 10 + 5);
34 }
35 if(isPrime(x * 10 + 7)){
36 q2.push(x * 10 + 7);
37 }
38 if(isPrime(x * 10 + 9)){
39 q2.push(x * 10 + 9);
40 }
41 }
42 q = q2;
43 queue<long long> q_empty;
44 swap(q2, q_empty);
45 cnt++;
46 }
47 while(!q.empty()){
48 cout << q.front() << " ";
49 q.pop();
50 }
51 return 0;
52}

Đánh giá độ phức tạp: cả bài chỉ có 83 số siêu nguyên tố (từ 1 đến 8 chữ số), nên tổng số lần gọi isPrime chỉ khoảng $5 \times 83 \approx 400$ lần, mỗi lần tối đa $\sqrt{X} \le 10^5$ vòng lặp. Thực tế phần lớn số bị loại ngay ở vài ước nhỏ đầu tiên, nên toàn bài chạy nhanh hơn ngưỡng $10^6$ rất nhiều.

Không gian$O(S)$ với $S$ là số lượng số siêu nguyên tố trong 1 tầng
Thời gian$O(5 \cdot S_{tong} \cdot \sqrt{X})$, rất nhỏ
Kết quả kiểm tra: code C++ và Python của mình được so với cách vét cạn (duyệt mọi số có $n$ chữ số, bỏ dần chữ số bên phải và kiểm tra nguyên tố) cho mọi $n$ từ 1 đến 8 – khớp hết. Vì $n$ chỉ có 10 giá trị nên mình chạy đủ cả 10 trường hợp: với $n = 9$ và $n = 10$ không có số nào nên chương trình in dòng trống. Với $n = 10$ bản C++ chạy khoảng 4 ms, bản Python khoảng 0,03 giây.
Rất vui khi anh em nào đã đọc đến những dòng cuối cùng này. Anh em đọc chắc cũng mỏi mắt luôn chứ 😅 vậy anh em nghĩ người viết ra hết đống ở trên còn thế nào nữa 🤪. Vậy nên đừng tiếc gì cho mình một lời bình luận hoặc chia sẻ bài viết để động viên mình nè. Chúc anh em học tốt và gặt hái nhiều thành công 😇

Mã hóa Ceasar

Mã hóa Ceasar
Nguồn: LuyenCode Độ khó: Dễ Xâu ký tự Mã ASCII

Đề bài

Trước công nguyên, nhà quân sự người La Mã Julius Ceasar đã nghĩ ra phương pháp mã hóa một bản tin như sau: thay thế mỗi chữ trong bản tin bằng chữ đứng sau nó $k$ vị trí trong bảng chữ cái. Giả sử chọn $k = 3$, ta có bảng chuyển đổi như sau:

Chữ ban đầuabcdefghijklmnopqrstuvwxyz
Chữ thay thếdefghijklmnopqrstuvwxyzabc

Giả sử bản tin là: 'attack' thì sau khi mã hóa sẽ có bản mã 'dwwdfn' và Ceasar gửi bản mã cho cấp dưới.

Nhận được bản mã và khóa, cấp dưới của Ceasar chưa biết giải mã làm sao. Bạn hãy giúp họ đi nào.

Dữ liệu vào:

  • Dòng đầu tiên là bản tin đã được mã hóa, chỉ gồm các chữ cái la tinh thường, chiều dài không quá 100 ký tự.
  • Dòng thứ 2 là số nguyên thể hiện khóa $k$ ($1 \le k \le 25$).

Dữ liệu ra: bản tin ban đầu.

Ví dụ – input
wyvnyhttpun
7
Ví dụ – output
programming

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 😇. Các bạn nào muốn thử nộp bài để hệ thống chấm có đúng hay không thì link bài đây nha:

Dành cho tất cả mọi ngườiNộp bài trên LuyenCode

Link này công khai, bạn nào muốn thử nộp bài để hệ thống chấm xem có đúng hay không thì dùng link này nhé. Chưa có tài khoản thì tạo tài khoản rồi đăng nhập nộp bài thử hen.

Nộp bài trên LuyenCode

Lộ trình lời giải, bấm vào từng phần để nhảy tới:

Chuẩn bịGiới hạn 20 ms thì được lặp bao nhiêu lần?

Trước khi đi vào vấn đề chính của bài thì mình muốn các bạn phải đảm bảo biết qua điều này để về sau các bạn xem mới hiểu.

Với các hệ thống web chấm bài giới hạn thời gian chạy tối đa 1 giây thì các bạn sẽ rất dễ bị rơi vào lỗi Time Limit Exceeded (TLE: chạy quá giới hạn thời gian cho phép). Như mình đã nói rất nhiều lần: nếu không muốn bị TLE thì tối đa số phép toán trên toàn bài của các bạn chỉ nên ở ngưỡng $(3 \text{ đến } 5) \cdot 10^7$ – ngưỡng an toàn với phần lớn hệ thống chấm bài; với một số hệ thống server mạnh hơn thì có thể nâng lên $10^8$ nhưng cũng hên xui nhé.

Thực ra ta không phải đếm chi tiết từng phép toán mà dựa theo số lần vòng lặp chạy, bởi bên trong vòng lặp nó xử lý lặp đi lặp lại nhiều phép toán. Lưu ý ngưỡng trên là với C/C++ thôi nhé, các ngôn ngữ chạy chậm hơn như Python, C#, Java thì ngưỡng phải thấp hơn. Còn nếu đề cho 2 giây thì các bạn cứ tỷ lệ thuận nhân 2 lên, 3 giây thì nhân 3, 100 ms tức là $\frac{1}{10}$ giây thì chia 10, cứ thế thôi.

Áp dụng cho bài này
Giới hạn thời gian là 20 ms $= \frac{20}{1000} = \frac{1}{50}$ giây. Lấy ngưỡng 1 giây chia cho 50 → ngưỡng cho 20 ms là khoảng $(6 \text{ đến } 10) \cdot 10^5$, cao lắm là $2 \cdot 10^6$ lần lặp.

Bài này phải nói là nên ở mức dễ sẽ đúng hơn là mức trung bình, vì chuỗi chỉ dài tối đa 100 là quá nhỏ, dù cho cách làm tệ nhất vẫn không sợ bị chạy quá thời gian (tất nhiên nếu tệ quá mức thì vẫn bị nhé 🤪). Tuy nhiên chưa chắc bạn nào cũng giải quyết theo cách gọn gàng – mình đã xem qua code của bạn hỏi bài thì thấy bạn xử lý khá rườm rà phức tạp, nên ở đây mình sẽ hướng dẫn một cách rất gọn, các bạn chú ý nha.

Nền tảngKý tự thực chất là số nguyên (mã ASCII)

Để làm được bài này thì các bạn phải hiểu quy luật mã ASCII của các ký tự. Bản chất kiểu dữ liệu ký tự trong C/C++ chính là kiểu số nguyên, bởi các ký tự được quản lý thông qua mã ASCII của nó – có thể hiểu là "mã số" của ký tự. 26 chữ cái từ 'a' đến 'z' tương ứng với các số nguyên từ 97 đến 122: 97 là mã của 'a', 98 là mã của 'b', 99 là mã của 'c', ... đến 122 là mã của 'z'.

Ký tựabcdefghijklmnopqrstuvwxyz
Mã ASCII979899100101102103104105106107108109110111112113114115116117118119120121122

Ví dụ đoạn code sau:

#include <iostream>
using namespace std;
int main(){
    char x = 'a';
    cout << x << " " << (int)x;
    return 0;
}
x = 'a'
print(x, ord(x))

Sẽ thấy kết quả in ra là: a 97.

Nếu trước dòng cout ta thêm câu lệnh x++; thì kết quả sẽ là b 98, vì $x$ đang là 97 được cộng thêm 1 thành 98, in ra giá trị $x$ lúc này chính là ký tự 'b'. Tương tự nếu x += 3; thì 97 + 3 = 100 là ký tự 'd'. Rồi từ 100 mà x -= 2 còn 98 thì in ra chính là 'b'. Các bạn chạy thử đoạn code sau:

#include <iostream>
using namespace std;
int main(){
    char x = 'a';
    x += 3;
    cout << x << " " << (int)x << endl;
    x -= 2;
    cout << x << " " << (int)x;
    return 0;
}
x = 'a'
x = chr(ord(x) + 3)
print(x, ord(x))
x = chr(ord(x) - 2)
print(x, ord(x))

Sẽ thấy kết quả in ra là:

d 100
b 98

Đến đây thì các bạn đã hiểu quy luật thao tác với mã ASCII của các ký tự rồi hen. Nó chính là mấu chốt để ta giải quyết nhanh gọn bài này.

Ý tưởngLùi $k$ bước, tụt khỏi 'a' thì cộng 26

Nhìn lại cách mã hóa

Quy luật mã hóa là từ vị trí ký tự hiện tại cộng lên $k$ để lấy ra ký tự thay thế. Mã ASCII của các chữ cái chạy từ 97 ('a') đến 122 ('z'); cứ cộng lên $k$ nếu vượt quá 122 thì bắt đầu lại từ 97. Ví dụ ký tự đang xét là 'x' (mã 120) và $k = 4$ thì ký tự thay thế sẽ là 'b' (mã 98): $120 + 4 = 124$ đã vượt 122 nên trừ cho 122 được $124 - 122 = 2$; rồi do bắt đầu lại từ 'a' (mã 97) nên $2 + 97 - 1 = 98$. Ta phải $-1$ vì chính ký tự 'a' cũng được tính. Còn nếu cộng $k$ mà vẫn $\le 122$ thì quá khoẻ, cứ in ra ký tự có mã đó thôi.

a97b98c99d100…v118w119x120y121z122 -1 -1: tụt khỏi 'a' thì quay về 'z' -1 -1 'b' lùi 4 bước: 98 - 4 = 94 < 97 94 + 26 = 120 = 'x'

Giải mã: làm ngược lại

Bài này muốn ta đi giải mã thì làm ngược lại thôi: ký tự hiện tại trừ đi $k$, nếu nhỏ hơn 97 (mã của 'a') thì bắt đầu lại từ 122 (mã của 'z') và lùi tiếp cho đủ. Ví dụ ngược lại với trường hợp trên: ký tự đang xét là 'b' (mã 98), $k = 4$ thì $98 - 4 = 94 < 97$, nghĩa là cần lùi thêm $97 - 94 = 3$ ký tự bắt đầu từ 'z': $122 - 3 = 119$, rồi nhớ $+1$ do tính luôn chính ký tự 'z' → kết quả là 120, mã của 'x'.

Vậy khi xét mã ASCII của ký tự $s[i] - k$ mà ra kết quả $< 97$ thì đáp án là ký tự có mã:

$$\begin{aligned}&122 - (97 - (s[i] - k)) + 1\\ =\ &122 - 97 + s[i] - k + 1\\ =\ &25 + s[i] - k + 1\\ =\ &s[i] - k + 26\end{aligned}$$
Chốt lại
Xét đến ký tự $s[i]$ thì cứ lấy s[i] -= k; nếu kết quả nhỏ hơn 97 thì cộng thêm 26, rồi in ký tự có mã đó ra – đó chính là ký tự gốc. Còn nếu trừ $k$ mà vẫn $\ge 97$ thì cứ in ra luôn, khỏi phải nặng đầu tính quy luật gì cả.

Bài này đơn giản ở chỗ $k$ tối đa chỉ 25 (nhỏ hơn 26 chữ cái), nên khi mã hóa hay giải mã thì cao lắm chỉ quay vòng 1 lần thôi: dù là 'a' (97) với $k = 25$ thì $97 - 25 = 72 < 97$, cộng 26 được 98 là 'b' – vẫn chỉ cần cộng 26 đúng 1 lần.

Minh hoạGiải mã từng ký tự

Em nhập bản mã (chữ thường) và khóa $k$ rồi bấm Tiếp để xem từng ký tự lùi $k$ bước trên bảng chữ cái, khi nào bị tụt khỏi 'a' thì quay vòng về cuối:

Ví dụ nhanh:

CodeGiải mã – O(N)

Source code các bạn tham khảo để nhìn thấy rõ với tất cả những gì mình đã phân tích ở trên:

#include <bits/stdc++.h>
using namespace std;
int main(){
    string s;
    int k;
    cin >> s >> k;
    for(int i = 0; i < (int)s.length(); ++i){
        s[i] -= k;
        if(s[i] < 97){
            s[i] += 26;
        }
        cout << s[i];
    }
}
s = input()
k = int(input())
for i in range(len(s)):
    c = ord(s[i]) - k
    if c < ord('a'):
        c += 26
    print(chr(c), end="")
C++▶ Chạy tay từng dòng codeGiải mã: trừ k, tụt dưới 97 thì cộng 26 – O(N).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3int main(){
4 string s;
5 int k;
6 cin >> s >> k;
7 for(int i = 0; i < (int)s.length(); ++i){
8 s[i] -= k;
9 if(s[i] < 97){
10 s[i] += 26;
11 }
12 cout << s[i];
13 }
14}

Đánh giá độ phức tạp của cách làm này:

Không gian$O(1)$
Thời gian$O(N)$ với $N$ là độ dài chuỗi

Chuỗi dài tối đa 100 thì vòng lặp chỉ chạy 100 lần, yên tâm là nhỏ hơn rất nhiều so với ngưỡng $(6 \text{ đến } 10) \cdot 10^5$ của 20 ms mình nói đầu bài. Vậy cách làm này sẽ không bị TLE.

Lưu ý với Python
Code Python ở trên nộp vào bài này sẽ bị TLE, không phải do thuật toán mà do giới hạn 20 ms quá chặt – riêng việc khởi động trình thông dịch Python đã tốn hơn thế. Nếu đề cho 1 giây thì code Python trên sẽ AC. Các bạn có thể thử nộp code Python cho bài giống hệt bên NTUCoder sẽ AC: NTUCoder – bài 90.

Mở rộngNếu $k$ rất lớn? Nếu đề bắt mã hóa?

Khi $k$ lên tới 100 triệu

Đến đây hết rồi nhưng đã lỡ làm bài này mình cũng nói luôn: giả sử có bài tương tự nhưng $k$ có thể vượt quá 25, ví dụ tối đa 100 triệu, nghĩa là có thể phải quay vòng bảng chữ cái rất nhiều lần chứ không phải tối đa 1 lần như bài hiện tại. Sẽ rất dở nếu bạn nào cứ lùi từng bước đủ $k$ lần, 100 triệu lần thì quá 1 giây luôn rồi chứ đừng nói 20 ms.

Nhưng tinh ý một chút: bảng chữ cái có 26 chữ, cứ lùi đủ 26 bước là quay về đúng chữ cũ. Nên ta chỉ cần lấy phần dư k %= 26, phần $\lfloor k / 26 \rfloor$ vòng trọn vẹn thì bỏ qua vì chẳng làm thay đổi gì, sau đó giữ nguyên bước xử lý như bài hiện tại. Ví dụ đang xét 'a' với $k = 26$ thì k %= 26 còn 0 – vẫn là 'a'; với $k = 27$ thì còn 1 – lùi 1 ký tự được 'z'.

cin >> s >> k;
k %= 26;   // bỏ qua các vòng trọn vẹn, chỉ giữ phần dư
// ... phần còn lại giữ nguyên như code giải mã ở trên
Ghi chú của mình: trong bài gốc trên Facebook mình có viết nhầm thành k %= 25; đúng phải là k %= 26 vì chu kỳ quay vòng bằng đúng số chữ cái (26). Mình đã sửa lại ở trên cho chuẩn.

Nếu đề yêu cầu mã hóa thay vì giải mã

Lúc này làm như phân tích đầu tiên: xét mã ASCII của $s[i] + k$, nếu ra kết quả $> 122$ thì đáp án là ký tự có mã:

$$s[i] + k - 122 + 97 - 1 = s[i] + k - 26$$

Nghĩa là xét đến ký tự $s[i]$ ta cứ cộng $k$, nếu kết quả lớn hơn 122 thì trừ đi 26 rồi in ký tự có mã đó ra; còn nếu vẫn $\le 122$ thì in ra luôn.

Code mã hóa (chiều ngược lại)
#include <bits/stdc++.h>
using namespace std;
int main(){
    string s;
    int k;
    cin >> s >> k;
    for(int i = 0; i < (int)s.length(); ++i){
        int c = s[i] + k;
        if(c > 122){
            c -= 26;
        }
        cout << (char)c;
    }
}
s = input()
k = int(input())
for c in s:
    c = ord(c) + k
    if c > ord('z'):
        c -= 26
    print(chr(c), end="")
C++▶ Chạy tay từng dòng codeMã hóa: cộng k vào biến int c, vượt 122 thì trừ 26 – O(N).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3int main(){
4 string s;
5 int k;
6 cin >> s >> k;
7 for(int i = 0; i < (int)s.length(); ++i){
8 int c = s[i] + k;
9 if(c > 122){
10 c -= 26;
11 }
12 cout << (char)c;
13 }
14}
Không gian$O(1)$
Thời gian$O(N)$
Cẩn thận tràn kiểu char
Ở bài gốc mình viết s[i] += k; rồi if(s[i] > 122) giống bản giải mã. Nhưng kiểu char chỉ chứa tối đa 127, mà 'z' + 25 = 147 sẽ bị tràn thành số âm, điều kiện > 122 sai luôn và in ra ký tự rác (ví dụ xyz với $k = 25$). Bản giải mã không gặp chuyện này vì trừ đi thì nhỏ nhất cũng là $97 - 25 = 72$. Nên ở code trên mình tính trong biến int c cho an toàn.
Kết quả kiểm tra: code C++ và Python (giải mã lẫn mã hóa) đã được chạy với 250 bộ test ngẫu nhiên (chuỗi dài tới 100, đủ mọi $k$ từ 1 đến 25, có cả chuỗi chứa đủ 26 chữ cái), so với cách vét cạn lùi/tiến từng bước một trên bảng chữ cái – khớp hết. Với chuỗi dài 100 ký tự, bản C++ chạy chưa tới 1 ms.
Rất vui khi anh em nào đã đọc đến những dòng cuối cùng này. Anh em đọc chắc cũng mỏi mắt luôn chứ 😅 vậy anh em nghĩ người viết ra hết đống ở trên còn thế nào nữa 🤪. Vậy nên đừng tiếc gì cho mình một lời bình luận hoặc chia sẻ bài viết để động viên mình nè. Chúc anh em học tốt và gặt hái nhiều thành công 😇