Bài 9.3: Số thuận nghịch và phân tích trong C++

Chào mừng các bạn đã quay trở lại với chuỗi bài viết về C++! Hôm nay, chúng ta sẽ cùng khám phá một khái niệm khá thú vị trong toán học ứng dụng vào lập trình: Số thuận nghịch, hay còn gọi là Palindrome Number. Đồng thời, chúng ta sẽ tìm hiểu các kỹ thuật để phân tích một số, tức là trích xuất và xử lý các chữ số tạo nên nó – một kỹ năng nền tảng khi làm việc với các bài toán số học.

Số Thuận Nghịch (Palindrome Number) là gì?

Một số thuận nghịch là một số nguyên mà khi đọc từ trái sang phải cũng cho kết quả giống như khi đọc từ phải sang trái. Nói cách khác, thứ tự các chữ số của nó không thay đổi khi bị đảo ngược.

Ví dụ về số thuận nghịch:

  • 121
  • 3443
  • 9
  • 1001
  • 567765

Ví dụ về số không thuận nghịch:

  • 123 (đảo ngược là 321)
  • 4567 (đảo ngược là 7654)
  • 10 (đảo ngược là 01 hay 1)

Kiểm tra một số có phải là số thuận nghịch hay không là một bài toán cổ điển, thường dùng để rèn luyện kỹ năng thao tác với các chữ số của một số.

Phân Tích Một Số - Trọng Tâm Để Nhận Biết Số Thuận Nghịch

Để biết một số có phải là thuận nghịch hay không, chúng ta cần phải "nhìn" vào các chữ số tạo nên nó. Quá trình "nhìn" này chính là phân tích số đó thành các chữ số thành phần.

Trong C++ và hầu hết các ngôn ngữ lập trình, hai toán tử đóng vai trò cốt lõi trong việc trích xuất chữ số từ một số nguyên là:

  1. Toán tử chia lấy phần dư (%): Khi chia một số nguyên cho 10, phần dư chính là chữ số cuối cùng (chữ số hàng đơn vị) của số đó.

    • Ví dụ: 123 % 10 cho kết quả 3.
    • Ví dụ: 4567 % 10 cho kết quả 7.
  2. Toán tử chia lấy phần nguyên (/): Khi chia một số nguyên cho 10 và lấy phần nguyên, chúng ta sẽ loại bỏ chữ số cuối cùng của số đó.

    • Ví dụ: 123 / 10 cho kết quả 12.
    • Ví dụ: 4567 / 10 cho kết quả 456.

Bằng cách lặp lại hai thao tác này, chúng ta có thể "bóc tách" từng chữ số của một số nguyên từ hàng đơn vị trở đi.

Ví dụ: Trích xuất và in các chữ số

Hãy xem một ví dụ đơn giản về cách dùng % 10/ 10 để trích xuất và in ra từng chữ số của một số:

#include <iostream>
using namespace std;

int main() {
    int so = 54321;
    int t = so;

    cout << "Cac chu so cua so " << so << " (tu cuoi len):" << endl;

    while (t > 0) {
        int c = t % 10;
        cout << c << " ";
        t /= 10;
    }

    cout << endl;

    return 0;
}
Cac chu so cua so 54321 (tu cuoi len):
1 2 3 4 5

Giải thích code:

  • Chúng ta sử dụng một biến t để sao chép giá trị của so. Điều này quan trọng vì vòng lặp sẽ làm giảm t cho đến khi nó về 0, chúng ta cần giữ giá trị gốc của so để sau này so sánh hoặc xử lý khác.
  • Vòng lặp while (t > 0) tiếp tục chừng nào t còn lớn hơn 0, tức là nó còn ít nhất một chữ số.
  • Trong mỗi lần lặp:
    • t % 10 lấy chữ số hàng đơn vị hiện tại của t.
    • cout << c << " "; in chữ số vừa trích xuất.
    • t /= 10 loại bỏ chữ số hàng đơn vị khỏi t, chuẩn bị cho lần lặp kế tiếp (lúc này chữ số hàng chục cũ sẽ trở thành hàng đơn vị mới).
  • Quá trình lặp lại cho đến khi t trở thành 0, tức là tất cả các chữ số đã được xử lý.

Kết quả chạy code trên sẽ là: 1 2 3 4 5. Như bạn thấy, các chữ số được trích xuất từ phải sang trái (hàng đơn vị, hàng chục, ...).

Kiểm Tra Số Thuận Nghịch Bằng Cách Đảo Ngược Số

Cách phổ biến nhất để kiểm tra một số có phải là số thuận nghịch là tạo ra số đảo ngược của nó và so sánh với số gốc.

Quy trình đảo ngược một số tương tự như quy trình trích xuất chữ số, nhưng chúng ta sẽ "xây dựng" một số mới từ các chữ số đã trích xuất, theo thứ tự ngược lại.

Ví dụ: Đảo ngược một số
#include <iostream>
using namespace std;

int main() {
    int soGoc = 12345;
    int t = soGoc;
    int soDao = 0;

    while (t > 0) {
        int c = t % 10;
        soDao = soDao * 10 + c;
        t /= 10;
    }

    cout << "So goc: " << soGoc << endl;
    cout << "So dao nguoc: " << soDao << endl;

    return 0;
}
So goc: 12345
So dao nguoc: 54321

Giải thích code:

  • Biến soGoc lưu trữ số ban đầu.
  • t là bản sao dùng trong quá trình đảo ngược.
  • soDao khởi tạo bằng 0. Đây là nơi chúng ta sẽ xây dựng số đảo ngược.
  • Trong vòng lặp while (t > 0):
    • int c = t % 10; lấy chữ số cuối cùng của t.
    • soDao = soDao * 10 + c; là bước mấu chốt.
      • soDao * 10 dịch tất cả các chữ số hiện có của soDao sang trái một vị trí (nhân 10).
      • + c thêm chữ số mới (vừa trích xuất) vào vị trí hàng đơn vị mới.
    • t /= 10; loại bỏ chữ số đã xử lý khỏi t.
  • Sau vòng lặp, soDao chứa số đảo ngược của soGoc.

Kết quả chạy code trên với soGoc = 12345 sẽ là: So goc: 12345 So dao nguoc: 54321

Ví dụ: Hàm kiểm tra số thuận nghịch sử dụng phương pháp đảo ngược

Bây giờ, kết hợp kỹ thuật đảo ngược số, chúng ta có thể xây dựng một hàm kiểm tra xem một số có phải là thuận nghịch hay không.

#include <iostream>
using namespace std;

bool laTN(int n) {
    if (n < 0) return false;
    if (n < 10) return true;

    int goc = n;
    int dao = 0;

    while (n > 0) {
        int c = n % 10;
        dao = dao * 10 + c;
        n /= 10;
    }
    return goc == dao;
}

int main() {
    int so1 = 121;
    int so2 = 123;
    int so3 = 4554;
    int so4 = 10;
    int so5 = 7;

    cout << so1 << (laTN(so1) ? " la" : " khong la") << " so thuan nghich." << endl;
    cout << so2 << (laTN(so2) ? " la" : " khong la") << " so thuan nghich." << endl;
    cout << so3 << (laTN(so3) ? " la" : " khong la") << " so thuan nghich." << endl;
    cout << so4 << (laTN(so4) ? " la" : " khong la") << " so thuan nghich." << endl;
    cout << so5 << (laTN(so5) ? " la" : " khong la") << " so thuan nghich." << endl;
    cout << "-121" << (laTN(-121) ? " la" : " khong la") << " so thuan nghich." << endl;

    return 0;
}
121 la so thuan nghich.
123 khong la so thuan nghich.
4554 la so thuan nghich.
10 khong la so thuan nghich.
7 la so thuan nghich.
-121 khong la so thuan nghich.

Giải thích code:

  • Hàm laTN(int n) nhận vào một số nguyên n.
  • Hàm xử lý các trường hợp đặc biệt: số âm không thuận nghịch, các số từ 0 đến 9 luôn thuận nghịch.
  • Lưu lại giá trị gốc của n vào goc trước khi thực hiện quá trình đảo ngược (vì n sẽ bị thay đổi trong vòng lặp).
  • Sử dụng vòng lặp while và các toán tử % 10, /= 10 để xây dựng dao giống như ví dụ trước.
  • Cuối cùng, hàm trả về kết quả của phép so sánh goc == dao. Nếu bằng nhau, số đó là thuận nghịch (true); ngược lại, không phải (false).
  • Trong main, chúng ta gọi hàm laTN với nhiều số khác nhau và in kết quả ra màn hình sử dụng toán tử ba ngôi ?: để cho output ngắn gọn.

Kết quả chạy code trên sẽ là: 121 la so thuan nghich. 123 khong la so thuan nghich. 4554 la so thuan nghich. 10 khong la so thuan nghich. 7 la so thuan nghich. -121 khong la so thuan nghich.

Kiểm Tra Số Thuận Nghịch Bằng Cách Chuyển Thành Chuỗi Ký Tự

Một cách tiếp cận khác để kiểm tra số thuận nghịch là chuyển số nguyên sang dạng chuỗi ký tự (string). Khi ở dạng chuỗi, việc kiểm tra thuận nghịch trở nên đơn giản hơn, tương tự như kiểm tra một từ có phải là palindrome (ví dụ: "madam", "level").

Để kiểm tra một chuỗi có phải palindrome không, chúng ta có thể dùng hai con trỏ (hoặc chỉ số), một bắt đầu từ đầu chuỗi và một bắt đầu từ cuối chuỗi. Di chuyển hai con trỏ vào trong, so sánh các ký tự tại vị trí hiện tại của chúng. Nếu tìm thấy cặp ký tự nào khác nhau, chuỗi đó không phải palindrome. Nếu hai con trỏ gặp nhau hoặc vượt qua nhau mà không tìm thấy sự khác biệt nào, thì chuỗi đó là palindrome.

Ví dụ: Hàm kiểm tra số thuận nghịch bằng cách chuyển chuỗi
#include <iostream>
#include <string>
using namespace std;

bool laTN_chuoi(int n) {
    if (n < 0) return false;

    string s = to_string(n);

    int l = 0;
    int r = s.length() - 1;

    while (l < r) {
        if (s[l] != s[r]) {
            return false;
        }
        l++;
        r--;
    }
    return true;
}

int main() {
    int so1 = 121;
    int so2 = 987;
    int so3 = 5005;
    int so4 = 0;
    int so5 = -5;

    cout << so1 << (laTN_chuoi(so1) ? " la" : " khong la") << " so thuan nghich (string). " << endl;
    cout << so2 << (laTN_chuoi(so2) ? " la" : " khong la") << " so thuan nghich (string). " << endl;
    cout << so3 << (laTN_chuoi(so3) ? " la" : " khong la") << " so thuan nghich (string). " << endl;
    cout << so4 << (laTN_chuoi(so4) ? " la" : " khong la") << " so thuan nghich (string). " << endl;
    cout << so5 << (laTN_chuoi(so5) ? " la" : " khong la") << " so thuan nghich (string). " << endl;

    return 0;
}
121 la so thuan nghich (string). 
987 khong la so thuan nghich (string). 
5005 la so thuan nghich (string). 
0 la so thuan nghich (string). 
-5 khong la so thuan nghich (string).

Giải thích code:

  • Hàm laTN_chuoi(int n) nhận một số nguyên n.
  • to_string(n) chuyển số nguyên n thành một đối tượng string.
  • Chúng ta dùng hai biến lr làm chỉ số truy cập các ký tự trong chuỗi s. l bắt đầu từ 0 (ký tự đầu tiên), r bắt đầu từ chỉ số cuối cùng (s.length() - 1).
  • Vòng lặp while (l < r) tiếp tục chừng nào hai chỉ số chưa gặp hoặc vượt qua nhau.
  • Trong mỗi lần lặp, chúng ta so sánh ký tự tại s[l]s[r].
  • Nếu chúng khác nhau (s[l] != s[r]), số đó không phải thuận nghịch, hàm trả về false ngay lập tức.
  • Nếu chúng bằng nhau, chúng ta di chuyển l tiến lên (l++) và r lùi lại (r--) để so sánh cặp ký tự tiếp theo.
  • Nếu vòng lặp kết thúc (tức là l không còn nhỏ hơn r nữa) mà không tìm thấy cặp khác nhau nào, điều đó có nghĩa là tất cả các cặp đối xứng đều giống nhau, và số đó là thuận nghịch, hàm trả về true.

Kết quả chạy code trên sẽ là: 121 la so thuan nghich (string). 987 khong la so thuan nghich (string). 5005 la so thuan nghich (string). 0 la so thuan nghich (string). -5 khong la so thuan nghich (string).

Phương pháp chuyển sang chuỗi thường dễ codedễ hiểu hơn so với phương pháp đảo ngược số bằng toán học thuần túy, đặc biệt với người mới bắt đầu. Tuy nhiên, nó có thể tốn kém tài nguyên hơn một chút vì phải tạo ra một đối tượng chuỗi mới.

Phân Tích Số Mở Rộng: Tính Tổng Chữ Số, Đếm Chữ Số

Kỹ thuật trích xuất chữ số bằng % 10/= 10 không chỉ dùng để kiểm tra số thuận nghịch. Nó là nền tảng cho rất nhiều bài toán liên quan đến các chữ số của một số nguyên. Dưới đây là hai ví dụ đơn giản khác về "phân tích" số: tính tổng các chữ số và đếm số lượng chữ số.

Ví dụ: Tính tổng và đếm số lượng chữ số
#include <iostream>
using namespace std;

void phanTichSo(int n) {
    if (n < 0) {
        cout << "Xin vui long nhap so nguyen khong am." << endl;
        return;
    }
    if (n == 0) {
        cout << "So 0 co 1 chu so, tong cac chu so la 0." << endl;
        return;
    }

    int t = n;
    int tong = 0;
    int dem = 0;

    cout << "Phan tich so: " << n << endl;
    cout << "Cac chu so (tu cuoi len): ";

    while (t > 0) {
        int c = t % 10;
        cout << c << " ";
        tong += c;
        dem++;
        t /= 10;
    }

    cout << endl;
    cout << "Tong cac chu so: " << tong << endl;
    cout << "So luong chu so: " << dem << endl;
}

int main() {
    phanTichSo(12345);
    cout << "---" << endl;
    phanTichSo(909);
    cout << "---" << endl;
    phanTichSo(7);
    cout << "---" << endl;
    phanTichSo(0);
    cout << "---" << endl;
    phanTichSo(-100);

    return 0;
}
Phan tich so: 12345
Cac chu so (tu cuoi len): 5 4 3 2 1 
Tong cac chu so: 15
So luong chu so: 5
---
Phan tich so: 909
Cac chu so (tu cuoi len): 9 0 9 
Tong cac chu so: 18
So luong chu so: 3
---
Phan tich so: 7
Cac chu so (tu cuoi len): 7 
Tong cac chu so: 7
So luong chu so: 1
---
So 0 co 1 chu so, tong cac chu so la 0.
---
Xin vui long nhap so nguyen khong am.

Giải thích code:

  • Hàm phanTichSo(int n) nhận một số nguyên không âm.
  • Xử lý các trường hợp đặc biệt cho số âm và số 0.
  • Sử dụng biến t để lặp.
  • tongdem được khởi tạo bằng 0 để lưu kết quả.
  • Vòng lặp while (t > 0) thực hiện quá trình trích xuất chữ số quen thuộc.
  • Trong mỗi lần lặp:
    • c = t % 10; trích xuất chữ số.
    • tong += c; cộng chữ số vào tổng.
    • dem++; tăng biến đếm lên 1.
    • t /= 10; loại bỏ chữ số.
  • Sau vòng lặp, tong chứa tổng các chữ số, và dem chứa số lượng chữ số của số gốc (trừ trường hợp số 0 được xử lý riêng).
  • In ra kết quả thu được.

Kết quả chạy code trên sẽ là:

Phan tich so: 12345
Cac chu so (tu cuoi len): 5 4 3 2 1 
Tong cac chu so: 15
So luong chu so: 5
---
Phan tich so: 909
Cac chu so (tu cuoi len): 9 0 9 
Tong cac chu so: 18
So luong chu so: 3
---
Phan tich so: 7
Cac chu so (tu cuoi len): 7 
Tong cac chu so: 7
So luong chu so: 1
---
So 0 co 1 chu so, tong cac chu so la 0.
---
Xin vui long nhap so nguyen khong am.
# Bài tập ví dụ: C++ Bài 4.B2: Số đặc biệt
Một số nguyên dương ~n~ được gọi là số đặc biệt nếu ~n~ chia hết cho tổng các chữ số của chính nó. Ví dụ, số ~27~ là số đặc biệt, còn hai số ~11~ và ~2013~ thì không phải là số đặc biệt.

Cho số nguyên dương ~n~. Hãy kiểm tra xem số ~n~ có phải là số đặc biệt hay không?
## INPUT FORMAT
Dòng đầu tiên chứa số nguyên dương ~n(1\leq n \leq 10^{18})~.

## OUTPUT FORMAT
Nếu ~n~ là số đặc biệt in ra `1`, nếu không phải in ra `0`. 
.
## Ví dụ 1:
### Input

27

### Ouput

1

**Giải thích ví dụ mẫu:**

- **Ví dụ 1:**
  - **Input:** `27`
  - **Giải thích:** 27 chia hết cho tổng các chữ số của nó (2 + 7 = 9).


**Ý tưởng chính:**

Một số `n` là số đặc biệt nếu `n` chia hết cho tổng các chữ số của nó. Do đó, chúng ta cần thực hiện hai bước chính:
1.  Tính tổng các chữ số của số `n`.
2.  Kiểm tra xem `n` có chia hết cho tổng đó hay không.

**Các bước thực hiện:**

1.  **Đọc đầu vào:**
    *   Sử dụng `cin` để đọc số nguyên dương `n`.
    *   Vì `n` có thể lên tới `10^18`, kiểu dữ liệu `int` thông thường sẽ không đủ. Bạn cần sử dụng kiểu dữ liệu có kích thước lớn hơn, đó là `long long` trong C++.

2.  **Tính tổng các chữ số:**
    *   Để tính tổng các chữ số của một số, bạn có thể lặp qua các chữ số của nó.
    *   Một cách phổ biến để làm điều này là sử dụng vòng lặp `while`.
    *   Trong mỗi bước lặp:
        *   Lấy chữ số cuối cùng của số bằng toán tử modulo (`%`). Ví dụ, `so % 10` sẽ cho chữ số hàng đơn vị.
        *   Cộng chữ số này vào một biến lưu tổng (khởi tạo bằng 0).
        *   Loại bỏ chữ số cuối cùng bằng cách chia số cho 10 bằng phép chia nguyên (`/`). Ví dụ, `so / 10` sẽ bỏ đi chữ số hàng đơn vị.
    *   Lặp lại cho đến khi số ban đầu trở thành 0.
    *   **Quan trọng:** Bạn cần giữ lại giá trị gốc của `n` để kiểm tra tính chia hết ở bước sau. Vì vậy, hãy sử dụng một biến tạm thời (ví dụ: `temp_n`) để thực hiện việc tính tổng chữ số, trong khi biến `n` vẫn giữ giá trị gốc.

3.  **Kiểm tra tính chia hết:**
    *   Sau khi tính được tổng các chữ số (gọi là `sum_digits`), hãy kiểm tra xem số gốc `n` có chia hết cho `sum_digits` hay không.
    *   Sử dụng toán tử modulo (`%`). Nếu `n % sum_digits == 0`, tức là `n` chia hết cho tổng các chữ số của nó.

4.  **Xuất kết quả:**
    *   Sử dụng `cout` để in ra `1` nếu `n` là số đặc biệt (điều kiện chia hết đúng).
    *   In ra `0` nếu `n` không phải là số đặc biệt (điều kiện chia hết sai).

**Gợi ý về cấu trúc code (không phải code hoàn chỉnh):**

*   Bao gồm thư viện `iostream` (`#include <iostream>`).
*   Sử dụng `cin` và `cout`.
*   Khai báo biến `n` kiểu `long long`.
*   Đọc `n`.
*   Khai báo biến `sum_digits` (có thể là `int` vì tổng chữ số của `10^18` tối đa là `9 * 19 = 171`, vẫn nằm trong phạm vi `int`), khởi tạo bằng 0.
*   Khai báo biến tạm `temp_n` kiểu `long long`, gán `temp_n = n`.
*   Sử dụng vòng lặp `while (temp_n > 0)` để tính tổng chữ số.
*   Bên trong vòng lặp: lấy chữ số (`temp_n % 10`), cộng vào `sum_digits`, cập nhật `temp_n = temp_n / 10`.
*   Sau vòng lặp, kiểm tra điều kiện `n % sum_digits == 0`.
*   Dùng câu lệnh điều kiện (`if/else`) để in ra `1` hoặc `0`.
*   Có thể thêm `ios_base::sync_with_stdio(false); cin.tie(NULL);` ở đầu hàm `main` để tăng tốc độ nhập/xuất, dù với một số lượng input ít như thế này có thể không quá cần thiết.

```cpp
#include <iostream>
using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    long long n;
    cin >> n;

    long long t = n;
    int tong = 0;

    while (t > 0) {
        tong += t % 10;
        t /= 10;
    }

    if (n % tong == 0) {
        cout << 1 << endl;
    } else {
        cout << 0 << endl;
    }

    return 0;
}
Input:
27
Output:
1

Làm thêm nhiều bài tập miễn phí tại đây

Comments

There are no comments at the moment.