E - Clique Connect
https://atcoder.jp/contests/abc352/tasks/abc352_e
最小生成樹
先複習一下最小生成樹,這裡用Kruscal
- 生成樹(spanning tree):一個連通無向圖的生成子圖,同時要求是樹。也即在圖的邊集中選擇 \(n - 1\) 條,將所有頂點連通。
- 最小生成(Minimum Spanning Tree,MST):邊權和最小的生成樹。
運用任意一棵最小生成樹一定包含無向圖中權值最小的邊這個結論,對所有邊按權值從小到大排序,貪心加入所有能加入的邊即可。
https://www.luogu.com.cn/problem/P3366
signed main()
{
std::cin.tie(nullptr)->sync_with_stdio(false);
int n, m;
std::cin >> n >> m;
std::vector<std::tuple<int, int, int>> edges(m);//跟正常存邊方式不一樣,因為要對邊排序
for (int i = 0, u, v, w; i < m; i++) {std::cin >> u >> v >> w; --u; --v; edges[i] = {w, u, v};}
ranges::sort(edges);
DSU dsu(n);
i64 ans = 0;//維護最小邊權和
int cnt = 0;//維護生成樹的邊數
for (const auto&[w, u, v] : edges) if (not dsu.same(u, v)) {//如果該點的兩邊不連通,就讓其聯通, 否則說明會成環,只能跳過
dsu.merge(u, v); ans += w; cnt += 1;
}
if (cnt < n - 1) {
std::cout << "orz\n";//說明無解
} else {
std::cout << ans << '\n';
}
return 0;
}
那麼針對這道題,他是給出了很多組邊集,按邊權分類。
如果直接對所有邊暴力跑Kruscal,肯定會T。
注意題目只要求我們求這個圖的最小生成樹。用 Kruskal 求最小生成樹時,用到的核心思想是並查集判斷聯通。我們考慮藉助這個思想,跳過建圖的過程,直接求最小生成樹。
由於每次給定了我們很多點,並要求其中每個點之間都建一條給定權值的邊。所以建一個集合的邊,就相當於把該集合全都併到一個並查集裡。
我們按照邊權從小到大給輸入的集合排個序,這樣每個集合中的點第一次聯通建的就一定是權值最小的邊。我們每在連通塊中加入一個新節點,就在一個累加器中加入該邊的權值。當圖第一次聯通時,輸出該累加器即可。這一部分除了存邊的形式,其他都和 Kruskal 演算法一樣。
signed main() {
std::cin.tie(nullptr)->sync_with_stdio(false);
int n, m;
std::cin >> n >> m;
std::vector<int> k(m), c(m);
std::vector a(m, std::vector<int>());
for (int i = 0; i < m; i++) {
std::cin >> k[i] >> c[i];
a[i].resize(k[i], 0);
for (auto& j : a[i]) {std::cin >> j; --j;}
}
std::vector<int> ord(m);
std::iota(ord.begin(), ord.end(), 0);
ranges::sort(ord, [&](int i, int j){return c[i] < c[j];});
DSU dsu(n);
i64 ans = 0;
int cnt = 0;
for (auto& i : ord) {
for (int j = 1; j < k[i]; j++) if (not dsu.same(a[i][0], a[i][j])) {//隨便選一個,這裡我選的第0個
dsu.merge(a[i][0], a[i][j]); ans += c[i]; cnt += 1;
}
}
std::cout << (cnt == n - 1 ? ans : -1) << '\n';
return 0;
}
F - Estimate Order
https://atcoder.jp/contests/abc352/tasks/abc352_f
好難的狀壓
- 有一個長度為 \(N\) 的排列 \(A_i\),現在給定若干對關係,每對關係形如 \((x,y,c)\),表示 \(A_x-A_y=c\)。
- 對於所有 \(i\in[1,N]\),若 \(A_i\) 只有一種取值,輸出 \(A_i\),否則輸出 \(-1\)。
- \(1\le N\le 16\),保證至少一種合法的序列 \(A_i\)。
容易想到用並查集處理有哪些 \(A_i\) 是有關聯的,具體來說,儲存 \(rank_{i,j}\) 表示若 \(A_j=A_i+rank_{i,j}\),不存在則為 \(0\),這個在並查集時可以暴力維護,複雜度 \(O(N^2)\)。
那麼處理出相關聯的集合後應該怎麼做呢?
注意到 \(N\le 16\),那麼容易想到狀態壓縮,可以用一個 \(16\) 位的整數儲存某集合 \(S\) 中數的相對大小(比如 \(S_i=\{i_1,i_2,i_3,i_4\}\),其中 \(A_{i_2}=A_{i_1}+3,A_{i_3}=A_{i_1}+4,A_{i_4}=A_{i_1}+15\),那麼可以用 \(B_i=1000000000011001\) 表示 \(S\)),然後問題就變成了若干個數 \(1\) 的總數為 \(n\),將它們每個左移若干位,使得或起來恰好為 \(2^N-1\),且不相交。那麼若 \(B_i\) 有且僅有一種左移的位數能在加入其它數後變成一個合法的解,就說明 \(S_i\) 中所有元素均有唯一解,並且 \(A_i\) 的具體值是容易得到的。
於是容易想到 dp,具體來說,設 \(f_{i,msk}\) 表示將前 \(i\) 個數合併起來後能否得到 \(msk\),轉移比較簡單,儲存上一次塞了 \(i-1\) 的所有 \(msk\),然後列舉 \(B_i\) 左移多少位從這些 \(msk\) 轉移即可。
這樣只能求出有無解(但是題目保證有解捏),看起來很蠢啊。但是容易發現這說明 \(f_{N,2^N-1}\) 的所有前驅都能在進行一定操作後變成一個合法的解,那麼我們可以儲存 \(B_i\) 的每種左移的位數能轉移到哪些 \(f_{i,msk}\),再看有哪些左移位數所轉移到的狀態集合中有 \(f_{N,2^N-1}\) 的前驅,就能得到 \(S_i\) 有中的元素是否有唯一解了。
因為每個 \(msk\) 只會出現一次,所以其實可以把 \(i\) 這一維壓掉,複雜度就是 \(O(n^2+n2^n)\),非常可以接受。
signed main()
{
std::cin.tie(nullptr)->sync_with_stdio(false);
int N, M; std::cin >> N >> M;
//維護依賴關係構成的連通塊
std::vector<int> A(M), B(M), C(M);
DSU dsu(N); for (int i = 0; i < M; i++) {std::cin >> A[i] >> B[i] >> C[i]; --A[i]; --B[i]; dsu.merge(A[i], B[i]);}
std::vector g(N, std::vector<std::pair<int, int>>());//用依賴關係建邊構成的圖
for (int i = 0; i < M; i++) {g[A[i]].emplace_back(B[i], -C[i]); g[B[i]].emplace_back(A[i], C[i]);}
std::vector<int> block_masks, state_masks(N), shift(N);
// 處理出每一個選手為最高排名時的連通塊的所有資訊
for (int now = 0; now < N; now++) {
std::vector<int> val(N, -1); val[now] = N;//當前是最高排名
std::vector<int> block_points{now};
//遍歷整個連通塊,更新所有點到當前起點的花費
for (int i = 0; i < SZ(block_points); i++) for (auto&[to, delta] : g[block_points[i]]) if (val[to] == -1) {
val[to] = val[block_points[i]] + delta; block_points.push_back(to);
}
int mn = N * 2; for (auto& x : block_points) {mn = std::min(mn, val[x]);}//維護出整個連通塊的最小价值
for (auto& x : block_points) {state_masks[now] |= (1 << (val[x] - mn));}//狀態用所有點到最小花費的相對值來表示,並壓縮
shift[now] = val[now] - mn;//要左移的距離
if (dsu.find(now) == now) {block_masks.push_back(state_masks[now]);}//是連通塊的祖先,加入dp序列
}
for (int now = 0; now < N; now++) {
std::vector<bool> dp(1 << N); dp[0] = true;
bool skipped = false;
for (auto& now_mask : block_masks) {
if (not skipped and now_mask == state_masks[now]) {skipped = true; continue;}//其中一個狀態是和自己重合的,跳過並只跳過一次
std::vector<bool> ndp(1 << N);
for (int mask = 0; mask < (1 << N); mask++) if (dp[mask]) {
int now_state = now_mask;
while (now_state < (1 << N)) {//左移當前狀態更新狀態
if ((mask & now_state) == 0) {ndp[now_state | mask] = true;}//只要不相交,即與為0,那麼或起來的狀態可以到達
now_state <<= 1;
}
}
dp = ndp;
}
int now_state = state_masks[now];
int add = 0;
std::vector<int> res;
while (now_state < (1 << N)) {
int left = (1 << N) - 1 - now_state;
if (dp[left]) {res.push_back(add + shift[now]);}//如果能到達左移後的位置
now_state <<= 1; add += 1;
}
std::cout << (SZ(res) > 1 ? -1 : res.front() + 1) << " \n"[now == N - 1];
}
return 0;
}