bzoj1977_次小生成树(加强版)

10 篇文章 0 订阅

(又是某日, xxz出现了吊打的一幕)

DG: DL, 你完全不行, 上次讲的那个次小生成树的求法太水了。

DL: ...莫非还有O(n²+mlogm)不能做的次小生成树?

DG: 自己过来看^_^

DL: (凝视屏幕许久)!!!n、m居然有十万, 这能做?!!

DG: 所以说你太水了。

jjz: 你们在做什么题啊。(瞟)哦, 这一题有机智的做法, 是O(nlogn+mlogm)的哟。


回顾上次的次小生成树求法, 由于n比较小, 所以可以处理出每个点之间的所谓"最小瓶颈路"然后枚举, 而这一次n、m规模已经超过了O(n²+mlogm)可以接受的范围, 显然会果断挂掉, DL就被秒了(QAQ)...且就算能预处理, 本题要求严格次小, 还是要跪(DL又被秒)...

那么是时候考虑机智的做法了。 首先回归本质, 次小生成树必然是在原最小生成树的基础上去掉一条边再加上一条新边得到的, 只是现在变成了必须比原边大的而已。 故还是先用kruskal求MST, 然后枚举每一条新边。

枚举后必然要有处理, 这时的复杂度就是机智的关键。 暂时先把严格次小放在一边, 在一棵树上, 我们需要找到两点之间权值最大的一条边。 最大, 最大...对了, 不是可以用LCA求吗? 选择倍增算法的LCA, 只要O(nlogn)的预处理, 此后O(logn)的查询就有两点间最大值了。 程序框架还是熟悉的味道, 枚举每条不在树上的边, 找到两点间的最大边并去掉, 更新答案。 感觉又活过来了, over!

不要忘了严格次小这件事, 为了以防万一, 我们不仅要记录两点间最大的边, 还要记录两点间次大的边, 这里也是严格次大的, 否则可能得到原最小生成树或比次小生成树更大的答案(样例可能输出10或12), 所以要在预处理最大边时同时处理次大边。 程序的各种小优化也是必不可少的。

#include <cstdio>
#include <algorithm>
#include <queue>
#define N 100000 + 10
#define M 600000 + 10

using namespace std;

typedef long long LL;
struct line
{
    int l, r, w;
}l[(M)/2];
struct edge
{
    int to, w, next;
}e[M];
int n, m, num, top, lca, tmp;
int st[N], p[N], d[N], flag[N], used[M];
int fa[N][18], maxs[N][18], mini[N][18];
LL ans, sum;
void read(int &x)
{
    x = 0;
    char c = getchar();
    while(c < '0' || c > '9') c = getchar();
    while(c >= '0' && c <= '9')
    {
        (x *= 10) += c - '0';
        c = getchar();
    }
}
bool cmp(line a, line b)
{
    return a.w < b.w;
}
void add(int x, int y, int z)
{
    e[++num].to = y;
    e[num].w = z;
    e[num].next = p[x];
    p[x] = num;
}
int find(int x)
{
    st[++top] = x;
    while(x != fa[x][0]) st[++top] = (x = fa[x][0]);
    while(top) fa[st[top--]][0] = x;
    return x;//如果用递归形式可能爆栈
}
void init()
{
    read(n), read(m);
    for (int i = 1; i <= m; ++i)
    read(l[i].l), read(l[i].r), read(l[i].w);
}
void kruskal()
{
    int x, y, fx, fy;
    sort(l+1, l+m+1, cmp);
    for (int i = 1; i <= n; ++i)
    fa[i][0] = i;
    for (int i = 1; i <= m; ++i)
    {
        x = l[i].l, y = l[i].r;
        fx = find(x), fy = find(y);
        if (fx == fy) continue;
        fa[fx][0] = fy;
        add(x, y, l[i].w), add(y, x, l[i].w);
        used[i] = 1; sum += l[i].w;
    }
}
void bfs()
{
    fa[1][0] = 0;
    queue<int>q;
    q.push(1);
    flag[1] = 1;
    while(!q.empty())
    {
        int x = q.front();
        q.pop();
        for (int i = p[x]; i; i = e[i].next)
        {
            int k = e[i].to;
            if (!flag[k])
            {
                flag[k] = 1;
                d[k] = d[x] + 1;
                fa[k][0] = x;
                maxs[k][0] = e[i].w;
                q.push(k);
            }
        }
    }//预处理树上点的数据
}
void pre_work()
{
    for (int i = 1; i <= 17; ++i)
    for (int j = 1; j <= n; ++j)
    if (d[j] >= (1<<i))
    {
        fa[j][i] = fa[fa[j][i-1]][i-1];
        maxs[j][i] = max(maxs[j][i-1], maxs[fa[j][i-1]][i-1]);
        mini[j][i] = max(mini[j][i-1], mini[fa[j][i-1]][i-1]);
        if (maxs[j][i-1] > maxs[fa[j][i-1]][i-1] && mini[j][i] < maxs[fa[j][i-1]][i-1])
        mini[j][i] = maxs[fa[j][i-1]][i-1];
        else if (maxs[j][i-1] < maxs[fa[j][i-1]][i-1] && mini[j][i] < maxs[j][i-1])
        mini[j][i] = maxs[j][i-1];//次大边的更新可能有多种情况
    }
}
int get(int x, int y, int last)
{
    if (d[x] > d[y]) swap(x, y);
    int l = x, r = y, now = 0;
    for (int mid = d[y] - d[x], i = 0; mid; ++i, mid >>= 1)
    if (mid & 1) r = fa[r][i];
    if (l == r) lca = l;
    else
    {
        for (int i = 17; i >= 0; i--)
        {
            if (fa[l][i] == fa[r][i]) continue;
            l = fa[l][i], r = fa[r][i];
        }
        lca = fa[r][0];
    }//倍增lca求法
    l = x, r = y;
    for (int i = 17; i >= 0; i--)
    {
        if (d[l] - d[lca] >= (1 << i))
        {
            if (maxs[l][i] != last) now = max(now, maxs[l][i]);
            else now = max(now, mini[l][i]);//排除枚举的边权值和最大边权值相同的情况
            l = fa[l][i];
        }
        if (d[r] - d[lca] >= (1 << i))
        {
            if (maxs[r][i] != last) now = max(now, maxs[r][i]);
            else now = max(now, mini[r][i]);
            r = fa[r][i];
        }
    }
    return now;
}
void deal()
{
    kruskal();
    bfs();
    pre_work();
    for (int i = 1; i <= m; ++i)
    if (!used[i])
    {
        tmp = get(l[i].l, l[i].r, l[i].w);
        if (ans == 0 && tmp != l[i].w) ans = sum - tmp + l[i].w;
        if (tmp < l[i].w && sum - tmp + l[i].w < ans)
        ans = sum - tmp + l[i].w;
    }
    printf("%lld\n", ans);
}
int main()
{
    init();
    deal();
    return 0;
}


  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值