Type to search.

ICPC Regional Hanoi 2024 · Part 2 of 4

Length: 3380 wordsReading time: 31 min read

ICPC Regional Hanoi 2024 Editorial - Y

Published:


Lời giải của 4 bài K, B, J, H. 

Trong kì thi này làm được 9 bài hoặc 8 bài với penalty thấp là đủ để vào được ICPC APAC Championship 2025.

K: Kangokutou Exodus 

Đề bài

Cho một mê cung gồm nn hành lang vuông góc liên tiếp nhau, mỗi hành lang có độ dài lil_{i}. Bạn đứng ở điểm bắt đầu của hành lang 1.

Trước khi vượt mê cung. bạn có thể chọn trước độ dài tốc biến mm nguyên để di chuyển từ điểm bắt đầu của hành lang 1 đến điểm kết thúc của hành lang nn.

Ở mỗi thời điểm nguyên bạn sẽ tốc biến đúng mm đơn vị độ dài về phía trước trong 1 giây, nếu cú tốc biến này không chuẩn (khoảng cách từ điểm bạn tốc biến đến điểm cuối của hành lang hiện tại nhỏ hơn mm đơn vị) thì bạn sẽ tốc biến đến điểm cuối của hành lang và bị choáng 11 giây, điều này áp dụng cả với hành lang cuối. Ngược lại, bạn có thể tốc biến tiếp ngay lập tức mà không bị choáng.

Tính thời gian ngắn nhất có thể để ra khỏi mê cung khi chọn mm tối ưu.

Giới hạn: 1≤n≤106,1≤li≤109.1\le n\le10^6,1\le l_{i}\le10^9.

Lời giải

Vì quá trình di chuyển qua các hành lang là độc lập với nhau, ta có thể sắp xếp lại các hành lang theo thứ tự: l1≤l2≤⋯≤lnl_1 \le l_2 \le \dots \le l_n.

Chúng ta bắt đầu với một số nhận xét tham lam như sau về đáp án tối ưu:

  1. Với mm cho trước thì ta có công thức tổng thời gian hoàn thành:
f(m)=∑i=1n[lim]+[li mod m≠0]f(m) = \sum_{i=1}^n \left[ \frac{l_i}{m} \right] + [l_i \bmod m \neq 0]
  1. Ta nhận thấy rằng đáp án tối ưu có cận dưới và cận trên hiển nhiên là nn và 2n−12n - 1. Cận dưới xảy ra khi toàn bộ lil_i bằng nhau, khi đó ta chọn m=lim = l_i, còn cận trên xảy ra khi ta cho m=lnm = l_n.
  2. Luôn tồn tại mm tối ưu là một trong các lil_i, nên ta chỉ cần xét m∈{l1,l2,…,ln}m \in \{l_1, l_2, \dots, l_n\}. Thật vậy, giả sử giá trị mm tối ưu là m1m_1 không trùng với bất cứ lil_i nào. Khi này với các li<m1l_i < m_1 thì ta sẽ mất đúng 2 giây để đi qua, còn với li>m1l_i > m_1 thì ta sẽ mất ít nhất 2 giây để đi qua. Do đó f(m1)≥2n>2n−1≥f(ln)f(m_1) \ge 2n > 2n - 1 \ge f(l_n), mâu thuẫn với việc m1m_1 tối ưu nên ta có điều phải chứng minh.

Nhận xét thứ 3 gợi ý rằng chúng ta cần duyệt qua tất cả các lil_i khác nhau và tính nhanh được f(li)f(l_i). Chúng ta chia mảng thành 3 phần:

  1. Phần mà lj<lil_j < l_i, mỗi jj mất 2 giây.
  2. Phần lj=lil_j = l_i, mỗi jj mất 1 giây.
  3. Phần còn lại với mỗi jj mất [ljli]+[lj mod li≠0]\left[ \frac{l_j}{l_i} \right] + [l_j \bmod l_i \neq 0] giây.

Mẫu chốt của bài toán là tính phần 3 đủ nhanh khi 2 phần trước ta có thể tính trong O(1)\mathcal{O}(1). 

C++
#include <bits/stdc++.h>
using namespace std;
// Hàm tính thời gian đi qua hành lang độ dài len với bước nhảy m
// Công thức: ceil(len/m) + (1 nếu len không chia hết cho m)
int calculate_cost(int len, int m) {
if (len % m == 0) {
return len / m;
} else {
return (len / m) + 2; // 1 bước cuối + 1 giây choáng
}
}
void solve() {
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; ++i) {
cin >> a[i];
}
// Sắp xếp để gom nhóm các hành lang có cùng độ dài
// và dễ dàng xử lý các phần tử nhỏ hơn/lớn hơn.
sort(a.begin(), a.end());
// Khởi tạo kết quả ban đầu: trường hợp tệ nhất mỗi hành lang tốn 2s
// (ví dụ chọn m > max(a_i), tất cả đều nhảy quá đà và bị choáng)
long long min_total_time = 2LL * n;
for (int i = 0; i < n;) {
int m = a[i]; // Chọn độ dài bước nhảy m bằng độ dài hành lang hiện tại
// Tìm vị trí cuối cùng có giá trị bằng a[i]
// Đoạn [i, j] là các hành lang có độ dài bằng m (tốn 1s)
int j = i;
while (j < n && a[j] == m) {
j++;
}
// j lúc này là chỉ số của phần tử đầu tiên > m (hoặc n)
// Tính chi phí cơ sở cho lần chọn m này:
// 1. Các hành lang < m: tốn 2s (nhảy quá đà ngay bước 1)
// 2. Các hành lang = m (từ i đến j-1): tốn 1s
// 3. Các hành lang > m (từ j đến n-1): giả sử tạm tính tối thiểu là 2s
// (Thực tế sẽ >= 2s. Ta sẽ cộng phần chênh lệch sau)
long long current_time = i * 2 + (j - i) * 1 + (n - j) * 2;
// Tối ưu: Duyệt các phần tử lớn hơn m từ lớn về bé.
// Cộng thêm chi phí thực tế chênh lệch so với giả định (2s).
// Nếu tổng vượt quá kết quả tốt nhất hiện có thì dừng ngay.
for (int k = n - 1; k >= j; --k) {
int actual_cost = calculate_cost(a[k], m);
current_time += (actual_cost - 2);
if (current_time >= min_total_time) {
break;
}
}
min_total_time = min(min_total_time, current_time);
// Chuyển sang nhóm độ dài tiếp theo
i = j;
}
cout << min_total_time << "\n";
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int tc;
cin >> tc;
while (tc--)
solve();
return 0;
}

Ý tưởng tóm gọn như sau: Ở một thời điểm bất kỳ, giá trị của min_total_time là ngưỡng quyết định xem l[i] hiện tại có tối ưu hơn không. Để có thể quyết định nhanh, ta duyệt qua các giá trị từ lớn đến nhỏ, nếu đến một lúc nào đó current_time lớn hơn ngưỡng tối ưu hiện tại thì ta có thể loại luôn l[i].

Độ phức tạp của 2 vòng lặp khi này không phải O(n2)\mathcal{O}(n^2) mà là O(n)\mathcal{O}(n). Thật vậy, xét một độ dài phân biệt mm với tần suất freq(m):

  1. current_time khởi đầu bằng 2n−freq(m)2n - \text{freq}(m) (mọi hành lang >m> m được tạm tính 2 giây).
  2. min_total_time luôn ≤2n−1\le 2n - 1 sau lần xét đầu tiên (nhận xét 2).
  3. Mỗi hành lang có độ dài l>ml > m và l≠2ml \neq 2m tốn ít nhất 3 giây: nếu m<l<2mm < l < 2m thì ⌈lm⌉=2\left\lceil \frac{l}{m} \right\rceil = 2 và không chia hết, nếu l>2ml > 2m thì ⌈lm⌉≥3\left\lceil \frac{l}{m} \right\rceil \ge 3. Mỗi hành lang như vậy cộng thêm ít nhất 1 vào current_time. Hành lang l=2ml = 2m tốn đúng 2 giây, cộng thêm 0.

Do đó sau khi duyệt qua không quá freq(m)\text{freq}(m) hành lang loại thứ nhất, current_time đã đạt min_total_time và vòng lặp trong dừng; xen giữa có thể có thêm không quá freq(2m)\text{freq}(2m) hành lang loại thứ hai. Vậy với mỗi mm phân biệt, vòng lặp trong chạy không quá freq(m)+freq(2m)+1\text{freq}(m) + \text{freq}(2m) + 1 lần, và tổng đại lượng này trên mọi mm phân biệt là O(n)\mathcal{O}(n). 

Độ phức tạp cuối cùng của bài là O(nlog⁡n)\mathcal{O}(n \log n) do sắp xếp.

B: Bullet Train 

Đề bài

Cho đồ thị vô hướng đầy đủ gồm nn đỉnh, với ma trận trọng số di,jd_{i,j} biểu diễn khoảng cách giữa mọi cặp đỉnh (i,j)(i, j). Một đường đi đơn gồm tt đỉnh phân biệt, với giới hạn độ dài 2≤t≤k2 \le t \le k. Chi phí của một tuyến đường bằng tổng trọng số các cạnh liên tiếp trên đường đi đó.

Cho mm cặp đỉnh (u,v)(u, v). Cần chọn một tập các tuyến đường sao cho với mỗi cặp (u,v)(u, v), tồn tại ít nhất một tuyến đường trong tập đó chứa cả hai đỉnh uu và vv. Tìm tập các tuyến đường thỏa mãn điều kiện phủ sao cho tổng chi phí của tất cả các tuyến đường được chọn là nhỏ nhất. In ra các đường đi đó.

Giới hạn: 2≤k≤n≤19,1≤m≤152\le k\le n\le19,1\le m\le15.

Lời giải

Đây là một bài toán ghép cơ học của ba bài toán quy hoạch động lại với nhau, và cả ba bài đều phải lưu thông tin truy vết.

Bài toán con 1: TSP DP trên tập đỉnh

Đặt f(S,u)f(S, u) là chi phí nhỏ nhất của một đường đi đơn ghé qua chính xác tập đỉnh S⊆VS \subseteq V và kết thúc tại đỉnh u∈Su \in S. Ta có f({u},u)=0 ∀u∈Vf(\{u\}, u) = 0 \ \forall u \in V.

Công thức truy hồi: Với mọi S⊆VS \subseteq V thỏa mãn ∣S∣<K\vert{}S\vert{} < K, với u∈Su \in S và v∈V∖Sv \in V \setminus S:

f(S∪{v},v)=min⁡u∈S(f(S,u)+d(u,v))f(S \cup \{v\}, v) = \min_{u \in S} \Big( f(S, u) + d(u, v) \Big)

Chi phí nhỏ nhất để tạo thành một tuyến đường hợp lệ đi qua đúng tập đỉnh SS là:

C(S)=min⁡u∈Sf(S,u)C(S) = \min_{u \in S} f(S, u)

Truy vết: p1(S,u)p_1(S, u) là mảng lưu lại đỉnh ngay trước uu trên hành trình đi qua tập đỉnh SS và kết thúc tại uu.

Độ phức tạp: O(n2×2n)\mathcal{O}(n^2\times2^{n}).

Bài toán con 2: DP tính chi phí nhỏ nhất của tuyến đường đơn phủ tập cặp

Xét một tập các cặp quan trọng P⊆RP \subseteq R (kí hiệu RR là tập tất cả các cặp quan trọng). Đặt span(P)⊆V\text{span}(P) \subseteq V là tập hợp tất cả các đỉnh của các cặp nằm trong PP:

span(P)=⋃(u,v)∈P{u,v}\text{span}(P) = \bigcup_{(u, v) \in P} \{u, v\}

Ta có các nhận xét sau:

  1. Điều kiện hợp lệ: Nếu ∣span(P)∣>K\vert{}\text{span}(P)\vert{} > K, không tồn tại một tuyến đường đơn nào có thể phủ toàn bộ tập cặp PP.
  2. Thêm đỉnh bổ trợ: Nếu ∣span(P)∣≤K\vert{}\text{span}(P)\vert{} \le K, tuyến đường đơn phủ PP có thể ghé qua các đỉnh trong PP cùng một tập các đỉnh bổ trợ X⊆V∖span(P)X \subseteq V \setminus \text{span}(P).

Chi phí nhỏ nhất của chỉ một tuyến đường đơn để phủ trọn vẹn tập cặp PP được định nghĩa bởi hàm g(P)g(P), duyệt qua tất cả các tập đỉnh bổ trợ XX sao cho vẫn thoả mãn điều kiện hợp lệ.

g(P)=min⁡XC(span(P)∪X)g(P) = \min_X C\big(\text{span}(P) \cup X\big)

Truy vết: p2(P)=(SP,uP)p_2(P) = (S_P, u_P) đạt chi phí cực tiểu cho hàm g(P)g(P), trong đó SP=span(P)∪XPS_P = \text{span}(P) \cup X_P là tập đỉnh hoàn chỉnh và uP∈SPu_P \in S_P là đỉnh kết thúc của tuyến đường đó.

Độ phức tạp: Không thể tính chính xác do có nhiều ràng buộc (tập đỉnh span, điều kiện hợp lệ). Tuy nhiên cũng chính vì vậy mà phần này sẽ chạy tương đối nhanh.

Bài toán con 3: DP phân hoạch tập cặp

Đặt h(P)h(P) là tổng chi phí nhỏ nhất để phủ tập các cặp P⊆RP \subseteq R bằng một hoặc nhiều tuyến đường. Ta có h(∅)=0h(\emptyset) = 0. Với mọi tập cặp P⊆RP \subseteq R khác rỗng, ta tách PP thành một tập con Q⊆PQ \subseteq P (do một tuyến đường đơn đảm nhận) và phần còn lại P∖QP \setminus Q, và tính hàm hh như sau:

h(P)=min⁡Q⊆P,  Q≠∅(h(P∖Q)+g(Q))h(P) = \min_{Q \subseteq P, \; Q \neq \emptyset} \Big( h(P \setminus Q) + g(Q) \Big)

Giá trị h(R)h(R) chính là kết quả tối ưu của bài toán.

Truy vết: p3(P)p_3(P) lưu lại tập con các cặp Q⊆PQ \subseteq P được chọn để đảm nhận bởi 1 tuyến đường đơn tại bước chuyển trạng thái của h(P)h(P).

Độ phức tạp: O(3n)\mathcal{O}(3^n) (do duyệt submask).

Truy vết

Bắt đầu từ tập RR:

  1. Dùng p3(R)p_3(R) để phân rã tập RR thành các tập con rời nhau Q1,Q2,…,QrQ_1, Q_2, \dots, Q_r.
  2. Với mỗi QiQ_i, dùng p2(Qi)p_2(Q_i) để lấy tập đỉnh Si=span(Qi)∪XiS_i = \text{span}(Q_i) \cup X_i đạt giá trị cực tiểu của hàm g(Qi)g(Q_i) và đỉnh kết thúc uu.
  3. Với mỗi tập SiS_{i} và uu thu được, dùng p1(Si,u)p_1(S_i, u) để đi ngược từ đỉnh kết thúc về đỉnh xuất phát, thu được thứ tự xuất hiện của các đỉnh trên tuyến đường.
  4. Đảo ngược thứ tự đỉnh thu được của các tuyến đường và in ra đáp án.

Code

C++
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 19;
const int INF = 0x3f3f3f3f;
int n, k, m;
int dist[N][N];
int dp[1<<N][N];
int pre[1<<N][N];
void solve() {
cin >> n >> k >> m;
// 1) read distances
for(int i = 0; i < n; i++) for(int j = 0; j < n; j++) dist[i][j] = (i == j ? 0 : INF);
for(int i = 0; i < n; ++i) for(int j = i+1; j < n; ++j) {
cin >> dist[i][j];
dist[j][i] = dist[i][j];
}
vector<int> pu(m), pv(m);
for(int i = 0; i < m; ++i) {
cin >> pu[i] >> pv[i];
--pu[i]; --pv[i];
}
int ALLV = 1<<n;
// 2) TSP DP over subsets up to size k
for(int mask = 0; mask < ALLV; ++mask)
for(int v = 0; v < n; ++v)
dp[mask][v] = INF;
for(int i = 0; i < n; ++i) dp[1<<i][i] = 0;
for(int mask = 1; mask < ALLV; ++mask) {
int pc = __builtin_popcount(mask);
if (pc > k) continue;
for(int u = 0; u < n; ++u) if (mask & (1<<u)) {
int cur = dp[mask][u];
if (cur == INF) continue;
for(int v = 0; v < n; ++v) if (!(mask & (1<<v))) {
int m2 = mask | (1<<v);
if (dp[m2][v] > cur + dist[u][v]) {
dp[m2][v] = cur + dist[u][v];
pre[m2][v] = u;
}
}
}
}
// 3) bestCost[S]: min dp[S][i] over i in S
const ll INFLL = (ll)4e18;
vector<ll> bestCost(ALLV, INFLL);
vector<int> bestEnd(ALLV, -1);
for(int mask = 1; mask < ALLV; ++mask) {
int pc = __builtin_popcount(mask);
if (pc > k) continue;
for(int i = 0; i < n; ++i) if (mask & (1<<i)) {
if (dp[mask][i] < bestCost[mask]) {
bestCost[mask] = dp[mask][i];
bestEnd[mask] = i;
}
}
}
// 4) min_cost for each pair-mask, including helper vertices
int ALLP = 1<<m;
vector<ll> mc_cost(ALLP, INFLL);
vector<int> mc_end(ALLP, -1);
for(int pm = 0; pm < ALLP; ++pm) {
// collect required vertices
int base = 0;
for(int i = 0; i < m; ++i) if (pm & (1<<i)) {
base |= (1<<pu[i]);
base |= (1<<pv[i]);
}
int base_pc = __builtin_popcount(base);
if (base_pc > k) continue;
int free_mask = ((1<<n)-1) ^ base;
// enumerate extra subsets
for(int extra = free_mask; ; extra = (extra-1) & free_mask) {
int S = base | extra;
if (__builtin_popcount(S) <= k && bestCost[S] < INFLL) {
if (bestCost[S] < mc_cost[pm]) {
mc_cost[pm] = bestCost[S];
mc_end[pm] = bestEnd[S];
}
}
if (extra == 0) break;
}
}
// 5) DP over pair-subsets
vector<ll> dp2(ALLP, INFLL);
vector<int> pre2(ALLP, -1);
dp2[0] = 0;
for(int pm = 1; pm < ALLP; ++pm) {
// one batch covers pm
dp2[pm] = mc_cost[pm];
pre2[pm] = 0;
// partition into sub + rest
for(int sub = (pm-1)&pm; sub; sub = (sub-1)&pm) {
ll cand = dp2[pm^sub] + mc_cost[sub];
if (cand < dp2[pm]) {
dp2[pm] = cand;
pre2[pm] = pm^sub;
}
}
}
// 6) reconstruct
vector<vector<int>> routes;
int curp = ALLP - 1;
while (curp > 0) {
int prev = pre2[curp];
int pick = curp ^ prev;
// get vertex mask
int base = 0;
for(int i = 0; i < m; ++i) if (pick & (1<<i)) {
base |= (1<<pu[i]);
base |= (1<<pv[i]);
}
// find S that gave mc_cost[pick]
int free_mask = ((1<<n)-1) ^ base;
int endv = mc_end[pick];
// find correct superset
int found_mask = -1;
for(int extra = free_mask; ; extra = (extra-1)&free_mask) {
int S = base | extra;
if (__builtin_popcount(S) <= k && bestEnd[S] == endv && bestCost[S] == mc_cost[pick]) {
found_mask = S;
break;
}
if (extra == 0) break;
}
assert(found_mask >= 0);
// backtrack path
vector<int> path;
int mask2 = found_mask;
int u = endv;
while (mask2) {
path.push_back(u);
int prev_u = pre[mask2][u];
mask2 ^= (1<<u);
u = prev_u;
}
reverse(path.begin(), path.end());
routes.push_back(path);
curp = prev;
}
// 7) output
cout << dp2[ALLP-1] << " " << routes.size() << "\n";
for(auto &r : routes) {
cout << r.size();
for(int x : r) cout << " " << x+1;
cout << "\n";
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T; cin >> T;
while(T--) solve();
return 0;
}

J: Jumbled Journey 

Đề bài

Cho đồ thị liên thông vô hướng có nn đỉnh, mm cạnh có trọng số.

Bạn T chọn một hoán vị p1,p2,…,pnp_1,p_2,\ldots,p_n của các đỉnh từ 11 đến nn, thể hiện lịch trình vận chuyển hàng gồm n−1n - 1 chặng, mỗi chặng từ pip_i đến pi+1p_{i + 1}. Bạn M nhận được hoán vị của T và cần tìm hành trình cụ thể cho từng chặng đảm bảo tổng chi phí của toàn bộ lịch trình là nhỏ nhất có thể.

  • Chi phí của một chặng là trọng số lớn nhất của cạnh trên đường đi qua.
  • Tổng chi phí của một hoán vị là tổng chi phí của n−1n - 1 chặng.

T muốn tìm một hoán vị sao cho tổng chi phí mà M tìm được là lớn nhất có thể. Tìm hoán vị này và in ra chi phí mà M tìm được cho hoán vị đó. Nếu có nhiều hoán vị thoả mãn thì có thể chọn một hoán vị bất kì.

Giới hạn: 2≤n≤5×105,n−1≤m≤5×1052 \le n \le 5 \times 10^5, n - 1 \le m \le 5 \times 10^5, trọng số cạnh không quá 10910^9.

Lời giải

Phân tích hành vi của M và T

Sau khi nhận được hoán vị của bạn T, bạn M muốn cực tiểu hóa tổng chi phí của n−1n - 1 chặng. Do đó, với mỗi chặng, bạn M muốn tìm đường đi từ đi từ pip_i đến pi+1p_{i + 1} sao cho cạnh có trọng số lớn nhất của đường đi là nhỏ nhất. Ta có một kết quả quen thuộc như sau: Đường đi giữa hai đỉnh uu và vv có (max trọng số cạnh) nhỏ nhất chính là đường đi trên cây khung nhỏ nhất (MST). Thật vậy:

  1. Nếu ta có một đường đi từ uu đến vv có trọng số cạnh lớn nhất là CC, thì mọi cạnh trên đường đi đó đều có trọng số ≤C\le C.
  2. Giả sử rằng, giá trị nhỏ nhất của (trọng số cạnh lớn nhất) của một đường đi bất kì từ uu đến vv là Cmin⁡C_{\min} thì:
    1. Nếu sử dụng các cạnh trọng số ≤Cmin⁡\le C_{\min} thì có thể đi được giữa uu và vv.
    2. Nếu chỉ sử dụng các cạnh trọng số ≤Cmin⁡−1\le C_{\min} - 1 thì không thể đi được giữa uu và vv.

Kết quả trên cũng là hệ quả trực tiếp của thuật toán Kruskal: trước khi xét một cạnh (u,v)(u, v) trọng số cc nào đó; ta đã xét qua mọi cạnh có trọng số ≤c−1\le c - 1 và thử cho chúng vào cây khung. Như vậy, nếu một cạnh (u,v)(u, v) trọng số cc mà được cho vào cây khung, đồng nghĩa với việc chỉ sử dụng các cạnh trọng số ≤c−1\le c - 1 là không đủ để liên thông uu và vv.

Qua đó ta rút ra nhận xét quan trọng sau: Hành vi của bạn M thực chất là tìm ra một MST bất kỳ, và với mỗi cặp pi,pi+1p_i, p_{i + 1} trong hoán vị của bạn T, bạn M tìm đường đi từ pip_i đến pi+1p_{i + 1} trên MST đó.

T nhận ra được suy nghĩ của M, do đó, T cũng dựng ra một MST bất kỳ. Để cực đại hóa tổng chi phí, T giải bài toán sau: Tìm hoán vị pp sao cho tổng (trọng số cạnh lớn nhất) trên tất cả các đường đi từ pip_i đến pi+1p_{i + 1} là lớn nhất có thể.

Kruskal Reconstruction Tree (KRT)

Xét một MST bất kì của đồ thị. Gọi cạnh có trọng số lớn nhất trên MST đó là Cmax⁡C_{\max}, và u,vu, v là hai đỉnh thuộc hai thành phần liên thông khác nhau bị tách ra bởi Cmax⁡C_{\max}. Khi đó, cạnh trọng số lớn nhất trên đường đi giữa u và v luôn là Cmax⁡C_{\max}. Ta có thể tiếp tục áp dụng nhận xét này một cách đệ quy:

  1. dùng cạnh lớn nhất chia cây thành hai phần;
  2. trong mỗi phần, tiếp tục dùng cạnh lớn nhất để chia cây thành hai phần nhỏ hơn nữa
  3. lặp lại cho đến khi mỗi phần chỉ còn một đỉnh.

Quá trình chia tách này tạo thành một cây nhị phân. Các lá đại diện cho các đỉnh ban đầu, còn mỗi đỉnh trong đại diện cho cạnh đã chia một cây con thành hai phần. Cây này được gọi là Kruskal Reconstruction Tree (KRT) và có thể được dựng trong quá trình chạy thuật toán Kruskal. Đầu tiên, ta sắp xếp các cạnh theo trọng số tăng dần. Mỗi đỉnh của đồ thị ban đầu là một thành phần liên thông và được biểu diễn bởi một đỉnh lá trên KRT. Khi xét một cạnh trọng số ww nối hai thành phần khác nhau, ta thực hiện các bước:

  1. tạo một đỉnh mới xx có trọng số ww;
  2. đặt hai gốc đại diện cho hai thành phần làm hai con trực tiếp của xx;
  3. dùng DSU hợp nhất hai thành phần;
  4. lấy xx làm gốc đại diện cho thành phần mới.

Thuật toán tính kết quả tối ưu

Gọi sz(x)sz(x) là số lá trong cây con gốc xx của KRT, và wxw_x là trọng số của đỉnh xx. Ta bắt đầu từ gốc của KRT và đi xuống một nhánh duy nhất. Tại đỉnh hiện tại xx, gọi hai cây con là AA và BB, giả sử sz(A)≥sz(B)sz(A) \ge sz(B).

Nếu sz(A)>n2sz(A) > \frac{n}{2}, ta tiếp tục đi xuống AA và coi toàn bộ cây con BB là một nhóm đã được tách ra khỏi nhánh chính.

Ngược lại, nếu cả hai cây con đều có không quá n2\frac{n}{2} lá, ta dừng lại tại xx. Đây là đỉnh đầu tiên trên đường đi mà không còn cây con nào chiếm quá nửa tổng số lá. Như vậy, các lá được chia thành ba nhóm:

  • ​first: các lá trong cây con thứ nhất của xx;
  • ​second: các lá trong cây con thứ hai của xx;
  • ​other: các lá thuộc những cây con nhỏ đã bị tách ra trong quá trình đi từ gốc xuống xx.

Đặt kích thước của ba nhóm lần lượt là aa, bb và oo. Ta có a+b+o=na + b + o = n. Đồng thời, do cách chọn đỉnh dừng, ta có a≤n2a \le \frac{n}{2} và b≤n2b \le \frac{n}{2}.

Xét một bước trên đường đi, tại đó ta đi từ đỉnh yy xuống cây con lớn và tách ra cây con nhỏ SS. Mọi lá thuộc SS có LCA với mọi lá thuộc nhánh lớn bằng yy. Vì vậy, nếu một lá của SS được đặt giữa hai lá thuộc nhánh lớn trong hoán vị, nó tạo ra hai cặp liên tiếp có đóng góp bằng wyw_y : ..., u, s, v, ... với s∈Ss \in S và u,vu,v thuộc nhánh lớn. Mỗi đỉnh trong một hoán vị có nhiều nhất hai hàng xóm, nên một lá của SS có thể tạo nhiều nhất hai cặp đi qua phép chia tại yy. Do đó, toàn bộ cây con SS đóng góp nhiều nhất 2⋅sz(S)⋅wy.2 \cdot sz(S) \cdot w_y.

Thuật toán sẽ dựng hoán vị sao cho mọi lá của SS đều nằm giữa hai lá thuộc nhánh chính, nên giới hạn này đạt được. Vì vậy, mỗi khi tách một cây con nhỏ SS, ta cộng 2⋅sz(S)⋅wy2 \cdot sz(S) \cdot w_y vào đáp án. Sau đó ta tiếp tục đi xuống cây con lớn.

Dựng hoán vị thoả mãn

Sau khi tìm được đỉnh dừng xx, ta thu thập các lá của hai cây con vào hai danh sách first​ và second​, còn tất cả lá đã bị tách ra được đưa vào other. Ta dựng hoán vị như sau, với ý tưởng chính là xen kẽ các danh sách lá sao cho đạt được các giới hạn trên đã chỉ ra.

Đầu tiên, với mỗi lá vv trong other:

  1. lấy một lá uu nào đó từ danh sách đang có nhiều phần tử hơn giữa first​ và second;
  2. đưa lá đó vào hoán vị;
  3. đưa tiếp vv vào hoán vị.

Thứ tự được tạo ra có dạng: u1,v1,u2,v2,…,uo,vou_1, v_1, u_2, v_2, \dots, u_o, v_o trong đó mỗi uiu_i thuộc cây con của đỉnh dừng, còn mỗi viv_i thuộc other.

Sau khi dùng hết other, thuật toán tiếp tục lấy một lá từ danh sách đang có nhiều phần tử hơn cho tới khi cả hai danh sách rỗng. Ta có ∣a−b∣≤o.|a-b| \le o. Thật vậy, giả sử a≥ba \ge b. Vì a≤n2a \le \frac{n}{2} nên a−b≤n−a−b=oa-b \le n-a-b=o.

Trong bước đầu tiên, ta đã lấy đúng oo lá từ first​ và second, mỗi lần đều lấy từ danh sách lớn hơn. Vì vậy, sau khi ghép hết với oo lá trong other, số phần tử còn lại của hai danh sách là bằng nhau. Ta sẽ xếp xen kẽ từng cặp phần tử của hai danh sách này ngay tiếp sau bước 1, và hoán vị sẽ có dạng: u1,v1,u2,v2,…,uo,vo,a1,b1,a2,b2,…u_1, v_1, u_2, v_2, \dots, u_o, v_o, a_1, b_1, a_2, b_2, \dots . Mọi cặp ai,bia_i, b_i​ liên tiếp còn lại đều gồm một lá thuộc first​ và một lá thuộc second, nên LCA của chúng chính là đỉnh dừng xx. Mỗi cặp như vậy đóng góp wxw_x.

Toàn bộ hoán vị có n−1n-1 cặp liên tiếp. Trong đó 2o2o cặp liên tiếp đã được tạo bởi các lá thuộc other (u1,v1,u2,v2,…,uo,vo,a1u_1, v_1, u_2, v_2, \dots, u_o, v_o, a_1), nên số cặp còn lại là n−1−2on - 1 - 2o. Vì vậy, tại đỉnh dừng xx, ta cộng thêm (n−1−2o)wx(n-1-2o)w_x vào đáp án.

Chứng minh tính đúng đắn

Tại mỗi đỉnh yy trên đường đi xuống, cây con nhỏ SS có sz(S)sz(S) lá. Mỗi lá chỉ có tối đa hai hàng xóm trong hoán vị, nên số cặp nối một lá của SS với phần còn lại không thể vượt quá 2⋅sz(S)2 \cdot sz(S). Chỉ những cặp như vậy mới có thể có LCA bằng yy. Do đó, đóng góp tại phép chia này không thể vượt quá 2⋅sz(S)⋅wy2 \cdot sz(S) \cdot w_y, Thuật toán đặt mỗi lá của SS giữa hai lá thuộc nhánh lớn, nên đạt đúng giới hạn trên.

Tại đỉnh dừng xx, sau khi dành 2o2o cặp cho các lá thuộc other, chỉ còn n−1−2on-1-2o cặp. Trọng số của mỗi cặp này không thể lớn hơn wxw_x, vì cả hai đầu đều nằm trong cây con của xx. Thuật toán xen kẽ hai cây con của xx, khiến tất cả các cặp còn lại có LCA đúng bằng xx và cùng đóng góp wxw_x.

Như vậy, ta đã chứng minh thuật toán dựng hoán vị đạt giới hạn trên, nên hoán vị dựng được là tối ưu.

Code

C++
#include <bits/stdc++.h>
using namespace std;
#define FOR(i, a, b) for (int i = (a); i <= (b); ++i)
#define REP(i, n) for (int i = 0; i < (n); ++i)
#define left ___left
#define right ___right
class DisjointSet {
private:
vector<int> lab;
public:
DisjointSet(int n = 0) {
if (n > 0) lab.assign(n + 7, -1);
}
int find(int x) {
return lab[x] < 0 ? x : lab[x] = find(lab[x]);
}
bool join(int u, int v) {
int x = find(u), y = find(v);
if (x == y) return false;
if (lab[x] > lab[y]) swap(x, y);
lab[x] += lab[y];
lab[y] = x;
return true;
}
};
struct Edge {
int u, v, cost;
void input() {
cin >> u >> v >> cost;
}
bool operator < (const Edge &e) const {
return cost < e.cost;
}
};
const int MAX = 500500;
int numNode, numEdge;
Edge edges[MAX];
int lab[MAX], par[MAX << 1], left[MAX << 1], right[MAX << 1], cntLeaf[MAX << 1], root, cost[MAX << 1];
bool inTree[MAX];
vector<int> first, second, other, perm;
void loadGraph() {
cin >> numNode >> numEdge;
FOR(i, 1, numEdge) edges[i].input();
}
void getLeaves(int node, vector<int> &leaves) {
if (node <= numNode) return leaves.push_back(node);
getLeaves(left[node], leaves);
getLeaves(right[node], leaves);
}
void process() {
sort(edges + 1, edges + numEdge + 1);
DisjointSet dsu(numNode);
FOR(i, 1, numEdge) inTree[i] = dsu.join(edges[i].u, edges[i].v);
FOR(i, 1, numNode) {
left[i] = right[i] = -1;
cntLeaf[i] = 1;
lab[i] = i;
}
root = numNode;
dsu = DisjointSet(numNode);
FOR(i, 1, numEdge) if (inTree[i]) {
int u = edges[i].u, v = edges[i].v;
int labU = lab[dsu.find(u)];
int labV = lab[dsu.find(v)];
root++;
left[root] = labU;
right[root] = labV;
par[labU] = par[labV] = root;
cost[root] = edges[i].cost;
cntLeaf[root] = cntLeaf[labU] + cntLeaf[labV];
dsu.join(u, v);
lab[dsu.find(u)] = root;
}
par[root] = -1;
int cur = root;
long long totCost = 0;
while (true) {
int big = left[cur], small = right[cur];
if (cntLeaf[big] < cntLeaf[small]) swap(big, small);
if (cntLeaf[big] * 2 > numNode) {
totCost += 2LL * cntLeaf[small] * cost[cur];
cur = big;
continue;
}
first.clear();
second.clear();
other.clear();
getLeaves(left[cur], first);
getLeaves(right[cur], second);
for (int u = cur; u != root; u = par[u])
getLeaves(left[par[u]] ^ right[par[u]] ^ u, other);
totCost += 1LL * cost[cur] * (numNode - 1 - 2 * (int)other.size());
perm.assign(numNode, 0);
int permID = 0;
REP(i, other.size()) {
vector<int> &bigger = first.size() > second.size() ? first : second;
perm[permID++] = bigger.back(); bigger.pop_back();
perm[permID++] = other.back(); other.pop_back();
}
while (!first.empty() || !second.empty()) {
vector<int> &bigger = first.size() > second.size() ? first : second;
perm[permID++] = bigger.back(); bigger.pop_back();
}
cout << totCost << "\n";
for (int x : perm) cout << x << " ";
cout << "\n";
break;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int numTest;
cin >> numTest;
while (numTest--) {
loadGraph();
process();
}
return 0;
}

H: Hash-shashin 

Đề bài

Cho một xâu nhị phân ss độ dài nn (si∈{0,1}s_i \in \{0, 1\}) và một số nguyên dương m≥3m \ge 3. Dãy hh được tạo theo công thức:

  • h0=0h_0 = 0
  • hi=(2⋅hi−1+si) mod mh_i = (2 \cdot h_{i-1} + s_i) \bmod m với mọi 1≤i≤n1 \le i \le n

Cho xâu kk độ dài nn biểu diễn quan hệ so sánh giữa các cặp phần tử liên tiếp trong dãy hh:

  • ki=k_i = '<' nếu hi−1<hih_{i-1} < h_i
  • ki=k_i = ‘=’ nếu hi−1=hih_{i-1} = h_i
  • ki=k_i = '>' nếu hi−1>hih_{i-1} > h_i

Yêu cầu: Kiểm tra xem xâu kk có hợp lệ hay không (tức là có tồn tại xâu nhị phân ss nào tạo ra kk này hay không). 

  1. Nếu xâu kk không hợp lệ, in ra impossible. 
  2. Nếu xâu kk hợp lệ, với mỗi vị trí ii (1≤i≤n1 \le i \le n), xác định xem ký tự sis_i có cố định (luôn là 0 hoặc luôn là 1 trong mọi xâu ss thỏa mãn kk) hay không, nếu không cố định in ra ? ở vị trí đó.

Lời giải

Theo định nghĩa của dãy hh và phép chia lấy dư, tồn tại qi∈{0,1}q_{i}\in\left\lbrace0,1\right\rbrace sao cho: 

2⋅hi−1+si=hi+qi⋅m  ⟹  hi=2⋅hi−1+si−qi⋅m2 \cdot h_{i-1} + s_i = h_i + q_i \cdot m \implies h_i = 2 \cdot h_{i-1} + s_i - q_i \cdot m

Xét các trường hợp của kik_{i}:

Trường hợp 1: ki=k_{i}= ‘>’

Ta có: 

2⋅hi−1+si−qi⋅m>hi−1  ⟹  hi−1+si>qi⋅m2 \cdot h_{i-1} + s_i - q_i \cdot m > h_{i-1} \implies h_{i-1} + s_i > q_i \cdot m

Vì hi−1≤m−1h_{i-1} \le m - 1 và si≤1s_i \le 1, ta có hi−1+si≤mh_{i-1} + s_i \le m. Nếu qi=1q_i = 1, bất đẳng thức buộc hi−1=m−1h_{i-1} = m - 1 và si=1s_i = 1, dẫn đến hi=m−1=hi−1h_i = m - 1 = h_{i-1} (mâu thuẫn).

Do đó bắt buộc qi=0q_i = 0, suy ra khoảng cho phép của hih_i là [Ai,Bi]=[1,m−1][A_i, B_i] = [1, m - 1].

Trường hợp 2: ki=k_{i}= ‘<’

Lập luận tương tự, bắt buộc qi=1q_i = 1, suy ra khoảng cho phép của hih_i là [Ai,Bi]=[0,m−2][A_i, B_i] = [0, m - 2].

Trường hợp 3: ki=k_{i}= ‘=’

Nếu qi=0  ⟹  hi−1=0,si=0  ⟹  hi=0q_i = 0 \implies h_{i-1} = 0, s_i = 0 \implies h_i = 0. 

Nếu qi=1  ⟹  hi−1=m−1,si=1  ⟹  hi=m−1q_i = 1 \implies h_{i-1} = m - 1, s_i = 1 \implies h_i = m - 1.

Do đó qiq_i giữ nguyên giá trị qq từ trạng thái so sánh ngặt ('<' →q=0\to q = 0 hoặc '>'→q=1\to q = 1) gần nhất trước đó. Khoảng cho phép khi này thu hẹp thành điểm đơn [Ai,Bi]=[X,X][A_i, B_i] = [X, X] với X=qi⋅(m−1)X = q_i \cdot (m - 1).

Như vậy, toàn bộ dãy q0,q1,…,qn−1q_0, q_1, \dots, q_{n-1} và khoảng giá trị cho phép [Ai,Bi][A_i, B_i] của hi+1h_{i+1} hoàn toàn bị cố định bởi xâu kk.

Vì si∈{0,1}s_i \in \{0, 1\}, với mỗi khoảng giá trị khả dĩ hi∈[Li,Ri]h_i \in [L_i, R_i], giá trị hi+1h_{i+1} sẽ nằm trong khoảng 

[2⋅Li−qi⋅m,  2⋅Ri+1−qi⋅m][2 \cdot L_i - q_i \cdot m, \; 2 \cdot R_i + 1 - q_i \cdot m]

Ta có h0=0h_0=0 nên L0=R0=0L_0=R_0=0. Vì mỗi hih_{i} đều có liên quan đến phần tử ở trước nó và ở sau nó, ta sẽ duyệt hai chiều để tìm được khoảng giá trị thực sự.

Chiều xuôi: Duyệt từ i=0i = 0 đến n−1n - 1 để xác định tập giá trị có thể đạt được của hi+1h_{i+1}, giao với khoảng cho phép [Ai,Bi][A_i, B_i]:

Li+1=max⁡(Ai,2⋅Li−qi⋅m)L_{i+1}=\max(A_{i},2\cdot L_{i}-q_{i}\cdot m)Ri+1=min⁡(Bi,2⋅Ri+1−qi⋅m)R_{i+1}=\min(B_{i},2\cdot R_{i}+1-q_{i}\cdot m)

Nếu tại bất cứ thời điểm nào mà Li+1>Ri+1L_{i + 1} > R_{i + 1}, hệ vô nghiệm, in ra impossible.

Chiều ngược: Khởi tạo khoảng hợp lệ ngược [U,V]=[0,m−1][U, V] = [0, m - 1] tại vị trí nn. Duyệt từ i=n−1i = n - 1 về 00, duy trì khoảng giá trị thực sự [U,V][U, V] của hi+1h_{i+1} từ phía sau. Giao khoảng xuôi [Li+1,Ri+1][L_{i+1}, R_{i+1}] và khoảng [U,V][U, V] ta sẽ thu được đoạn giá trị thực sự [X,Y][X, Y] của hi+1h_{i+1}:

X=max⁡(Li+1,U),Y=min⁡(Ri+1,V)X = \max(L_{i+1}, U), \quad Y = \min(R_{i+1}, V)

Ta có mối liên hệ tính chẵn lẻ giữa hh và ss:

si=(hi+1+qi⋅m) mod 2s_i = (h_{i+1} + q_i \cdot m) \bmod 2

Giá trị sis_i cố định khi và chỉ khi mọi hi+1∈[X,Y]h_{i+1} \in [X, Y] đều cho cùng một tính chẵn lẻ. Từ đó hiển nhiên ta có sis_{i} cố định khi và chỉ khi X=YX = Y, suy ra si=(X+qi⋅m) mod 2s_i = (X + q_i \cdot m) \bmod 2. Nếu không thì sis_i nhận giá trị ?. 

Để hi+1h_{i+1} rơi đúng vào đoạn [X,Y][X, Y], giá trị hih_i trước đó phải thỏa mãn X≤2⋅hi+si−qi⋅m≤YX \le 2 \cdot h_i + s_i - q_i \cdot m \le Y. Ta cập nhật khoảng hợp lệ [U,V][U, V] mới truyền về cho hih_i:

U=⌊X+qi⋅m2⌋,V=⌊Y+qi⋅m2⌋U = \left\lfloor \frac{X + q_i \cdot m}{2} \right\rfloor, \quad V = \left\lfloor \frac{Y + q_i \cdot m}{2} \right\rfloor

Code

C++
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
int n;
long long m;
string key;
cin >> n >> m >> key;
vector<int> q(n);
int eq_q = 0;
for (int i = 0; i < n; i++) {
if (key[i] == '<') {
q[i] = 0;
eq_q = 1;
} else if (key[i] == '>') {
q[i] = 1;
eq_q = 0;
} else {
q[i] = eq_q;
}
}
auto allowed = [&](int i) -> pair<long long, long long> {
if (key[i] == '<') return {1, m - 1};
if (key[i] == '>') return {0, m - 2};
long long x = q[i] ? m - 1 : 0;
return {x, x};
};
vector<long long> fl(n + 1), fr(n + 1);
fl[0] = fr[0] = 0;
for (int i = 0; i < n; i++) {
auto [lo, hi] = allowed(i);
if (fl[i] > fr[i]) {
fl[i + 1] = 1;
fr[i + 1] = 0;
continue;
}
fl[i + 1] = max(lo, 2 * fl[i] - q[i] * m);
fr[i + 1] = min(hi, 2 * fr[i] + 1 - q[i] * m);
}
if (fl[n] > fr[n]) {
cout << "impossible\n";
continue;
}
string ans(n, '?');
long long bl = 0;
long long br = m - 1;
for (int i = n - 1; i >= 0; i--) {
long long l = max(fl[i + 1], bl);
long long r = min(fr[i + 1], br);
if (l == r) {
ans[i] = char('0' + ((l + q[i] * m) & 1));
}
auto [lo, hi] = allowed(i);
l = max(bl, lo);
r = min(br, hi);
if (l > r) {
bl = 1;
br = 0;
} else {
bl = (l + q[i] * m) / 2;
br = (r + q[i] * m) / 2;
}
}
cout << ans << '\n';
}
}