Hướng dẫn giải của Tam giác màu


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, sashimivssushi, itachivsshisui, thitxongkhoiladay

Hiểu bài toán

Cho n điểm được đánh số từ 1 đến n trên mặt phẳng. Ta nối mỗi cặp điểm (i, j) với nhau bằng một sợi dây. Màu sắc của sợi dây được xác định bởi tính chất của tổng i + j:

  • Nếu i + j là số nguyên tố, sợi dây có màu Xanh.
  • Nếu i + j không phải là số nguyên tố, sợi dây có màu Vàng. Yêu cầu: Đếm số lượng hình tam giác có 3 đỉnh trong tập n điểm sao cho cả 3 cạnh của tam giác đều cùng một màu (toàn xanh hoặc toàn vàng).

Ví dụ với n = 5:

  • Các cạnh xanh: (1,2) (tổng 3), (1,4) (tổng 5), (2,3) (tổng 5), (3,4) (tổng 7). Tam giác (1,2,4) có các cạnh (1,2), (2,4), (1,4). Trong đó (1,2) xanh, (1,4) xanh, (2,4) tổng 6 (vàng) -> không tính.
  • Tam giác (1,3,4): (1,3) tổng 4 (vàng), (3,4) xanh, (1,4) xanh -> không tính.
  • Chỉ có tam giác (1,2,3) toàn xanh? (1,2) xanh, (2,3) xanh, (1,3) tổng 4 (vàng) -> không.
  • Chỉ có tam giác (2,3,4) toàn xanh? (2,3) xanh, (3,4) xanh, (2,4) tổng 6 (vàng) -> không.
  • Vậy phải có tam giác toàn vàng. Xét (1,3,5): (1,3) vàng, (3,5) tổng 8 (vàng), (1,5) tổng 6 (vàng). Vậy (1,3,5) là tam giác toàn vàng. Đúng 1.

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

Cách Brute Force (Quy hoạch động)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const int MAX = 2000005;
bool isPrime[MAX];

void sieve() {
    fill(isPrime, isPrime + MAX, true);
    isPrime[0] = isPrime[1] = false;
    for (int i = 2; i * i < MAX; i++) {
        if (isPrime[i]) {
            for (int j = i * i; j < MAX; j += i) isPrime[j] = false;
        }
    }
}

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

    sieve();

    int T;
    cin >> T;
    while (T--) {
        int n;
        cin >> n;

        // deg[i] là số lượng đỉnh kề với i bằng dây xanh (prime sum)
        vector<int> deg(n + 1, 0);
        for (int i = 1; i <= n; i++) {
            for (int j = i + 1; j <= n; j++) {
                if (isPrime[i + j]) {
                    deg[i]++;
                    deg[j]++;
                }
            }
        }

        ll total_triangles = (ll)n * (n - 1) * (n - 2) / 6;
        ll mixed_triangles = 0;

        // Tính số tam giác lai (mixed) dựa trên độ xanh
        for (int i = 1; i <= n; i++) {
            // Với mỗi đỉnh i, số tam giác lai có 1 đỉnh là i và 2 đỉnh kề xanh là C(deg[i], 2)
            // Chia 2 vì mỗi tam giác lai có đúng 2 đỉnh kề xanh (và 1 đỉnh kề vàng)
            mixed_triangles += (ll)deg[i] * (deg[i] - 1) / 2;
        }

        // Số tam giác toàn xanh = tổng tam giác - tam giác lai
        cout << total_triangles - mixed_triangles << '\n';
    }
    return 0;
}
  • Time Complexity: O(n^2)
  • Space Complexity: O(n)

Phương pháp này dựa trên việc tính tổng số tam giác trừ đi số tam giác có đủ 3 màu (lai).

  1. Tính tổng số tam giác: Với n đỉnh, tổng số tam giác bất kỳ là C(n, 3) = n(n-1)(n-2)/6.

  2. Tính số tam giác lai: Một tam giác được coi là 'lai' nếu nó có ít nhất 2 màu khác nhau (ví dụ: 2 xanh 1 vàng, hoặc 1 xanh 2 vàng). Tuy nhiên, ta có thể tính trực tiếp các tam giác có đúng 2 cạnh xanh và 1 cạnh vàng.

    • Xét một đỉnh i. Gọi deg[i] là số lượng đỉnh j (j != i) sao cho i+j là số nguyên tố (cạnh (i,j) màu xanh).
    • Nếu ta chọn 2 đỉnh kề i bằng xanh, ví dụ j và k, thì tam giác (i,j,k) có 2 cạnh xanh là (i,j) và (i,k). Cạnh còn lại (j,k) có thể xanh hoặc vàng.
    • Tuy nhiên, phương pháp này của tác giả submission 1 lại tính mixed_triangles += (ll)deg[i] * (deg[i] - 1) / 2.
    • Phân tích: deg[i] * (deg[i] - 1) / 2 là số cặp (j,k) sao cho (i,j) và (i,k) đều xanh. Nếu (j,k) cũng xanh, tam giác toàn xanh. Nếu (j,k) vàng, tam giác lai (2 xanh 1 vàng).
    • Nhưng tại sao chia 2?
    • Công thức thực tế từ Submission 1 là: ans = total - mixed. Submission 2 tính d += x * (n - 1 - x) rồi res = total - d/2.
    • x ở đây là deg[i]. n - 1 - x là số vàng kề i. x * (n - 1 - x) là số cặp (xanh, vàng) kề i.
    • Xét tam giác lai: nó có thể là (2 xanh, 1 vàng) hoặc (1 xanh, 2 vàng).
    • Nếu tam giác có 2 xanh (j,k) và 1 vàng (i,j) hay (i,k)?
    • Thật ra, ta chỉ cần đếm các tam giác không phải toàn xanh.
    • Ta có thể dùng Bổ đề: Số tam giác toàn xanh = sum(C(deg[i], 2)) - X, trong đó X là số tam giác toàn xanh bị đếm trùng.
    • Nhưng cách của tác giả Submission 1 & 2 là: Total_Mixed = sum_{i=1 to n} (deg[i] * (n - 1 - deg[i])) / 2 Answer = Total_Triangles - Total_Mixed

      Giải thích: deg[i] * (n - 1 - deg[i]) là số lượng tam giác có đỉnh tại i và 2 đỉnh còn lại kề i lần lượt bằng xanh và vàng. Gọi là tam giác có 2 cạnh khác màu gặp tại i. Mỗi tam giác lai (ví dụ: 2 xanh 1 vàng) sẽ có đúng 2 đỉnh là giao của 2 cạnh khác màu (góc nhọn tại đó có 1 xanh 1 vàng). Ví dụ: Tam giác (xanh, xanh, vàng). Góc tại đỉnh xanh-xanh là 2 xanh -> không tính. Góc tại 2 đỉnh còn lại là 1 xanh 1 vàng -> tính. Do đó, mỗi tam giác lai được đếm đúng 2 lần. Nên ta cộng dồn và chia 2.

      Lưu ý: Công thức này chỉ đúng nếu xét 'tam giác lai' là tam giác không đồng nhất 3 cạnh. Nhưng Submission 1 lại trừ từ tổng số tam giác. Thử lại với n=3: Cacanh: (1,2)xanh, (1,3)vàng, (2,3)xanh. Total = 1. i=1: deg[1]=1 (kề 2), n-1-deg[1]=1 (kề 3). 11 = 1. i=2: deg[2]=2 (kề 1,3), n-1-deg[2]=0. 20 = 0. i=3: deg[3]=1 (kề 2), n-1-deg[3]=1 (kề 1). 1*1 = 1. Sum = 2. d/2 = 1. Total - 1 = 0. Đúng.

      Vậy cách này hoạt động bằng cách đếm số cặp (đỉnh i, cạnh (j,k)) sao cho tại i, có 1 xanh 1 vàng. Rồi trừ đi.

Cách Mathematical Optimization
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const int MAX = 2000005;
bool isPrime[MAX];
int cnt[MAX]; // cnt[s] là số cách viết s = i + j (i < j) trong [1, n]

void sieve() {
    fill(isPrime, isPrime + MAX, true);
    isPrime[0] = isPrime[1] = false;
    for (int i = 2; i * i < MAX; i++) {
        if (isPrime[i]) {
            for (int j = i * i; j < MAX; j += i) isPrime[j] = false;
        }
    }
}

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

    int T;
    cin >> T;
    while (T--) {
        int n;
        cin >> n;

        // 1. Tính tổng số cạnh xanh trong đồ thị
        // S = sum(deg[i]) / 2
        // Thay vào đó, ta dùng mảng cnt để đếm tần số tổng.

        // 2. Tính tổng các tích deg[i] * (n - 1 - deg[i])
        // sum(deg[i] * (n - 1 - deg[i])) = (n - 1) * sum(deg[i]) - sum(deg[i]^2)

        // sum(deg[i]) = 2 * S
        // sum(deg[i]^2) = sum(sum_{j} (indicator(i,j) + indicator(j,i))^2) / 2? Không.
        // sum(deg[i]^2) = sum_{i} (sum_{j} indicator(i,j))^2
        //               = sum_{i} sum_{j} sum_{k} indicator(i,j) indicator(i,k)
        //               = sum_{j, k} degree(common neighbors of j and k)
        //               = sum_{j, k} (nghệp của j và k)

        // Tuy nhiên, có một cách đếm trực tiếp hơn:
        // Xét 2 đỉnh j, k. 
        // Nếu (j, k) là xanh, thì có bao nhiêu đỉnh i tạo thành tam giác xanh với j, k?
        // Nếu (j, k) là vàng, thì có bao nhiêu đỉnh i tạo thành tam giác xanh với j, k (tức là i-j xanh và i-k xanh)?

        // Cach 2 trong solutions: 
        // d = sum_{i=1 to n} ( (so chan xanh tu i+) * (so chan vang tu i+) )
        // Su dung precomputed array cnt.

        // Reset cnt for range [2, 2n]
        // Tính cnt[s]: số cặp (i, j) i < j, i+j = s, 1 <= i, j <= n.
        // i + j = s => i >= max(1, s - n) va i <= min(n, s - 1)
        // i < j => i < s - i => 2i < s => i < s/2.

        // De toi uu hoa, ta co the tinh truc tiep.
        // Suy dinh lai bai toan:
        // Ket qua = Tong so tam giac - (sum(deg[i] * (n - 1 - deg[i])) / 2)
        // Can tinh: S1 = sum(deg[i]) va S2 = sum(deg[i]^2)

        // S1 = sum_{s la so nguyen to} cnt[s]
        // S2 = sum_{s} cnt[s] * (cnt[s] - 1) + sum_{s} cnt[s] * (so cap (i, j) i+j=s ma co 1 diem chung)
        // Cach nay phuc tap.

        // Tot hon, ta chi can tinh S1 va S2.
        // S1 = sum_{s} cnt[s]
        // S2 = sum_{i} deg[i]^2
        // S2 = sum_{i} (sum_{j} indicator(i, j))^2 = sum_{i} sum_{j} sum_{k} indicator(i, j) indicator(i, k)
        //     = sum_{j, k} (so diem i chung cua j va k)
        //     = sum_{j, k} (deg[j] + deg[k] - ...)? Không.
        //     = sum_{x} (deg[x]) * (deg[x])? Không.

        // S2 = sum_{s1} sum_{s2} (so cap (i, j) sao cho i+j = s1 và i+k = s2)? 
        // De don gian, ta chi can tinh S2 bang cach thuc hien vong lap.
        // Nhung kich thuoc n len toi 10^6, n^2 khong duoc.

        // Cach 2 trong solutions su dung vong lap O(n) tinh xong toan bo.
        // Cach do:
        // d = 0;
        // for(i = 1; i <= n; ++i) {
        //     x = s[n + i] - s[i] - (i == 1); // x = so luong so nguyen to trong (i, n+i] - so luong so nguyen to trong [1, i]
        //                                      // That ra x la so luong so nguyen to p sao cho i < p <= n + i.
        //     // De co the tao canh xanh (i, j), can i < j <= n va i + j la nguyen to.
        //     // i + j la nguyen to p => j = p - i.
        //     // Dieu kien: i < p - i <= n => 2i < p <= n + i.
        //     // Do do x chinh la deg[i] (so luong j).
        //     d += x * (n - 1 - x);
        // }
        // cout << n*(n-1)*(n-2)/6 - d/2 << endl;

        // Day la phien ban O(N) sau khi co Sieve.
        // Chac chan la toi uu nhat.

        // Thuc hien:
        vector<int> pref(2 * MAX, 0); // Phan bao tu Sieve
        for(int i = 1; i < 2 * MAX; i++) {
            pref[i] = pref[i-1] + isPrime[i];
        }

        // Tinh S1 va S2 bang cach truc tiep theo cong thuc tren
        // De tiet kiem bien, ta chi can tinh d truc tiep.

        ll d = 0;
        for (int i = 1; i <= n; i++) {
            // x = deg[i]
            // Can tim so p: i < p - i <= n, p la nguyen to
            // 2i < p <= n + i
            int x = pref[n + i] - pref[2 * i]; 
            // Kiem tra: p > 2i. p <= n + i.
            // Neu i > 1, p >= 2i + 1.
            // Neu i = 1, p > 2. 
            // Cong thuc tren chinh xac.

            d += (ll)x * (n - 1 - x);
        }

        cout << (ll)n * (n - 1) * (n - 2) / 6 - d / 2 << '\n';
    }
    return 0;
}
  • Time Complexity: O(N log log N + T * N)
  • Space Complexity: O(N)

Phương pháp này tối ưu hóa việc tính deg[i] và tổng d.

  1. Số phức tạp: N lên tới 10^6, T <= 10. Nên O(N log log N) hoặc O(N) cho mỗi test là chấp nhận được. O(N^2) chắc chắn TLE.

  2. Tính deg[i] nhanh:

    • deg[i] là số lượng j sao cho i < j <= ni + j là số nguyên tố.
    • Đặt p = i + j. Ta cần p là số nguyên tố và i < p - i <= n.
    • Ta cần 2i < p <= n + i.
    • Do đó, deg[i] bằng số lượng số nguyên tố trong khoảng (2i, n + i].
    • Ta có thể precompute mảng pref lưu số lượng số nguyên tố từ 1 đến k.
    • deg[i] = pref[n + i] - pref[2i].
  3. Tính tổng d:

    • d = sum(deg[i] * (n - 1 - deg[i])).
    • Ta chỉ cần vòng lặp từ 1 đến n, tính deg[i] bằng mảng pref rồi cộng dồn.
  4. Kết quả: total - d / 2.

Đây là cách tiếp cận tối ưu nhất được trình bày trong các solutions.

Cách Direct Formula (Refined)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const int MAX = 2000005;
bool isPrime[MAX];
int pref[MAX];

void sieve() {
    fill(isPrime, isPrime + MAX, true);
    isPrime[0] = isPrime[1] = false;
    for (int i = 2; i * i < MAX; i++) {
        if (isPrime[i]) {
            for (int j = i * i; j < MAX; j += i) isPrime[j] = false;
        }
    }
    // Tính mảng tiền tố
    for (int i = 1; i < MAX; i++) {
        pref[i] = pref[i-1] + isPrime[i];
    }
}

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

    int T;
    cin >> T;
    while (T--) {
        int n;
        cin >> n;

        // Tính S1 = sum(deg[i]) và S2 = sum(deg[i]^2) trực tiếp
        ll S1 = 0;
        ll S2 = 0;

        for (int i = 1; i <= n; i++) {
            // deg[i] = count of primes p such that 2i < p <= n + i
            int deg = pref[n + i] - pref[2 * i];
            S1 += deg;
            S2 += (ll)deg * deg;
        }

        // D = sum(deg[i] * (n - 1 - deg[i])) = (n - 1) * S1 - S2
        ll D = (ll)(n - 1) * S1 - S2;

        ll total = (ll)n * (n - 1) * (n - 2) / 6;

        cout << total - D / 2 << '\n';
    }
    return 0;
}
  • Time Complexity: O(N log log N + T * N)
  • Space Complexity: O(N)

Đây là biến thể của phương pháp Mathematical Optimization, sử dụng công thức đại số để tính tổng D thay vì lặp cộng dồn.

D = sum(deg[i] * (n - 1 - deg[i])) = sum((n - 1) * deg[i] - deg[i]^2) = (n - 1) * sum(deg[i]) - sum(deg[i]^2) = (n - 1) * S1 - S2

Cách này slightly cleaner và đảm bảo tính toán chính xác số học.

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

Cách tiếp cận Time Space Tên
1 O(n^2) O(n) Brute Force (Quy hoạch động)
2 O(N log log N + T * N) O(N) Mathematical Optimization
3 O(N log log N + T * N) O(N) Direct Formula (Refined)

Bài học kinh nghiệm

  • Bài toán có thể quy về đếm tổng số tam giác trừ đi số tam giác không đồng nhất màu.
  • Số tam giác không đồng nhất màu có thể được đếm bằng cách xét từng đỉnh i và nhân số lượng cạnh xanh tại i với số lượng cạnh vàng tại i, rồi cộng dồn lại và chia 2.
  • Vì n lớn (10^6), ta cần tối ưu việc tính độ xanh deg[i]. deg[i] là số lượng số nguyên tố trong khoảng (2i, n + i].
  • Phép tính này cần được tối ưu bằng sàng Eratosthenes và mảng tiền tố (prefix sum).

Lỗi thường gặp

  • Làm sai phạm vi maxN. Tổng của 2 số lên tới 210^6, nên mảng sàng phải đủ lớn (210^6 + 5).
  • Sai công thức tính deg[i]. Phải là pref[n + i] - pref[2 * i] (hoặc 2*i + 1 tùy vào cách xét biên).
  • Quên chia 2 khi tính d. d là tổng của các cạnh đã đếm 2 lần.
  • Dùng số nguyên 32-bit cho phép nhân lớn. n có thể 10^6, n^3 lên tới 10^18, cần 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.