ARTICLE DETAIL

资讯详情

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

Floyd算法与二分法在环境治理问题中的实战应用

Floyd算法与二分法在环境治理问题中的实战应用 1. 项目概述从“环境治理”到算法实战看到“[蓝桥杯 2022 国 A] 环境治理”这个标题很多算法竞赛选手的第一反应可能是“这又是个图论题”。没错这道题确实披着“环境治理”的外衣内核却是一个经典的最短路与优化决策问题。它要求我们模拟一个城市群的环境治理过程通过有限的“治理天数”来降低城市间的“灰尘度”即路径权重最终使得整个城市网络的“灰尘度”总和即所有点对之间的最短路径之和降低到一个目标值以下。这听起来像是一个城市规划问题但解题的核心钥匙是Floyd算法和二分法。我当年打比赛时第一次遇到这种将现实问题抽象为图论模型再结合二分搜索寻找最优解的题目感觉非常巧妙。它不仅考察了你对基础算法的掌握更考验了你将复杂问题分解、建模的能力。今天我就来彻底拆解这道题从题意理解、模型建立、算法选择到代码实现的每一个细节并分享一些只有实战过才能悟到的调试技巧和优化心法。2. 核心思路拆解为什么是Floyd二分2.1 问题本质与图论建模题目描述了一个有N个城市的网络给出了一个初始的N*N矩阵D其中D[i][j]表示从城市i到城市j的“灰尘度”。治理行动发生在每条道路上每天你可以选择若干条道路进行治理每条被治理的道路其灰尘度会减少1但有一个下限值L即灰尘度不能低于L。治理持续P天。我们需要判断经过最多P天的治理后能否使得整个网络的“环境指标” —— 即所有点对(i, j)之间的最短路径长度之和 —— 不超过一个目标值Q。这里第一个关键点在于“最短路径”。为什么是“最短路径”之和而不是直接使用原始灰尘度矩阵的和因为在实际交通或污染扩散中从i到j的“影响”通常会沿着最优最短路径传播。因此我们需要计算的是治理后新矩阵下的全源最短路径。这直接指向了Floyd算法因为Floyd正是用于计算图中所有顶点对之间最短路径的经典算法其O(N^3)的复杂度在N通常较小蓝桥杯题目N一般≤100时是可以接受的。所以问题模型建立如下图将每个城市视为图的一个顶点。边权城市i到j的初始灰尘度D[i][j]视为边(i, j)的初始权重。注意题目可能暗示了图是无向的即D[i][j] D[j][i]或者直接给出的是邻接矩阵。操作每天你可以让任意一条边(i, j)的权重减少1但不低于L。这相当于有P个单位的“治理力”可以分配到不同的边上。目标寻找一种分配P天治理力即决定每条边减少多少的方案使得得到的新权重矩阵W经过Floyd算法计算出的全源最短路径矩阵S其所有元素之和sum(S[i][j])不超过Q。输出我们需要找到满足上述条件的最小治理天数P。如果初始状态0天就已经满足则输出0如果即使给满题目允许的最大天数或根据题意推断的天数上限仍无法满足则输出-1。2.2 二分法的引入与可行性判断最直接的想法是枚举每一天模拟治理过程然后计算最短路径和。但天数P可能很大这种线性枚举会超时。这时就需要二分法。我们发现随着治理天数P的增加我们能够降低的灰尘度总和越多计算得到的最短路径和total_dust应该是一个非递增的函数。这满足了二分法应用的前提单调性。我们可以二分搜索这个天数P。二分法的框架如下确定二分范围[left, right]。left通常为0不治理。right需要设定一个上界可以是一个足够大的数如1e9或者根据题目数据范围估算。在每次循环中计算中点mid (left right) / 2判断是否能在mid天内使得最短路径和 ≤ Q。如果可行check(mid) true说明答案可能更小令right mid或在标准写法中记录答案并令right mid - 1。如果不可行check(mid) false说明天数不够令left mid 1。循环直到left right。整个问题的核心难点就转移到了如何高效实现check(mid)函数给定一个治理天数days判断能否通过分配这些“治理力”使得全图最短路径和 ≤ Q。2.3 可行性函数check(days)的设计这是本题的第二个关键点也是一个容易出错的地方。我们不能直接模拟每天治理哪条边因为那是组合爆炸的。我们需要换一个角度思考。对于每条边(i, j)设其初始灰尘度为D[i][j]下限为L。在days天的治理中它最多能被治理days天如果每天都治理它因此其灰尘度最低可以降到max(L, D[i][j] - days)。但反过来我们并不需要知道具体哪条边被治理了多少天。我们只关心最终每条边的灰尘度是多少只要这个最终值W[i][j]满足L ≤ W[i][j] ≤ D[i][j]治理后灰尘度在[L, 初始值]之间。所有边的(D[i][j] - W[i][j])之和 ≤days因为每天治理一条边一次总共治理天数就是所有边减少量的总和。由W矩阵计算出的全源最短路径和 ≤ Q。那么对于一个给定的days我们如何构造一个可能的W矩阵使得计算出的最短路径和尽可能小呢一个直观且正确的贪心策略是优先治理那些对全局最短路径和影响最大的边即“瓶颈”边。但是直接找“瓶颈”边是困难的因为边的影响是相互关联的。这里需要一个更巧妙的转化。我们注意到对于固定的days每条边(i, j)有一个可能达到的最小灰尘度min_possible[i][j] max(L, D[i][j] - days)。如果我们直接把所有边的灰尘度都设为这个“最小可能值”得到矩阵W_min那么这肯定是一种合法的治理方案满足条件1和2。由W_min计算出的最短路径和是所有可能方案中最小的吗不一定但它是一个下界。因为任何方案下的灰尘度都不会低于W_min所以对应的最短路径和也不会低于用W_min算出的结果。因此check(days)函数可以这样实现根据days构建一个新矩阵W其中W[i][j] max(L, D[i][j] - days)。这代表了在days天内我们尽可能努力治理后能得到的最好灰尘度最低的图。在这个新矩阵W上运行Floyd算法计算全源最短路径矩阵dist。计算total sum(dist[i][j]) for all i, j。如果total Q返回true说明即使在最理想的情况下days天就够了否则返回false说明即使拼命治理days天也不够。注意这里有一个非常重要的逻辑点。我们用了“可能的最小边权”矩阵来计算最短路径和。如果这个“最小边权”图对应的最短路径和已经满足要求那么一定存在一种具体的治理方案不一定需要每条边都治理到下限使得结果满足要求。因为我们可以先按这个最小边权图来治理如果某些边治理“过度”了即实际治理天数分配使得某些边权低于了计算最短路径时的值我们可以减少对这些边的治理把天数分配到其他边上这不会使最终的最短路径和变大因为边权增加了。所以check(days)为true是答案可行的充分条件。反之如果“最小边权”图都不满足那么任何其他治理方案边权更大就更不可能满足了。所以这也是必要条件。因此这个贪心构造是正确且高效的。3. 算法细节与实现解析3.1 Floyd算法的正确应用与优化在check函数中我们需要对W矩阵跑一遍Floyd。标准的Floyd算法是三重循环复杂度O(N^3)。对于N100单次check是100^3 1e6次操作在时间限制内可以接受。但这里有一些细节需要注意初始化dist矩阵初始值就是W矩阵。注意处理自环dist[i][i] 0。循环顺序必须是k循环在最外层。这个顺序不能错它代表了依次考虑每个顶点作为中转点。vectorvectorlong long dist W; // 假设W是long long类型 for (int i 0; i n; i) dist[i][i] 0; // 自环为0 for (int k 0; k n; k) { for (int i 0; i n; i) { // 一个小优化如果dist[i][k]已经是无穷大则i经k到j的路径也无效 if (dist[i][k] INF) continue; for (int j 0; j n; j) { if (dist[k][j] INF) continue; if (dist[i][j] dist[i][k] dist[k][j]) { dist[i][j] dist[i][k] dist[k][j]; } } } }数据类型灰尘度、最短路径和都可能很大需要用long long64位整数来存储避免溢出。无穷大设置如果题目中存在不直接相连的城市灰尘度可能为无穷大或用一个很大值表示需要在代码中用INF表示。INF的值要足够大如1e18但两个INF相加不能溢出。在比较dist[i][j] dist[i][k] dist[k][j]时如果dist[i][k]或dist[k][j]是INF则加法可能会溢出所以需要像上面代码那样先判断。3.2 二分法的边界与写法二分查找最小满足条件的天数。long long left 0, right MAX_DAYS; // MAX_DAYS 需要设定一个足够大的上界 long long ans -1; // 存储答案初始为-1表示找不到 while (left right) { long long mid left (right - left) / 2; // 防溢出写法 if (check(mid)) { // 如果mid天可行 ans mid; // 记录可行解 right mid - 1; // 尝试寻找更小的可行天数 } else { left mid 1; // 天数不足需要增加 } } cout ans endl;上界MAX_DAYS的设定这是一个关键。理论上每条边最多可以从初始值D[i][j]治理到下限L所以对于单条边最大治理天数是D[i][j] - L。但我们可以治理多条边总天数没有明确上限。一个简单安全的做法是设一个很大的数比如1e9。但更精确的做法是考虑最坏情况我们需要把每条边都治理到下限L那么总治理天数需求最多是sum(D[i][j] - L)。我们可以用这个和作为上界或者取其两倍作为安全上界。因为二分是对数复杂度上界大一些对速度影响很小但上界太小会导致找不到解。3.3 整体代码框架将以上各部分组合起来完整的代码结构如下读入N, Q, L以及初始灰尘度矩阵D。实现check(long long days)函数逻辑如前所述。设定二分查找的左右边界。执行二分查找得到最小天数ans。输出ans。4. 实战技巧与避坑指南这道题思路清晰后实现起来并不复杂但我在实战和教学中发现了一些常见的“坑点”。4.1 精度与溢出问题这是最大的坑。题目中的灰尘度、天数、最短路径和都可能达到很大的数量级。用long long这是必须的。int类型通常只有32位最大值约21亿很可能溢出。所有与灰尘度、路径和、天数相关的变量都应使用long long。INF的设置在Floyd算法中如果需要表示“不连通”要设置一个很大的数作为INF。这个INF必须满足比任何可能的最短路径都大。两个INF相加不能溢出long long的范围9e18左右。通常可以设为0x3f3f3f3f3f3f3f3f一个很大的数且满足INF INF不溢出或者1e18。二分中的中间值计算使用mid left (right - left) / 2而不是(left right) / 2可以防止leftright可能出现的溢出。4.2 图的性质与初始化自环处理城市到自身的灰尘度应该为0。在初始化dist矩阵时务必设置dist[i][i] 0。否则在Floyd算法中如果D[i][i]不为0可能会错误地更新其他路径。无向图题目通常暗示道路是双向的即D[i][j] D[j][i]。读入数据后可以检查一下。我们的算法对邻接矩阵是否对称没有要求但如果是无向图矩阵对称可以作为一个合理性检查。治理下限L在计算W[i][j] max(L, D[i][j] - days)时max函数确保了灰尘度不会低于L。这是题目硬性规定。4.3 二分查找的细节循环条件while (left right)是标准的二分查找模板易于理解和处理边界。更新策略当check(mid)为真时我们找到了一个可行解但可能还有更小的。所以记录答案ans mid然后让right mid - 1去左侧搜索。如果为假则让left mid 1。初始解在二分开始前可以先check(0)即不治理的情况。如果初始状态就满足total Q那么答案就是0可以直接输出避免二分。这是一个有效的剪枝。无解判断如果二分结束后ans仍为初始值如-1说明即使上界MAX_DAYS天也无法满足按题目要求输出-1。但我们需要确保MAX_DAYS设得足够大否则可能因为上界不足而误判为无解。一个技巧是如果check(MAX_DAYS)为假那确实是无解如果为真但我们的二分没找到那可能是上界或二分写法有问题。4.4 性能优化点虽然O(N^3)的Floyd对于N100可以接受但check函数会被调用 O(log(MAX_DAYS)) 次大约是30次左右。总复杂度约为 O(30 * N^3) 30 * 1e6 3e7 次操作在C中通常可以在1秒内完成。但如果想更稳健可以注意将二维数组用vectorvectorlong long存储避免使用大尺寸的静态数组如long long dist[100][100]导致栈溢出虽然1001008字节80KB通常没问题。使用vector更安全灵活。在Floyd的内层循环中加入对dist[i][k]和dist[k][j]是否为INF的判断可以避免无效的加法运算和比较。如果N更大比如达到200O(N^3)的Floyd在多次调用下可能压力较大。但本题数据规模下无需过度优化。5. 代码实现与注释下面给出一个完整的C实现包含了详细的注释和上述提到的所有要点。#include iostream #include vector #include algorithm #include climits using namespace std; typedef long long LL; const LL INF 1e18; // 定义一个足够大的无穷大 int n; // 城市数量 LL Q; // 目标环境指标 LL L; // 灰尘度下限 vectorvectorLL D; // 初始灰尘度矩阵 // 检查在days天内能否使全图最短路径和 Q bool check(LL days) { // 1. 构建治理days天后的“最优可能”边权矩阵W vectorvectorLL W(n, vectorLL(n)); for (int i 0; i n; i) { for (int j 0; j n; j) { // 每条边最多治理days天灰尘度最低降到L W[i][j] max(L, D[i][j] - days); } } // 2. 在W上运行Floyd算法计算全源最短路径 vectorvectorLL dist W; for (int i 0; i n; i) { dist[i][i] 0; // 自己到自己的距离为0 } for (int k 0; k n; k) { for (int i 0; i n; i) { if (dist[i][k] INF) continue; // 优化跳过无效中转 for (int j 0; j n; j) { if (dist[k][j] INF) continue; // 松弛操作 if (dist[i][j] dist[i][k] dist[k][j]) { dist[i][j] dist[i][k] dist[k][j]; } } } } // 3. 计算最短路径和 LL total 0; for (int i 0; i n; i) { for (int j 0; j n; j) { total dist[i][j]; // 如果累加过程中已经超过Q可以提前退出节省时间 if (total Q) { return false; } } } return total Q; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cin n Q L; D.assign(n, vectorLL(n)); LL max_dust 0; for (int i 0; i n; i) { for (int j 0; j n; j) { cin D[i][j]; max_dust max(max_dust, D[i][j]); } } // 特判如果0天就满足条件 if (check(0)) { cout 0 endl; return 0; } // 确定二分上界一个宽松的上界比如所有边都从最大值治理到L所需的天数 // 最坏情况每条边都需要治理 (max_dust - L) 天共有 n*n 条边包括自环但自环不影响 // 实际上治理是并行的每天可以治理多条边所以上界不需要是 n*n*(max_dust-L) // 一个简单的足够大的上界是 1e9或者 max_dust * n 经验值 LL left 1, right 2e9; // 2e9是一个足够大的安全值 LL ans -1; while (left right) { LL mid left (right - left) / 2; if (check(mid)) { ans mid; right mid - 1; // 寻找更小的可行天数 } else { left mid 1; } } cout ans endl; return 0; }6. 总结与扩展思考回顾这道“环境治理”题它的巧妙之处在于将一个看似复杂的资源分配优化问题通过二分答案和贪心构造可行性判断转化为了经典图论算法Floyd的多次应用。这种“二分答案 贪心/模拟验证”的套路在算法竞赛中非常常见尤其适用于“最小化最大值”或“最大化最小值”这类问题或者像本题一样答案具有单调性且直接求解困难但给定一个答案后判断其可行性相对容易。在实战中我强烈建议按照以下步骤思考此类问题理解题意与建模剥离背景故事抽象出核心元素点、边、权值、操作、目标。分析单调性判断答案通常是天数、次数、容量等是否满足单调性。即如果X天可行那么X1天是否一定可行如果满足就可以二分。设计check函数这是最关键的一步。思考“给定一个候选答案如何高效判断它是否可行” 往往需要一些贪心策略或已知算法。确定边界与数据类型仔细估算数据范围选择合适的数据类型int还是long long设定二分的初始左右边界。编写与调试实现代码特别注意边界条件如二分循环的终止条件、无解情况和潜在的性能瓶颈。对于想要进一步挑战自己的同学可以思考以下变种如果治理效果不是线性的比如治理第一天效果显著后续递减check函数该如何修改如果每天治理的道路数量有限制比如每天只能治理K条路问题将变得复杂很多可能需要用网络流或动态规划来设计check函数。如果目标不是全局最短路径和而是“最脏路径”的灰尘度即所有点对最短路径中的最大值这就是一个典型的“最小化最大值”问题同样可以用二分Floyd判断是否存在一条路径灰尘度超过mid来解决。算法竞赛的魅力就在于将千变万化的实际问题凝练成清晰的数学模型和精巧的算法组合。希望这篇详细的拆解能帮助你不仅AC这道题更能掌握这一类问题的思考方法。
返回列表