CF_GYM_105930_H
对于树来说,创造一颗树,比优化一棵树要简单得多。
如下面这道题:CF_GYM_105930_H 最小生成树, 这题就是变量下,最优化最小生成树,一般来说,我们很容易想到两种思路,一种是先做一遍最小生成树,然后再利用这 \(k\) 条边优化,但是稍微一试,就容易发现,这并不简单,一方面是要优化哪一条边,其次要用哪条边去优化,而且这个最小生成树之外的边是否还需要使用?这些使得我们,通过 “优化” 的方式,很难求出这棵树,于是可以考虑构造这棵树。
(其实这部分有点优化的意思)。从集合方面来考虑,如果目前有且只有两个联通块(整体思想,特殊情况),此时只需要从两个联通块里面各取一个点连接即可,可以发现,对于联通来说,我们并不关心是哪两个点构成了联通,因此是任意的两个点,又因为是最优化,所以我们很自然的会选择尽量接近的两个点连接,那么这时候有个问题,这两个联通块里面是否一定存在两个点 \(a,b\),使得 \(|a - b| = 1\)?而这点是一定的,假如一个联通块里面有 1,那么 2,要么再这个联通块,要么在另一个,如果在另一个联通块,那么就有 \(|a - b| = 1\),如果在这个联通块,那么 3 的话要么在这个联通块,要么在另一个 ,你会发现是递推的 ,推到最后,因为是两个连通快,所以一定会有一个在另一个联通块,形成 \(|a - b| = 1\)。也就是说,对于两个联通块,我们一定可以找出来一条边权为 \(1\) 的边来联通这个块,那么如果有3个联通块 \(a,b,c\),那么我们可以把 \(b,c\),看成一个块 \(d\),那么就可以把 \(a\) 和 \(d\),接下来同理联通 \(b,c\),最终就可以把三个给连接到一块。也就是说,我们一定可以用边权为 \(1\) 的边把整个森林给联通,也就是说,我这 \(k\) 条边,如果能用到的话,一定都是 \(1\) 边权。那么知道这点就简单了。
我们考虑去构造这个棵树,kruscal 的原理是贪心,其本质是吧边排序后能加进去,就加进去,不能加就跳过。那么我们可以先给现有边排序,把边权小于等于 1 的边直接进行 kruscal,那么剩下来的原有边的边权一定大于 \(1\)。
按照 kruscal 来说,我们接下里应该去枚举加入 \(k\) 条 \(1\) 权边,但是这样搞的话,最后有可能无法形成一颗树,为了保障生成树,这 \(k\) 条 \(1\) 权边需要最后加入,“补全” 联通块。
接下来的话我们给这 \(k\) 条可用边留出来位置(因为原有的边有 \(0\) 权和 \(1\) 权边,可能最后我们用不完这 k 条)。然后对剩下的边进行 kruscal。搞完后对 \(1\) 权的再做 kruscal 就可以了。看看代码应该会很清晰。
/*
对于树来说,创造树,远远比优化树要来的方便的多
*/
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
typedef long long LL;
const int N = 200010, M = N * 2;
int n, m, k;
int p[N];
vector<int> res;
LL sum;
struct Node
{
int a, b, w, id;
bool operator<(const Node &W)const
{
return w < W.w;
}
}edges[N], eddge[N];
int find(int x)
{
if (x != p[x]) p[x] = find(p[x]);
return p[x];
}
void kruscal(int len)
{
int cnt = 0;
for (int i = 1; i <= m; i ++ )
{
if (cnt == len) break;
auto e = edges[i];
int pa = find(e.a), pb = find(e.b);
if (pa == pb) continue;
cnt ++ ;
p[pb] = pa;
res.push_back(e.id);
sum += e.w;
}
}
int main()
{
int T;
cin >> T;
while (T -- )
{
res.clear();
sum = 0;
cin >> n >> m >> k;
for (int i = 1; i <= n; i ++ ) p[i] = i;
for (int i = 1; i <= m; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
edges[i] = {a, b, c, i};
}
for (int i = 1; i < n; i ++ )
{
eddge[i] = {i, i + 1, 1};
}
sort(edges + 1, edges + 1 + m);
// 处理小于等于1的
for (int i = 1; i <= m; i ++ )
{
auto e = edges[i];
if (e.w > 1) break;
int pa = find(e.a), pb = find(e.b);
if (pa == pb) continue;
p[pb] = pa;
res.push_back(e.id);
sum += e.w;
}
//处理剩余边
kruscal(max(n - 1 - k - (int)res.size(), 0));
// 处理自选边
int len = n - 1 - (int)res.size(), cnt = 0;
cout << len << '\n';
for (int i = 1; i < n; i ++ )
{
if (res.size() == n - 1) break;
if (cnt == k) break;
// cout << 'a';
auto e = eddge[i];
// if (e.w != 0) break;
int pa = find(e.a), pb = find(e.b);
if (pa == pb) continue;
p[pb] = pa;
cnt ++ ;
res.push_back(m + cnt);
sum += e.w;
cout << e.a << ' ' << e.b << '\n';
}
cout << sum << '\n';
for (int i : res) cout << i << ' ';
puts("");
}
return 0;
}
启发
对于树来说,创造远远比调整优化要简单的多,kruscal 的贪心虽然简单,但其实包含了很大的智慧啊。而整体看待和特殊化的思想也非常重要,这是突破桎梏的关键。比赛中大模拟数据还是不太现实的,整体的话会发现小的零碎的数据所带不出来的惊喜。

浙公网安备 33010602011771号