Hướng dẫn giải của Tổng bit


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, WTrungs, asenen_kiet, blablablabkabcu

Hiểu bài toán

Bài toán yêu cầu tính tổng số lượng bit '1' trong tất cả các số nguyên không âm từ 0 đến N. Ví dụ, với N = 5, các số là 0 (0), 1 (1), 2 (10), 3 (11), 4 (100), 5 (101). Tổng số bit 1 là 0 + 1 + 1 + 2 + 1 + 2 = 7. N có thể lên tới 10^14, do đó giải thuật duyệt từng số (O(N)) là không khả thi.

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

Cách Quy hoạch động theo-bit (Digit DP)
#include <bits/stdc++.h>
using namespace std;

#define ll long long

ll n;
int arr[41];
ll f[41][2][41];

// dp: tính tổng số bit 1 từ các số có độ dài 'id' bit
// tight: 1 nếu số đang xét bị giới hạn bởi N, 0 nếu không
// sum: tổng số bit 1 đã chọn từ các bit trước đó
ll calc(int id, int tight, int sum) {
    if (id == 0) {
        return sum; // Base case: trả về tổng số bit 1 của một cấu hình
    }
    if (f[id][tight][sum] != -1) {
        return f[id][tight][sum];
    }
    int maxDigit = 1;
    if (tight) maxDigit = arr[id]; // Nếu bị giới hạn, bit tối đa là bit tương ứng của N
    ll res = 0;
    for (int digit = 0; digit <= maxDigit; digit++) {
        bool newTight = tight && (digit == maxDigit);
        res += calc(id - 1, newTight, sum + digit);
    }
    return f[id][tight][sum] = res;
}

ll solve(ll n) {
    if (n == 0) return 0;
    // Chuyển N sang mảng bit (arr[1] là bit thấp nhất)
    int len = 0;
    while (n > 0) {
        arr[++len] = n % 2;
        n /= 2;
    }
    // Khởi tạo mảng DP với -1
    memset(f, -1, sizeof(f));
    return calc(len, 1, 0);
}

int main() {
    cin >> n;
    cout << solve(n);
}
  • Time Complexity: O(log^2 N) ~ O(40^2)
  • Space Complexity: O(log^2 N) ~ O(40^2)

Giải pháp này sử dụng quy hoạch động trên các bit của số N (Digit DP).

  1. Chia nhỏ vấn đề: Xét các số từ 0 đến N bit này qua bit khác.
  2. Trạng thái DP: dp[id][tight][sum] là tổng số bit 1 của các số có độ dài id bit, với ràng buộc tight và tổng bit 1 ở các bit cao hơn là sum.
  3. Quy hoạch: Với mỗi bit hiện tại, ta thử các giá trị 0 và 1 (nếu tight cho phép). Nếu chọn 1, ta cộng vào tổng.
  4. Kết quả: Gọi calc(len, 1, 0) để tính toàn bộ dãy. Phương pháp này tổng quát và dễ hiểu nhưng hơi rườm rà so với công thức toán học.
Cách Công thức quy luật bit (Toán học)
#include <bits/stdc++.h>
using namespace std;

long long countBits(long long n) {
    long long ans = 0;

    // Duyệt qua từng bit từ bit 0 đến bit cao nhất của n
    for (long long k = 0; (1LL << k) <= n; k++) {
        // Chu kỳ lặp lại của bit thứ k là 2^(k+1)
        long long cycle = 1LL << (k + 1);
        // Số chu kỳ đầy đủ trong khoảng [0, n]
        long long full = n / cycle;
        // Mỗi chu kỳ đầy đủ có 2^k số có bit thứ k là 1
        ans += full * (1LL << k);

        // Phần dư của chu kỳ cuối
        long long rem = n % cycle;
        // Trong phần dư, bit 1 bắt đầu từ 2^k và kéo dài đến hết
        ans += max(0LL, rem - (1LL << k) + 1);
    }

    return ans;
}

int main() {
    long long n;
    cin >> n;
    cout << countBits(n);
}
  • Time Complexity: O(log N)
  • Space Complexity: O(1)

Đây là giải thuật tối ưu nhất, dựa trên quy luật lặp của từng bit.

  1. Với bit thứ k (giá trị 2^k), bit này sẽ bằng 1 sau mỗi 2^(k+1) số.
  2. Trong một chu kỳ 2^(k+1) số, có đúng 2^k số mà bit k là 1.
  3. Ta tính số chu kỳ đầy đủ full = n / cycle. Tổng số bit 1 từ các chu kỳ này là full * 2^k.
  4. Phần còn lại rem = n % cycle có thể chứa thêm bit 1. Nếu phần dư này >= 2^k, nó chứa các bit 1 từ 2^k đến rem. Số lượng là rem - 2^k + 1.
  5. Lặp lại cho đến khi 2^k > N.
Cách Quy hoạch động tương tự (Digit DP variant)
#include <iostream>
#include <fstream>
using namespace std;

long long n, c;

long long countTotalOnes(long long n) 
{
    long long c = 0;
    for (long long i = 0; (1LL << i) <= n; ++i) 
    {
        long long p = 1LL << (i + 1);
        long long f = n / p;
        c += f * (p / 2);
        long long r = n % p;
        if (r >= (p / 2)) 
        {
            c += r - (p / 2) + 1;
        }
    }
    return c;
}

int main()
{
    ifstream in("BITSUM.INP");
    ofstream out("BITSUM.OUT");

    in >> n;
    c = countTotalOnes(n);
    out << c << endl;

    in.close();
    out.close();

    return 0;
}
  • Time Complexity: O(log N)
  • Space Complexity: O(1)

Đây là một cách viết khác của giải thuật toán học, cùng bản chất với Solution 1 và Solution 3. Nó tính tổng bit 1 bằng cách duyệt qua từng bit (i).

  • p = 2^(i+1) là chu kỳ.
  • f = n / p là số chu kỳ đầy đủ.
  • c += f * (p/2) là số bit 1 từ các chu kỳ đầy đủ.
  • r = n % p là phần dư.
  • Nếu r >= p/2, phần dư chứa bit 1, cộng thêm r - p/2 + 1. Cách này tối ưu về thời gian và bộ nhớ.

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

Cách tiếp cận Time Space Tên
1 O(log^2 N) ~ O(40^2) O(log^2 N) ~ O(40^2) Quy hoạch động theo-bit (Digit DP)
2 O(log N) O(1) Công thức quy luật bit (Toán học)
3 O(log N) O(1) Quy hoạch động tương tự (Digit DP variant)

Bài học kinh nghiệm

  • Bài toán có thể giảm độ phức tạp từ O(N) xuống O(log N) bằng cách phân tích quy luật lặp của bit thay vì duyệt từng số.
  • Công thức cơ bản: Với mỗi bit i, tổng số lần nó xuất hiện bằng (số lượng số có bit i là 1 từ 0 đến N).

Lỗi thường gặp

  • Sử dụng kiểu dữ liệu nhỏ (int) cho N (lên tới 10^14) gây tràn số. Phải dùng long long.
  • Sai công thức tính số lượng bit 1 trong phần dư (remainder): phải kiểm tra xem phần dư có vượt quá nửa chu kỳ hay không.

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.