图论专题-学习笔记:次短路与次小生成树

图论专题-学习笔记:次短路与次小生成树

1. 前言

次短路与次小生成树,是由最短路与最小生成树扩展而来的算法。

在往下看之前,请先确保你了解最短路与最小生成树。

没有学过建议左转洛谷,右转百度搜索。

难道泥萌没有发现上面两个字对应的模板是不一样的吗qwq

2. 次短路

本文采用 dijkstra 求最短路。

例题:P2865 [USACO06NOV]Roadblocks G

题意简述:已知一张 n n n m m m 边的图,求出这张图的严格次短路长度。

  • 严格次短路:在一张图中,其长度是所有路径中第 2 小的,所有长度相同的路径按照一条计算。

这道题有两种做法:一遍 dijkstra 求出次短路 与 枚举可能边求出次短路。

先讲前面这一种(这种做法笔者没有写代码):

这一种做法的大致思路是在做 dijkstra 的时候同时维护两个 d i s dis dis 数组, d i s 1 dis1 dis1 表示最短路, d i s 2 dis2 dis2 表示次短路,然后及时的更新次短路即可。

优点:码量小,速度快。

缺点:细节多,容易考虑漏。

注意例题的数据其实是比较弱的,因为如果用这个方法写,一些假的做法是能过的。

这些做法多多少少都漏掉了一些情况,包括很多题解,而且这些题解都是可以被 hack 的。

但是笔者找不到更好的例题了,因此只能拿这道题。

那么看看后一种做法。

后一种做法的大致思路是首先做两边 dijkstra,分别求出正向图中从 1 号点开始的单源最短路径,反向图中从 n n n 号点开始的单源最短路径。

接下来要做的就是枚举所有边。

设当前枚举的边为 ( u , v ) (u,v) (u,v),那么可能的路径有 1 − > u − > v − > n 1->u->v->n 1>u>v>n 或者 1 − > v − > u − > n 1->v->u->n 1>v>u>n

而为了做到次短路,显然 1 − > u , 1 − > v , u − > n , v − > n 1->u,1->v,u->n,v->n 1>u,1>v,u>n,v>n 需要采用最短路径。

注意原图是无向图,因此 u − > n , v − > n u->n,v->n u>n,v>n 等价于 n − > u , n − > v n->u,n->v n>u,n>v

然后计算一下,在路径长度不为最短路的时候更新答案。

换句话说,我们枚举所有边,要求从 1 − > n 1->n 1>n 的路径中必须经过这条边。

那么为什么这样做是对的呢?

首先这里有一个事实:次短路与最短路至少有一条边不同。

这个很显然吧qwq

那么因此为了控制这一条边,就需要枚举所有边然后强制经过这条边。

为了防止与最短路重合,需要特判。

那么有的人会问了:会不会有多条边不同呢?此时还能保证正确性吗?

当然可以,而且正确性同样成立。

假设现在的图是这样的:

在这里插入图片描述

其中每条边之间可能都有多个点,这只是简化版本。

A − > B − > D A->B->D A>B>D 为最短路, A − > C − > D A->C->D A>C>D 为次短路。

那么这里就有两条边不一样了。

但是在枚举边 ( A , C ) (A,C) (A,C) 的时候,我们需要 C − > n C->n C>n 的最短路,这个时候路径是 C − > D − > n C->D->n C>D>n,这样不就是枚举到 ( C , D ) (C,D) (C,D) 了吗?

更多的情况也是同理的。

实现的时候需要注意去除最短路的情况。

代码:

/*
========= 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 算法求最小生成树。

例题:P4180 [BJWC2010]严格次小生成树

考虑如何做次小生成树。

与次短路一样的,次小生成树与最小生成树至少有一条边不一样,那么我们就可以求出一棵最小生成树,然后强制枚举一条边转移到树上即可。

那么既然枚举了一条边转移到树上,树上肯定要删掉一条边。

假设当前枚举的边是 ( u , v ) (u,v) (u,v),那么我们需要删掉的边是树上 u − > v u->v u>v 的路径上边权最大的边。

快速查找这一个可以采用类似倍增求解 LCA 的方法或者是树链剖分。

而且实际上我们可以严格证明,次小生成树有且仅有一条边与最小生成树不同。

证明很简单,假设有一棵次小生成树有 k ( k > 1 ) k(k>1) k(k>1) 条边与最小生成树不同,那么这 k k k 条边肯定换出来了 k k k 条比这些加进去的边边权更大的边。

那么我们从这里面挑出一条边还原,那么总边权肯定更小,而且这个时候的树仍然不是最小生成树( k > 1 k>1 k>1)。

因此,原先树的边权,大于还原了一条边的树的边权,大于最小生成树的边权。

这与原先树是次小生成树矛盾。

因此只能有一条边,证毕。

注意代码里面的细节还是比较多的。

代码(树剖):

/*
========= 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. 总结

  • 次短路:枚举一条边使得路径强制经过这条边。
  • 次小生成树:枚举一条边使得生成树强制包含这条边。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值