E - Clique Connect
https://atcoder.jp/contests/abc352/tasks/abc352_e
最小生成树
先复习一下最小生成树,这里用Kruscal
- 生成树(spanning tree):一个连通无向图的生成子图,同时要求是树。也即在图的边集中选择 \(n - 1\) 条,将所有顶点连通。
- 最小生成(Minimum Spanning Tree,MST):边权和最小的生成树。
运用任意一棵最小生成树一定包含无向图中权值最小的边这个结论,对所有边按权值从小到大排序,贪心加入所有能加入的边即可。
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;
}
标签:std,int,dsu,cin,ABC352,vector,block
From: https://www.cnblogs.com/kdlyh/p/18248479