Floyd算法与二分法在环境治理问题中的实战应用
2026/8/28 2:56:13 网站建设 项目流程

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)时是可以接受的。

所以,问题模型建立如下:

  1. :将每个城市视为图的一个顶点。
  2. 边权:城市i到j的初始灰尘度D[i][j]视为边(i, j)的初始权重。注意,题目可能暗示了图是无向的(即D[i][j] = D[j][i]),或者直接给出的是邻接矩阵。
  3. 操作:每天,你可以让任意一条边(i, j)的权重减少1(但不低于L)。这相当于有P个单位的“治理力”,可以分配到不同的边上。
  4. 目标:寻找一种分配P天治理力(即决定每条边减少多少)的方案,使得得到的新权重矩阵W,经过Floyd算法计算出的全源最短路径矩阵S,其所有元素之和sum(S[i][j])不超过Q。
  5. 输出:我们需要找到满足上述条件的最小治理天数P。如果初始状态(0天)就已经满足,则输出0;如果即使给满题目允许的最大天数(或根据题意推断的天数上限)仍无法满足,则输出-1。

2.2 二分法的引入与可行性判断

最直接的想法是:枚举每一天,模拟治理过程,然后计算最短路径和。但天数P可能很大,这种线性枚举会超时。这时就需要二分法

我们发现,随着治理天数P的增加,我们能够降低的灰尘度总和越多,计算得到的最短路径和total_dust应该是一个非递增的函数。这满足了二分法应用的前提:单调性。我们可以二分搜索这个天数P。

二分法的框架如下:

  1. 确定二分范围[left, right]left通常为0(不治理)。right需要设定一个上界,可以是一个足够大的数(如1e9),或者根据题目数据范围估算。
  2. 在每次循环中,计算中点mid = (left + right) / 2,判断是否能在mid天内使得最短路径和 ≤ Q
  3. 如果可行(check(mid) == true),说明答案可能更小,令right = mid(或在标准写法中,记录答案并令right = mid - 1)。
  4. 如果不可行(check(mid) == false),说明天数不够,令left = mid + 1
  5. 循环直到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]满足:

  1. L ≤ W[i][j] ≤ D[i][j](治理后灰尘度在[L, 初始值]之间)。
  2. 所有边的(D[i][j] - W[i][j])之和 ≤days(因为每天治理一条边一次,总共治理天数就是所有边减少量的总和)。
  3. 由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)函数可以这样实现:

  1. 根据days,构建一个新矩阵W,其中W[i][j] = max(L, D[i][j] - days)。这代表了在days天内,我们尽可能努力治理后能得到的最好(灰尘度最低)的图。
  2. 在这个新矩阵W上运行Floyd算法,计算全源最短路径矩阵dist
  3. 计算total = sum(dist[i][j]) for all i, j
  4. 如果total <= Q,返回true(说明即使在最理想的情况下,days天就够了);否则返回false(说明即使拼命治理,days天也不够)。

注意:这里有一个非常重要的逻辑点。我们用了“可能的最小边权”矩阵来计算最短路径和。如果这个“最小边权”图对应的最短路径和已经满足要求,那么一定存在一种具体的治理方案(不一定需要每条边都治理到下限)使得结果满足要求。因为我们可以先按这个最小边权图来治理,如果某些边治理“过度”了(即实际治理天数分配使得某些边权低于了计算最短路径时的值),我们可以减少对这些边的治理,把天数分配到其他边上,这不会使最终的最短路径和变大(因为边权增加了)。所以,check(days)true是答案可行的充分条件。反之,如果“最小边权”图都不满足,那么任何其他治理方案(边权更大)就更不可能满足了。所以这也是必要条件。因此,这个贪心构造是正确且高效的。

3. 算法细节与实现解析

3.1 Floyd算法的正确应用与优化

check函数中,我们需要对W矩阵跑一遍Floyd。标准的Floyd算法是三重循环,复杂度O(N^3)。对于N=100,单次check100^3 = 1e6次操作,在时间限制内可以接受。但这里有一些细节需要注意:

  1. 初始化dist矩阵初始值就是W矩阵。注意处理自环(dist[i][i] = 0)。
  2. 循环顺序:必须是k循环在最外层。这个顺序不能错,它代表了依次考虑每个顶点作为中转点。
    vector<vector<long 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]; } } } }
  3. 数据类型:灰尘度、最短路径和都可能很大,需要用long long(64位整数)来存储,避免溢出。
  4. 无穷大设置:如果题目中存在不直接相连的城市(灰尘度可能为无穷大或用一个很大值表示),需要在代码中用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 整体代码框架

将以上各部分组合起来,完整的代码结构如下:

  1. 读入N, Q, L,以及初始灰尘度矩阵D
  2. 实现check(long long days)函数,逻辑如前所述。
  3. 设定二分查找的左右边界。
  4. 执行二分查找,得到最小天数ans
  5. 输出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,可以防止left+right可能出现的溢出。

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对于N<=100可以接受,但check函数会被调用 O(log(MAX_DAYS)) 次,大约是30次左右。总复杂度约为 O(30 * N^3) = 30 * 1e6 = 3e7 次操作,在C++中通常可以在1秒内完成。但如果想更稳健,可以注意:

  • 将二维数组用vector<vector<long long>>存储,避免使用大尺寸的静态数组(如long long dist[100][100])导致栈溢出(虽然1001008字节=80KB,通常没问题)。使用vector更安全灵活。
  • 在Floyd的内层循环中,加入对dist[i][k]dist[k][j]是否为INF的判断,可以避免无效的加法运算和比较。
  • 如果N更大(比如达到200),O(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; // 灰尘度下限 vector<vector<LL>> D; // 初始灰尘度矩阵 // 检查在days天内能否使全图最短路径和 <= Q bool check(LL days) { // 1. 构建治理days天后的“最优可能”边权矩阵W vector<vector<LL>> W(n, vector<LL>(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算法,计算全源最短路径 vector<vector<LL>> 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, vector<LL>(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)的多次应用。这种“二分答案 + 贪心/模拟验证”的套路,在算法竞赛中非常常见,尤其适用于“最小化最大值”或“最大化最小值”这类问题,或者像本题一样,答案具有单调性,且直接求解困难,但给定一个答案后判断其可行性相对容易。

在实战中,我强烈建议按照以下步骤思考此类问题:

  1. 理解题意与建模:剥离背景故事,抽象出核心元素(点、边、权值、操作、目标)。
  2. 分析单调性:判断答案(通常是天数、次数、容量等)是否满足单调性。即,如果X天可行,那么X+1天是否一定可行?如果满足,就可以二分。
  3. 设计check函数:这是最关键的一步。思考“给定一个候选答案,如何高效判断它是否可行?” 往往需要一些贪心策略或已知算法。
  4. 确定边界与数据类型:仔细估算数据范围,选择合适的数据类型(int还是long long),设定二分的初始左右边界。
  5. 编写与调试:实现代码,特别注意边界条件(如二分循环的终止条件、无解情况)和潜在的性能瓶颈。

对于想要进一步挑战自己的同学,可以思考以下变种:

  • 如果治理效果不是线性的(比如治理第一天效果显著,后续递减),check函数该如何修改?
  • 如果每天治理的道路数量有限制(比如每天只能治理K条路),问题将变得复杂很多,可能需要用网络流或动态规划来设计check函数。
  • 如果目标不是全局最短路径和,而是“最脏路径”的灰尘度(即所有点对最短路径中的最大值),这就是一个典型的“最小化最大值”问题,同样可以用二分+Floyd(判断是否存在一条路径灰尘度超过mid)来解决。

算法竞赛的魅力,就在于将千变万化的实际问题,凝练成清晰的数学模型和精巧的算法组合。希望这篇详细的拆解,能帮助你不仅AC这道题,更能掌握这一类问题的思考方法。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询