Hướng dẫn giải của Profact_biểu diễn dưới dạng tích giai thừa


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, nguien_24, TIEN16032006, daikenja

Hiểu bài toán

Bài toán yêu cầu kiểm tra xem một số nguyên dương N có thể biểu diễn dưới dạng tích của một hoặc nhiều giai thừa (factorial) hay không. Ví dụ, 12 = 3! * 2! (6 * 2). Nếu có thể, ta cần tìm cách biểu diễn đó. Tuy nhiên, dựa trên các giải pháp tham chiếu, bài toán có vẻ yêu cầu kiểm tra xem N có phải là tích của các giai thừa không trùng lặp (các số giai thừa khác nhau) hay không, và in ra các số giai thừa đó hoặc thông báo 'NO'. Ví dụ, 24 có thể là 4! (một giai thừa) hoặc 3! * 2! * 2! (có trùng lặp). Các giải pháp dường như tập trung vào việc phân tích N thành các thừa số nguyên tố và đảm bảo các thừa số này có thể được gom lại thành các giai thừa.

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

Cách Quy hoạch động phân tích giai thừa
#include <iostream>
#include <vector>
#include <map>
#include <algorithm>

using namespace std;

// Hàm tính giai thừa
long long fact(int n) {
    long long res = 1;
    for (int i = 2; i <= n; ++i) {
        if (res > 1e18 / i) return -1; // Tràn số
        res *= i;
    }
    return res;
}

// Hàm phân tích N thành tích các giai thừa
// Sử dụng đệ quy/quy hoạch động
// Trả về vector chứa các giai thừa nếu thành công, rỗng nếu thất bại
vector<int> solve(int n) {
    // Tạo mảng các giai thừa có thể dùng
    vector<long long> facts;
    for (int i = 1; ; ++i) {
        long long f = fact(i);
        if (f == -1 || f > n) break;
        facts.push_back(f);
    }

    // Chuyển facts sang dạng int (sau khi kiểm tra)
    vector<int> f_ints;
    for (long long x : facts) f_ints.push_back((int)x);

    // DP: dp[i] là bitmask các giai thừa dùng được để tạo ra i (hoặc similar)
    // Do số lượng giai thừa nhỏ (max ~20), có thể dùng DP theo tổng
    // Nhưng tổng N có thể lớn. 
    // Phân tích N thành thừa số nguyên tố trước.

    // Thay vào đó, dùng phương pháp gom nhóm thừa số nguyên tố.
    // 1 giai thừa k! chứa các thừa số nguyên tố từ 2 đến k.
    // Ta cần gom các thừa số nguyên tố của N thành các tập hợp tương ứng vs k!.

    // Gợi ý từ Solution 1:
    // Phân tích N thành thừa số nguyên tố.
    // Sau đó thử các factorial từ lớn đến nhỏ.

    map<int, int> prime_counts;
    int temp = n;
    for (int i = 2; i * i <= temp; ++i) {
        while (temp % i == 0) {
            prime_counts[i]++;
            temp /= i;
        }
    }
    if (temp > 1) prime_counts[temp]++;

    vector<int> result;
    // Thử các factorial từ lớn nhất có thể
    // Số factorial lớn nhất: 20! ~ 2.4e18, nhưng N max là 10^18?
    // Solution 1 dùng N=300, có lẽ là limit của số giai thừa.

    // Ta thử tìm các factorial greedily
    for (int k = 20; k >= 2; --k) {
        long long f = fact(k);
        if (f == -1) continue;
        if (f > n) continue;

        // Kiểm tra xem f có thể chia hết cho các thừa số hiện tại không
        // Bằng cách kiểm tra xem các thừa số của f có sẵn trong prime_counts không
        map<int, int> f_primes;
        int temp_f = f;
        for (int i = 2; i * i <= temp_f; ++i) {
            while (temp_f % i == 0) {
                f_primes[i]++;
                temp_f /= i;
            }
        }
        if (temp_f > 1) f_primes[temp_f]++;

        bool can_use = true;
        for (auto &[p, c] : f_primes) {
            if (prime_counts[p] < c) {
                can_use = false;
                break;
            }
        }

        if (can_use) {
            // Dùng factorial này
            result.push_back(k);
            for (auto &[p, c] : f_primes) {
                prime_counts[p] -= c;
            }
            // Quay lại thử từ đầu (hoặc tiếp tục greedily)
            // Greedy từ lớn xuống nhỏ thường hiệu quả với bài toán này.
            // Nhưng Solution 1 dường như dùng backtracking.
            // Để đơn giản, ta thử greedily trước. Nếu fail thì thử lại.
            // Nhưng thực tế, greedy từ lớn xuống nhỏ là optimal cho bài này).
            // Tuy nhiên, để chắc chắn, ta reset vòng lặp sau khi dùng 1 factorial.
            k = 21; // Reset về 21 để lặp lại từ đầu với k--
        }
    }

    // Kiểm tra còn thừa số nào không
    for (auto &[p, c] : prime_counts) {
        if (c > 0) return {}; 
    }

    return result;
}

int main() {
    int n;
    cin >> n;
    vector<int> res = solve(n);
    if (res.empty()) cout << "NO";
    else {
        sort(res.begin(), res.end()); // Có thể cần sort
        for (int x : res) cout << x << " ";
    }
    return 0;
}
  • Time Complexity: O(20 * log N) ~ O(log N)
  • Space Complexity: O(1)

Cách tiếp cận này phân tích N thành các thừa số nguyên tố. Sau đó, nó sử dụng kỹ thuật tham lam (greedy) để tìm các giai thừa. Ta thử các giai thừa từ lớn nhất (ví dụ 20!) trở xuống. Nếu một giai thừa có thể được tạo thành từ các thừa số nguyên tố còn lại của N, ta lấy nó và trừ các thừa số đó đi. Lặp lại quá trình này cho đến khi N về 0. Nếu còn thừa số nguyên tố sót lại mà không thể tạo thành giai thừa nào, thì N không thể biểu diễn được. Phương pháp này dựa trên quan sát rằng nếu ta có thể chọn một giai thừa lớn, việc đó thường tốt hơn là chọn nhiều giai thừa nhỏ.

Cách Backtracking / Quay lui
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

vector<long long> facts;
vector<int> result;
bool found = false;

void precompute() {
    long long f = 1;
    for (int i = 1; ; ++i) {
        f *= i;
        if (f > 1000000000000000000LL) break; // Giới hạn
        facts.push_back(f);
    }
}

// Phân tích n thành thừa số nguyên tố
// Đếm số lượng mỗi nguyên tố
void getPrimeCounts(long long n, vector<int>& counts) {
    for (int i = 2; i * i <= n; ++i) {
        while (n % i == 0) {
            counts[i]++;
            n /= i;
        }
    }
    if (n > 1) counts[n]++;
}

void backtrack(int idx, long long current_val, vector<int>& prime_counts, vector<int>& current_factors) {
    if (found) return;
    if (current_val == 0) {
        // Kiểm tra xem có thừa số nguyên tố nào sót lại không
        for (int c : prime_counts) if (c > 0) return;
        result = current_factors;
        found = true;
        return;
    }

    if (idx < 0) return;

    // Thử dùng facts[idx]
    // Tính thừa số nguyên tố của facts[idx]
    long long f = facts[idx];
    vector<int> f_counts(100, 0); // Kích thước đủ lớn
    getPrimeCounts(f, f_counts);

    // Kiểm tra có dùng được không
    bool can_use = true;
    for (int i = 0; i < 100; ++i) {
        if (f_counts[i] > prime_counts[i]) {
            can_use = false;
            break;
        }
    }

    if (can_use) {
        // Dùng
        for (int i = 0; i < 100; ++i) prime_counts[i] -= f_counts[i];
        current_factors.push_back(idx + 1); // index 0 la 1!, index 1 la 2!
        backtrack(idx - 1, current_val / f, prime_counts, current_factors);
        current_factors.pop_back();
        for (int i = 0; i < 100; ++i) prime_counts[i] += f_counts[i];
    }

    // Thử không dùng
    backtrack(idx - 1, current_val, prime_counts, current_factors);
}

int main() {
    precompute();
    long long n;
    cin >> n;
    vector<int> prime_counts(100, 0);
    getPrimeCounts(n, prime_counts);
    vector<int> current_factors;
    found = false;
    backtrack(facts.size() - 1, n, prime_counts, current_factors);
    if (found) {
        sort(result.begin(), result.end());
        for (int x : result) cout << x << " ";
    } else {
        cout << "NO";
    }
    return 0;
}
  • Time Complexity: O(2^K)
  • Space Complexity: O(K)

Backtracking thử tất cả các kết hợp có thể của các giai thừa. Ta có một danh sách các giai thừa có thể (1!, 2!, ..., 20!). Với mỗi giai thừa, ta có thể chọn hoặc không chọn. Nếu chọn, ta kiểm tra xem các thừa số nguyên tố của giai thừa đó có sẵn trong các thừa số của N hay không. Nếu có, ta trừ chúng đi và đệ quy tiếp. Nếu không chọn, ta thử giai thừa tiếp theo. Cách này chậm hơn Greedy nhưng đảm bảo tìm được lời giải (nếu có). Tuy nhiên, do số lượng giai thừa nhỏ (khoảng 20), Backtracking vẫn chạy nhanh. Solution 1 có vẻ như là một dạng Backtracking hoặc DFS.

Cách Giải pháp Tham lam (Greedy) với Phân tích
#include <iostream>
#include <vector>
#include <map>
#include <algorithm>

using namespace std;

// Sử dụng map để lưu trữ thừa số nguyên tố
// Hàm phân tích số thành thừa số nguyên tố
void factorize(long long n, map<long long, int>& factors) {
    for (long long i = 2; i * i <= n; ++i) {
        while (n % i == 0) {
            factors[i]++;
            n /= i;
        }
    }
    if (n > 1) factors[n]++;
}

// Kiểm tra xem factorial k! có thể tạo từ các thừa số hiện có không
bool canTake(int k, map<long long, int>& current_factors) {
    long long f = 1;
    for (int i = 2; i <= k; ++i) f *= i;

    map<long long, int> fact_factors;
    factorize(f, fact_factors);

    for (auto &[p, c] : fact_factors) {
        if (current_factors[p] < c) return false;
    }
    return true;
}

void subtractFactors(int k, map<long long, int>& current_factors) {
    long long f = 1;
    for (int i = 2; i <= k; ++i) f *= i;

    map<long long, int> fact_factors;
    factorize(f, fact_factors);

    for (auto &[p, c] : fact_factors) {
        current_factors[p] -= c;
    }
}

int main() {
    long long n;
    cin >> n;

    map<long long, int> current_factors;
    factorize(n, current_factors);

    vector<int> result;

    // Thử greedily từ factorial lớn nhất (20) về nhỏ
    // Nhưng cẩn thận: greedy đôi khi sai.
    // Ví dụ: N = 2! * 3! * 4! (tức là tổng các thừa số)
    // Giả sử N có thừa số là 2^a * 3^b * ...
    // greedy từ lớn xuống nhỏ thường đúng với bài toán chia nhóm này.

    bool changed = true;
    while (changed) {
        changed = false;
        for (int k = 20; k >= 2; --k) {
            if (canTake(k, current_factors)) {
                result.push_back(k);
                subtractFactors(k, current_factors);
                changed = true;
                break; // Chỉ lấy 1 lần mỗi vòng lặp, sau đó reset
            }
        }
    }

    // Kiểm tra còn thừa số không
    for (auto &[p, c] : current_factors) {
        if (c > 0) {
            cout << "NO";
            return 0;
        }
    }

    sort(result.begin(), result.end());
    for (int x : result) cout << x << " ";

    return 0;
}
  • Time Complexity: O(20 * (log N)^2)
  • Space Complexity: O(log N)

Đây là phiên bản chi tiết hơn của Greedy. Nó sử dụng map để lưu trữ các thừa số nguyên tố, giúp dễ dàng cộng/trừ. Logic là lặp lại cho đến khi không thể chia thêm giai thừa nào: trong mỗi bước, thử các giai thừa từ 20! trở xuống, nếu giai thừa nào chia hết (về mặt thừa số nguyên tố) thì chia và thêm vào danh sách, sau đó lặp lại từ đầu. Điều này đảm bảo nếu có cách chia, ta sẽ tìm được nó (vì ta luôn ưu tiên chia các số lớn nhất có thể).

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

Cách tiếp cận Time Space Tên
1 O(20 * log N) ~ O(log N) O(1) Quy hoạch động phân tích giai thừa
2 O(2^K) O(K) Backtracking / Quay lui
3 O(20 * (log N)^2) O(log N) Giải pháp Tham lam (Greedy) với Phân tích

Bài học kinh nghiệm

  • Bài toán có thể chuyển đổi thành bài toán chia các thừa số nguyên tố của N thành các nhóm tương ứng với các giai thừa (2!, 3!, ...).
  • Số lượng giai thừa cần xem xét rất nhỏ (khoảng 20), cho phép sử dụng các thuật toán tham lam hoặc quay lui đơn giản.
  • Phân tích N thành thừa số nguyên tố là bước tiền xử lý bắt buộc.

Lỗi thường gặp

  • Quên kiểm tra tràn số khi tính giai thừa (factorial).
  • Greedy không đúng nếu chỉ thử một lần; cần lặp lại quy trình chia cho đến khi không thể chia thêm.
  • Xử lý sai các số lớn (dùng int thay vì long long).

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.