ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OI-wiki 图论建模指南:拆点(结点拆边)与分层图最短路实战

OI-wiki 图论建模指南:拆点(结点拆边)与分层图最短路实战 OI-wiki 图论建模指南拆点结点拆边与分层图最短路实战【免费下载链接】OI-wiki:star2: Wiki of OI / ICPC for everyone. 某大型游戏线上攻略内含炫酷算术魔法项目地址: https://gitcode.com/GitHub_Trending/oi/OI-wiki本文以 OI-wiki 图论板块的《拆点》为核心系统讲解两种高频图论建模技巧将点权 / 点流量限制转化为边权后套用网络流板子以及通过分层图把「至多 $k$ 次零代价操作」的 DP 状态直接映射为图上的最短路。读完本文你将掌握拆点的完整实施步骤、分层图最短路的 DP 转移推导以及一个可复制的「JLOI2011」飞行路线P4568标准解法。拆点是 OI / ICPC 竞赛中非常基础且实用的图论建模思想常见于网络流处理点权或点的流量限制问题与分层图两种场景。OI-wiki 在 docs/graph/node.md 中给出了核心定义与两套完整示例本文在此基础上结合仓库内网络流、最短路相关文档进一步展开。拆点的基本思想把点流量限制转化为边流量限制标准的网络流问题中容量capacity是定义在边上的对于每条边 $(u,v)$流经它的流量不得超过其容量 $c(u,v)$详见 网络流基础定义。但许多实际问题会给结点本身附加流量限制例如“每个城市一天最多接待多少辆车”“每个格子最多被经过多少次”等。这类点权或点流量限制无法直接用常规的最大流板子求解。拆点的思路是把结点转化成边让原本无法表达的点限制落到边上从而套用现成的网络流模板。具体构造方法如下把原图中有流量限制的结点替换为由两个结点 $u$、$v$ 和一条边 $\left\langle u,v \right\rangle$ 组成的部分结点 $u$承接所有「从原图其他点出发、到达原图该点」的入边结点 $v$引出所有「从原图该点出发、到达原图其他点」的出边中间那条边 $\left\langle u,v \right\rangle$ 的流量限制等于原图该点的流量限制。经过这样的转化原图中的“点容量”就被完整地迁移到了新加的内部边上之后直接套最大流板子即可。这就是拆点的基本思想。下图展示了原图与拆点后的对比图片源文件分别为 docs/graph/images/node.svg 与 docs/graph/images/node-split.svg可以直观地看到原图中一个同时拥有入边和出边的结点在拆点后变成一进一出的两个结点二者之间由一条内部边连接——这条内部边就是“点容量”的载体。拆点的关键约定与实现要点入边全接 $u$出边全接 $v$方向不能混淆。$u$ 是“入口”接收所有进入该点的流量$v$ 是“出口”送出所有离开该点的流量。这样从入边流进的流量必然先经过 $u \to v$ 的内部边才会流出从而被点容量截流。内部边容量即点容量若原图某点限制为 $c$则内部边 $\left\langle u,v \right\rangle$ 的容量设为 $c$如果点无限流可以设为无穷大或一个充分大的数。拆点后规模一个有 $n$ 个点、$m$ 条边的图拆点后最多变成 $2n$ 个结点、$mn$ 条边每个被拆结点贡献一条内部边建图与算法复杂度的量级不变。特别地源点 $s$ 与汇点 $t$ 通常不需要拆它们的流量一般不受限若题目确实限制了源汇点同样可以拆。该技巧在 OI-wiki 仓库内还有更复杂的应用佐证在 块森林block-forest 中求解点双连通相关问题时会“给除了源点、汇点和 $c$ 之外的每个点赋上 $1$ 的容量这可以通过拆点实现”因为“割掉一个节点拆点形成的边等价于删除一个点”——拆点把“删除点”这一操作直接转化成了图上的边割是拆点思想在非网络流问题中的经典迁移。分层图最短路如果说拆点解决的是“点有限制”的问题那么分层图解决的是“路径上附带额外状态”的问题。分层图最短路的一个典型模型有 $k$ 次零代价通过一条路径边的机会求从起点到终点的最小总花费。例如允许免费乘坐 $k$ 次航班/经过 $k$ 条道路。DP 视角状态与转移对于这类问题可以采用 DP 相关的思想。设 $\text{dis}_{i, j}$ 表示当前从起点到达 $i$ 号结点且已经使用了 $j$ 次免费通行权限后的最短路径。显然$\text{dis}$ 数组可以按如下方式转移$$\text{dis}{i, j} \min{\min{\text{dis}{from, j - 1}}, \min{\text{dis}_{from, j} w}}$$其中$from$ 表示 $i$ 的父结点即前驱结点$w$ 表示当前所走的边的边权$\text{dis}_{from, j - 1}$ 对应“本次走这条边使用免费权限”代价为 $0$$\text{dis}_{from, j} w$ 对应“本次走这条边不使用免费权限”正常支付边权 $w$当 $j - 1 \geq k$即免费次数已用尽时令 $\text{dis}_{from, j} \infty$表示该状态不可达。图论视角把每个结点拆成 $k1$ 个事实上这个 DP 就相当于把每个结点拆分成 $k1$ 个结点每个新结点代表「使用不同次数免费通行后到达的原图结点」。换句话说结点 $u_i$ 表示使用 $i$ 次免费通行权限后到达 $u$ 结点。这就是“分层图”名称的由来原图被复制为 $k1$ 层第 $0$ 层到第 $k$ 层层号即已使用的免费次数。层间由“免费边”代价为 $0$连接层内由原边代价为 $w$连接层内边$(u_j, v_j)$ 边权为 $w$表示在第 $j$ 层正常付费行走免费次数不变跨层边$(u_j, v_{j1})$ 边权为 $0$表示使用一次免费权限免费次数加一。建好分层图后直接在这张 $n \times (k1)$ 规模的新图上跑一次最短路如 Dijkstra答案就是 $\min_{0 \leq j \leq k} \text{dis}_{t, j}$——即到达终点时任意免费次数下取最优。在 OI-wiki 仓库中最短路shortest-path 的「一些特殊情形」一节也明确将“允许至多 $k$ 次改变路径成本等操作的最短路问题”归类为分层图最短路并直接链接到本文所讲的 分层图最短路。这说明分层图是竞赛最短路问题的一个重要分支值得作为独立建模模板掌握。复杂度分析设原图有 $n$ 个点、$m$ 条边最多免费 $k$ 次建图后结点数为 $(k1) \cdot n$边数约为 $(k1) \cdot m$每层复制 跨层免费边使用堆优化的 Dijkstra时间复杂度为 $O((k1)(m n) \log((k1)n))$空间复杂度 $O((k1)(m n))$。由于 $k$ 通常较小如 $k \leq 10$分层图方法在竞赛中是可接受的常规做法。完整例题「JLOI2011」飞行路线题目「JLOI2011」飞行路线P4568原文以折叠块形式收录于 docs/graph/node.md。题意有一个 $n$ 个点、$m$ 条边的无向图你可以选择 $k$ 条道路以零代价通行求从 $s$ 到 $t$ 的最小花费。思路这是分层图最短路的直接应用将每个结点拆成 $k1$ 层第 $j$ 层表示“已使用 $j$ 次免费权限”。跑一遍带层信息的多维 Dijkstra最后对 $\text{dis}[t][0 \dots k]$ 取最小值。参考核心代码原文给出的完整 C 参考实现如下包含优先队列状态结构体、分层 Dijkstra 以及主程序struct State { // 优先队列的结点结构体 int v, w, cnt; // cnt 表示已经使用多少次免费通行权限 State() {} State(int v, int w, int cnt) : v(v), w(w), cnt(cnt) {} bool operator(const State rhs) const { return w rhs.w; } }; void dijkstra() { memset(dis, 0x3f, sizeof dis); dis[s][0] 0; pq.push(State(s, 0, 0)); // 到起点不需要使用免费通行权距离为零 while (!pq.empty()) { const State top pq.top(); pq.pop(); int u top.v, nowCnt top.cnt; if (done[u][nowCnt]) continue; done[u][nowCnt] true; for (int i head[u]; i; i edge[i].next) { int v edge[i].v, w edge[i].w; if (nowCnt k dis[v][nowCnt 1] dis[u][nowCnt]) { // 可以免费通行 dis[v][nowCnt 1] dis[u][nowCnt]; pq.push(State(v, dis[v][nowCnt 1], nowCnt 1)); } if (dis[v][nowCnt] dis[u][nowCnt] w) { // 不可以免费通行 dis[v][nowCnt] dis[u][nowCnt] w; pq.push(State(v, dis[v][nowCnt], nowCnt)); } } } } int main() { n read(), m read(), k read(); // 笔者习惯从 1 到 n 编号而这道题是从 0 到 n - 1所以要处理一下 s read() 1, t read() 1; while (m--) { int u read() 1, v read() 1, w read(); add(u, v, w), add(v, u, w); // 这道题是双向边 } dijkstra(); int ans std::numeric_limitsint::max(); // ans 取 int 最大值为初值 for (int i 0; i k; i) ans std::min(ans, dis[t][i]); // 对到达终点的所有情况取最优值 println(ans); }代码要点解析状态三元组(v, w, cnt)优先队列中不仅记录结点与当前最短路还记录已使用的免费次数cnt。operator按w升序排列保证每次弹出当前代价最小的状态。二维dis数组dis[u][j]表示“使用恰好 $j$ 次免费权限到达 $u$ 的最小代价”对应前述 DP 状态 $\text{dis}_{u, j}$。两种转移分支nowCnt k时允许“免费走这条边”dis[v][nowCnt 1] dis[u][nowCnt]代价不加边权 $w$免费次数加一任何时候都可以“付费走这条边”dis[v][nowCnt] dis[u][nowCnt] w代价加上 $w$次数不变。done[u][nowCnt]去重每个“结点 × 次数”的二维状态只松弛一次保证算法复杂度可控。答案统计终点 $t$ 可以带着任意剩余未用尽的免费次数到达因此最终对dis[t][0..k]取最小值。下标偏移题目输入编号为 $0 \sim n-1$代码统一1转换为 $1 \sim n$避免数组下标处理出错。小结拆点结点拆边把“点容量”迁移到新加的内部边上将点权/点流量限制问题转化为标准网络流问题内部边容量即点容量入边接 $u$、出边接 $v$。分层图把「至多 $k$ 次免费/特殊操作」建模为 $k1$ 层图层内边付费、跨层边免费等价于二维 DP 状态 $\text{dis}_{i,j}$最终用 Dijkstra 一次求解。两者本质相通都是通过“复制/拆分结点”把额外限制与额外状态显式编码进图结构从而复用成熟算法模板。OI-wiki 中这两个主题分别与 网络流、最短路 章节相互引用读者可结合阅读拆点在 块森林block-forest 中的应用也展示了它超越网络流、用于点删除建模的扩展价值。【免费下载链接】OI-wiki:star2: Wiki of OI / ICPC for everyone. 某大型游戏线上攻略内含炫酷算术魔法项目地址: https://gitcode.com/GitHub_Trending/oi/OI-wiki创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表