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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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 算法一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 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])) { 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(n2+n2 n)),非常可以接受。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 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]);} } 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 ;} 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 ; }