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 →

Vỏ lãi (Đề tự luyện HSG Tin quốc gia – Bài 3)

Nguồn: Đề tự luyện HSG Tin học quốc gia #06 – Bài 3 (đề tự soạn)Độ khó: KhóTô màu đồ thị hai phíaCái túi trên độ lệchGom nhóm √S giá trịBitset

Đề bài

Vỏ lãi. Thầy Sơn chở $N$ học trò qua sông bằng hai chiếc vỏ lãi; em thứ $i$ nặng $w_i$ kg, mỗi em ngồi đúng một chiếc (một chiếc có thể trống). Có $M$ ghi chú: loại $1$ "$u$ $v$" — hai em phải cùng chiếc; loại $2$ "$u$ $v$" — hai em phải khác chiếc. Tìm độ chênh lệch nhỏ nhất giữa tổng cân nặng hai chiếc khi thỏa mọi ghi chú, hoặc in $-1$ nếu không thể.

Dữ liệu vào: dòng 1: $N$, $M$; dòng 2: $w_1, \ldots, w_N$; $M$ dòng tiếp: $t$, $u$, $v$. $1 \le N \le 3 \cdot 10^5$, $0 \le M \le 3 \cdot 10^5$, $w_i \ge 1$, $\sum w_i \le 5 \cdot 10^6$.

Kết quả: độ chênh lệch nhỏ nhất, hoặc $-1$.

  • Subtask 1 (15%): $N \le 20$.
  • Subtask 2 (20%): $M = 0$, $N \le 2000$, $\sum w_i \le 2 \cdot 10^5$.
  • Subtask 3 (25%): $N \le 2000$, $\sum w_i \le 2 \cdot 10^5$.
  • Subtask 4 (40%): không có ràng buộc gì thêm.

Giới hạn thời gian: C++ 1 giây, Python/PyPy 3 giây.

Input
8 4
5 4 2 4 3 7 4 9
2 6 1
1 6 4
2 5 7
1 7 8
Output
2
Giải thích

Chiếc thứ nhất chở các em $2, 4, 5, 6$ (tổng $4 + 4 + 3 + 7 = 18$), chiếc thứ hai chở các em $1, 3, 7, 8$ (tổng $5 + 2 + 4 + 9 = 20$). Mọi ghi chú đều thỏa, độ chênh $2$; không có cách nào chênh ít hơn.

Dành cho học viênNộp bài trên ClueOJ

Bạn nào muốn thử nộp bài để hệ thống chấm xem có đúng hay không thì dùng link này nhé. Bộ test có chia subtask và có cả những test "bẫy", nên code đúng ý tưởng nhưng sai chi tiết nhỏ cũng sẽ bị trừ điểm đó.

Nộp bài Vỏ lãi

Lời mở đầu

Nếu không có ghi chú nào, đây chính là bài chia kẹo quen thuộc: chọn một nhóm em có tổng cân nặng gần $\frac{S}{2}$ nhất (với $S$ là tổng cân nặng), làm bằng quy hoạch động cái túi. Ghi chú làm bài khó hơn ở hai chỗ, và cả bài là hành trình gỡ hai chỗ đó:

  1. Ghi chú ràng buộc các em thành từng nhóm. Coi mỗi em là một đỉnh, mỗi ghi chú là một cạnh. Trong một thành phần liên thông, chỉ cần biết chiếc của một em là suy ra chiếc của tất cả các em còn lại (cạnh loại 1 giữ nguyên, cạnh loại 2 đổi chiếc). Vậy mỗi nhóm chỉ có đúng hai cách xếp, và hai cách đó đối xứng nhau: nếu nhóm chia thành hai phía nặng $A$ và $B$ thì nhóm góp vào hiệu (chiếc 1 − chiếc 2) đúng $+(A - B)$ hoặc $-(A - B)$. Nếu khi suy ra gặp mâu thuẫn (một em vừa phải ở chiếc này vừa phải ở chiếc kia) thì in $-1$. Đây là phép tô hai màu đồ thị bằng BFS.
  2. Sau khi gom nhóm, bài toán thành chọn dấu. Gọi $d_i = |A_i - B_i|$ là độ lệch của nhóm $i$, $D = \sum d_i$. Ta cần chọn dấu $\pm$ cho từng $d_i$ để $|\pm d_1 \pm d_2 \pm \ldots|$ nhỏ nhất, tức là chọn một tập các $d_i$ có tổng $T$ lớn nhất mà $T \le \frac{D}{2}$; đáp án là $D - 2T$. Lại là cái túi — nhưng có tới $3 \cdot 10^5$ nhóm và $D$ tới $5 \cdot 10^6$, nên cái túi thường $O(K \cdot D)$ chạy không nổi.
Ví dụ đề: 4 nhóm. Phía tô xanh và phía tô cam của một nhóm luôn phải ngồi khác chiếc. Nhóm {1, 4, 6} 7em 6 4em 4 5em 1 11 và 5 ⇒ d = 6 Nhóm {5, 7, 8} 4em 7 9em 8 3em 5 13 và 3 ⇒ d = 10 Nhóm {2} 4em 2 d = 4 Nhóm {3} 2em 3 d = 2 Chọn dấu cho các độ lệch: + 10 − 6 − 4 + 2 = 2 ⇔ chọn tập {6, 4} có tổng 10 ≤ D/2 = 11 lớn nhất ⇒ đáp án D − 2·10 = 22 − 20 = 2

Hình 1. Mỗi nhóm có đúng hai cách xếp (đổi chỗ phía xanh và phía cam), nên chỉ còn lại độ lệch $d$ của nó là quan trọng.

Nút thắt cuối cùng nằm ở một quan sát nhỏ mà rất "đắt": vì $\sum d_i \le \sum w_i \le 5 \cdot 10^6$, số giá trị khác nhau của $d$ không quá khoảng $\sqrt{2 \cdot 5 \cdot 10^6} \approx 3162$ (các số khác nhau $1 + 2 + \ldots + k$ đã bằng $\frac{k(k+1)}{2}$). Gom các nhóm cùng độ lệch lại, tách số lượng theo lũy thừa của $2$, rồi làm cái túi bằng bitset — vậy là xong.

Cách 1Thử mọi cách xếp – O(2^N · M) · 15%

Với $N \le 20$, ta thử cả $2^N$ cách xếp bằng đệ quy quay lui: em thứ $i$ ngồi chiếc 1 hoặc chiếc 2. Khi đã xếp đủ $N$ em, kiểm tra từng ghi chú; nếu thỏa hết thì cập nhật độ chênh $|S - 2s|$ với $s$ là tổng chiếc 1. Không cách nào thỏa thì đáp án giữ nguyên $-1$.

#include <bits/stdc++.h>
using namespace std;
int n, m;
vector<long long> w;
vector<int> t, u, v, boat;
long long tot, best = -1;
void tryAll(int i, long long s) {          // s = tong can nang chiec thu nhat
    if (i == n) {
        for (int j = 0; j < m; j++)
            if ((t[j] == 1) != (boat[u[j]] == boat[v[j]])) return;
        long long d = llabs(tot - 2 * s);
        if (best == -1 || d < best) best = d;
        return;
    }
    boat[i] = 0; tryAll(i + 1, s + w[i]);
    boat[i] = 1; tryAll(i + 1, s);
}
int main() {
    cin >> n >> m;
    w.resize(n); boat.resize(n);
    for (auto &x : w) { cin >> x; tot += x; }
    t.resize(m); u.resize(m); v.resize(m);
    for (int j = 0; j < m; j++) { cin >> t[j] >> u[j] >> v[j]; u[j]--; v[j]--; }
    tryAll(0, 0);
    cout << best << "\n";
}
import sys
sys.setrecursionlimit(10000)


def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0]); m = int(data[1])
    w = [int(x) for x in data[2:2 + n]]
    cons = [(int(data[2 + n + 3 * j]), int(data[3 + n + 3 * j]) - 1, int(data[4 + n + 3 * j]) - 1) for j in range(m)]
    tot = sum(w)
    boat = [0] * n
    best = [-1]

    def try_all(i, s):                     # s = tong can nang chiec thu nhat
        if i == n:
            for t, u, v in cons:
                if (t == 1) != (boat[u] == boat[v]):
                    return
            d = abs(tot - 2 * s)
            if best[0] == -1 or d < best[0]:
                best[0] = d
            return
        boat[i] = 0; try_all(i + 1, s + w[i])
        boat[i] = 1; try_all(i + 1, s)

    try_all(0, 0)
    print(best[0])


main()
Không gian$O(N + M)$
Thời gian$O(2^N \cdot M)$

Cách 2Không có ghi chú: cái túi 0/1 – O(N · S) · 35%

Khi $M = 0$, ta chỉ cần biết những tổng $s$ nào "ghép được" từ một nhóm em: $f[s] = 1$ nếu có nhóm em nặng đúng $s$. Ban đầu $f[0] = 1$; thêm em nặng $w$ thì với $s$ giảm dần từ $\frac{S}{2}$ về $w$: nếu $f[s - w] = 1$ thì $f[s] = 1$ (duyệt giảm để mỗi em chỉ được dùng một lần). Cuối cùng lấy $T$ lớn nhất có $f[T] = 1$, đáp án $S - 2T$.

Với ví dụ không có ghi chú (cùng 8 cân nặng, $S = 38$), ta ghép được $19$ nên đáp án là $0$ — chính ghi chú trong đề đã đẩy đáp án lên $2$. Code dưới đây gọi lại Cách 1 khi $N \le 20$ để ăn trọn cả Subtask 1.

#include <bits/stdc++.h>
using namespace std;
int main() {
    int n, m; cin >> n >> m;
    vector<int> w(n), t(m), u(m), v(m);
    long long tot = 0;
    for (auto &x : w) { cin >> x; tot += x; }
    for (int j = 0; j < m; j++) { cin >> t[j] >> u[j] >> v[j]; u[j]--; v[j]--; }
    if (n <= 20) {                         // Cach 1: thu het 2^n cach xep (bit i = chiec cua em i)
        long long best = -1;
        for (long long mask = 0; mask < (1LL << n); mask++) {
            bool ok = true;
            for (int j = 0; j < m && ok; j++)
                if ((t[j] == 1) != ((mask >> u[j] & 1) == (mask >> v[j] & 1))) ok = false;
            if (!ok) continue;
            long long s = 0;
            for (int i = 0; i < n; i++) if (mask >> i & 1) s += w[i];
            if (best == -1 || llabs(tot - 2 * s) < best) best = llabs(tot - 2 * s);
        }
        cout << best << "\n";
        return 0;
    }
    int H = tot / 2;                       // Cach 2 (m = 0): cai tui 0/1 tren can nang
    vector<char> f(H + 1, 0);              // f[s] = 1 neu chon duoc nhom em co tong s
    f[0] = 1;
    for (int i = 0; i < n; i++)
        for (int s = H; s >= w[i]; s--)
            if (f[s - w[i]]) f[s] = 1;
    int best = H;
    while (!f[best]) best--;
    cout << tot - 2 * best << "\n";
}
import sys


def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0]); m = int(data[1])
    w = [int(x) for x in data[2:2 + n]]
    tot = sum(w)
    if n <= 20:                            # Cach 1: thu het 2^n cach xep (bit i = chiec cua em i)
        cons = [(int(data[2 + n + 3 * j]), int(data[3 + n + 3 * j]) - 1, int(data[4 + n + 3 * j]) - 1) for j in range(m)]
        best = -1
        for mask in range(1 << n):
            ok = True
            for t, u, v in cons:
                if (t == 1) != ((mask >> u & 1) == (mask >> v & 1)):
                    ok = False
                    break
            if ok:
                s = 0
                for i in range(n):
                    if mask >> i & 1:
                        s += w[i]
                if best == -1 or abs(tot - 2 * s) < best:
                    best = abs(tot - 2 * s)
        print(best)
        return
    H = tot // 2                           # Cach 2 (m = 0): cai tui 0/1 tren can nang
    mask = (1 << (H + 1)) - 1
    f = 1                                  # bit s = 1 neu chon duoc nhom em co tong s
    for x in w:
        f = (f | (f << x)) & mask
    best = f.bit_length() - 1
    print(tot - 2 * best)


main()
Python: "bitset" có sẵn
Trong Python, số nguyên lớn chính là một dãy bit dài vô hạn: bit thứ $s$ của f bằng $1$ nếu ghép được tổng $s$. Thêm một em nặng $w$ chỉ là f = f | (f << w), rồi & mask để cắt bỏ các tổng vượt $\frac{S}{2}$. Phép dịch và phép OR trên số lớn chạy trong C nên nhanh hơn vòng lặp Python hàng chục lần.
Không gian$O(S)$
Thời gian$O(N \cdot S)$

Cách 3Tô hai màu từng nhóm, rồi cái túi trên độ lệch – O(N + M + K · D) · 60%

Như phần mở đầu: duyệt BFS từ một em chưa thăm $s$, cho em đó màu $0$. Đi theo cạnh $(u, v)$ loại $t$: màu của $v$ phải là col[u] ^ (t - 1) (loại 1 giữ màu, loại 2 đổi màu). Gặp đỉnh đã tô mà màu khác yêu cầu ⇒ mâu thuẫn, in $-1$. Trong lúc BFS cộng cân nặng vào side[0] hoặc side[1]; xong một nhóm thì $d = |side_0 - side_1|$. Các em không có ghi chú nào là nhóm một người, $d = w$.

Sau đó chạy đúng cái túi của Cách 2 trên dãy $d_1, d_2, \ldots, d_K$ ($K$ là số nhóm) với $D = \sum d_i$ thay cho $S$. Mô phỏng dưới đây cho em xem phần tô màu: ô xanh là màu 0, ô cam là màu 1.

Ví dụ nhanh:
#include <bits/stdc++.h>
using namespace std;
int main() {
    int n, m; cin >> n >> m;
    vector<long long> w(n + 1);
    for (int i = 1; i <= n; i++) cin >> w[i];
    vector<vector<pair<int, int>>> adj(n + 1);
    for (int j = 0; j < m; j++) {
        int t, u, v; cin >> t >> u >> v;
        adj[u].push_back({v, t - 1});
        adj[v].push_back({u, t - 1});
    }
    vector<int> col(n + 1, -1), q, ds;
    long long D = 0;
    for (int s = 1; s <= n; s++) {
        if (col[s] != -1) continue;
        long long side[2] = {0, 0};
        q.assign(1, s); col[s] = 0;
        for (int h = 0; h < (int)q.size(); h++) {
            int u = q[h]; side[col[u]] += w[u];
            for (auto [v, p] : adj[u]) {
                int c = col[u] ^ p;
                if (col[v] == -1) { col[v] = c; q.push_back(v); }
                else if (col[v] != c) { cout << -1 << "\n"; return 0; }
            }
        }
        int d = llabs(side[0] - side[1]);
        ds.push_back(d); D += d;
    }
    int H = D / 2;
    vector<char> f(H + 1, 0);              // cai tui 0/1 tren cac do lech d
    f[0] = 1;
    for (int d : ds)
        for (int s = H; s >= d; s--)
            if (f[s - d]) f[s] = 1;
    int best = H;
    while (!f[best]) best--;
    cout << D - 2 * best << "\n";
}
import sys
from collections import deque


def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0]); m = int(data[1])
    w = [0] + [int(x) for x in data[2:2 + n]]
    adj = [[] for _ in range(n + 1)]
    for j in range(m):
        t = int(data[2 + n + 3 * j]); u = int(data[3 + n + 3 * j]); v = int(data[4 + n + 3 * j])
        adj[u].append((v, t - 1)); adj[v].append((u, t - 1))
    col = [-1] * (n + 1)
    ds = []
    for s in range(1, n + 1):
        if col[s] != -1:
            continue
        side = [0, 0]
        col[s] = 0; q = deque([s])
        while q:
            u = q.popleft(); side[col[u]] += w[u]
            for v, p in adj[u]:
                c = col[u] ^ p
                if col[v] == -1:
                    col[v] = c; q.append(v)
                elif col[v] != c:
                    print(-1)
                    return
        ds.append(abs(side[0] - side[1]))
    D = sum(ds); H = D // 2
    mask = (1 << (H + 1)) - 1
    f = 1                                  # cai tui 0/1 tren cac do lech d (bit s)
    for d in ds:
        f = (f | (f << d)) & mask
    best = f.bit_length() - 1
    print(D - 2 * best)


main()
Không gian$O(N + M + D)$
Thời gian$O(N + M + K \cdot D)$
Tham lam không ăn thua
Nhiều bạn sẽ thử: xếp nhóm có $d$ lớn trước, đặt phía nặng vào chiếc đang nhẹ hơn. Ví dụ trong đề tình cờ cho cách này ra đúng $2$, nhưng với độ lệch $\{3, 3, 2, 2, 2\}$ tham lam ra $2$ (đặt $3$ | $3$, rồi $2$ | $2$, rồi $2$) trong khi chia $3 + 3 = 2 + 2 + 2$ được $0$. Trên bộ test, cách này chỉ được 29,25 điểm.

Cách 4Gom √S giá trị khác nhau, tách lũy thừa 2, bitset – O(N + M + D·√D/64) · 100%

Ở Subtask 4 có thể có $3 \cdot 10^5$ nhóm và $D$ tới $5 \cdot 10^6$: cái túi $O(K \cdot D)$ cần cỡ $10^{12}$ phép, ngay cả bitset (nhanh gấp 64 lần) cũng còn $2 \cdot 10^{10}$. Hai nhận xét cứu ta:

  1. Có ít giá trị khác nhau. Nếu có $k$ giá trị $d$ khác nhau (và dương) thì $D \ge 1 + 2 + \ldots + k = \frac{k(k+1)}{2}$, nên $k \le \sqrt{2D} \approx 3162$. Đếm cnt[v] = số nhóm có độ lệch $v$. (Nhóm có $d = 0$ thì bỏ qua — đặt kiểu nào cũng như nhau.)
  2. Tách số lượng theo lũy thừa 2. $c$ món cùng giá trị $v$ (được chọn $0..c$ món) tương đương với các "gói" $1, 2, 4, \ldots, 2^{j}$ và một gói phần dư $r = c - (2^{j+1} - 1)$: mọi số từ $0$ tới $c$ đều viết được thành tổng một số gói, và không tổng nào vượt $c$. Vậy giá trị $v$ chỉ cần khoảng $\log_2 c$ món "gói" $take \cdot v$ chứ không phải $c$ món.

Tổng số gói chỉ cỡ vài nghìn, mỗi gói là một lần dp |= dp << (take * v) trên bitset $2{,}5 \cdot 10^6$ bit (khoảng $4 \cdot 10^4$ phép trên số 64 bit) — tổng cộng cỡ $10^8$ phép, chạy dưới $0{,}3$ giây. Mô phỏng sau cho em xem cách gom, tách gói và các tổng ghép được (ô xanh) sau mỗi gói:

Ví dụ nhanh:

Hai chi tiết nhỏ trong code: chỉ cần duyệt $v \le H = \lfloor D/2 \rfloor$ (một độ lệch lớn hơn $H$ không bao giờ nằm trong tập có tổng $\le H$), và đáp án là $D - 2T$ với $T$ là bit $1$ cao nhất không vượt $H$.

#include <bits/stdc++.h>
using namespace std;
const int HMAX = 2500000;          // D/2 <= (tong w)/2 <= 2,5 trieu
bitset<HMAX + 1> dp;
int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, m; cin >> n >> m;
    vector<long long> w(n + 1);
    for (int i = 1; i <= n; i++) cin >> w[i];
    vector<vector<pair<int, int>>> adj(n + 1);
    for (int j = 0; j < m; j++) {
        int t, u, v; cin >> t >> u >> v;
        adj[u].push_back({v, t - 1});  // 0: cung xuong, 1: khac xuong
        adj[v].push_back({u, t - 1});
    }
    vector<int> col(n + 1, -1), q;
    vector<long long> ds; long long D = 0;
    for (int s = 1; s <= n; s++) {
        if (col[s] != -1) continue;
        long long side[2] = {0, 0};
        q.assign(1, s); col[s] = 0;
        for (int h = 0; h < (int)q.size(); h++) {
            int u = q[h]; side[col[u]] += w[u];
            for (auto [v, p] : adj[u]) {
                int c = col[u] ^ p;
                if (col[v] == -1) { col[v] = c; q.push_back(v); }
                else if (col[v] != c) { cout << -1 << "\n"; return 0; }
            }
        }
        long long d = llabs(side[0] - side[1]);
        ds.push_back(d); D += d;
    }
    vector<int> cnt(D + 1, 0);
    for (long long d : ds) cnt[d]++;
    long long H = D / 2;
    dp[0] = 1;
    for (long long v = 1; v <= H; v++) {
        for (long long k = 1, c = cnt[v]; c > 0; k *= 2) {
            long long take = min(k, c);   // tach c = 1 + 2 + 4 + ... + phan du
            dp |= dp << (take * v);
            c -= take;
        }
    }
    long long best = H;
    while (!dp[best]) best--;
    cout << D - 2 * best << "\n";
    return 0;
}
import sys


def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0]); m = int(data[1])
    w = [0] * (n + 1)
    for i in range(1, n + 1):
        w[i] = int(data[1 + i])
    # danh sach ke dang mang phang (CSR)
    deg = [0] * (n + 2)
    T = data[2 + n::3]; U = data[3 + n::3]; V = data[4 + n::3]
    eu = list(map(int, U)); ev = list(map(int, V)); et = list(map(int, T))
    for j in range(m):
        deg[eu[j]] += 1; deg[ev[j]] += 1
    st = [0] * (n + 2)
    for i in range(1, n + 1):
        st[i + 1] = st[i] + deg[i]
    pos = st[:]
    to = [0] * (2 * m); par = [0] * (2 * m)
    for j in range(m):
        u = eu[j]; v = ev[j]; p = et[j] - 1
        k = pos[u]; to[k] = v; par[k] = p; pos[u] = k + 1
        k = pos[v]; to[k] = u; par[k] = p; pos[v] = k + 1
    col = [-1] * (n + 1)
    ds = []
    D = 0
    for s in range(1, n + 1):
        if col[s] != -1:
            continue
        side0 = 0; side1 = 0
        q = [s]; col[s] = 0; h = 0
        while h < len(q):
            u = q[h]; h += 1
            cu = col[u]
            if cu:
                side1 += w[u]
            else:
                side0 += w[u]
            for k in range(st[u], st[u + 1]):
                v = to[k]; c = cu ^ par[k]
                if col[v] == -1:
                    col[v] = c; q.append(v)
                elif col[v] != c:
                    sys.stdout.write("-1\n")
                    return
        d = side0 - side1
        if d < 0:
            d = -d
        ds.append(d); D += d
    H = D // 2
    cnt = {}
    for d in ds:
        if 0 < d <= H:
            cnt[d] = cnt.get(d, 0) + 1
    mask = (1 << (H + 1)) - 1
    dp = 1
    for v in sorted(cnt):
        c = cnt[v]; k = 1
        while c > 0:
            take = k if k < c else c   # tach c = 1 + 2 + 4 + ... + phan du
            dp = (dp | (dp << (take * v))) & mask
            c -= take; k *= 2
    best = dp.bit_length() - 1
    sys.stdout.write(str(D - 2 * best) + "\n")


main()
Không gian$O(N + M + D)$
Thời gian$O(N + M + \frac{D \sqrt{D}}{64})$
Đừng quên phần dư khi tách gói
Viết vòng lặp kiểu for (k = 1; k <= c; k *= 2) { ...; c -= k; } sẽ bỏ sót phần dư: với $c = 5$ chỉ tạo hai gói $1, 2$ (dừng vì $4 > 2$), mất $2$ món. Lỗi này hiếm khi lộ, vì thường chỉ cần dùng khoảng một nửa số món — nhưng bộ test có những test "khe hở lớn" (ba nhóm rất nặng cùng rất nhiều nhóm độ lệch nhỏ) buộc phải dùng hết các món nhỏ.
C++▶ Chạy tay từng dòng codeTô màu từng nhóm bằng BFS, gom độ lệch, rồi cái túi bằng bitset.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3const int HMAX = 2500000; // D/2 <= (tong w)/2 <= 2,5 trieu
4bitset<HMAX + 1> dp;
5int main() {
6 ios::sync_with_stdio(false); cin.tie(nullptr);
7 int n, m; cin >> n >> m;
8 vector<long long> w(n + 1);
9 for (int i = 1; i <= n; i++) cin >> w[i];
10 vector<vector<pair<int, int>>> adj(n + 1);
11 for (int j = 0; j < m; j++) {
12 int t, u, v; cin >> t >> u >> v;
13 adj[u].push_back({v, t - 1}); // 0: cung xuong, 1: khac xuong
14 adj[v].push_back({u, t - 1});
15 }
16 vector<int> col(n + 1, -1), q;
17 vector<long long> ds; long long D = 0;
18 for (int s = 1; s <= n; s++) {
19 if (col[s] != -1) continue;
20 long long side[2] = {0, 0};
21 q.assign(1, s); col[s] = 0;
22 for (int h = 0; h < (int)q.size(); h++) {
23 int u = q[h]; side[col[u]] += w[u];
24 for (auto [v, p] : adj[u]) {
25 int c = col[u] ^ p;
26 if (col[v] == -1) { col[v] = c; q.push_back(v); }
27 else if (col[v] != c) { cout << -1 << "\n"; return 0; }
28 }
29 }
30 long long d = llabs(side[0] - side[1]);
31 ds.push_back(d); D += d;
32 }
33 vector<int> cnt(D + 1, 0);
34 for (long long d : ds) cnt[d]++;
35 long long H = D / 2;
36 dp[0] = 1;
37 for (long long v = 1; v <= H; v++) {
38 for (long long k = 1, c = cnt[v]; c > 0; k *= 2) {
39 long long take = min(k, c); // tach c = 1 + 2 + 4 + ... + phan du
40 dp |= dp << (take * v);
41 c -= take;
42 }
43 }
44 long long best = H;
45 while (!dp[best]) best--;
46 cout << D - 2 * best << "\n";
47 return 0;
48}

Bộ test chấm "bắt" những lỗi nào?

Bộ test có 36 test (8 – 8 – 10 – 10 theo subtask). Các test lớn có $N = 3 \cdot 10^5$, $\sum w_i$ tới $5 \cdot 10^6$ và từ khoảng $10^5$ tới $3 \cdot 10^5$ nhóm có độ lệch khác $0$. Để không ai "đoán" được đáp án (với rất nhiều món nhỏ, chia kẹo ngẫu nhiên gần như luôn ra $0$ hoặc $1$), hầu hết các test có cấu trúc: đa số độ lệch là bội của một số $g$, cộng vài em "đặc biệt" được chọn bằng máy sao cho tham lam và các cách đoán đều sai — đáp án vì thế rất đa dạng ($2$, $7$, $22$, $1101$, $50000$, $800001$…). Có các test "khe hở lớn" buộc phải dùng hết các nhóm nhỏ, và test $-1$ (mâu thuẫn nằm sâu trong một nhóm lớn).

Cách làmSub 1Sub 2Sub 3Sub 4Điểm
Cách 1 – thử 2^N cách8/8TLETLERE/TLE15
Cách 2 – M = 0: cái túi 0/18/88/8WATLE35
Cách 3 – tô màu + cái túi trên d8/88/810/10TLE60
Cách 4 – gom √S giá trị + bitset (lời giải đầy đủ)8/88/810/1010/10100
Chưa đủ: bitset từng nhóm, không gom8/88/810/10TLE60
Sai: quên phần dư khi tách lũy thừa 27/87/89/108/1085,12
Sai: không kiểm tra mâu thuẫn6/88/89/1010/1093,75
Sai: tham lam "d lớn trước, đặt vào chiếc nhẹ"6/82/82/102/1029,25
Sai: đoán "D mod 2" (hoặc hiệu nếu một nhóm áp đảo)4/80/81/100/1010

Để ý: ví dụ trong đề được chọn sao cho tham lam, quên phần dư và quên kiểm tra mâu thuẫn đều vẫn in đúng 2 — đừng tin một lời giải chỉ vì nó qua ví dụ nhé.

Ghi chú của mình: các cách C++ ở trên đã được chấm thử với đúng bộ test của bài (đo thời gian CPU): lời giải chuẩn chạy tối đa khoảng 0,2 giây (giới hạn 1 giây); các ô TLE đều chạy quá 3 giây, ô RE của Cách 1 là tràn ngăn xếp đệ quy. Lời giải chuẩn được đối chiếu với chương trình vét cạn trên 600 test nhỏ, không lệch test nào; bản Python cho kết quả trùng khớp trên mọi test và chỉ tốn khoảng 0,8 giây CPU ngay cả trên CPython.

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