首页 > 其他分享 >题解 QOJ5048【[ECFinal19K] All Pair Maximum Flow】

题解 QOJ5048【[ECFinal19K] All Pair Maximum Flow】

时间:2024-10-12 08:52:50浏览次数:8  
标签:return int 题解 rhs Flow Maximum 最小 lhs modint

题目描述

给你一个 \(n\) 个点 \(m\) 条边的图,它是平面上的正 \(n\) 边形和一些对角线,节点按逆时针方向编号为 \(1\) 到 \(n\)。对角线只可能在节点处相交。每条边有一个容量,求每个点对之间的最大流的和。\(n\leq 200000, m\leq 400000\)。

solution

做法

每次找出边权最小的边 \(e\),满足 \(e\) 有恰好一侧是无界的,删去,在它所在的最小环 \(C\) 的其他边的边权加上 \(C\)。最终当图是一棵树时,可以乱做。

证明大概是证了:

  • 最小割不经过 \(C\) 的边,或者经过 \(e\) 和 \(C\) 的另外一条边。考虑对偶图,把无界区域划分成 \(n\) 份放到树上,具体不知道。

  • 然后通过最小割的性质和构造说明这样改不影响最大流。

平面图对偶图定理

平面图最小割等于对偶图最短路。

实际上这个定理说了和说了一样,含糊不清,我们来点图片:感谢 cjy 老师

如图,左边那个是平面图。将平面图的每一个面(包括有限面和无限面)当成点,两个面之间的边作为边,建立的图叫作对偶图。然后平面图上 \(s\to t\) 的最小割,是按照 \(\vec{s t}\) 这条直线把无限面切开以后两个无限面的代表点的最短路。可以看看第三幅图,虚线切开了无限面。

交了定义之后要证明原命题。观察到,平面图的每一个割都对应一个对偶图路径,最小割 \(\geq\) 最短路;对偶图的每一条路径都对应了平面图的一个割,可以感性理解一下就是这条路径把平面图劈开了,所以最短路 \(\geq\) 最小割。这么一看最小割 \(=\) 最短路就证完了。

性质一

对于一个包含边 \(e\) 的边数最小环 \(C\),其中 \(e\) 的边权 \(\leq\) (当前连通的)多边形中恰有一侧是无限面(注意表述)的边的边权。那么任意两点(假装是 \(s, t\))的最小割要么不经过 \(C\) 的边,要么经过 \(e\) 和 \(C\) 的另外一条边。

证明。在对偶图上考虑。目测一下因为这个环没有把两个无限面包括进去,所以最短路确实只能是经过了零条或者两条 \(C\) 中的边(不能经过一条边吧,出不去;经过三条边也不优)。考虑为什么它一定能切 \(e\)。

将多边形的每一个顶点向外引一条射线。\(s\) 引出的射线上方和 \(t\) 引出的射线上方交出的无限面是对偶图的起点,可以随意选择一块区域进入;\(s\) 引出的射线下方和 \(t\) 引出的射线下方交出的无限面是对偶图的终点,可以随意选择一块区域离开。

因为我没有数位板所以直接贺课件的图了,不是商用,希望图没事:

起点是那些黄色点里面选,终点是紫色点里面选,对偶图的边是绿色的,\(C\) 用红色标注。

然后必定经过 \(e\) 就显见了,直接钦定起点跨过 \(e\) 或者终点跨过 \(e\),注意这里的比较对象是蓝边,是说把第一步踩到的蓝边(加上红边)换成 \(e\) 是更优的,即使红边非常短也没有关系。就是右下角的图,原来是 \(x\to y\) 的,你把 \(x\) 搬到 \(v\) 区域内,就能踩到 \(e\) 了。

性质二

删除了多边形上的一条两侧恰好有一侧无界的边 \(e\),并将他的边权加到最小环上,删掉 \(e\),完了以后不影响两个点之间的最大流。

证明。

  1. 原图最小割 \(\geq\) 新图最小割。

    因为如果这个割与 \(C\) 无关,则这个割的大小相同。如果这个割经过 \(e\) 和 \(C\) 的另一条边,则这个割因为改边权大小仍然相同。

  2. 新图最小割 \(\geq\) 原图最小割。

    设新图最小割为 \(E\)。如果 \(E\) 和 \(C\) 无关,则大小相同。否则(注意这个 \(C\) 已经不是环,不能套用性质一),\(E\cup\{e\}\) 是原图的一个割,这个割比 \(E\) 小或者相同。

所以新图最小割 \(=\) 原图最小割。

细节

你发现一旦我们知道树是什么了,可以马上建立 Kruskal 重构树计算答案,因为树上最小割是路径上最小边权,使用这条边作为最小边权的是 Kruskal 重构树上这条边的两个子树跨过去的点对。但是怎么求?

首先就是说找到多边形的最小的“两侧恰有一侧无界”的边 \(e\) 和它所在的最小环 \(C\)。将 \(C\) 上的边边权 \(+e\),然后删去 \(e\)。发现"两侧都无界"的边必然不在其它边的最小环上,也不会被拿出来切掉。所以操作后 \(C\) 中的边会划分"两侧都无界"和“两侧恰有一侧无界”的边,前者加完边权之后就直接被删掉。

如果你真的这样写的话可能会爆炸,不如这样考虑,你建出定理一证明中的对偶图:

  1. 先建出这个对偶图(注意对偶图是一棵树),过程等价于划分多边形区域。随机一个点为一号点,然后随机选方向标号(哦题目标好了是吧),将对角线们的左右两端,强制左 \(<\) 右之后按照左端点降序排序(相同则右端点升序),然后按照顺序拿出一条对角线,将对角线对应的区间的边全部删去并连边后再加入对角线(意思是这个对角线划出了一个新的面,维护一个 map 表示每个位置上的面,每次将那些面拿出来与这次对角线划分出的面连边)。最后 map 还会剩下最后一个跨过 \((1, n)\) 的面也要存下来。
  2. 维护一个 priority_queue 记了边权和边。初始时所有叶子到父亲的边加进去(叶子的父亲明显是它有唯一连边的点)。然后拿一个 \(e\) 出来,它是最小边,在它父亲 \(u\) 那里暴力枚举,除了 \(e\) 之外的边都 \(+e\)。拆掉父亲对应的面之后,这个面就无界了,需要拿父亲的所有边(包括其儿子和父亲)出来枚举,将新的“两侧恰有一侧无界”的边加入优先队列。这里需要维护 \(vis\) 表示每个面是否被拆了(是否无界),初始时所有叶子 \(vis=1\),父亲枚举出边时先将自己 \(vis=1\),然后往 \(vis=0\) 的面插入优先队列(否则一个面会被拆两次)。
  3. 完了以后 Kruskal 重构树启动就完了。事实上 Kruskal 重构树不需要显式地建出来,而直接写并查集算一下就行。

code

#include <bits/stdc++.h>
using namespace std;
#ifdef LOCAL
#define debug(...) fprintf(stderr, ##__VA_ARGS__)
#else
#define endl "\n"
#define debug(...) void(0)
#endif
using LL = long long;
template <unsigned umod>
struct modint {/*{{{*/
  static constexpr int mod = umod;
  unsigned v;
  modint() = default;
  template <class T, enable_if_t<is_integral<T>::value, int> = 0>
    modint(const T& y) : v(y % mod + (is_signed<T>() && y < 0 ? mod : 0)) {}
  modint& operator+=(const modint& rhs) { v += rhs.v; if (v >= umod) v -= umod; return *this; }
  modint& operator-=(const modint& rhs) { v -= rhs.v; if (v >= umod) v += umod; return *this; }
  modint& operator*=(const modint& rhs) { v = (unsigned)(1ull * v * rhs.v % umod); return *this; }
  modint& operator/=(const modint& rhs) { assert(rhs.v); return *this *= qpow(rhs, mod - 2); }
  friend modint operator+(modint lhs, const modint& rhs) { return lhs += rhs; }
  friend modint operator-(modint lhs, const modint& rhs) { return lhs -= rhs; }
  friend modint operator*(modint lhs, const modint& rhs) { return lhs *= rhs; }
  friend modint operator/(modint lhs, const modint& rhs) { return lhs /= rhs; }
  template <class T> friend modint qpow(modint a, T b) {
    modint r = 1;
    for (assert(b >= 0); b; b >>= 1, a *= a) if (b & 1) r *= a;
    return r;
  }
  friend int raw(const modint& self) { return self.v; }
  friend ostream& operator<<(ostream& os, const modint& self) { return os << raw(self); }
  explicit operator bool() const { return v != 0; }
};/*}}}*/
using mint = modint<998244353>;
template <int N>
struct dsu {
  int fa[N + 10], siz[N + 10];
  dsu() { for (int i = 1; i <= N; i++) fa[i] = i, siz[i] = 1; }
  int find(int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); }
  bool merge(int x, int y) {
    x = find(x), y = find(y);
    if (x == y) return false;
    if (siz[x] < siz[y]) swap(x, y);
    siz[x] += siz[y], fa[y] = x;
    return true;
  }
};
template <class T>
using p_queue = priority_queue<T, vector<T>, greater<T>>;
struct range {
  int l, r, id;
};
int n, m, fa[400010], upc[400010];
vector<pair<int, LL>> g[400010];
pair<int, int> edg[400010];
LL nw[400010];
bool vis[400010];
dsu<200010> dsy;
void add(int u, int v, LL w) {
  if (fa[v] == u) swap(u, v);
  if (w == -1e18) nw[u] = -1e18;
  else nw[u] += w;
}
int main() {
#ifndef LOCAL
  cin.tie(nullptr)->sync_with_stdio(false);
#endif
  cin >> n >> m;
  vector<range> vec;
  map<int, int> mp;
  for (int i = 1, u, v, w; i <= m; i++) {
    cin >> u >> v >> upc[i];
    if (u > v) swap(u, v);
    edg[i] = make_pair(u, v);
    if (u + 1 == v) mp[u] = i;
    else if (u == 1 && v == n) mp[n] = i;
    else vec.push_back({.l = u, .r = v, .id = i});
  }
  sort(vec.begin(), vec.end(), [](range lhs, range rhs) { return lhs.l != rhs.l ? lhs.l > rhs.l : lhs.r < rhs.r; });
  for (auto line : vec) {
    int p = line.id;
    auto it = mp.lower_bound(line.l);
    while (it != mp.end() && it->first < line.r) {
      fa[it->second] = p;
      it = mp.erase(it);
    }
    mp[line.l] = p;
  }
  for (auto it : mp) fa[it.second] = m + 1;
  for (int i = 1; i <= m; i++) g[fa[i]].emplace_back(i, upc[i]), g[i].emplace_back(fa[i], upc[i]);
  p_queue<tuple<LL, int, int>> q;
  for (int i = 1; i <= m + 1; i++) if (g[i].size() == 1) q.emplace(g[i][0].second, i, g[i][0].first), vis[i] = true;
  while (!q.empty()) {
    int u, v;
    LL w;
    tie(w, u, v) = q.top();
    q.pop();
    if (vis[v]) add(u, v, w);
    else {
      vis[v] = true;
      add(u, v, -1e18);
      for (auto e : g[v]) if (e.first != u) {
        if (!vis[e.first]) q.emplace(w + e.second, v, e.first);
        else add(v, e.first, w);
      }
    }
  }
  vector<tuple<LL, int, int>> t;
  for (int i = 1; i <= m; i++) if (nw[i] >= 0) {
    t.emplace_back(nw[i], edg[i].first, edg[i].second);
    debug("(%d, %d, %lld)\n", edg[i].first, edg[i].second, nw[i]);
  }
  sort(t.begin(), t.end(), greater<>{});
  mint ans = 0;
  for (auto&& e : t) {
    int u, v;
    LL w;
    tie(w, u, v) = e;
    // ????????????                 
    u = dsy.find(u), v = dsy.find(v);
    ans += mint{w} * dsy.siz[u] * dsy.siz[v];
    dsy.merge(u, v);
  }
  cout << ans << endl;
  return 0;
}


标签:return,int,题解,rhs,Flow,Maximum,最小,lhs,modint
From: https://www.cnblogs.com/caijianhong/p/18459718/solution-QOJ5048

相关文章

  • 蓝桥杯真题 穿越时空之门(第十五届蓝桥杯省赛PythonB组A题) c++题解
    问题如下(附链接):穿越时空之门题解代码如下:#include<iostream>usingnamespacestd;intx1(inti){inta=0;while(i){a+=i%2;i/=2;}returna;}intx2(inti){intb=0;while(i){b+=i%4;i/=4;}returnb;}intmain()......
  • TensorFlow 学习笔记
    Tensorflow是谷歌开发的一款机器学习软件包。2019年,谷歌将Keras集成到Tensorflow中,并发布了Tensorflow2.0。Keras是FrançoisChollet独立开发的一个框架,为Tensorflow创建了一个简单的、以层为中心的接口。张量(Tensor)是数组的另一个名称。TensorFlow.orgimportte......
  • 线段树分治略解&杂题解析
    可能做到好题之后会再更新吧。总叙线段树与离线询问结合技巧又被称为线段树分治。使用线段树分治维护的信息通常会在某一个时间段内出现,要求在离线的前提下回答某一个时刻的信息并,则可以考虑使用线段树分治的技巧。以下是线段树分治的基本模板:change将信息按时间段覆盖在线......
  • tensorflow案例1--天气识别,包含(Tensorflow的检查是否GPU、图像数据加载与划分、拿取
    ......
  • 【交通标志识别系统】Python+卷积神经网络算法+人工智能+深度学习+图像识别+计算机课
    一、介绍交通标志识别系统。本系统使用Python作为主要编程语言,在交通标志图像识别功能实现中,基于TensorFlow搭建卷积神经网络算法模型,通过对收集到的58种常见的交通标志图像作为数据集,进行迭代训练最后得到一个识别精度较高的模型文件,然后保存为本地的h5格式文件。再使用Dj......
  • 【海洋生物识别系统】Python+卷积神经网络算法+人工智能+深度学习+计算机毕设项目+Ten
    一、介绍海洋生物识别系统。以Python作为主要编程语言,通过TensorFlow搭建ResNet50卷积神经网络算法,通过对22种常见的海洋生物(‘蛤蜊’,‘珊瑚’,‘螃蟹’,‘海豚’,‘鳗鱼’,‘水母’,‘龙虾’,‘海蛞蝓’,‘章鱼’,‘水獭’,‘企鹅’,‘河豚’,‘魔鬼鱼’,‘......
  • [USACO23FEB] Hungry Cow P 题解
    T7[USACO23FEB]HungryCowP这题就比较正常了,蓝紫之间的水平。我们发现Bessie能活\(10^{14}\)天(,导致我们不好直接按照值域来维护。发现给某一天送干草,影响到的是后面很多天,这是个区间问题。所以考虑动态开点线段树。线段树的每个节点维护\(\mathit{ans},\mathit{cnt},\ma......
  • csp-s真题题解
    csp题目讲解P8818[CSP-S2022]策略游戏学习笔记感觉非常复杂?对于现在的我还是有深度的,首先第一个大坑就是并不需要真的求出c矩阵,这个题意就是让你在区间中选数,但要求乘积最大,所以要分讨。你假定\(a_i\ge0\),那这时如果\(min(b_i)\ge0\)取\(max(a_i)\),否则取\(min(a_i\ge......
  • P3959 [NOIP2017 提高组] 宝藏 题解
    P3959[NOIP2017提高组]宝藏题解搜索魅力时刻怎么说,四种做法比较??的模拟退火跑得快但是正确性有问题的状压DP跑得慢但是一定正确的状压DP时间复杂度很玄学的DFS+剪枝我就选择了搜索的做法先打个暴搜,70pts点击查看暴搜代码#include<bits/stdc++.h>usingna......
  • 有关 OneDrive 中 Copilot 的常见问题解答
    很多小伙伴已经用上了OneDrive中的Copilot功能:Copilot重磅更新!OneDrive全新功能炸裂一个技巧实现在SharePoint中使用Copilot针对大家提问比较多的问题,在此做一个汇总:1、OneDrive中的Copilot目前在哪里可用?OneDrive中的Copilot目前在 OneDriveWeb上仅适用于商......