ICPC Regional Hanoi 2024 · Part 2 of 4
ICPC Regional Hanoi 2024 Editorial - Y
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 hành lang vuông góc liên tiếp nhau, mỗi hành lang có độ dà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 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 .
Ở mỗi thời điểm nguyên bạn sẽ tốc biến đúng đơ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 đơn vị) thì bạn sẽ tốc biến đến điểm cuối của hành lang và bị choáng 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 tối ưu.
Giới hạn:
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ự: .
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:
- Với cho trước thì ta có công thức tổng thời gian hoàn thành:
- 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à và . Cận dưới xảy ra khi toàn bộ bằng nhau, khi đó ta chọn , còn cận trên xảy ra khi ta cho .
- Luôn tồn tại tối ưu là một trong các , nên ta chỉ cần xét . Thật vậy, giả sử giá trị tối ưu là không trùng với bất cứ nào. Khi này với các thì ta sẽ mất đúng 2 giây để đi qua, còn với thì ta sẽ mất ít nhất 2 giây để đi qua. Do đó , mâu thuẫn với việc 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 khác nhau và tính nhanh được . Chúng ta chia mảng thành 3 phần:
- Phần mà , mỗi mất 2 giây.
- Phần , mỗi mất 1 giây.
- Phần còn lại với mỗi mất 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 .
#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 mà là . Thật vậy, xét một độ dài phân biệt với tần suất freq(m):
current_timekhởi đầu bằng (mọi hành lang được tạm tính 2 giây).min_total_timeluôn sau lần xét đầu tiên (nhận xét 2).- Mỗi hành lang có độ dài và tốn ít nhất 3 giây: nếu thì và không chia hết, nếu thì . Mỗi hành lang như vậy cộng thêm ít nhất 1 vào
current_time. Hành lang tốn đúng 2 giây, cộng thêm 0.
Do đó sau khi duyệt qua không quá 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á hành lang loại thứ hai. Vậy với mỗi phân biệt, vòng lặp trong chạy không quá lần, và tổng đại lượng này trên mọi phân biệt là .
Độ phức tạp cuối cùng của bài là do sắp xếp.
B: Bullet Train
Đề bài
Cho đồ thị vô hướng đầy đủ gồm đỉnh, với ma trận trọng số biểu diễn khoảng cách giữa mọi cặp đỉnh . Một đường đi đơn gồm đỉnh phân biệt, với giới hạn độ dài . 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 cặp đỉnh . Cần chọn một tập các tuyến đường sao cho với mỗi cặp , tồn tại ít nhất một tuyến đường trong tập đó chứa cả hai đỉnh và . 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: .
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 là chi phí nhỏ nhất của một đường đi đơn ghé qua chính xác tập đỉnh và kết thúc tại đỉnh . Ta có .
Công thức truy hồi: Với mọi thỏa mãn , với và :
Chi phí nhỏ nhất để tạo thành một tuyến đường hợp lệ đi qua đúng tập đỉnh là:
Truy vết: là mảng lưu lại đỉnh ngay trước trên hành trình đi qua tập đỉnh và kết thúc tại .
Độ phức tạp: .
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 (kí hiệu là tập tất cả các cặp quan trọng). Đặt là tập hợp tất cả các đỉnh của các cặp nằm trong :
Ta có các nhận xét sau:
- Điều kiện hợp lệ: Nếu , không tồn tại một tuyến đường đơn nào có thể phủ toàn bộ tập cặp .
- Thêm đỉnh bổ trợ: Nếu , tuyến đường đơn phủ có thể ghé qua các đỉnh trong cùng một tập các đỉnh bổ trợ .
Chi phí nhỏ nhất của chỉ một tuyến đường đơn để phủ trọn vẹn tập cặp được định nghĩa bởi hàm , duyệt qua tất cả các tập đỉnh bổ trợ sao cho vẫn thoả mãn điều kiện hợp lệ.
Truy vết: đạt chi phí cực tiểu cho hàm , trong đó là tập đỉnh hoàn chỉnh và 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 là tổng chi phí nhỏ nhất để phủ tập các cặp bằng một hoặc nhiều tuyến đường. Ta có . Với mọi tập cặp khác rỗng, ta tách thành một tập con (do một tuyến đường đơn đảm nhận) và phần còn lại , và tính hàm như sau:
Giá trị chính là kết quả tối ưu của bài toán.
Truy vết: lưu lại tập con các cặ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 .
Độ phức tạp: (do duyệt submask).
Truy vết
Bắt đầu từ tập :
- Dùng để phân rã tập thành các tập con rời nhau .
- Với mỗi , dùng để lấy tập đỉnh đạt giá trị cực tiểu của hàm và đỉnh kết thúc .
- Với mỗi tập và thu được, dùng để đ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.
- Đảo ngược thứ tự đỉnh thu được của các tuyến đường và in ra đáp án.
Code
#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)± 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ó đỉnh, cạnh có trọng số.
Bạn T chọn một hoán vị của các đỉnh từ đến , thể hiện lịch trình vận chuyển hàng gồm chặng, mỗi chặng từ đến . 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 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: , trọng số cạnh không quá .
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 chặng. Do đó, với mỗi chặng, bạn M muốn tìm đường đi từ đi từ đến 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 và 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:
- Nếu ta có một đường đi từ đến có trọng số cạnh lớn nhất là , thì mọi cạnh trên đường đi đó đều có trọng số .
- 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ừ đến là thì:
- Nếu sử dụng các cạnh trọng số thì có thể đi được giữa và .
- Nếu chỉ sử dụng các cạnh trọng số thì không thể đi được giữa và .
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 trọng số nào đó; ta đã xét qua mọi cạnh có trọng số và thử cho chúng vào cây khung. Như vậy, nếu một cạnh trọng số 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ố là không đủ để liên thông và .
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 trong hoán vị của bạn T, bạn M tìm đường đi từ đến 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ị sao cho tổng (trọng số cạnh lớn nhất) trên tất cả các đường đi từ đến 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à , và là hai đỉnh thuộc hai thành phần liên thông khác nhau bị tách ra bởi . Khi đó, cạnh trọng số lớn nhất trên đường đi giữa u và v luôn là . Ta có thể tiếp tục áp dụng nhận xét này một cách đệ quy:
- dùng cạnh lớn nhất chia cây thành hai phần;
- 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
- 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ố nối hai thành phần khác nhau, ta thực hiện các bước:
- tạo một đỉnh mới có trọng số ;
- đặt hai gốc đại diện cho hai thành phần làm hai con trực tiếp của ;
- dùng DSU hợp nhất hai thành phần;
- lấy 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 là số lá trong cây con gốc của KRT, và là trọng số của đỉnh . 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 , gọi hai cây con là và , giả sử .
Nếu , ta tiếp tục đi xuống và coi toàn bộ cây con 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á lá, ta dừng lại tại . Đâ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 ; -
second: các lá trong cây con thứ hai của ; -
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 .
Đặt kích thước của ba nhóm lần lượt là , và . Ta có . Đồng thời, do cách chọn đỉnh dừng, ta có và .
Xét một bước trên đường đi, tại đó ta đi từ đỉnh xuống cây con lớn và tách ra cây con nhỏ . Mọi lá thuộc có LCA với mọi lá thuộc nhánh lớn bằng . Vì vậy, nếu một lá của đượ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 : ..., u, s, v, ... với 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 có thể tạo nhiều nhất hai cặp đi qua phép chia tại . Do đó, toàn bộ cây con đóng góp nhiều nhất
Thuật toán sẽ dựng hoán vị sao cho mọi lá của đề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ỏ , ta cộng 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 , 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á trong other:
- lấy một lá nào đó từ danh sách đang có nhiều phần tử hơn giữa
first vàsecond; - đưa lá đó vào hoán vị;
- đưa tiếp vào hoán vị.
Thứ tự được tạo ra có dạng: trong đó mỗi thuộc cây con của đỉnh dừng, còn mỗ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ó Thật vậy, giả sử . Vì nên .
Trong bước đầu tiên, ta đã lấy đúng 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 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: . Mọi cặp 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 . Mỗi cặp như vậy đóng góp .
Toàn bộ hoán vị có cặp liên tiếp. Trong đó cặp liên tiếp đã được tạo bởi các lá thuộc other (), nên số cặp còn lại là . Vì vậy, tại đỉnh dừng , ta cộng thêm vào đáp án.
Chứng minh tính đúng đắn
Tại mỗi đỉnh trên đường đi xuống, cây con nhỏ có 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 với phần còn lại không thể vượt quá . Chỉ những cặp như vậy mới có thể có LCA bằng . Do đó, đóng góp tại phép chia này không thể vượt quá , Thuật toán đặt mỗi lá của 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 , sau khi dành cặp cho các lá thuộc other, chỉ còn cặp. Trọng số của mỗi cặp này không thể lớn hơn , vì cả hai đầu đều nằm trong cây con của . Thuật toán xen kẽ hai cây con của , khiến tất cả các cặp còn lại có LCA đúng bằng và cùng đóng góp .
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
#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 độ dài () và một số nguyên dương . Dãy được tạo theo công thức:
- với mọi
Cho xâu độ dài 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 :
- '<' nếu
- ‘=’ nếu
- '>' nếu
Yêu cầu: Kiểm tra xem xâu có hợp lệ hay không (tức là có tồn tại xâu nhị phân nào tạo ra này hay không).
- Nếu xâu không hợp lệ, in ra
impossible. - Nếu xâu hợp lệ, với mỗi vị trí (), xác định xem ký tự có cố định (luôn là
0hoặc luôn là1trong mọi xâu thỏa mãn ) 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 và phép chia lấy dư, tồn tại sao cho:
Xét các trường hợp của :
Trường hợp 1: ‘>’
Ta có:
Vì và , ta có . Nếu , bất đẳng thức buộc và , dẫn đến (mâu thuẫn).
Do đó bắt buộc , suy ra khoảng cho phép của là .
Trường hợp 2: ‘<’
Lập luận tương tự, bắt buộc , suy ra khoảng cho phép của là .
Trường hợp 3: ‘=’
Nếu .
Nếu .
Do đó giữ nguyên giá trị từ trạng thái so sánh ngặt ('<' hoặc '>') gần nhất trước đó. Khoảng cho phép khi này thu hẹp thành điểm đơn với .
Như vậy, toàn bộ dãy và khoảng giá trị cho phép của hoàn toàn bị cố định bởi xâu .
Vì , với mỗi khoảng giá trị khả dĩ , giá trị sẽ nằm trong khoảng
Ta có nên . Vì mỗ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ừ đến để xác định tập giá trị có thể đạt được của , giao với khoảng cho phép :
Nếu tại bất cứ thời điểm nào mà , hệ vô nghiệm, in ra impossible.
Chiều ngược: Khởi tạo khoảng hợp lệ ngược tại vị trí . Duyệt từ về , duy trì khoảng giá trị thực sự của từ phía sau. Giao khoảng xuôi và khoảng ta sẽ thu được đoạn giá trị thực sự của :
Ta có mối liên hệ tính chẵn lẻ giữa và :
Giá trị cố định khi và chỉ khi mọi đều cho cùng một tính chẵn lẻ. Từ đó hiển nhiên ta có cố định khi và chỉ khi , suy ra . Nếu không thì nhận giá trị ?.
Để rơi đúng vào đoạn , giá trị trước đó phải thỏa mãn . Ta cập nhật khoảng hợp lệ mới truyền về cho :
Code
#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'; }}