- 最小生成树
- 1. 算法分析
- 2. 板子
- 2.1 prime算法
- 2.2 kruskal算法
- 3. 典型例题
- 3.1 同时有点权和边权的最小生成树
- 3.2 选定边集最小生成树
- 3.3 最大边最小--生成树/森林
- 3.4 最优比率生成树
- 3.5 寻找存在于所有最小生成树的边
- 3.6 最小生成树恢复成完全图
- 3.7 最小生成森林
- 3.8 最短路径树
- 3.8.1 求最短路径树的数目
- 3.8.2 最短路径树必经边
- 3.9 次小生成树
最小生成树
1. 算法分析
mst性质
-
最小生成树的题目都是无向图;
-
连通图必存在最小生成树;
-
不连通图没有最小生成树(用这个判断是否有最小生成树)
-
任意一颗最小生成树一定可以包含无向图中权值最小的边
-
kruskal枚举的边是递增的,集合的数目递减,具有单调性,因此很多二分的题目利用这个性质就可以省去二分的步骤
-
kruskal能够处理最小生成森林的问题
-
一张图有多少个mst
-
哪些边存在于所有的mst
-
最大边最小的mst是哪个
最大生成树
- 将图中所有边的边权变为相反数,再跑一遍最小生成树算法。相反数最小,原数就最大。
- 对于kruskal,将“从小到大排序”改为“从大到小排序”;
- 对于prim,将“每次选到所有蓝点代价最小的白点”改为“每次选到所有蓝点代价最大的点”。
最短路径树
最短路径树就是以一个节点为根,然后根节点到其他所有点的距离最短,然后形成了一棵树,把不必要的边删除,其实我们用dij的时候求一个点到其他点的距离的时候就已经会把根节点到其他所有点的最短距离求出来了,只是我们不确定是哪些边构成的
假设我们要求的是从1出发的最短路径树,那么我们就先求出最短路并标记用到了的边:
2. 板子
2.1 prime算法
#include
using namespace std;
int const N = 5e2 + 10, M = 1e5 + 10;
int g[N][N], dis[N], st[N];
int n, m;
// prime算法
int prime()
{
memset(dis, 0x3f, sizeof dis); // dis初始化
int res = 0; // 记录最小生成树的距离和
for (int i = 0; i < n; ++i) // 外循环n次
{
int t = -1; // 记录到集合最小的点
for (int j = 1; j <= n; ++j)
if (!st[j] && (t == -1 || dis[j] < dis[t]))
t = j; // 找到t
if (i && dis[t] == 0x3f3f3f3f) return dis[t]; // 如果集合内有点且t点到集合的距离为无穷(即表示t不在连通图内), 不存在最小生成树
if (i) res += dis[t]; // 如果集合内有点,累加res
for (int j = 1; j <= n; ++j) // 更新所有与t连接的点
dis[j] = min(dis[j], g[t][j]);
st[t] = 1; // 把t放入集合
}
return res; // 返回res
}
int main()
{
cin >> n >> m; // 读入顶点和边数
memset(g, 0x3f, sizeof g); // 初始化邻接矩阵
for (int i = 0; i < m; ++i) // 读入边信息
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
g[a][b] = g[b][a] = min(g[a][b], c); // 无向图注意要赋值两次
}
int t = prime();
if (t == 0x3f3f3f3f) cout << "impossible\n";
else cout << t << endl;
return 0;
}
2.2 kruskal算法
#include
using namespace std;
int const N = 1e5 + 10;
int p[N];
struct Edge // 定义边的数据结构,且按权值从小到大排序
{
int a, b, w;
bool operator< (const Edge &W)const
{
return w < W.w;
}
}edge[N * 2];
int n, m;
// 并查集查找操作
int find(int x)
{
if (x != p[x]) p[x] = find(p[x]);
return p[x];
}
// kruskal算法
int kruskal()
{
sort(edge, edge + m); // 排序,使得从最小权值的边开始
int res = 0, cnt = 0; // res记录最小生成树的权值,cnt记录当前最小生成树内有几条边
for (int i = 1; i <= n; ++i) p[i] = i; // 并查集初始化
for (int i = 0; i < m ; ++i) // 枚举每一条边
{
int a = edge[i].a, b = edge[i].b, w = edge[i].w; // 边a, b, 权值为w
a = find(a), b = find(b); // 得到a的父节点和b的父节点
if (a != b) // 判断a和b是否在同一个集合内
{
res += w; // 在的话放入集合内
cnt++;
p[a] = b;
}
}
if (cnt < n -1) return 0x3f3f3f3f; // 最小生成树内边数如果小于n-1,说明无法得到最小生成树
else return res;
}
int main()
{
scanf("%d%d", &n, &m); // 输入顶点和边数目
for (int i = 0; i < m; ++i) // 输入边信息
{
int a, b ,w;
scanf("%d %d %d", &a, &b, &w);
edge[i] = {a, b, w};
}
int t = kruskal();
if (t == 0x3f3f3f3f) cout << "impossible\n";
else cout << t << endl;
return 0;
}
3. 典型例题
3.1 同时有点权和边权的最小生成树
acwing1146新的开始时
题意: 发展采矿业当然首先得有矿井,小 F 花了上次探险获得的千分之一的财富请人在岛上挖了 n 口矿井,但他似乎忘记了考虑矿井供电问题。
为了保证电力的供应,小 F 想到了两种办法:
- 在矿井 i 上建立一个发电站,费用为 vi(发电站的输出功率可以供给任意多个矿井)。
- 将这口矿井 i 与另外的已经有电力供应的矿井 j 之间建立电网,费用为 pi,j。
1≤n≤300,0≤vi,pi,j≤105
题解: 本题有点权有边权,考虑把点权转化为边权,只需要把虚拟出一个节点n+1,把每个点和虚拟节点连接一条边,这条边的权值为点的权值,然后跑最小生成树即可
代码:
#include
using namespace std;
int const N = 3e2 + 10;
int n;
int g[N][N], st[N], dis[N];
int prime() {
int res = 0;
memset(st, 0, sizeof st);
memset(dis, 0x3f, sizeof dis);
for (int i = 0; i < n + 1; ++i) {
int t = -1;
for (int j = 1; j <= n + 1; ++j)
if (!st[j] && (t == -1 || dis[t] > dis[j])) t = j;
st[t] = 1;
if (i) res += dis[t];
for (int j = 1; j <= n + 1; ++j) dis[j] = min(dis[j], g[t][j]);
}
return res;
}
int main() {
cin >> n;
// 读边,建立与虚拟节点的边
for (int i = 1; i <= n; ++i) {
int t;
cin >> t;
g[i][n + 1] = t;
g[n + 1][i] = t;
}
for (int i = 1 ; i <= n; ++i)
for (int j = 1; j <= n; ++j)
cin >> g[i][j];
cout << prime() << endl;
return 0;
}
小 FF 希望你帮他想出一个保证所有矿井电力供应的最小花费方案。
3.2 选定边集最小生成树
acwing1143联络员
题意: n个点m条边的无向图,其中m条边分为两个集合,集合1必须出现在最小生成树中,集合2中的边不一定出现在最小生成树中。求出一个最小生成树。1≤n≤2000,1≤m≤10000
题解: 先把集合1放入最小生成树,然后再判断集合2的边是否能够放入即可
代码:
#include
using namespace std;
int const N = 2e3 + 10;
struct Edge {
int a, b, w;
bool operator< (const Edge &W) {
return w < W.w;
}
}edge[N];
int fa[N];
int n, m;
int get(int x) {
if (x == fa[x]) return x;
return fa[x] = get(fa[x]);
}
int main() {
cin >> n >> m;
vector must, no_must;
for (int i = 0; i < m; ++i) {
int p, a, b, w;
scanf("%d%d%d%d", &p, &a, &b, &w);
if (p == 1) must.push_back({a, b, w});
else no_must.push_back({a, b, w});
}
for (int i = 1; i <= n; ++i) fa[i] = i;
// 加入必须边(同时直接达到缩点效果)
int res = 0;
for (auto m: must) {
int a = m.a, b = m.b, w = m.w;
int pa = get(a), pb = get(b);
res += w;
if (pa != pb) {
fa[pa] = pb;
}
}
// 加入非必选边
sort(no_must.begin(), no_must.end());
for (auto nm: no_must) {
int a = nm.a, b = nm.b, w = nm.w;
int pa = get(a), pb = get(b);
if (pa != pb) {
fa[pa] = pb;
res += w;
}
}
printf("%d\n", res);
return 0;
}
3.3 最大边最小--生成树/森林
acwing1142繁忙的都市
题意: 一张n个点m条边的无向图,找出一个最大边权最小的最小生成树。1≤n≤300,1≤m≤8000,1≤边权c≤10000
题解: 本题要求最大边值最小的生成树。由于kruskal保证了在枚举边的时候是按照边权从小到大来枚举的,因此一旦产生最小生成树时就保证了当前边的是生成树的最大边,且这条边在所有的情况下最小
代码:
#include
using namespace std;
int const N = 8e3 + 10;
int n, m;
struct Edge {
int a, b, w;
bool operator< (const Edge &W) {
return w < W.w;
}
}edge[N];
int fa[N];
int get(int x) {
if (x == fa[x]) return x;
return fa[x] = get(fa[x]);
}
void kruskal() {
int res = 0, cnt = 0;
sort(edge, edge + m);
for (int i = 1; i <= n; ++i) fa[i] = i;
for (int i = 0; i < m; ++i) {
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
int pa = get(a), pb = get(b);
if (pa != pb) {
res = max(res, w);
fa[pa] = pb;
cnt ++;
}
if (cnt == n - 1) break;
}
cout << n - 1 << " " << res << endl;
}
int main() {
cin >> n >> m;
for (int i = 0; i < m; ++i) {
int a, b, w;
scanf("%d %d %d", &a, &b, &w);
edge[i] = {a, b, w};
}
kruskal();
return 0;
}
acwing1145北极通讯网络
题意: 北极的某区域共有 n 座村庄,每座村庄的坐标用一对整数 (x,y) 表示。为了加强联系,决定在村庄之间建立通讯网络,使每两座村庄之间都可以直接或间接通讯。通讯工具可以是无线电收发机,也可以是卫星设备。无线电收发机有多种不同型号,不同型号的无线电收发机有一个不同的参数 d,两座村庄之间的距离如果不超过 d,就可以用该型号的无线电收发机直接通讯,d 值越大的型号价格越贵。现在要先选择某一种型号的无线电收发机,然后t统一给所有村庄配备,数量不限,但型号都是 相同的。配备卫星设备的两座村庄无论相距多远都可以直接通讯,但卫星设备是有限的,只能给一部分村庄配备。现在有 k 台卫星设备,请你编一个程序,计算出应该如何分配这 k 台卫星设备,才能使所配备的无线电收发机的 d 值最小。
题解: 把本题翻译过来就是求给定一个d,删去图中权值大于d的所有边,做最小生成树,得到的连通块数目要求小于等于k,求这么个d
通常的思路是二分d,然后求连通块个数,判断连通块个数和k的关系
但是kruskal具有单调性,枚举的边从小到大,集合的数目从大到小,具有单调性,因此不需要二分,只需要每次枚举完判断一下当前的集合个数是否等于k,如果等于k,那么说明当前放入的这条边的权值就是d。
代码:
#include
using namespace std;
typedef pair PDD;
unordered_map grid;
int n, k, cnt, m;
int const N = 5e2 + 10, M = N * N;
int fa[N];
struct Edge {
int a, b;
double w;
bool operator< (const Edge &W) {
return w < W.w;
}
}edge[M];
int get(int x) {
if (x == fa[x]) return x;
return fa[x] = get(fa[x]);
}
void kruskal() {
for (int i = 1; i <= cnt; ++i) fa[i] = i;
int cnt_con = cnt; // 记录连通块个数
if (cnt_con < k) {
cout << 0.00 << endl;
return;
}
// 从小到大枚举边
sort(edge, edge + m);
for (int i = 0; i < m; ++i) {
int a = edge[i].a, b = edge[i].b;
double w = edge[i].w;
int pa = get(a), pb = get(b);
if (pa != pb) {
fa[pa] = pb;
cnt_con--; // 每次加入边后,如果两个端点不在一个连通块内,那么合并后连通块个数将会减一
}
if (cnt_con == k) { // 一旦减到k,说明当前的边权值即为d
printf("%.2lf", w);
return;
}
}
}
int main() {
cin >> n >> k;
// 给每个节点标号
for (int i = 0; i < n; ++i) {
int x, y;
cin >> x >> y;
grid[++cnt] = {x, y};
}
// 计算每个节点间的距离
for (int i = 1; i <= cnt; ++i)
for (int j = 1; j <= cnt; ++j) {
int dx = grid[i].first - grid[j].first, dy = grid[i].second - grid[j].second;
edge[m++] = {i, j, sqrt(dx * dx + dy * dy)};
}
// 做最小生成树
kruskal();
return 0;
}
3.4 最优比率生成树
acwing348 沙漠之王
题意: 大卫希望渠道的总成本和总长度的比值能够达到最小。他只希望建立必要的渠道,为所有的村庄提供水资源,这意味着每个村庄都有且仅有一条路径连接至首都。他的工程师对所有村庄的地理位置和高度都做了调查,发现所有渠道必须直接在两个村庄之间水平建造。由于任意两个村庄的高度均不同,所以每个渠道都需要安装一个垂直的升降机,从而使得水能够上升或下降。建设渠道的成本只跟升降机的高度有关,换句话说只和渠道连接的两个村庄的高度差有关。需注意,所有村庄(包括首都)的高度都不同,不同渠道之间不能共享升降机。
题解: 本题是最优比率生成树
要求找出一棵(a1/b1) + (a2/b2) +... + (an/bn)之和最大生成树
考察的是01分数规划模型,即我们设(a1/b1) + (a2/b2) +... + (an/bn) = mid。那么对应于每一条边我们可以得到一条新边ai-mid * bi,采用二分的方式枚举mid,如果得到的使用新边ai-mid * bi建成的最小生成树的权值之和为0,那么这个mid就是我们的答案;否则,找其他的mid
代码:
#include
using namespace std;
int const N = 2e3 + 10;
int n;
double dis[N], d[N][N], h[N][N]; // dis记录到最小生成树的最小距离,d数组记录两个点的最小距离,h数组记录两个点的最小高度
double x[N], y[N], z[N]; // 记录每个点输入的位置
bool vis[N]; // 判断每个点是否在最小生成树内
// prime算法查找新边建立的最小生成树是否满足条件
bool prime (double mid)
{
memset(dis, 0x3f, sizeof dis);
memset(vis, 0, sizeof vis);
double sum = 0; // 最小生成树的边权值和
vis[1] = 1;
// 把1号点放入集合后,计算和1号点相邻的所有点的距离
for (int i = 2; i <= n; ++i)
dis[i] = h[1][i] - mid * d[1][i];
for (int i = 2; i <= n; ++i)
{
// 找出到最小生成树距离最小的那个点
double mini = 0x3f3f3f3f;
int u = -1;;
for (int j = 2; j <= n; ++j)
{
if (!vis[j] && dis[j] < mini)
{
mini = dis[j], u = j;
}
}
// 放入最小生成树内
vis[u] = 1;
sum += dis[u];
// 更新所有与u点相邻的点
for (int j = 2; j <= n; ++j)
{
if (!vis[j] && dis[j] > h[u][j] - mid * d[u][j])
dis[j] = h[u][j] - mid * d[u][j]; // 用广义边更新
}
}
// 判断是否满足条件
if (sum >= 0) return false; // mid取太小
else return true;
}
int main()
{
while (scanf("%d", &n) != EOF && n)
{
memset(x, 0, sizeof x);
memset(y, 0, sizeof y);
memset(z, 0, sizeof z);
memset(d, 0, sizeof d);
memset(h, 0, sizeof h);
for (int i = 1; i <= n; ++i)
{
cin >> x[i] >> y[i] >> z[i];
// 建立一张完全图
for (int j = 1; j < i; ++j)
{
d[i][j] = d[j][i] = sqrt((x[i] - x[j]) * (x[i] - x[j]) + (y[i] - y[j]) * (y[i] - y[j])); // 记录i点和前j个点的距离
h[i][j] = h[j][i] = fabs(z[i] - z[j]); // 记录高度差
}
}
// 01分数规划,二分查找答案
double l = 0, r = 1000.0, mid;
while (r - l > 1e-6)
{
mid = (l + r) / 2;
if (prime(mid)) r = mid; // 完全图采用prime算法
else l = mid;
}
printf("%.3f\n", mid);
}
return 0;
}
3.5 寻找存在于所有最小生成树的边
2014-2015 ACM-ICPC, Asia Tokyo Regional Contest F.There is No Alternative
题意: 找出存在于所有最小生成树的边,打印出这些边的数目和权值和。3≤点数N≤500,N−1≤边数M≤min(50000,N(N−1)/2), 1≤边权Ci≤10000
题解: 可以先用kruskal求出该图的最小生成树,在求的过程中标记上存在于最小生成树的边。由于存在于所有最小生成树的边必然存在于某棵最小生成树上,因此可以暴力枚举每条边,然后删除这条边,判断是否还能构成最小生成树,或者构成的最小生成树比最早得到的最小生成树大
代码:
#include
using namespace std;
int n, m;
int const N = 5e4 + 10;
struct E {
int u, v, w;
bool operator<(const E &ed) {
return w < ed.w;
}
}edge[N];
int p[N], st[N];
vector used;
int find(int x) {
if (x == p[x]) return x;
return p[x] = find(p[x]);
}
// kruskal算法
int kruskal(int cn)
{
int res = 0, cnt = 0; // res记录最小生成树的权值,cnt记录当前最小生成树内有几条边
for (int i = 1; i <= n; ++i) p[i] = i; // 并查集初始化
for (int i = 1; i <= m ; ++i) // 枚举每一条边
{
if (st[i]) continue;
int a = edge[i].u, b = edge[i].v, w = edge[i].w; // 边a, b, 权值为w
a = find(a), b = find(b); // 得到a的父节点和b的父节点
if (a != b) // 判断a和b是否在同一个集合内
{
res += w; // 在的话放入集合内
if (cn == -1) used.push_back(i);
cnt++;
p[a] = b;
}
}
if (cnt < n -1) return -1;
else return res;
}
int main() {
memset(st, 0, sizeof st);
cin >> n >> m;
for (int i = 1, a, b, c; i <= m; ++i) {
scanf("%d%d%d", &a, &b, &c);
edge[i] = {a, b, c};
}
sort(edge + 1, edge + 1 + m);
int mst = kruskal(-1); // 最早的最小生成树
vector res;
int sum = 0;
for (int i = 0; i < used.size(); ++i) { // 暴力枚举删除的边
memset(st, 0, sizeof st);
int idx = used[i]; // 边的下标
st[idx] = 1; // 该边不能使用
int new_mst = kruskal(i);
if (new_mst != mst || new_mst == -1) { // 如果无法生成最小生成树或者生成的最小生成树大于原先的最小生成树
res.push_back(i);
sum += edge[idx].w;
}
}
cout << res.size() << " " << sum;
return 0;
}
3.6 最小生成树恢复成完全图
acwign346走廊泼水节
题意: 给定一棵N个节点的树,要求增加若干条边,把这棵树扩充为完全图,并满足图的唯一最小生成树仍然是这棵树。求增加的边的权值总和最小是多少。注意: 树中的所有边权均为整数,且新加的所有边权也必须为整数。1≤N≤6000,1≤Z≤100
题解: 要把一个最小生成树补成完全图,同时保证完全图的最小生成树不变。考虑kruskal的过程,每次把不同集合的连边s加入最小生成树,那么把这两个集合的其他边互联,只要互联的边的权值大于s就能保证这边边不会进入最小生成树,同时kruskal按照边权从小到大枚举保证了这样得到的完全图的边权和最小
算法步骤:
1.从小到大枚举每条边
2.一旦某条边的两点属于不同的集合,设这条边权值为w,那么这条边属于最小生成树,这两个集合的其他连边属于外部的完全图,
因此答案加上(size[a] * size[b] - 1) * (w + 1)
代码:
#include
using namespace std;
int const N = 6e3 + 10;
struct Edge {
int a, b, w;
bool operator< (const Edge &W) {
return w < W.w;
}
}edge[N * N / 2];
int fa[N], sizes[N];
int n;
int get(int x) {
if (x == fa[x]) return x;
return fa[x] = get(fa[x]);
}
int main() {
int t;
cin >> t;
while (t--) {
// 读边
cin >> n;
for (int i = 0; i < n - 1; ++i) {
int a, b, c;
cin >> a >> b >> c;
edge[i] = {a, b, c};
}
// 初始化
for (int i = 1; i <= n; ++i) fa[i] = i, sizes[i] = 1;
sort(edge, edge + n - 1);
// 枚举每条边
int res = 0;
for (int i = 0; i < n - 1; ++i) {
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
int pa = get(a), pb = get(b);
if (pa != pb) { // 一旦属于不同的集合
res += (sizes[pa] * sizes[pb] - 1) * (w + 1); // 把集合中除了edge[i]的其他边连起来
sizes[pb] += sizes[pa];
fa[pa] = pb;
}
}
printf("%d\n", res);
}
return 0;
}
3.7 最小生成森林
acwing1141 局域网
题意: 一张n个点k条边的无向图,可能不是联通的,记点i到点j的边权为f(i, j)。让你删除掉一些边,使得原来互相连通的点还是互相联通。求出删除的边的权值最大为多少。1≤n≤1000≤k≤200,1≤f(i,j)≤1000
题解: kruskal在计算过程中生成的最小生成森林一直都是最优的,而prime算法没有这个特性。因此可以使用kruskal算法来得到最小生成森林(换言之,就是把最小生成树去掉几条边)
代码:
#include
using namespace std;
int const N = 2e2 + 10;
int n, k;
struct Edge {
int a, b, w;
bool operator< (const Edge &W) {
return w < W.w;
}
}edge[N * 2];
int fa[N];
int get(int x) {
if (x == fa[x])return x;
return fa[x] = get(fa[x]);
}
int kruskal() {
for (int i = 1; i <= n; ++i) fa[i] = i;
int res = 0;
for (int i = 0; i < k; ++i) {
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
int pa = get(a), pb = get(b);
if (pa != pb) {
res += w;
fa[pa] = pb;
}
}
return res;
}
int main() {
int sum = 0;
cin >> n >> k;
for (int i = 0; i < k; ++i) {
int a, b, w;
scanf("%d %d %d", &a, &b, &w);
edge[i] = {a, b, w};
sum += w;
}
sort(edge, edge + k);
cout << sum - kruskal() << endl;
return 0;
}
3.8 最短路径树
3.8.1 求最短路径树的数目
acwing349 黑暗城堡
题意: 计算一张n个点m条边的无向图,以1号点为顶点的最短路径树有多少个。2≤点数N≤1000,N−1≤边数M≤N(N−1)/2,1≤边权L≤100
题解: 先做dijkstra算法,求出每个点到1号点的最短路径。然后计数到达每个点的最短路径条数,最后的答案就是到达每个点的最短路径条数的累乘。
代码:
#include
using namespace std;
int const N = 1e3 + 10;
int g[N][N], dis[N];
int n, m;
int cnt[N]; // 记录到达每个点的最短路径条数
bool vis[N];
// 计算每个点的最短路径
void dijkstra() {
memset(dis, 0x3f, sizeof dis);
dis[1] = 0;
for (int i = 1; i <= n; ++i) {
int t = -1;
for (int j = 1; j <= n ; ++j) {
if ( !vis[j] && (t == -1 || dis[j] < dis[t]))
t = j;
}
// 更新
vis[t] = 1;
for (int j = 1; j <= n; ++j)
dis[j] = min(dis[j], dis[t] + g[t][j]);
}
return;
}
int main() {
cin >> n >> m;
memset(g, 0x3f, sizeof g);
for (int i = 1; i <= m; ++i) {
int a, b, c;
scanf("%d %d %d", &a, &b, &c);
g[a][b] = g[b][a] = min(g[a][b], c);
}
// dijkstra得到最短路径
dijkstra();
// 遍历每个点,判断它的延伸点是否在最短路径中
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
if (g[i][j] != 0x3f3f3f3f && dis[j] == dis[i] + g[i][j])
cnt[j]++; // 计数到达每个点的最短路径条数
}
}
// 统计答案,把每个点的次数相乘
long long ans = 1;
for (int i = 1; i <= n; ++i) if (cnt[i]) ans = ( ans * cnt[i] ) % (2147483647);
cout << ans << endl;
return 0;
}
3.8.2 最短路径树必经边
题意: n个城市用m条双向公路连接,使得任意两个城市都能直接或间接地连通。其中城市编号为1..n,公路编号为1..m。任意个两个城市间的货物运输会选择最短路径,把这n*(n-1)条最短路径的和记为S。现在你来寻找关键公路r,公路r必须满足:当r堵塞之后,S的值会变大(如果r堵塞后使得城市u和v不可达,则S为无穷大)。
题解: 说白了就是求有多少条边是所有最短路的必经边。对于一个点u,如果删去的边不在它到其它点的最短路上,那么S是不会变的。考虑到两点之间的最短路不止一条,我们可以给每个点先建出一棵最短路径树出来,然后枚举最短路径树上的每条边并把它删掉,再跑一遍最短路,看S是否变大即可。如果最短路用dijkstra+heap来做,这个过程的时间复杂度就是:O(N∗((N+M)log(N+M)+N∗(N+M)log(N+M)))
代码:
#include
#include
#include
#include
#include
#define maxn 101
#define maxm 3001
using namespace std;
vector to[maxn],w[maxn],id[maxn];
int dis[maxn],treeid[maxn];
bool vis[maxn],lzs[maxm];
int n,m;
inline int read(){
register int x(0),f(1); register char c(getchar());
while(c<'0'||'9',vector< pair >,greater< pair > > q;
memset(vis,false,sizeof vis),memset(dis,0x3f,sizeof dis);
q.push(make_pair(0,s)),dis[s]=0;
while(q.size()){
int u=q.top().second; q.pop();
if(vis[u]) continue; vis[u]=true;
for(register int i=0;idis[u]+w[u][i]){
dis[v]=dis[u]+w[u][i],q.push(make_pair(dis[v],v));
if(!del) treeid[v]=id[u][i];
}
}
}
}
inline void out(int a){
if(a>=10)out(a/10);
putchar(a%10+'0');
}
int main(){
n=read(),m=read();
memset(dis,0x3f,sizeof dis);
for(register int i=1;i<=m;i++){
int u=read(),v=read(),_w=read();
to[u].push_back(v),w[u].push_back(_w),id[u].push_back(i);
to[v].push_back(u),w[v].push_back(_w),id[v].push_back(i);
}
for(register int i=1;i<=n;i++){
dijkstra(i,0);
int sum=0; for(register int j=1;j<=n;j++) sum+=dis[j];
for(register int j=1;j<=n;j++) if(treeid[j]&&!lzs[treeid[j]]){
dijkstra(i,treeid[j]);
int cnt=0; for(register int k=1;k<=n;k++) cnt+=dis[k];
if(cnt>sum) lzs[treeid[j]]=true;
}
}
bool flag=false;
for(register int i=1;i<=m;i++) if(lzs[i]){
out(i),putchar('\n');
}
return 0;
}
3.9 次小生成树
acwing356 次小生成树
题意: 给定一张 N 个点 M 条边的无向图,求无向图的严格次小生成树。设最小生成树的边权之和为sum,严格次小生成树就是指边权之和大于sum的生成树中最小的一个。N≤10^5^,M≤3∗10^5^
题解: 本题的思路是在求出最小生成树的基础上,找出一条非树边a->b,然后再树上找出a->b的最大值,删除这个最大值,加上非树边。基于这个思路,目标就是要找出这个a->b在树边的最大值。找出a->b在树边的最大值,可以先在树上进行预处理,\(fa[i][j]\)表示i向上走\(2^j\)步到达的点,\(d1[i][j]\)表示i向上走\(2^j\)步范围内的最大值,\(d2[i][j]\)表示i向上走\(2^j\)步范围内的次大值,然后每次在找lca时顺便找出,x到lca的最大值和次大值,y到lca的最大值和次大值,比较即可
代码:
#include
using namespace std;
typedef long long LL;
const int N = 100010, M = 300010, INF = 0x3f3f3f3f;
int n, m;
struct Edge {
int a, b, w;
bool used;
bool operator< (const Edge &t) const {
return w < t.w;
}
}edge[M];
int p[N];
int h[N], e[M], w[M], ne[M], idx;
int depth[N], fa[N][17], d1[N][17], d2[N][17];
int q[N];
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 找最小生成树
LL kruskal() {
for (int i = 1; i <= n; i ++ ) p[i] = i;
sort(edge, edge + m);
LL res = 0;
for (int i = 0; i < m; i ++ ) {
int a = find(edge[i].a), b = find(edge[i].b), w = edge[i].w;
if (a != b) {
p[a] = b;
res += w;
edge[i].used = true;
}
}
return res;
}
// 建树
void build() {
memset(h, -1, sizeof h);
for (int i = 0; i < m; i ++ )
if (edge[i].used) {
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
add(a, b, w), add(b, a, w);
}
}
// 预处理fa,d1,d2,depth
void bfs() {
memset(depth, 0x3f, sizeof depth);
depth[0] = 0, depth[1] = 1;
q[0] = 1; // 把1当成根节点
int hh = 0, tt = 0;
while (hh <= tt) {
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
if (depth[j] > depth[t] + 1) {
depth[j] = depth[t] + 1; // 更新j的深度
q[ ++ tt] = j;
fa[j][0] = t;
d1[j][0] = w[i], d2[j][0] = -INF; // 求出d1和d2
for (int k = 1; k <= 16; k ++ ) {
int anc = fa[j][k - 1];
fa[j][k] = fa[anc][k - 1];
int distance[4] = {d1[j][k - 1], d2[j][k - 1], d1[anc][k - 1], d2[anc][k - 1]};
d1[j][k] = d2[j][k] = -INF;
for (int u = 0; u < 4; u ++ ) {
int d = distance[u];
if (d > d1[j][k]) d2[j][k] = d1[j][k], d1[j][k] = d;
else if (d != d1[j][k] && d > d2[j][k]) d2[j][k] = d;
}
}
}
}
}
}
// 找出a和b的lca,顺便求出a到b之间的最大值和次大值
int lca(int a, int b, int w) {
static int distance[N * 2];
int cnt = 0;
if (depth[a] < depth[b]) swap(a, b);
// 把a和b拉到同一个深度
for (int k = 16; k >= 0; k -- )
if (depth[fa[a][k]] >= depth[b]) {
distance[cnt ++ ] = d1[a][k];
distance[cnt ++ ] = d2[a][k];
a = fa[a][k];
}
// 把a和b之间的d1和d2的所有备选项求出来
if (a != b) {
for (int k = 16; k >= 0; k -- )
if (fa[a][k] != fa[b][k]) {
distance[cnt ++ ] = d1[a][k];
distance[cnt ++ ] = d2[a][k];
distance[cnt ++ ] = d1[b][k];
distance[cnt ++ ] = d2[b][k];
a = fa[a][k], b = fa[b][k];
}
distance[cnt ++ ] = d1[a][0];
distance[cnt ++ ] = d1[b][0];
}
// 把a和b之间的d1和d2求出来
int dist1 = -INF, dist2 = -INF;
for (int i = 0; i < cnt; i ++ ) {
int d = distance[i];
if (d > dist1) dist2 = dist1, dist1 = d;
else if (d != dist1 && d > dist2) dist2 = d;
}
if (w > dist1) return w - dist1;
if (w > dist2) return w - dist2;
return INF;
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 0; i < m; i ++ ) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
edge[i] = {a, b, c};
}
LL sum = kruskal(); // 计算最小生成树的值
build(); // 把所有的树边建树
bfs(); // 预处理出d1,d2,fa,depth数组
LL res = 1e18;
for (int i = 0; i < m; i ++ )
if (!edge[i].used) { // 找出每条非树边,然后替换掉最大的那条树边
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
res = min(res, sum + lca(a, b, w));
}
printf("%lld\n", res);
return 0;
}