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
Thi thử miễn phí
🏆 Thi thử HSG Tin học tuần này trên ClueOJ
Mở công khai cho mọi người · đề do thầy Nguyễn Việt Nam Sơn biên soạn
Lịch thi, đề cũ & lời giải →

The Number of Good Subsets (LeetCode 1994)

The Number of Good Subsets (LeetCode 1994)
Nguồn: LeetCode 1994 Độ khó: Khó Quy hoạch động Bitmask Số nguyên tố Tổ hợp

Đề bài

1994. The Number of Good Subsets – Số lượng tập con tốt (LeetCode, độ khó Hard)

Cho một mảng số nguyên nums. Ta gọi một tập con của nums là tốt nếu tích các phần tử của nó có thể biểu diễn thành tích của một hoặc nhiều số nguyên tố phân biệt.

  • Ví dụ, nếu nums = [1, 2, 3, 4]:
    • [2, 3], [1, 2, 3] và [1, 3] là các tập con tốt với tích lần lượt là 6 = 2*3, 6 = 2*3 và 3 = 3.
    • [1, 4] và [4] không phải tập con tốt, với tích lần lượt là 4 = 2*2 và 4 = 2*2.

Hãy trả về số lượng tập con tốt khác nhau của nums, lấy modulo $10^9 + 7$.

Một tập con của nums là một mảng bất kỳ thu được bằng cách xoá đi một số phần tử (có thể không xoá hoặc xoá hết) của nums. Hai tập con được coi là khác nhau khi và chỉ khi tập các chỉ số bị xoá của chúng khác nhau.

Ràng buộc:

  • $1 \le$ nums.length $\le 10^5$
  • $1 \le$ nums[i] $\le 30$

Định dạng vào/ra để chạy thử trên máy: trên LeetCode các bạn chỉ cần viết hàm numberOfGoodSubsets trong class Solution. Để chạy như một chương trình bình thường (và dùng khung chạy tay bên dưới), mình đặt thêm định dạng:

  • Dữ liệu vào: dòng đầu là số nguyên $n$ (độ dài mảng), dòng thứ hai gồm $n$ số nguyên nums[0], nums[1], ..., nums[n-1].
  • Kết quả: một số nguyên là số tập con tốt, lấy modulo $10^9 + 7$.
Ví dụ 1 – LeetCode
Input: nums = [1,2,3,4]
Output: 6
Ví dụ 1 – stdin / stdout
4
1 2 3 4
---
6
Ví dụ 2 – LeetCode
Input: nums = [4,2,3,15]
Output: 5
Ví dụ 2 – stdin / stdout
4
4 2 3 15
---
5

Giải thích ví dụ của đề:

  • Ví dụ 1: các tập con tốt là
    • [1,2]: tích bằng 2, là tích của số nguyên tố phân biệt 2.
    • [1,2,3]: tích bằng 6, là tích của các số nguyên tố phân biệt 2 và 3.
    • [1,3]: tích bằng 3, là tích của số nguyên tố phân biệt 3.
    • [2]: tích bằng 2, là tích của số nguyên tố phân biệt 2.
    • [2,3]: tích bằng 6, là tích của các số nguyên tố phân biệt 2 và 3.
    • [3]: tích bằng 3, là tích của số nguyên tố phân biệt 3.
  • Ví dụ 2: các tập con tốt là
    • [2]: tích bằng 2, là tích của số nguyên tố phân biệt 2.
    • [2,3]: tích bằng 6, là tích của các số nguyên tố phân biệt 2 và 3.
    • [2,15]: tích bằng 30, là tích của các số nguyên tố phân biệt 2, 3 và 5.
    • [3]: tích bằng 3, là tích của số nguyên tố phân biệt 3.
    • [15]: tích bằng 15, là tích của các số nguyên tố phân biệt 3 và 5.

1994. The Number of Good Subsets (LeetCode, Hard)

You are given an integer array nums. We call a subset of nums good if its product can be represented as a product of one or more distinct prime numbers.

  • For example, if nums = [1, 2, 3, 4]:
    • [2, 3], [1, 2, 3], and [1, 3] are good subsets with products 6 = 2*3, 6 = 2*3, and 3 = 3 respectively.
    • [1, 4] and [4] are not good subsets with products 4 = 2*2 and 4 = 2*2 respectively.

Return the number of different good subsets in nums modulo $10^9 + 7$.

A subset of nums is any array that can be obtained by deleting some (possibly none or all) elements from nums. Two subsets are different if and only if the chosen indices to delete are different.

Example 1:

Input: nums = [1,2,3,4]
Output: 6
Explanation: The good subsets are:
- [1,2]: product is 2, which is the product of distinct prime 2.
- [1,2,3]: product is 6, which is the product of distinct primes 2 and 3.
- [1,3]: product is 3, which is the product of distinct prime 3.
- [2]: product is 2, which is the product of distinct prime 2.
- [2,3]: product is 6, which is the product of distinct primes 2 and 3.
- [3]: product is 3, which is the product of distinct prime 3.

Example 2:

Input: nums = [4,2,3,15]
Output: 5
Explanation: The good subsets are:
- [2]: product is 2, which is the product of distinct prime 2.
- [2,3]: product is 6, which is the product of distinct primes 2 and 3.
- [2,15]: product is 30, which is the product of distinct primes 2, 3, and 5.
- [3]: product is 3, which is the product of distinct prime 3.
- [15]: product is 15, which is the product of distinct primes 3 and 5.

Constraints:

  • $1 \le$ nums.length $\le 10^5$
  • $1 \le$ nums[i] $\le 30$

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 😇.

Lưu ý nhỏ: khi nộp trên CLUE OJ các bạn dán toàn bộ code, gồm cả hàm main, vì đề trên CLUE OJ đọc dữ liệu vào và in kết quả đúng theo định dạng ở phần đề bài, giống như khi các bạn chạy thử trên máy hoặc ở trang Chạy code online của blog.

Nói gọn lại, một tập con là tốt khi tích của nó lớn hơn 1 và không chia hết cho bình phương của số nguyên tố nào. Ở ví dụ 2 ([4,2,3,15]): số 4 = 2·2 nên tập nào có số 4 đều hỏng; còn [3,15] có tích 45 = 3·3·5 bị lặp số 3 nên cũng hỏng. Ngoài ra số 1 không làm thay đổi tích, nên thêm hay bớt số 1 vào một tập con tốt thì vẫn tốt – đó là lý do ví dụ 1 có cả [2] lẫn [1,2]. Riêng tập chỉ gồm toàn số 1 thì tích bằng 1, không phải tích của "một hoặc nhiều" số nguyên tố nên không tính.

Link nộp bài

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 OJ

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

Cách 1Duyệt tất cả tập con – O(2^N · N)

Làm đúng theo đề: mỗi tập con ứng với một mask $n$ bit. Ta lần lượt nhân các phần tử được chọn vào biến product, và trước khi nhân số $x$ thì kiểm tra:

  • $x$ không được chia hết cho bình phương của số nguyên tố nào (như 4, 8, 9, 12, 18, 25...);
  • $x$ không được có thừa số nguyên tố chung với tích hiện tại, tức gcd(product, x) == 1.

Cuối cùng tập con là tốt nếu qua được hết các kiểm tra và tích lớn hơn 1. 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;

class Solution {
public:
    // x không chia hết cho bình phương của số nguyên tố nào
    bool squareFree(int x) {
        for (int p = 2; p * p <= x; p++) {
            if (x % (p * p) == 0) return false;
        }
        return true;
    }
    int numberOfGoodSubsets(vector<int>& nums) {
        int n = (int) nums.size();
        long long ans = 0;
        for (int mask = 1; mask < (1 << n); mask++) {
            long long product = 1;
            bool ok = true;
            for (int i = 0; i < n && ok; i++) {
                if (mask >> i & 1) {
                    int x = nums[i];
                    // x phải không có thừa số lặp và không chung thừa số nào với tích hiện tại
                    if (!squareFree(x) || __gcd(product, (long long) x) > 1) ok = false;
                    else product *= x;
                }
            }
            if (ok && product > 1) ans++;
        }
        return ans % 1000000007;
    }
};

int main() {
    int n;
    cin >> n;
    vector<int> nums(n);
    for (int i = 0; i < n; i++) cin >> nums[i];
    Solution sol;
    cout << sol.numberOfGoodSubsets(nums);
    return 0;
}
import sys
from math import gcd
from typing import List

class Solution:
    # x không chia hết cho bình phương của số nguyên tố nào
    def squareFree(self, x: int) -> bool:
        p = 2
        while p * p <= x:
            if x % (p * p) == 0:
                return False
            p += 1
        return True

    def numberOfGoodSubsets(self, nums: List[int]) -> int:
        n = len(nums)
        ans = 0
        for mask in range(1, 1 << n):
            product = 1
            ok = True
            for i in range(n):
                if mask >> i & 1:
                    x = nums[i]
                    # x phải không có thừa số lặp và không chung thừa số nào với tích hiện tại
                    if not self.squareFree(x) or gcd(product, x) > 1:
                        ok = False
                        break
                    product *= x
            if ok and product > 1:
                ans += 1
        return ans % 1000000007

def main():
    data = sys.stdin.read().split()
    n = int(data[0])
    nums = list(map(int, data[1:1 + n]))
    print(Solution().numberOfGoodSubsets(nums))

if __name__ == "__main__":
    main()
Không gian$O(1)$
Thời gian$O(2^N \cdot N)$
Chỉ dùng được khi n nhỏ
Có $2^n$ tập con, mà $n$ lên tới $10^5$ nên cách này chắc chắn TLE. Mình chỉ dùng nó để hiểu đề và làm code vét cạn đối chiếu cho các cách sau.

Cách 2Quy hoạch động bitmask trên các số nguyên tố – O(N + 30 · 2^10)

Phương pháp xử lý: Quy hoạch động bitmask + tổ hợp. Cụ thể như sau.

Ta nhìn ra điều này: số trong dãy khá nhỏ ($1 \le$ nums[i] $\le 30$) nên nếu phân tích các số này ra thành tích các thừa số nguyên tố thì số lượng số nguyên tố chỉ $\le 10$, đó là các số $\{2, 3, 5, 7, 11, 13, 17, 19, 23, 29\}$.

Trước hết ta gọi $cnt[x]$ là số lượng số $x$ có trong dãy. Ví dụ dãy nums = [1, 2, 5, 2, 4, 1, 4] thì $cnt[1] = 2$, $cnt[2] = 2$, $cnt[4] = 2$, $cnt[5] = 1$. Vì giá trị chỉ từ 1 tới 30, thay vì xét từng phần tử ta xét từng giá trị $x$: trong một tập con tốt, mỗi giá trị $x \gt 1$ chỉ được lấy tối đa 1 lần (lấy 2 số $x$ thì thừa số của $x$ bị lặp), và nếu lấy thì có đúng $cnt[x]$ cách chọn xem lấy số $x$ ở vị trí nào.

Tiếp theo ta sàng lọc toàn bộ số mà khi phân tích ra có thừa số nguyên tố với lũy thừa $\gt 1$, ví dụ như mấy số 4, 8, 9, 12, 25... bỏ hết ra khỏi dãy, bởi vì mấy số này vô dụng (có mặt là tập con hỏng ngay). Sau đó gọi $listPrime[x]$ là tập hợp các thừa số nguyên tố khi phân tích $x$.

listPrime[x] là một dãy bit
Bit thứ $p$ bằng 1 nghĩa là số nguyên tố thứ $p$ (đếm từ 0 trong dãy $2, 3, 5, 7, \ldots$) có mặt. Ví dụ $listPrime[15] = 6$, bởi vì 6 đổi sang dãy bit là 110: bit thứ 1 và bit thứ 2 bằng 1, tức là có sự xuất hiện của thừa số nguyên tố 3 và 5. Còn với số bị loại (như 4, 12) mình gán $listPrime = -1$.

Rồi tới luôn 😄 nguyên liệu chuẩn bị đã xong xuôi, bây giờ ta quy hoạch động nào:

Trạng thái
Gọi $f[i][mask]$ là số cách chọn các số trong dãy khi đã xét các giá trị $1, 2, \ldots, i$, và $mask$ là dãy bit các số nguyên tố đã có mặt trong tích (bit $p$ bằng 1 tức là số nguyên tố thứ $p$ có mặt). Khởi tạo $f[0][0] = 1$.

Với $f[i][mask]$ hiện tại, xét giá trị $x = i + 1$:

  • Nếu bỏ qua giá trị $i + 1$: $f[i + 1][mask] \mathrel{+}= f[i][mask]$.
  • Nếu nhận giá trị $i + 1$ thì phải kiểm tra: khi kết nạp nó vào, đã có thừa số nguyên tố nào nằm sẵn trong $mask$ mà $i + 1$ cũng có hay chưa? Tức là kiểm tra mask & listPrime[i + 1] == 0 hay không. Nếu đúng thì: $f[i + 1][mask \mid listPrime[i + 1]] \mathrel{+}= f[i][mask] \cdot cnt[i + 1]$. Ta nhân với $cnt[i + 1]$ là bởi vì số lượng số $i + 1$ là $cnt[i + 1]$, nên sẽ có $cnt[i + 1]$ cách chọn.

Đáp án là tổng các $f[30][mask]$ với $mask$ từ $1$ tới $2^{10} - 1$ ($mask = 0$ là chưa chọn số nguyên tố nào, tích bằng 1, không tính).

Còn số 1 thì sao?
Số 1 có $listPrime[1] = 0$, nhưng không được đưa vào quy hoạch động như các số khác: số 1 lấy bao nhiêu cái cũng được (tích không đổi), mỗi số 1 đều có 2 lựa chọn lấy / không lấy. Vậy với mỗi tập con tốt không có số 1, ta có $2^{cnt[1]}$ cách thêm các số 1 vào. Đáp án cuối cùng:
$\text{ans} = 2^{cnt[1]} \cdot \sum_{mask = 1}^{2^{10} - 1} f[30][mask]$.

Minh hoạ quy hoạch động theo từng giá trị

Em nhập mảng nums (tối đa 15 số, mỗi số từ 1 tới 30) rồi bấm Tiếp. Mỗi ô là một trạng thái khác 0: phía trên ghi tập số nguyên tố trong $mask$, phía dưới là $f[mask]$. Mình chỉ dừng ở những giá trị $x$ có trong mảng (giá trị không có thì $cnt[x] = 0$, hàng $f$ giữ nguyên):

Ví dụ nhanh:

Source code để các bạn tham khảo nếu làm theo hướng này (hàm getMask chính là bước tính $listPrime[x]$, trả về $-1$ với số có thừa số lặp):

#include <bits/stdc++.h>
using namespace std;

const long long MOD = 1000000007;
int primes[10] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
long long f[31][1 << 10];

// dãy bit các thừa số nguyên tố của x, trả về -1 nếu x có thừa số mũ >= 2
int getMask(int x) {
    int res = 0;
    for (int p = 0; p < 10; p++) {
        int c = 0;
        while (x % primes[p] == 0) {
            x /= primes[p];
            c++;
        }
        if (c >= 2) return -1;
        if (c == 1) res |= (1 << p);
    }
    return res;
}

class Solution {
public:
    int numberOfGoodSubsets(vector<int>& nums) {
        long long cnt[31] = {0};
        int listPrime[31];
        for (int x : nums) cnt[x]++;
        for (int x = 1; x <= 30; x++) listPrime[x] = getMask(x);
        memset(f, 0, sizeof(f));
        f[0][0] = 1;
        for (int i = 0; i < 30; i++) {
            int x = i + 1;
            for (int mask = 0; mask < (1 << 10); mask++) {
                if (f[i][mask] == 0) continue;
                (f[i + 1][mask] += f[i][mask]) %= MOD;
                if (x > 1 && listPrime[x] != -1 && (mask & listPrime[x]) == 0) {
                    int nmask = mask | listPrime[x];
                    (f[i + 1][nmask] += f[i][mask] * cnt[x]) %= MOD;
                }
            }
        }
        long long ans = 0;
        for (int mask = 1; mask < (1 << 10); mask++) {
            (ans += f[30][mask]) %= MOD;
        }
        for (int k = 0; k < cnt[1]; k++) {
            (ans *= 2) %= MOD;
        }
        return (int) ans;
    }
};

int main() {
    int n;
    cin >> n;
    vector<int> nums(n);
    for (int i = 0; i < n; i++) cin >> nums[i];
    Solution sol;
    cout << sol.numberOfGoodSubsets(nums);
    return 0;
}
import sys
from typing import List

MOD = 1000000007
primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

# dãy bit các thừa số nguyên tố của x, trả về -1 nếu x có thừa số mũ >= 2
def getMask(x: int) -> int:
    res = 0
    for p in range(10):
        c = 0
        while x % primes[p] == 0:
            x //= primes[p]
            c += 1
        if c >= 2:
            return -1
        if c == 1:
            res |= 1 << p
    return res

class Solution:
    def numberOfGoodSubsets(self, nums: List[int]) -> int:
        cnt = [0] * 31
        for x in nums:
            cnt[x] += 1
        listPrime = [0] + [getMask(x) for x in range(1, 31)]
        f = [[0] * (1 << 10) for _ in range(31)]
        f[0][0] = 1
        for i in range(30):
            x = i + 1
            for mask in range(1 << 10):
                if f[i][mask] == 0:
                    continue
                f[i + 1][mask] = (f[i + 1][mask] + f[i][mask]) % MOD
                if x > 1 and listPrime[x] != -1 and (mask & listPrime[x]) == 0:
                    nmask = mask | listPrime[x]
                    f[i + 1][nmask] = (f[i + 1][nmask] + f[i][mask] * cnt[x]) % MOD
        ans = sum(f[30][1:]) % MOD
        ans = ans * pow(2, cnt[1], MOD) % MOD
        return ans

def main():
    data = sys.stdin.read().split()
    n = int(data[0])
    nums = list(map(int, data[1:1 + n]))
    print(Solution().numberOfGoodSubsets(nums))

if __name__ == "__main__":
    main()

Khung chạy tay bên dưới chạy theo code C++ ở trên. Vì vòng mask chạy tới $2^{10} = 1024$ lần mà đa số $f$ bằng 0 (bị continue), mình gom những lần đó lại, chỉ dừng ở các mask có $f \ne 0$; với các giá trị $x$ không có trong mảng hoặc bị loại thì gom cả vòng mask thành 1 bước "chép nguyên hàng". Trạng thái mask được ghi dưới dạng tập số nguyên tố cho dễ đọc:

C++▶ Chạy tay từng dòng codef[i][mask]: đã xét các giá trị 1..i, mask = tập số nguyên tố đã dùng; đáp án × 2^cnt[1].
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4const long long MOD = 1000000007;
5int primes[10] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
6long long f[31][1 << 10];
7
8// dãy bit các thừa số nguyên tố của x, trả về -1 nếu x có thừa số mũ >= 2
9int getMask(int x) {
10 int res = 0;
11 for (int p = 0; p < 10; p++) {
12 int c = 0;
13 while (x % primes[p] == 0) {
14 x /= primes[p];
15 c++;
16 }
17 if (c >= 2) return -1;
18 if (c == 1) res |= (1 << p);
19 }
20 return res;
21}
22
23class Solution {
24public:
25 int numberOfGoodSubsets(vector<int>& nums) {
26 long long cnt[31] = {0};
27 int listPrime[31];
28 for (int x : nums) cnt[x]++;
29 for (int x = 1; x <= 30; x++) listPrime[x] = getMask(x);
30 memset(f, 0, sizeof(f));
31 f[0][0] = 1;
32 for (int i = 0; i < 30; i++) {
33 int x = i + 1;
34 for (int mask = 0; mask < (1 << 10); mask++) {
35 if (f[i][mask] == 0) continue;
36 (f[i + 1][mask] += f[i][mask]) %= MOD;
37 if (x > 1 && listPrime[x] != -1 && (mask & listPrime[x]) == 0) {
38 int nmask = mask | listPrime[x];
39 (f[i + 1][nmask] += f[i][mask] * cnt[x]) %= MOD;
40 }
41 }
42 }
43 long long ans = 0;
44 for (int mask = 1; mask < (1 << 10); mask++) {
45 (ans += f[30][mask]) %= MOD;
46 }
47 for (int k = 0; k < cnt[1]; k++) {
48 (ans *= 2) %= MOD;
49 }
50 return (int) ans;
51 }
52};
53
54int main() {
55 int n;
56 cin >> n;
57 vector<int> nums(n);
58 for (int i = 0; i < n; i++) cin >> nums[i];
59 Solution sol;
60 cout << sol.numberOfGoodSubsets(nums);
61 return 0;
62}

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

  • Không gian: bảng $f$ có $31 \times 2^{10}$ ô, cỡ 31 nghìn ô, coi như hằng số: $O(30 \cdot 2^{10})$.
  • Thời gian: đếm $cnt$ mất $O(N)$, quy hoạch động mất $30 \times 1024 \approx 3 \cdot 10^4$ phép, nên tổng cộng $O(N + 30 \cdot 2^{10})$ – rất nhanh.
Không gian$O(30 \cdot 2^{10})$
Thời gian$O(N + 30 \cdot 2^{10})$
Tràn số khi nhân với cnt
$f[i][mask] \lt 10^9 + 7$ và $cnt[x] \le 10^5$ nên tích có thể tới $10^{14}$: bắt buộc dùng long long rồi mới lấy modulo.

Cách 3Code AC của mình: QHĐ theo từng phần tử + tổ hợp cho 17, 19, 23, 29 – O(N · 2^6)

Code dưới đây của mình đã AC trên LeetCode. Ý tưởng vẫn là quy hoạch động bitmask như cách 2, nhưng có hai điểm khác:

  • Quy hoạch động theo từng phần tử của mảng ($dp[i][mask]$: đã xét $i$ phần tử đầu), nên không cần nhân với $cnt$: mỗi phần tử tự có 2 lựa chọn lấy / không lấy. Số 1 có $listPrime = 0$ nên "lấy" luôn hợp lệ, tự động nhân đôi số cách – đúng bằng việc nhân $2^{cnt[1]}$.
  • Mask chỉ còn 6 bit cho các số nguyên tố $2, 3, 5, 7, 11, 13$. Các số nguyên tố lớn $17, 19, 23, 29$ có nhận xét đặc biệt: trong phạm vi tới 30, số duy nhất chia hết cho chúng là chính chúng (vì $17 \cdot 2 = 34 \gt 30$). Nên với mỗi số nguyên tố lớn $q$, ta chỉ có $cnt[q] + 1$ lựa chọn: không lấy, hoặc lấy đúng 1 trong $cnt[q]$ số $q$. Các lựa chọn này độc lập với phần còn lại, đếm bằng tổ hợp (quy tắc nhân).
Ghép đáp án
Gọi $S = \sum_{mask = 1}^{2^6 - 1} dp[n][mask]$ (các tập có ít nhất một số nguyên tố nhỏ, đã tính cả các số 1) và $Q = (cnt[17] + 1)(cnt[19] + 1)(cnt[23] + 1)(cnt[29] + 1)$. Khi đó:
  • Tập con có ít nhất một số nguyên tố nhỏ: $S \cdot Q$ (phần số nguyên tố lớn chọn tự do).
  • Tập con không có số nguyên tố nhỏ nào: phần số nguyên tố lớn phải chọn ít nhất 1 số, có $Q - 1$ cách, nhân với $2^{cnt[1]}$ cách chọn các số 1.
Đáp án: $S \cdot Q + (Q - 1) \cdot 2^{cnt[1]}$. Phần $2^{cnt[1]}$ mình tính bằng hàm lũy thừa nhanh POW.

Source code để các bạn tham khảo nếu làm theo hướng này (bản Python mình dùng mảng lăn 64 phần tử thay cho bảng $dp$ to, cùng logic):

#include <bits/stdc++.h>
using namespace std;

int prime[6] = {2, 3, 5, 7, 11, 13};
int otherPrime[4] = {17, 19, 23, 29};
int listPrime[100005];
int cnt[35];
const long long MOD = 1000000007;
long long dp[100005][(1 << 6) + 2];

long long POW(long long x, long long n) {
    if (n == 0) {
        return 1LL;
    }
    long long tmp = POW(x, n >> 1);
    (tmp *= tmp) %= MOD;
    if (n % 2 == 1) {
        (tmp *= x) %= MOD;
    }
    return tmp;
}

class Solution {
public:
    int numberOfGoodSubsets(vector<int>& nums) {
        memset(dp, 0, sizeof(dp));
        memset(listPrime, 0, sizeof(listPrime));
        memset(cnt, 0, sizeof(cnt));
        int n = (int) nums.size();
        for (int i = 1; i <= n; i++) {
            int value = nums[i - 1];
            if (value == 1) {
                cnt[1]++;
                continue;
            }
            bool found = false;
            for (int p = 0; p < 4; p++) {
                if (value == otherPrime[p]) {
                    listPrime[i] = -1;
                    cnt[value]++;
                    found = true;
                    break;
                }
            }
            if (found == true) {
                continue;
            }
            for (int p = 0; p < 6; p++) {
                int cntValue = 0;
                while ((value % prime[p]) == 0) {
                    cntValue++;
                    value /= prime[p];
                }
                if (cntValue >= 2) {
                    listPrime[i] = -1;
                    break;
                }
                if (cntValue == 1) {
                    listPrime[i] |= (1 << p);
                }
            }
        }
        dp[0][0] = 1;
        for (int i = 0; i < n; i++) {
            for (int mask = 0; mask < (1 << 6); mask++) {
                if (dp[i][mask] == 0) {
                    continue;
                }
                (dp[i + 1][mask] += dp[i][mask]) %= MOD;
                if (listPrime[i + 1] != -1 && (mask & listPrime[i + 1]) == 0) {
                    (dp[i + 1][mask | listPrime[i + 1]] += dp[i][mask]) %= MOD;
                }
            }
        }
        long long ans = 0;
        for (int mask = 1; mask < (1 << 6); mask++) {
            (ans += dp[n][mask]) %= MOD;
        }
        (ans *= (long long) (cnt[17] + 1)) %= MOD;
        (ans *= (long long) (cnt[19] + 1)) %= MOD;
        (ans *= (long long) (cnt[23] + 1)) %= MOD;
        (ans *= (long long) (cnt[29] + 1)) %= MOD;
        long long other = 1;
        (other *= (long long) (cnt[17] + 1)) %= MOD;
        (other *= (long long) (cnt[19] + 1)) %= MOD;
        (other *= (long long) (cnt[23] + 1)) %= MOD;
        (other *= (long long) (cnt[29] + 1)) %= MOD;
        other--;
        (other *= POW(2LL, cnt[1])) %= MOD;
        return (int) ((ans + other) % MOD);
    }
};

int main() {
    int n;
    cin >> n;
    vector<int> nums(n);
    for (int i = 0; i < n; i++) cin >> nums[i];
    Solution sol;
    cout << sol.numberOfGoodSubsets(nums);
    return 0;
}
import sys
from typing import List

MOD = 1000000007
prime = [2, 3, 5, 7, 11, 13]
otherPrime = [17, 19, 23, 29]

class Solution:
    def numberOfGoodSubsets(self, nums: List[int]) -> int:
        n = len(nums)
        listPrime = [0] * (n + 1)
        cnt = [0] * 35
        for i in range(1, n + 1):
            value = nums[i - 1]
            if value == 1:
                cnt[1] += 1
                continue
            if value in otherPrime:
                listPrime[i] = -1
                cnt[value] += 1
                continue
            for p in range(6):
                cntValue = 0
                while value % prime[p] == 0:
                    cntValue += 1
                    value //= prime[p]
                if cntValue >= 2:
                    listPrime[i] = -1
                    break
                if cntValue == 1:
                    listPrime[i] |= 1 << p
        # dp lăn: chỉ giữ hàng dp[i] hiện tại (64 trạng thái) cho đỡ tốn bộ nhớ
        dp = [0] * (1 << 6)
        dp[0] = 1
        for i in range(n):
            lp = listPrime[i + 1]
            ndp = dp[:]  # không chọn nums[i]
            if lp != -1:
                for mask in range(1 << 6):
                    if dp[mask] and (mask & lp) == 0:
                        ndp[mask | lp] = (ndp[mask | lp] + dp[mask]) % MOD
            dp = ndp
        ans = sum(dp[1:]) % MOD
        for q in otherPrime:
            ans = ans * (cnt[q] + 1) % MOD
        other = 1
        for q in otherPrime:
            other = other * (cnt[q] + 1) % MOD
        other -= 1
        other = other * pow(2, cnt[1], MOD) % MOD
        return (ans + other) % MOD

def main():
    data = sys.stdin.read().split()
    n = int(data[0])
    nums = list(map(int, data[1:1 + n]))
    print(Solution().numberOfGoodSubsets(nums))

if __name__ == "__main__":
    main()

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

  • Không gian: bảng dp[100005][66] kiểu long long, khoảng 53 MB, tức $O(N \cdot 2^6)$. Vì mỗi hàng chỉ phụ thuộc hàng trước nên có thể dùng mảng lăn để còn $O(2^6)$ như bản Python.
  • Thời gian: $n$ phần tử, mỗi phần tử duyệt 64 mask: $O(N \cdot 2^6)$, khoảng $6.4 \cdot 10^6$ phép với $n = 10^5$.
Không gian$O(N \cdot 2^6)$
Thời gian$O(N \cdot 2^6)$
So sánh cách 2 và cách 3
Cả hai đều đúng và đủ nhanh. Cách 2 gom theo giá trị nên vòng QHĐ không phụ thuộc $n$; cách 3 tách riêng 4 số nguyên tố lớn ra để mask nhỏ hơn (64 thay vì 1024). Mẹo "số nguyên tố lớn chỉ xuất hiện trong chính nó" rất hay dùng trong các bài bitmask trên số nguyên tố, các bạn nhớ nhé.
Ghi chú của mình: code C++ và Python của cả 3 cách đều cho đúng 2 ví dụ của đề, và đã được so với một chương trình vét cạn độc lập (nhân trực tiếp rồi kiểm tra tích) trên 300 bộ test ngẫu nhiên với $n \le 13$ – khớp hết. Với $n = 10^5$: cách 2 bản C++ chạy khoảng 0.04 giây, bản Python khoảng 0.15 giây; cách 3 bản C++ khoảng 0.15 giây, bản Python khoảng 1 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 😇
Tự học thuật toán: khó ở đâu?
Đọc xong một bài giải là bước đầu. Phần lớn học sinh tự học sẽ gặp những trở ngại sau:
  • 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.
Học cùng thầy Nam Sơn, các trở ngại đó được gỡ bỏ:
  • 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