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 →

Tour xuôi dòng (Đề tự luyện HSG Tin 12 – Bài 3)

Nguồn: Đề tự luyện HSG Tin học 12 – Bài 3 (đề tự soạn)Độ khó: KháSắp xếp tô pôQuy hoạch động trên DAGĐường quétHàng đợi ưu tiên

Đề bài

Tour xuôi dòng. Có $N$ bến và $M$ kênh một chiều $u \to v$, không có chu trình. Bến $i$ có độ hấp dẫn $b_i$. Một tour là một đường đi theo các kênh (có thể chỉ gồm một bến), độ hấp dẫn là tổng $b$ của các bến trên đường. Với mỗi bến $x = 1..N$: nếu bến $x$ đóng cửa (tour không được đi qua $x$), độ hấp dẫn lớn nhất của một tour là bao nhiêu? (Không còn tour nào thì đáp án là 0.)

Dữ liệu vào: dòng đầu $N, M$ ($1 \le N \le 2 \cdot 10^5$, $0 \le M \le 2 \cdot 10^5$); dòng hai $b_1, \ldots, b_N$ ($1 \le b_i \le 10^9$); $M$ dòng sau mỗi dòng một kênh $u\ v$ (không trùng kênh, không có chu trình).

Kết quả: một dòng $N$ số, số thứ $x$ là đáp án khi bến $x$ đóng cửa.

  • Subtask 1 (30%): $N, M \le 2000$.
  • Subtask 2 (30%): mọi kênh có $u < v \le u + 20$.
  • Subtask 3 (40%): không có ràng buộc gì thêm.
Input
7 10
9 7 8 4 5 1 2
3 7
6 7
2 6
1 7
2 7
3 5
4 5
4 6
3 4
2 3
Output
24 17 11 20 22 24 24
Giải thích

Tour tốt nhất khi đóng lần lượt bến 1..7: 2→3→4→5 (24); 3→4→5 (17); 1→7 (11); 2→3→5 (20); 2→3→4→6→7 (22); 2→3→4→5 (24); 2→3→4→5 (24).

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 Tour xuôi dòng

Lời mở đầu

Mạng kênh là một đồ thị có hướng không chu trình (DAG), tour là một đường đi. Nếu không bến nào đóng cửa, đây là bài quen thuộc "đường đi có tổng trọng số lớn nhất trên DAG": sắp xếp tô pô rồi quy hoạch động. Cái mới là phải trả lời cho từng bến bị đóng – $N$ câu hỏi, mỗi câu là một đồ thị khác nhau.

Bước 1 – thứ tự tô pô. Đề không hứa kênh luôn đi từ số nhỏ tới số lớn (ví dụ của đề tình cờ như vậy, nhưng test thật thì không). Ta dùng thuật toán Kahn: lần lượt lấy ra các bến không còn kênh nào đi vào. Được dãy ord[0..N-1] mà mọi kênh đều đi từ vị trí nhỏ sang vị trí lớn; pos[v] là vị trí của bến $v$.

Bước 2 – hai mảng QHĐ.

  • $f[v]$ = độ hấp dẫn lớn nhất của tour kết thúc ở $v$: $f[v] = b_v + \max(0, \max_{u \to v} f[u])$, tính theo thứ tự ord xuôi.
  • $g[v]$ = độ hấp dẫn lớn nhất của tour bắt đầu ở $v$: $g[v] = b_v + \max(0, \max_{v \to w} g[w])$, tính theo thứ tự ngược.

Với ví dụ, thứ tự tô pô là 1, 2, 3, 4, 5, 6, 7 và

bến1234567
b9784512
f971519242022
g1124179532

Bước 3 – nhận xét chìa khóa. Đóng bến $x$ ở vị trí $h$. Đi dọc một tour thì vị trí tăng dần, nên một tour không qua $x$ chỉ có thể là một trong ba loại:

  1. nằm hẳn bên trái (mọi bến ở vị trí $< h$): tốt nhất là $\text{pre}[h] = \max\{f[\text{ord}[i]] : i < h\}$ – vì $f$ của các bến bên trái chỉ dùng các bến còn ở bên trái hơn, không bao giờ đụng tới $x$;
  2. nằm hẳn bên phải: tốt nhất là $\text{suf}[h+1] = \max\{g[\text{ord}[i]] : i > h\}$;
  3. có bến ở cả hai bên: khi đó tour phải đi qua đúng một kênh $u \to w$ với $\text{pos}[u] < h < \text{pos}[w]$ – mình gọi là kênh "bắc qua" vị trí $h$. Tour tốt nhất dùng kênh đó là $f[u] + g[w]$.
Các bến xếp theo thứ tự tô pô (mọi kênh đi từ trái sang phải). Bến 4 (vị trí 3) đóng cửa:1710119201vị trí 02vị trí 13vị trí 24vị trí 35vị trí 46vị trí 57vị trí 6Tour không qua bến 4: nằm hẳn bên trái (tốt nhất 15: 2 → 3), nằm hẳn bên phải (tốt nhất 5: bến 5),hoặc đi qua đúng một kênh bắc qua vị trí 3; số trên kênh u → w là f[u] + g[w].Tốt nhất: kênh 3 → 5 cho 15 + 5 = 20 = đáp án khi bến 4 đóng cửa.

Hình 1. Ví dụ của đề, bến 4 đóng cửa. Năm kênh bắc qua vị trí 3 (màu xanh), kênh 3 → 5 cho tour tốt nhất 2 → 3 → 5.

Vậy $$\text{ans}[\text{ord}[h]] = \max\Big(\text{pre}[h],\ \text{suf}[h+1],\ \max_{\text{pos}[u] < h < \text{pos}[w]} \big(f[u] + g[w]\big)\Big).$$ Chú ý dấu nhỏ hơn hẳn ở cả hai đầu: kênh đi vào hoặc đi ra từ chính bến $x$ thì không dùng được. Tổng độ hấp dẫn có thể tới $2 \cdot 10^5 \cdot 10^9$, phải dùng long long.

Cách 1Đóng từng bến, QHĐ lại từ đầu – O(N·(N + M)) · 30%

Chưa cần nhận xét ở bước 3: với mỗi $x$, chạy lại QHĐ $f$ theo thứ tự tô pô nhưng bỏ qua bến $x$ (cho $f[x] = 0$ để không tour nào nối tiếp qua $x$), đáp án là $\max f$. Mỗi lần $O(N + M)$, tổng $O(N \cdot (N + M))$ – khoảng $8 \cdot 10^6$ khi $N, M \le 2000$.

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, m;
    cin >> n >> m;
    vector<ll> b(n + 1);
    for (int i = 1; i <= n; i++) cin >> b[i];
    vector<vector<int>> ra(n + 1), vao(n + 1);
    vector<int> deg(n + 1, 0);
    for (int j = 0; j < m; j++) {
        int u, v; cin >> u >> v;
        ra[u].push_back(v); vao[v].push_back(u); deg[v]++;
    }
    vector<int> ord;                         // thu tu topo (Kahn)
    for (int v = 1; v <= n; v++) if (deg[v] == 0) ord.push_back(v);
    for (int h = 0; h < (int)ord.size(); h++)
        for (int w : ra[ord[h]]) if (--deg[w] == 0) ord.push_back(w);
    vector<ll> f(n + 1);
    for (int x = 1; x <= n; x++) {           // dong ben x roi QHD lai tu dau
        ll best = 0;
        for (int v : ord) {
            if (v == x) { f[v] = 0; continue; }  // f[x] = 0: khong ai noi tiep qua x
            f[v] = 0;
            for (int u : vao[v]) f[v] = max(f[v], f[u]);
            f[v] += b[v];
            best = max(best, f[v]);
        }
        cout << best << (x < n ? ' ' : '\n');
    }
    return 0;
}
import sys


def main():
    data = sys.stdin.buffer.read().split()
    n, m = int(data[0]), int(data[1])
    b = [0] + list(map(int, data[2:2 + n]))
    ra = [[] for _ in range(n + 1)]
    vao = [[] for _ in range(n + 1)]
    deg = [0] * (n + 1)
    p = 2 + n
    for _ in range(m):
        u = int(data[p]); v = int(data[p + 1]); p += 2
        ra[u].append(v); vao[v].append(u); deg[v] += 1
    ord_ = [v for v in range(1, n + 1) if deg[v] == 0]   # thu tu topo (Kahn)
    h = 0
    while h < len(ord_):
        for w in ra[ord_[h]]:
            deg[w] -= 1
            if deg[w] == 0:
                ord_.append(w)
        h += 1
    f = [0] * (n + 1)
    out = []
    for x in range(1, n + 1):          # dong ben x roi QHD lai tu dau
        best = 0
        for v in ord_:
            if v == x:
                f[v] = 0               # f[x] = 0: khong ai noi tiep qua x
                continue
            mx = 0
            for u in vao[v]:
                if f[u] > mx:
                    mx = f[u]
            f[v] = mx + b[v]
            if f[v] > best:
                best = f[v]
        out.append(best)
    sys.stdout.write(" ".join(map(str, out)) + "\n")


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

Cách 2Cập nhật từng vị trí bị bắc qua – O(N + M + tổng độ dài kênh) · 60%

Dùng công thức ở bước 3. Phần $\text{pre}$, $\text{suf}$ tính trong $O(N)$. Phần kênh bắc qua: với mỗi kênh $u \to w$, duyệt mọi vị trí $h$ từ $\text{pos}[u] + 1$ tới $\text{pos}[w] - 1$ và cập nhật qua[h] = max(qua[h], f[u] + g[w]).

Ở subtask 2, nếu Kahn luôn lấy bến có số hiệu nhỏ nhất trước (dùng priority_queue thay cho hàng đợi thường) thì thứ tự tô pô chính là $1, 2, \ldots, N$ (vì mọi kênh $u < v$), nên mỗi kênh bắc qua tối đa 19 vị trí – rất nhanh. Nhưng ở subtask 3, một kênh có thể bắc qua hàng trăm nghìn vị trí và tổng có thể lên tới $M \cdot N \approx 4 \cdot 10^{10}$.

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, m;
    cin >> n >> m;
    vector<ll> b(n + 1);
    for (int i = 1; i <= n; i++) cin >> b[i];
    vector<int> eu(m), ev(m), deg(n + 1, 0);
    vector<vector<int>> ra(n + 1), vao(n + 1);
    for (int j = 0; j < m; j++) {
        cin >> eu[j] >> ev[j];
        ra[eu[j]].push_back(ev[j]); vao[ev[j]].push_back(eu[j]); deg[ev[j]]++;
    }
    // Kahn, luon lay ben co so hieu nho nhat: o subtask 2 thu tu nay chinh la 1, 2, ..., n
    priority_queue<int, vector<int>, greater<int>> q;
    for (int v = 1; v <= n; v++) if (deg[v] == 0) q.push(v);
    vector<int> ord, pos(n + 1);
    while (!q.empty()) {
        int u = q.top(); q.pop();
        pos[u] = ord.size(); ord.push_back(u);
        for (int w : ra[u]) if (--deg[w] == 0) q.push(w);
    }
    vector<ll> f(n + 1, 0), g(n + 1, 0);
    for (int h = 0; h < n; h++) {
        int v = ord[h];
        for (int u : vao[v]) f[v] = max(f[v], f[u]);
        f[v] += b[v];
    }
    for (int h = n - 1; h >= 0; h--) {
        int v = ord[h];
        for (int w : ra[v]) g[v] = max(g[v], g[w]);
        g[v] += b[v];
    }
    vector<ll> pre(n + 1, 0), suf(n + 1, 0), qua(n, 0);
    for (int h = 0; h < n; h++) pre[h + 1] = max(pre[h], f[ord[h]]);
    for (int h = n - 1; h >= 0; h--) suf[h] = max(suf[h + 1], g[ord[h]]);
    for (int j = 0; j < m; j++)              // kenh u->w bac qua tung vi tri nam giua
        for (int h = pos[eu[j]] + 1; h < pos[ev[j]]; h++)
            qua[h] = max(qua[h], f[eu[j]] + g[ev[j]]);
    vector<ll> ans(n + 1);
    for (int h = 0; h < n; h++) ans[ord[h]] = max({pre[h], suf[h + 1], qua[h]});
    for (int v = 1; v <= n; v++) cout << ans[v] << (v < n ? ' ' : '\n');
    return 0;
}
import sys
from heapq import heappush, heappop


def main():
    data = sys.stdin.buffer.read().split()
    n, m = int(data[0]), int(data[1])
    b = [0] + list(map(int, data[2:2 + n]))
    eu = list(map(int, data[2 + n:2 + n + 2 * m:2]))
    ev = list(map(int, data[3 + n:3 + n + 2 * m:2]))
    ra = [[] for _ in range(n + 1)]
    vao = [[] for _ in range(n + 1)]
    deg = [0] * (n + 1)
    for j in range(m):
        ra[eu[j]].append(ev[j]); vao[ev[j]].append(eu[j]); deg[ev[j]] += 1
    # Kahn, luon lay ben co so hieu nho nhat: o subtask 2 thu tu nay chinh la 1, 2, ..., n
    q = [v for v in range(1, n + 1) if deg[v] == 0]
    ord_ = []
    pos = [0] * (n + 1)
    while q:
        u = heappop(q)
        pos[u] = len(ord_); ord_.append(u)
        for w in ra[u]:
            deg[w] -= 1
            if deg[w] == 0:
                heappush(q, w)
    f = [0] * (n + 1)
    g = [0] * (n + 1)
    for v in ord_:
        mx = 0
        for u in vao[v]:
            if f[u] > mx:
                mx = f[u]
        f[v] = mx + b[v]
    for v in reversed(ord_):
        mx = 0
        for w in ra[v]:
            if g[w] > mx:
                mx = g[w]
        g[v] = mx + b[v]
    pre = [0] * (n + 1)
    suf = [0] * (n + 1)
    for h in range(n):
        pre[h + 1] = max(pre[h], f[ord_[h]])
    for h in range(n - 1, -1, -1):
        suf[h] = max(suf[h + 1], g[ord_[h]])
    qua = [0] * n
    for j in range(m):                 # kenh u->w bac qua tung vi tri nam giua
        val = f[eu[j]] + g[ev[j]]
        for h in range(pos[eu[j]] + 1, pos[ev[j]]):
            if val > qua[h]:
                qua[h] = val
    ans = [0] * (n + 1)
    for h in range(n):
        ans[ord_[h]] = max(pre[h], suf[h + 1], qua[h])
    sys.stdout.write(" ".join(map(str, ans[1:])) + "\n")


main()
Không gian$O(N + M)$
Thời gian$O((N + M)\log N + \sum (\text{pos}[w] - \text{pos}[u]))$

Cách 3Quét + hàng đợi ưu tiên – O((N + M) log M) · 100%

Mỗi kênh $u \to w$ "phủ" một đoạn vị trí liên tiếp $(\text{pos}[u], \text{pos}[w])$ với giá trị $f[u] + g[w]$; tại mỗi vị trí $h$ ta cần giá trị lớn nhất trong các đoạn đang phủ $h$. Đi $h$ từ trái sang phải (đường quét):

  • tới $h = \text{pos}[u] + 1$ thì kênh bắt đầu phủ: đẩy cặp (giá trị, $\text{pos}[w]$) vào một priority_queue lấy giá trị lớn nhất (mảng bd[h] chứa các kênh bắt đầu phủ tại $h$);
  • trước khi đọc đỉnh hàng đợi, bỏ đi các phần tử đỉnh đã hết phủ ($\text{pos}[w] \le h$). Phần tử hết hạn nằm sâu bên dưới thì cứ kệ nó – khi nào nó nổi lên đỉnh mới bỏ ("xóa lười").

Mỗi kênh vào và ra hàng đợi đúng một lần, nên phần quét chỉ tốn $O(M \log M)$.

Chạy thử với ví dụ (thứ tự tô pô 1..7, vị trí = số hiệu − 1):

Bến đóng (vị trí)presufKênh bắc qua: f[u] + g[w]Đáp án
1 (0)024–24
2 (1)9171→7: 9 + 2 = 1117
3 (2)991→7: 11; 2→6: 7 + 3 = 10; 2→7: 7 + 2 = 911
4 (3)1551→7: 11; 2→6: 10; 2→7: 9; 3→7: 15 + 2 = 17; 3→5: 15 + 5 = 2020
5 (4)1931→7: 11; 2→7: 9; 3→7: 17; 4→6: 19 + 3 = 2222
6 (5)2421→7: 11; 2→7: 9; 3→7: 1724
7 (6)240–24

Các bạn thử nhập mạng kênh của mình (tối đa 10 bến, 20 kênh; số hiệu bến có thể lộn xộn):

Ví dụ nhanh:
Bẫy thường gặp
  • Coi $1, 2, \ldots, N$ là thứ tự xuôi dòng (ví dụ của đề tình cờ đúng như vậy!) – test thật đánh số ngẫu nhiên, phải sắp xếp tô pô.
  • Quên loại tour thứ 3 (đi vòng qua bến đóng bằng một kênh bắc qua).
  • Lệch biên khi bỏ phần tử khỏi hàng đợi: kênh $u \to w$ với $\text{pos}[w] = h$ đi vào chính bến đóng, phải bỏ – điều kiện là pos[w] <= h. Ví dụ của đề không bắt được lỗi này.
  • Dùng int cho $f$, $g$: tổng tới $2 \cdot 10^{14}$.

Bản Python giống hệt C++: Kahn bằng danh sách, heapq là hàng đợi ưu tiên nhỏ nhất nên mình lưu giá trị âm để lấy lớn nhất.

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, m;
    cin >> n >> m;
    vector<ll> b(n + 1);
    for (int i = 1; i <= n; i++) cin >> b[i];
    vector<int> eu(m), ev(m), deg(n + 1, 0);
    vector<vector<int>> ra(n + 1), vao(n + 1);
    for (int j = 0; j < m; j++) {
        cin >> eu[j] >> ev[j];
        ra[eu[j]].push_back(ev[j]);
        vao[ev[j]].push_back(eu[j]);
        deg[ev[j]]++;
    }
    // 1. Thu tu topo (Kahn): ord[h] la ben o vi tri h, pos[v] la vi tri cua ben v
    vector<int> ord, pos(n + 1);
    for (int v = 1; v <= n; v++) if (deg[v] == 0) ord.push_back(v);
    for (int h = 0; h < (int)ord.size(); h++) {
        int u = ord[h];
        pos[u] = h;
        for (int w : ra[u]) if (--deg[w] == 0) ord.push_back(w);
    }
    // 2. f[v]: tour tot nhat KET THUC o v; g[v]: tour tot nhat BAT DAU o v
    vector<ll> f(n + 1, 0), g(n + 1, 0);
    for (int h = 0; h < n; h++) {
        int v = ord[h];
        for (int u : vao[v]) f[v] = max(f[v], f[u]);
        f[v] += b[v];
    }
    for (int h = n - 1; h >= 0; h--) {
        int v = ord[h];
        for (int w : ra[v]) g[v] = max(g[v], g[w]);
        g[v] += b[v];
    }
    // 3. pre[h] = max f cua cac ben o vi tri < h; suf[h] = max g cua cac ben o vi tri >= h
    vector<ll> pre(n + 1, 0), suf(n + 1, 0);
    for (int h = 0; h < n; h++) pre[h + 1] = max(pre[h], f[ord[h]]);
    for (int h = n - 1; h >= 0; h--) suf[h] = max(suf[h + 1], g[ord[h]]);
    // 4. Kenh u->w "bac qua" cac vi tri pos[u] < h < pos[w], cho tour f[u] + g[w]
    vector<vector<int>> bd(n);
    for (int j = 0; j < m; j++)
        if (pos[ev[j]] - pos[eu[j]] >= 2) bd[pos[eu[j]] + 1].push_back(j);
    priority_queue<pair<ll, int>> pq;        // (do hap dan cua tour, vi tri ket thuc pos[w])
    vector<ll> ans(n + 1);
    for (int h = 0; h < n; h++) {
        for (int j : bd[h]) pq.push({f[eu[j]] + g[ev[j]], pos[ev[j]]});
        while (!pq.empty() && pq.top().second <= h) pq.pop();
        ll best = max(pre[h], suf[h + 1]);
        if (!pq.empty()) best = max(best, pq.top().first);
        ans[ord[h]] = best;
    }
    for (int v = 1; v <= n; v++) cout << ans[v] << (v < n ? ' ' : '\n');
    return 0;
}
import sys
from heapq import heappush, heappop


def main():
    data = sys.stdin.buffer.read().split()
    n, m = int(data[0]), int(data[1])
    b = [0] + list(map(int, data[2:2 + n]))
    eu = list(map(int, data[2 + n:2 + n + 2 * m:2]))
    ev = list(map(int, data[3 + n:3 + n + 2 * m:2]))
    ra = [[] for _ in range(n + 1)]
    vao = [[] for _ in range(n + 1)]
    deg = [0] * (n + 1)
    for j in range(m):
        u = eu[j]; v = ev[j]
        ra[u].append(v); vao[v].append(u); deg[v] += 1
    # 1. Thu tu topo (Kahn)
    ord_ = [v for v in range(1, n + 1) if deg[v] == 0]
    h = 0
    while h < len(ord_):
        for w in ra[ord_[h]]:
            deg[w] -= 1
            if deg[w] == 0:
                ord_.append(w)
        h += 1
    pos = [0] * (n + 1)
    for h in range(n):
        pos[ord_[h]] = h
    # 2. f: tour tot nhat ket thuc o v; g: tour tot nhat bat dau o v
    f = [0] * (n + 1)
    g = [0] * (n + 1)
    for v in ord_:
        best = 0
        for u in vao[v]:
            if f[u] > best:
                best = f[u]
        f[v] = best + b[v]
    for v in reversed(ord_):
        best = 0
        for w in ra[v]:
            if g[w] > best:
                best = g[w]
        g[v] = best + b[v]
    # 3. pre[h] = max f o vi tri < h; suf[h] = max g o vi tri >= h
    pre = [0] * (n + 1)
    suf = [0] * (n + 1)
    for h in range(n):
        x = f[ord_[h]]
        pre[h + 1] = pre[h] if pre[h] > x else x
    for h in range(n - 1, -1, -1):
        x = g[ord_[h]]
        suf[h] = suf[h + 1] if suf[h + 1] > x else x
    # 4. Kenh u->w bac qua vi tri pos[u] < h < pos[w]
    bd = [[] for _ in range(n)]
    for j in range(m):
        pu = pos[eu[j]]; pw = pos[ev[j]]
        if pw - pu >= 2:
            bd[pu + 1].append((-(f[eu[j]] + g[ev[j]]), pw))
    pq = []
    ans = [0] * (n + 1)
    for h in range(n):
        for e in bd[h]:
            heappush(pq, e)
        while pq and pq[0][1] <= h:
            heappop(pq)
        best = pre[h] if pre[h] > suf[h + 1] else suf[h + 1]
        if pq and -pq[0][0] > best:
            best = -pq[0][0]
        ans[ord_[h]] = best
    sys.stdout.write(" ".join(map(str, ans[1:])) + "\n")


main()
Không gian$O(N + M)$
Thời gian$O((N + M) \log M)$
C++▶ Chạy tay từng dòng codeTheo dõi thứ tự tô pô, hai mảng f, g và hàng đợi ưu tiên khi quét.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3typedef long long ll;
4int main() {
5 ios::sync_with_stdio(false); cin.tie(nullptr);
6 int n, m;
7 cin >> n >> m;
8 vector<ll> b(n + 1);
9 for (int i = 1; i <= n; i++) cin >> b[i];
10 vector<int> eu(m), ev(m), deg(n + 1, 0);
11 vector<vector<int>> ra(n + 1), vao(n + 1);
12 for (int j = 0; j < m; j++) {
13 cin >> eu[j] >> ev[j];
14 ra[eu[j]].push_back(ev[j]);
15 vao[ev[j]].push_back(eu[j]);
16 deg[ev[j]]++;
17 }
18 // 1. Thu tu topo (Kahn): ord[h] la ben o vi tri h, pos[v] la vi tri cua ben v
19 vector<int> ord, pos(n + 1);
20 for (int v = 1; v <= n; v++) if (deg[v] == 0) ord.push_back(v);
21 for (int h = 0; h < (int)ord.size(); h++) {
22 int u = ord[h];
23 pos[u] = h;
24 for (int w : ra[u]) if (--deg[w] == 0) ord.push_back(w);
25 }
26 // 2. f[v]: tour tot nhat KET THUC o v; g[v]: tour tot nhat BAT DAU o v
27 vector<ll> f(n + 1, 0), g(n + 1, 0);
28 for (int h = 0; h < n; h++) {
29 int v = ord[h];
30 for (int u : vao[v]) f[v] = max(f[v], f[u]);
31 f[v] += b[v];
32 }
33 for (int h = n - 1; h >= 0; h--) {
34 int v = ord[h];
35 for (int w : ra[v]) g[v] = max(g[v], g[w]);
36 g[v] += b[v];
37 }
38 // 3. pre[h] = max f cua cac ben o vi tri < h; suf[h] = max g cua cac ben o vi tri >= h
39 vector<ll> pre(n + 1, 0), suf(n + 1, 0);
40 for (int h = 0; h < n; h++) pre[h + 1] = max(pre[h], f[ord[h]]);
41 for (int h = n - 1; h >= 0; h--) suf[h] = max(suf[h + 1], g[ord[h]]);
42 // 4. Kenh u->w "bac qua" cac vi tri pos[u] < h < pos[w], cho tour f[u] + g[w]
43 vector<vector<int>> bd(n);
44 for (int j = 0; j < m; j++)
45 if (pos[ev[j]] - pos[eu[j]] >= 2) bd[pos[eu[j]] + 1].push_back(j);
46 priority_queue<pair<ll, int>> pq; // (do hap dan cua tour, vi tri ket thuc pos[w])
47 vector<ll> ans(n + 1);
48 for (int h = 0; h < n; h++) {
49 for (int j : bd[h]) pq.push({f[eu[j]] + g[ev[j]], pos[ev[j]]});
50 while (!pq.empty() && pq.top().second <= h) pq.pop();
51 ll best = max(pre[h], suf[h + 1]);
52 if (!pq.empty()) best = max(best, pq.top().first);
53 ans[ord[h]] = best;
54 }
55 for (int v = 1; v <= n; v++) cout << ans[v] << (v < n ? ' ' : '\n');
56 return 0;
57}

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

Bộ test có 31 test: 12 test subtask 1 ($N, M \le 2000$, số hiệu bến xáo trộn), 9 test subtask 2 ($N \approx 10^5$, kênh ngắn $u < v \le u + 20$), 10 test subtask 3 ($N, M \approx 2 \cdot 10^5$, số hiệu xáo trộn, khoảng 70% kênh rất dài – mỗi kênh bắc qua hàng chục nghìn vị trí). Có test chỉ một bến, test không có kênh nào, test chỉ là một đường thẳng, test có vài bến trung tâm rất nhiều kênh, test độ hấp dẫn sát $10^9$.

Cách làmSub 1 (30%)Sub 2 (30%)Sub 3 (40%)Điểm
Cách 1 – đóng từng bến rồi QHĐ lạiĐúngTLETLE30
Cách 2 – cập nhật từng vị trí bị bắc quaĐúngĐúngTLE60
Cách 3 – quét + hàng đợi ưu tiênĐúngĐúngĐúng100
Sai 1 – chỉ xét tour hẳn bên trái/phải (quên kênh bắc qua)3/12 test1/9 testWA≈ 11
Sai 2 – lệch biên: kênh kết thúc đúng ở bến đóng vẫn tính5/12 test1/9 test1/10 test≈ 20
Sai 3 – coi 1, 2, …, N là thứ tự xuôi dòng3/12 testĐúngWA37,5
Sai 4 – dùng int5/12 test1/9 test2/10 test≈ 24

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: Cách 1 đúng 12/12 test subtask 1 và chạy quá 3 giây ở mọi test còn lại; Cách 2 đúng hết subtask 1, 2 và quá 3 giây ở mọi test subtask 3 (đo riêng khoảng 6 giây); Cách 3 đúng 31/31 test, test lâu nhất khoảng 0,3 giây. "Sai 3" được trọn subtask 2 vì ở đó thứ tự 1..N đúng là thứ tự xuôi dòng – nhưng trượt hết subtask 3. Các test mà những lời giải sai khác "ăn may" là ví dụ của đề, test một bến, test không có kênh và test chỉ là một đường thẳng (không có kênh nào bắc qua).

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