图论专题-学习笔记:次短路与次小生成树
1. 前言
次短路与次小生成树,是由最短路与最小生成树扩展而来的算法。
在往下看之前,请先确保你了解最短路与最小生成树。
难道泥萌没有发现上面两个字对应的模板是不一样的吗qwq
2. 次短路
本文采用 dijkstra 求最短路。
例题:P2865 [USACO06NOV]Roadblocks G
题意简述:已知一张 点 边的图,求出这张图的严格次短路长度。
- 严格次短路:在一张图中,其长度是所有路径中第 2 小的,所有长度相同的路径按照一条计算。
这道题有两种做法:一遍 dijkstra 求出次短路 与 枚举可能边求出次短路。
先讲前面这一种(这种做法笔者没有写代码):
这一种做法的大致思路是在做 dijkstra 的时候同时维护两个 数组, 表示最短路, 表示次短路,然后及时的更新次短路即可。
优点:码量小,速度快。
缺点:细节多,容易考虑漏。
注意例题的数据其实是比较弱的,因为如果用这个方法写,一些假的做法是能过的。
这些做法多多少少都漏掉了一些情况,包括很多题解,而且这些题解都是可以被 hack 的。
但是笔者找不到更好的例题了,因此只能拿这道题。
那么看看后一种做法。
后一种做法的大致思路是首先做两边 dijkstra,分别求出正向图中从 1 号点开始的单源最短路径,反向图中从 号点开始的单源最短路径。
接下来要做的就是枚举所有边。
设当前枚举的边为 ,那么可能的路径有 或者 。
而为了做到次短路,显然 需要采用最短路径。
注意原图是无向图,因此 等价于 。
然后计算一下,在路径长度不为最短路的时候更新答案。
换句话说,我们枚举所有边,要求从 的路径中必须经过这条边。
那么为什么这样做是对的呢?
首先这里有一个事实:次短路与最短路至少有一条边不同。
这个很显然吧qwq
那么因此为了控制这一条边,就需要枚举所有边然后强制经过这条边。
为了防止与最短路重合,需要特判。
那么有的人会问了:会不会有多条边不同呢?此时还能保证正确性吗?
当然可以,而且正确性同样成立。
假设现在的图是这样的:
其中每条边之间可能都有多个点,这只是简化版本。
设 为最短路, 为次短路。
那么这里就有两条边不一样了。
但是在枚举边 的时候,我们需要 的最短路,这个时候路径是 ,这样不就是枚举到 了吗?
更多的情况也是同理的。
实现的时候需要注意去除最短路的情况。
代码:
/*
========= Plozia =========
Author:Plozia
Problem:(次短路模板题)P2865 [USACO06NOV]Roadblocks G
Date:2021/4/20
========= Plozia =========
*/
#include <bits/stdc++.h>
using std::priority_queue;
typedef long long LL;
const int MAXN = 5000 + 10, MAXM = 100000 + 10;
int n, m, Head[MAXN], cnt_Edge = 1, dis[MAXN][2], ans, ans2 = 0x7f7f7f7f;
struct node { int to, val, Next; } Edge[MAXM << 1];
bool vis[MAXN];
struct cmp
{
int x, y;
bool operator <(const cmp &fir) const
{
return y > fir.y;
}
};
int read()
{
int sum = 0, fh = 1; char ch = getchar();
for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
return sum * fh;
}
void add_Edge(int x, int y, int z) { ++cnt_Edge; Edge[cnt_Edge] = (node){y, z, Head[x]}; Head[x] = cnt_Edge; }
int Min(int fir, int sec) { return (fir < sec) ? fir : sec; }
void dijkstra(int op)
{
memset(vis, 0, sizeof(vis));
priority_queue <cmp> q;
if (op == 0) q.push((cmp){1, 0}), dis[1][op] = 0;
else q.push((cmp){n, 0}), dis[n][op] = 0;
while (!q.empty())
{
cmp x = q.top(); q.pop();
if (vis[x.x]) continue ; vis[x.x] = 1;
for (int i = Head[x.x]; i; i = Edge[i].Next)
{
int u = Edge[i].to;
if (dis[u][op] > dis[x.x][op] + Edge[i].val)
{
dis[u][op] = dis[x.x][op] + Edge[i].val;
if (!vis[u]) q.push((cmp){u, dis[u][op]});
}
}
}
}
int main()
{
n = read(), m = read();
memset(dis, 0x7f, sizeof(dis));
for (int i = 1; i <= m; ++i)
{
int x = read(), y = read(), z = read();
add_Edge(x, y, z); add_Edge(y, x, z);
}
dijkstra(0); dijkstra(1); ans = dis[n][0];
for (int i = 2; i <= cnt_Edge; i += 2)
{
int u = Edge[i ^ 1].to, v = Edge[i].to, z = Edge[i].val;
int sum = dis[u][0] + dis[v][1] + z;
if (sum > ans) ans2 = Min(ans2, sum);
sum = dis[u][1] + dis[v][0] + z;
if (sum > ans) ans2 = Min(ans2, sum);
}
printf("%d\n", ans2); return 0;
}
3. 次小生成树
本文采用 Kruskal 算法求最小生成树。
考虑如何做次小生成树。
与次短路一样的,次小生成树与最小生成树至少有一条边不一样,那么我们就可以求出一棵最小生成树,然后强制枚举一条边转移到树上即可。
那么既然枚举了一条边转移到树上,树上肯定要删掉一条边。
假设当前枚举的边是 ,那么我们需要删掉的边是树上 的路径上边权最大的边。
快速查找这一个可以采用类似倍增求解 LCA 的方法或者是树链剖分。
而且实际上我们可以严格证明,次小生成树有且仅有一条边与最小生成树不同。
证明很简单,假设有一棵次小生成树有 条边与最小生成树不同,那么这 条边肯定换出来了 条比这些加进去的边边权更大的边。
那么我们从这里面挑出一条边还原,那么总边权肯定更小,而且这个时候的树仍然不是最小生成树()。
因此,原先树的边权,大于还原了一条边的树的边权,大于最小生成树的边权。
这与原先树是次小生成树矛盾。
因此只能有一条边,证毕。
注意代码里面的细节还是比较多的。
代码(树剖):
/*
========= Plozia =========
Author:Plozia
Problem:P4180 [BJWC2010]严格次小生成树
Date:2021/4/18
========= Plozia =========
*/
#include <bits/stdc++.h>
typedef long long LL;
const int MAXN = 1e5 + 10, MAXM = 3e5 + 10;
int n, m, Head[MAXM], cnt_Edge = 1, fa[MAXN];
LL ans, ans2, Plozia;
struct node1 { int to, Next; LL val; } Edge[MAXM << 1];
struct node2 { int x, y; LL z; int book; } a[MAXM];
int Fa[MAXN], Size[MAXN], Son[MAXN], Top[MAXN];
LL Old_val[MAXN], val[MAXN];
int id[MAXN], dep[MAXN], cnt;
struct node3
{
int l, r;
LL Maxn;
#define l(p) tree[p].l
#define r(p) tree[p].r
#define m(p) tree[p].Maxn
}tree[MAXN << 2];
int read()
{
int sum = 0, fh = 1; char ch = getchar();
for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
return sum * fh;
}
void add_Edge(int x, int y, LL z) { ++cnt_Edge; Edge[cnt_Edge] = (node1){y, Head[x], z}; Head[x] = cnt_Edge; }
LL Min(LL fir, LL sec) { return (fir < sec) ? fir : sec; }
LL Max(LL fir, LL sec) { return (fir > sec) ? fir : sec; }
bool cmp1(const node2 &fir, const node2 &sec) { return fir.z < sec.z; }
namespace Segment_Tree
{
void build(int p, int l, int r)
{
l(p) = l, r(p) = r;
if (l(p) == r(p)) { m(p) = val[l]; return ; }
int mid = (l + r) >> 1;
build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r);
m(p) = Max(m(p << 1), m(p << 1 | 1));
}
LL ask(int p, int l, int r)
{
if (l(p) >= l && r(p) <= r)
{
if (m(p) != Plozia) return m(p);
}
if (l(p) == r(p)) return 0;
int mid = (l(p) + r(p)) >> 1; LL ans = 0;
if (l <= mid) ans = Max(ans, ask(p << 1, l, r));
if (r > mid) ans = Max(ans, ask(p << 1 | 1, l, r));
return ans;
}
}
namespace Union
{
void init() { for (int i = 1; i <= n; ++i) fa[i] = i; }
int gf(int x) { return (fa[x] == x) ? x : fa[x] = gf(fa[x]); }
void hb(int x, int y) { if (gf(x) != gf(y)) fa[fa[x]] = fa[y]; }
}
void dfs1(int now, int Father, int depth)
{
Fa[now] = Father; Size[now] = 1; dep[now] = depth;
for (int i = Head[now]; i; i = Edge[i].Next)
{
int u = Edge[i].to;
if (u == Father) continue ;
dfs1(u, now, depth + 1);
Size[now] += Size[u];
if (Size[u] > Size[Son[now]]) Son[now] = u;
}
}
void dfs2(int now, int Top_father)
{
Top[now] = Top_father; ++cnt; id[now] = cnt; val[cnt] = Old_val[now];
if (!Son[now]) return ; dfs2(Son[now], Top_father);
for (int i = Head[now]; i; i = Edge[i].Next)
{
int u = Edge[i].to;
if (u == Fa[now] || u == Son[now]) continue ;
dfs2(u, u);
}
}
LL ask(int x, int y)
{
LL sum = 0;
while (Top[x] != Top[y])
{
if (dep[Top[x]] < dep[Top[y]]) std::swap(x, y);
LL ans = Segment_Tree::ask(1, id[Top[x]], id[x]);
sum = Max(ans, sum);
x = Fa[Top[x]];
}
if (dep[x] < dep[y]) std::swap(x, y);
if (x != y) sum = Max(sum, Segment_Tree::ask(1, id[y] + 1, id[x]));
return sum;
}
int main()
{
n = read(), m = read();
for (int i = 1; i <= m; ++i) a[i].x = read(), a[i].y = read(), a[i].z = read();
Union::init(); std::sort(a + 1, a + m + 1, cmp1);
int ssum = n;
for (int i = 1; i <= m; ++i)
{
if (Union::gf(a[i].x) == Union::gf(a[i].y)) continue;
ans += a[i].z; Union::hb(a[i].x, a[i].y); --ssum; a[i].book = 1;
add_Edge(a[i].x, a[i].y, a[i].z); add_Edge(a[i].y, a[i].x, a[i].z);
if (ssum == 1) break;
}
dfs1(1, 1, 1);
for (int i = 1; i <= m; ++i)
{
if (!a[i].book) continue ;
int x = a[i].x, y = a[i].y;
if (dep[x] < dep[y]) std::swap(x, y);
Old_val[x] = a[i].z;
}
dfs2(1, 1); Segment_Tree::build(1, 1, n);
ans2 = 0x7f7f7f7f7f7f7f7f;
for (int i = 1; i <= m; ++i)
{
if (a[i].book == 1) continue ;
Plozia = a[i].z;
LL sum = ask(a[i].x, a[i].y);
if (ans - sum + a[i].z > ans) ans2 = Min(ans2, ans - sum + a[i].z);
}
printf ("%lld\n", ans2); return 0;
}
4. 总结
- 次短路:枚举一条边使得路径强制经过这条边。
- 次小生成树:枚举一条边使得生成树强制包含这条边。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具