Hướng dẫn giải của Tìm số


Chỉ dùng lời giải này khi không có ý tưởng, và đừng copy-paste code từ lời giải này. Hãy tôn trọng người ra đề và người viết lời giải.
Nộp một lời giải chính thức trước khi tự giải là một hành động có thể bị ban.

Lời giải này đang bị ẩn cho đến khi bạn chọn mở ra.

Chúng tôi khuyên bạn nên tự thử giải bài trước. Việc mở lời giải có thể làm lộ mất ý tưởng chính trước khi bạn có cơ hội tự giải.

Bạn phải đăng nhập để mở lời giải này.

Đăng nhập

Tác giả: Hiếu Nguyễn, Khanhll123, 111_LeQuangTam, TTTazb3

Hiểu bài toán

Bài toán yêu cầu tìm số nguyên dương X nhỏ nhất (nếu có) sao cho tích các chữ số của X trong cơ số B (F_B(X)) bằng N. Với B > 2, để tích các chữ số bằng N, mỗi chữ số phải là ước của N và nhỏ hơn B. Do đó, chúng ta có thể phân tích N thành thừa số nguyên tố và phân phối chúng vào các chữ số. X cần phải có càng ít chữ số càng tốt (vì số có ít chữ số hơn thì nhỏ hơn) và các chữ số phải được sắp xếp tăng dần ở dạng số (ví dụ 23 nhỏ hơn 32). Tuy nhiên, do cách viết số trong hệ cơ số, để X nhỏ nhất, các chữ số phải được sắp xếp giảm dần theo giá trị (ví dụ 23 (210 + 3) nhỏ hơn 32 (310 + 2)).

Giải thuật: Phân tích N thành các thừa số nguyên tố. Thay vì dùng các thừa số cơ số (2, 3, 5, 7...), ta có thể gộp chúng tạo thành các số lớn nhất có thể nhưng < B (ví dụ 222 = 8, 2*3=6). Việc này giúp giảm số lượng chữ số. Ta dùng DFS (Backtracking) hoặc DP để tìm cách phân tích N thành các thừa số (chữ số) sao cho số lượng thừa số là ít nhất. Sau đó, sắp xếp các thừa số này theo thứ tự giảm dần để tạo thành số X nhỏ nhất (do chữ số lớn nhất ở vị trí cao nhất trong hệ cơ số).

Các cách tiếp cận

Cách DFS Phân tích N thành thừa số (Tối ưu hóa)
#include <bits/stdc++.h>
using namespace std;

typedef unsigned long long ull;
const ull LIMIT = (1ULL << 63) - 1;

int B;
ull N;
vector<ull> c; // Các thừa số có thể dùng (ước của N, < B)
vector<ull> best_digits; // Kết quả tốt nhất (số lượng ít nhất)

// So sánh 2 vector đại diện cho số chữ số (số lượng ít hơn thì tốt hơn)
// Nếu số lượng bằng, ta cần so sánh giá trị số học, nhưng do bước sau sort giảm dần,
// chỉ cần so sánh số lượng là đủ để prune.
bool is_better(const vector<ull>& a, const vector<ull>& b) {
    if (a.size() != b.size()) return a.size() < b.size();
    // Nếu số lượng bằng, ta không prune mà để phép DFS tìm tất cả cách phân tích,
    // sau đó chọn cách tạo ra số X nhỏ nhất.
    // Tuy nhiên, để tối ưu, ta chỉ cần tìm cách có số lượng chữ số ít nhất.
    // Nếu có nhiều cách cùng số lượng chữ số, ta phải so sánh X.
    return false;
}

// Tạo số X từ các chữ số
ull make_number(vector<ull>& digits) {
    sort(digits.rbegin(), digits.rend()); // Sắp xếp giảm dần để X nhỏ nhất
    ull res = 0;
    for (ull d : digits) {
        if (res > LIMIT / B) return 0; // Overflow
        res = res * B + d;
    }
    return res;
}

void dfs(ull n, vector<ull>& current_digits) {
    if (n == 1) {
        ull candidate = make_number(current_digits);
        if (candidate == 0) return; // Quá giới hạn
        if (best_digits.empty() || candidate < make_number(best_digits)) {
            best_digits = current_digits;
        }
        return;
    }

    // Pruning: Nếu số lượng chữ số hiện tại >= số lượng tốt nhất đã tìm được, dừng.
    if (!best_digits.empty() && current_digits.size() >= best_digits.size()) return;

    // Thử các thừa số từ lớn đến nhỏ để giảm số lượng chữ số
    for (int i = c.size() - 1; i >= 0; --i) {
        ull d = c[i];
        if (n % d == 0) {
            current_digits.push_back(d);
            dfs(n / d, current_digits);
            current_digits.pop_back();
        }
    }
}

void solve() {
    if (!(cin >> B >> N)) return;
    c.clear();
    best_digits.clear();

    // Tìm các ước của N nhỏ hơn B
    for (ull i = 2; i * i <= N; ++i) {
        if (N % i == 0) {
            if (i < B) c.push_back(i);
            ull j = N / i;
            if (j != i && j < B) c.push_back(j);
        }
    }
    if (N < B) c.push_back(N);

    // Bước quan trọng: Gộp các thừa số cơ số để tạo số lớn hơn, giảm số lượng chữ số
    // Ví dụ: 2*2*2 = 8 (nếu 8 < B)
    // Thay vì dùng DFS gộp, ta có thể tạo list các số composite < B là ước của N
    // Tuy nhiên, cách đơn giản và hiệu quả là dùng DFS chia N, ưu tiên các thừa số lớn.
    // Do N < 2^63, số lượng ước không quá lớn (khoảng vài nghìn). DFS chia N là khả thi.

    // Sắp xếp c tăng dần để khi backtracking thử từ cuối (lớn nhất) xuống trước
    sort(c.begin(), c.end());

    vector<ull> cur;
    dfs(N, cur);

    if (best_digits.empty()) {
        cout << "NO SOLUTION" << endl;
    } else {
        ull ans = make_number(best_digits);
        cout << ans << endl;
    }
}

int main() {
    ios_base::sync_with_stdio(false); cin.tie(NULL);
    int t;
    if (cin >> t) {
        while(t--) solve();
    } else {
        // Đọc đến EOF
        while(cin >> B >> N) solve();
    }
    return 0;
}
  • Time Complexity: O(D * K) (D là số cách phân tích N, K là số lượng thừa số)
  • Space Complexity: O(log N)

Cách tiếp cận này sử dụng DFS để phân tích N thành tích các thừa số. Các thừa số phải là ước của N và nhỏ hơn B. Mục tiêu là tìm cách phân tích có số lượng thừa số (chữ số) ít nhất.

  1. Tạo list các thừa số cơ bản (ước của N, < B).
  2. Dùng DFS thử chia N cho các thừa số. Ưu tiên các thừa số lớn hơn để giảm nhanh N và giảm số lượng chữ số.
  3. Khi N về 1, ta có được một bộ chữ số. Ta tính toán số X tương ứng (sau khi sort giảm dần).
  4. So sánh và lưu lại X nhỏ nhất. Để tăng hiệu quả, ta có thể tối ưu hóa list thừa số bằng cách loại bỏ các thừa số có thể tạo ra bằng cách nhân các thừa số khác (ví dụ nếu có 2, 4, 8 và 8 < B, ta có thể chỉ cần giữ 8). Tuy nhiên, DFS thử các thừa số lớn trước đã đủ tốt trong hầu hết trường hợp.
Cách Quy hoạch động (DP) tìm số lượng chữ số
// Skeleton cho DP approach
// Cần map lưu trữ kết quả: map<pair<ull, bool>, ull> dp;
// dp[n][is_first] trả về số chữ số tối thiểu để tạo ra n, hoặc -1 nếu không thể.
// is_first giúp xử lý trường hợp chữ số đầu tiên không được là 0 (nhưng ở đây N>0 nên không có chữ số 0).
// Tuy nhiên, do N lớn, cần tối ưu.

ull solve_dp(ull n, bool is_first) {
    if (n < B) return 1;
    if (dp.count({n, is_first})) return dp[{n, is_first}];
    ull best = -1; // -1 biểu thị vô nghiệm
    // Thử các thừa số d
    for (ull d : valid_digits) {
        if (n % d == 0) {
            ull sub = solve_dp(n / d, false);
            if (sub != -1) {
                if (best == -1 || sub + 1 < best) best = sub + 1;
            }
        }
    }
    return dp[{n, is_first}] = best;
}

// Sau khi có số lượng chữ số tối thiểu, ta cần tìm cách tạo X nhỏ nhất.
// Ta có thể làm 2 bước:
// 1. DP để tìm min số chữ số.
// 2. DFS lại để tạo số X nhỏ nhất với đúng số lượng chữ số đó.
  • Time Complexity: O(S * D) (S là số state, D là số digits)
  • Space Complexity: O(S)

Cách này chia bài toán thành các bài toán con: tìm số lượng chữ số tối thiểu để tạo ra tích N. State là giá trị N hiện tại. Ta dùng memoization (DP) để lưu kết quả. Ưu điểm: Tránh lặp lại tính toán cho các N giống nhau. Nhược điểm: State space lớn (N lên tới 2^63). Tuy nhiên, số lượng state thực tế được thăm bởi DFS chỉ là số lượng cách phân tích N, nên DP không khác nhiều so với DFS memoization. Phương pháp này thường được dùng để tìm độ dài (số chữ số) tối thiểu. Sau đó cần thêm bước Backtracking để dựng số X nhỏ nhất.

Cách Tối ưu hóa thừa số (Preprocessing)
// Bước tiền xử lý để giảm số lượng thừa số
// Thay vì dùng tất cả ước < B, ta chỉ dùng các ước mà không thể chia đôi bởi các ước khác < B
// Ví dụ: Nếu 2, 4, 8 đều là ước và 8 < B, ta loại 2 và 4 vì 8 hiệu quả hơn.

vector<ull> get_optimized_factors(ull N, int B) {
    vector<ull> raw;
    for (ull i = 2; i * i <= N; ++i) {
        if (N % i == 0) {
            if (i < B) raw.push_back(i);
            ull j = N / i;
            if (j < B && j != i) raw.push_back(j);
        }
    }
    if (N < B) raw.push_back(N);
    sort(raw.begin(), raw.end());

    vector<ull> optimized;
    // Duyệt từ lớn đến nhỏ, loại bỏ các số chia hết cho số đã chọn
    vector<bool> used(raw.size(), false);
    for (int i = raw.size() - 1; i >= 0; --i) {
        bool can_use = true;
        for (int j = 0; j < optimized.size(); ++j) {
            if (optimized[j] % raw[i] == 0) {
                can_use = false;
                break;
            }
        }
        if (can_use) optimized.push_back(raw[i]);
    }
    return optimized;
}

// Sử dụng optimized factors trong DFS
  • Time Complexity: O(sqrt(N) + K^2)
  • Space Complexity: O(K)

Trước khi chạy DFS, ta thực hiện lọc thừa số. Nguyên tắc: Nếu ta có thể dùng số lớn hơn A để thay thế cho nhiều số nhỏ hơn (ví dụ dùng 8 thay cho 222), ta nên ưu tiên số lớn hơn. Cụ thể, trong danh sách các ước < B, ta chỉ giữ lại các số mà không bị chia hết bởi bất kỳ số nào khác trong danh sách (và lớn hơn nó). Ví dụ: Danh sách {2, 4, 8}. 8 chia hết cho 4 và 2. Do đó ta chỉ giữ 8. Ví dụ: Danh sách {2, 3, 6}. 6 chia hết cho 2 và 3. Nếu dùng 6, ta chỉ cần 1 chữ số thay vì 2 (2 và 3). Nên ta ưu tiên 6. Tuy nhiên, phải cẩn thận: Đôi khi dùng 2 số nhỏ cho kết quả tốt hơn (ví dụ 4 và 4 tạo 44, 6 và 2 tạo 62 -> 44 < 62). Nhưng trong hầu hết trường hợp, giảm số lượng chữ số là ưu tiên hàng đầu. Phương pháp này giúp giảm đáng kể nhánh cây DFS.

Phân tích độ phức tạp

Cách tiếp cận Time Space Tên
1 O(D * K) (D là số cách phân tích N, K là số lượng thừa số) O(log N) DFS Phân tích N thành thừa số (Tối ưu hóa)
2 O(S * D) (S là số state, D là số digits) O(S) Quy hoạch động (DP) tìm số lượng chữ số
3 O(sqrt(N) + K^2) O(K) Tối ưu hóa thừa số (Preprocessing)

Bài học kinh nghiệm

  • N là tích của các chữ số, mỗi chữ số là ước của N và < B. Phân tích N thành thừa số là chìa khóa.
  • Số X nhỏ nhất tương ứng với các chữ số được sắp xếp giảm dần (ví dụ 98...1).
  • Giảm số lượng chữ số (bằng cách gộp các thừa số cơ số thành thừa số lớn hơn, ví dụ 222=8) là cách hiệu quả nhất để giảm giá trị X.

Lỗi thường gặp

  • Quên kiểm tra tràn số (overflow) khi tính giá trị X (X có thể >= 2^63).
  • Xử lý sai trường hợp N=1 (X có thể là số 1, nhưng 1 không phải là thừa số > 1, nên N=1 là trường hợp đặc biệt).
  • Sắp xếp sai thứ tự chữ số (phải giảm dần theo giá trị chữ số, không phải tăng dần).
  • Bỏ qua các ước không phải là số nguyên tố (ví dụ 4, 6, 8, 9) mà chỉ xét số nguyên tố.

Bình luận

Please read the guidelines before commenting.


Không có bình luận tại thời điểm này.