2023牛客暑期多校训练营2 B.Link with Railway Company(最大权闭合子图+线段树优化建图)

原题链接:B.Link with Railway Company


题目大意:


给出一棵 n n n 个点, n − 1 n - 1 n1 条边的树,每个点代表一个城市,每条边可以修建一条铁路,花费为 c i c_i ci

现在你有 m m m 个修建线路的方案,线路是从 a i a_i ai 城市到 b i b_i bi 城市的最短路径,修筑这条线路后你会获得 x i x_i xi 的价值,维护费是 y i y_i yi,即净利润为( x i − y i x_i - y_i xiyi)。

当且仅当在 a i a_i ai b i b_i bi 这条最短路上的所有边都修筑了一条铁路,一个方案才能完成。

你可以任选一些边,在上面花费 c i c_i ci 的价格修筑铁路,然后任选一些方案获得收入。

问你在你任选一些方案修筑铁路后,你能获得的最大价值为多少。

解题思路:


很典型的最大权闭合子图题目,我们把题目转化为下图:

在这里插入图片描述

左部点的点权为正值,对应方案的利润。
右部点的点权为负值,对应铁路的修建费。

闭合子图即选取一个点时,与其相连的所有点都必须选取。

那么题意就变成了,我们可以任意选取一部分的左部点,同时与左部有边相连的的右部点也必须被选取,问你选取的点最大点权和为多少。

考虑建立流网络,我们用最小割解决。

  • 源点向所有正权点连一条容量为点权的边。
  • 所有负权点向汇点连一条容量为点权绝对值的边。
  • 保留原图中的边,容量为正无穷。

在这里插入图片描述
在流网络上跑最小割,答案就是 所有正权点的点权和 − - 最小割

如果最小割割去的边为 S → V S\rightarrow V SV 的边,那么就代表这一个点(方案)不选,如果割去的边为 V → T V \rightarrow T VT 的边,那么就代表这一个点(铁路)要选。

这里不讲证明,根据直觉解释一下为什么这么做,和这么做的正确性。

  • 我们源点 S S S 向所有方案连了一条边,我们可以看作是我们可以获得的总金额,我们所有铁路向汇点 T T T 连了一条边,我们可以看作是我们的总花费。
  • 考虑最小割的情况,因为最小权闭合子图的最小割都是简单割,我们要么会割去 S → V S \rightarrow V SV 的边,要么会割去 V → T V \rightarrow T VT 的边,不会割去那些流量为正无穷的边。
  • 如果割去 S → V S \rightarrow V SV 的边,流量为 X X X,我们可以想象成我们本来能赚到 X X X 元,但我们都把所有的 X X X 元花在了修建铁路上,因此我们的 ∑ X \sum X X 要减去 X X X
  • 如果割去的是 V → T V \rightarrow T VT 的边,流量为 Y Y Y ,我们可以想象成那些所有的没被割去的 S → V S \rightarrow V SV 的边,本来能赚 ∑ X \sum X X 元,但是有一部分 Y Y Y 花到了修建铁路上,所以我们要减去 Y Y Y
  • 对于残量网络而言,显然答案就是那些没被割去的边 S → V S \rightarrow V SV 剩余容量之和,转化成最终答案,就是所有能赚的钱,减去这张图的最小割了。

回到原题来,我们就让方案和该方案所有要修建的铁路连一条正无穷的边,然后源点 S S S 与方案,铁路与汇点 T T T 按照上述方式相连就好了。

但是我们会发现节点非常多,要连的边条数可能会达到 O ( n m ) O(nm) O(nm) 的量级,显然不现实,考虑用什么方式优化一下建图。

注意到题目中方案需求构建的铁路线路在树上呈链状形式,我们先把所有边权转化到点权上,考虑用树剖(重链剖分)和线段树维护每一条链,优化边数至 O ( m log ⁡ 2 n ) O(m \log^2 n) O(mlog2n)条,最后建立流网络即可。

具体细节看代码即可。

时间复杂度: O ( n 2 m log ⁡ 2 n ) O(n^2m\log^2n) O(n2mlog2n)

AC代码:


#include <bits/stdc++.h>
#define YES return void(cout << "Yes\n")
#define NO return void(cout << "No\n")
using namespace std;

using u64 = unsigned long long;
using PII = pair<int, int>;
using i64 = long long;

template<class Ty>
struct maxFlow {
    const Ty INF = numeric_limits<Ty>::max();
    const int inf = INT32_MAX;
    int S, T, n; Ty MF = 0;
    struct Edge {
        int v, nxt; Ty f;
        Edge() : Edge(0, 0, 0) {};
        Edge(int v, int nxt, Ty f) : v(v), nxt(nxt), f(f) {};
    };

    vector<int> dist, cur, h;
    vector<Edge> E;

    maxFlow(int _) : n(_) { init(_); };

    void init(int _) {
        dist.resize(_ + 1);
        cur.resize(_ + 1);
        h.resize(_ + 1);
        E.resize(2);
    }

    void add(int u, int v, Ty f) {
        E.emplace_back(v, h[u], f), h[u] = int(E.size()) - 1;
    }

    void addEdge(int u, int v, Ty f) {
        add(u, v, f), add(v, u, 0);
    }

    bool BFS() {
        dist.assign(n + 1, inf);
        queue<int> que;
        dist[S] = 0, cur[S] = h[S];
        que.push(S);
        while (que.size()) {
            int u = que.front(); que.pop();
            for (int i = h[u]; i; i = E[i].nxt) {
                auto& [v, nxt, f] = E[i];
                if (f && dist[v] > dist[u] + 1) {
                    dist[v] = dist[u] + 1;
                    cur[v] = h[v];
                    que.push(v);
                }
            }
        }
        return dist[T] != inf;
    }

    Ty DFS(int u, Ty flow) {
        if (u == T) return flow;
        Ty last = flow;
        for (int i = cur[u]; i && last; i = E[i].nxt) {
            cur[u] = i;
            auto& [v, nxt, f] = E[i];
            if (f && dist[v] == dist[u] + 1) {
                Ty cost = DFS(v, min(f, last));
                if (!cost) dist[v] = INF;
                E[i].f -= cost, E[i ^ 1].f += cost;
                last -= cost;
            }
        }
        return flow - last;
    }

    void Dinic() {
        while (BFS()) {
            Ty flow = DFS(S, INF);
            MF += flow;
        }
    }
};

maxFlow<int> F(1e5);

const int N = 1e5 + 10;

vector<PII> g[N];

//重链剖分基本模板
int son[N], fa[N], siz[N], dep[N], arr[N];
void DFS1(int u) {
    siz[u] = 1, dep[u] = u[fa][dep] + 1;
    for (auto& [v, w] : g[u]) {
        if (v == fa[u]) continue;
        fa[v] = u, DFS1(v);
        siz[u] += siz[v];
        if (siz[v] > siz[son[u]]) son[u] = v;
        arr[v] = w;
    }
}

int dfn[N], top[N], id[N], idx = 0;
void DFS2(int u, int tp) {
    top[u] = tp, dfn[u] = ++idx, id[idx] = u;
    if (son[u]) DFS2(son[u], tp);
    for (auto& [v, w] : g[u]) {
        if (v == fa[u] || v == son[u]) continue;
        DFS2(v, v);
    }
}

#define lson k << 1, l, mid
#define rson k << 1 | 1, mid + 1, r
const int INF = INT32_MAX;
void build(int k, int l, int r) {
    if (l == r) {
        //叶子节点(铁路)向汇点T连边 容量为花费C
        F.addEdge(k, F.T, arr[id[l]]);
        return;
    }
    int mid = l + r >> 1;
    build(lson), build(rson);
    int ls = k << 1, rs = k << 1 | 1;
    F.addEdge(k, ls, INF);
    F.addEdge(k, rs, INF);
}

// 线段树优化建图
void connect(int k, int l, int r, int x, int y, int u) {
    if (l >= x && r <= y) {
        //方案和线段树节点连边
        F.addEdge(u, k, INF);
        return;
    }
    int mid = l + r >> 1;
    if (x <= mid) connect(lson, x, y, u);
    if (y > mid) connect(rson, x, y, u);
}

int n, m;
void addLink(int u, int x, int y) {
    while (top[x] != top[y]) {
        if (dep[top[x]] < dep[top[y]]) swap(x, y);
        connect(1, 1, n, dfn[top[x]], dfn[x], u);
        x = fa[top[x]];
    }
    if (dep[x] > dep[y]) swap(x, y);
    //点权转化到了边上,所以祖先x是不能算入的
    connect(1, 1, n, dfn[x] + 1, dfn[y], u);
}

void solve() {

    cin >> n >> m;
    for (int i = 1, u, v, w; i <= n - 1; ++i) {
        cin >> u >> v >> w;
        g[u].emplace_back(v, w);
        g[v].emplace_back(u, w);
    }

    //先跑一遍树剖
    DFS1(1), DFS2(1, 1);

    F.S = 0, F.T = n * 4;

    //把线段树图建出来
    build(1, 1, n);

    //idx为 每个方案对应的点的编号
    idx = n * 4;

    int sum = 0;
    for (int i = 1, u, v, x, y; i <= m; ++i) {
        cin >> u >> v >> x >> y;
        //利润为负显然不选
        if (x - y <= 0) continue;
        sum += x - y;

        //否则源点S和方案连一条边 容量为利润
        F.addEdge(F.S, ++idx, x - y);
        //方案和所有铁路连一条边
        addLink(idx, u, v);
    }

    //跑最大流
    F.Dinic();

    //方案利润和减去最小割即可
    cout << sum - F.MF << '\n';
}

signed main() {

    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);

    int t = 1; //cin >> t;
    while (t--) solve();

    return 0;
}


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柠檬味的橙汁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值