腐烂橘子问题详解:如何计算感染所有橘子所需的最小时间

在算法学习和面试准备的过程中,图论问题总是让人既兴奋又头疼。今天,我们不仅要解决一个经典的图遍历问题,还要深入探讨其中的优化策略。我们将一起研究“腐烂橘子”问题:在一个网格中,给定新鲜橘子、腐烂橘子和空单元格,计算出所有新鲜橘子腐烂所需的最小时间。如果无法让所有橘子腐烂,我们还需要能够识别这种情况。

在这篇文章中,我们将通过三个不同的维度来剖析这个问题:首先,我们会通过简单的迭代来模拟传染过程;其次,我们会尝试使用广度优先搜索(BFS)来达到最优的时间复杂度;最后,我们还会讨论一些边界情况和实际应用场景。准备好了吗?让我们开始这次算法之旅吧。

问题理解与核心逻辑

首先,让我们明确一下问题的规则,这对于构建解决方案至关重要。我们有一个 INLINECODEe1d54333 的矩阵 INLINECODEbdbe297a,其中的数值代表三种状态:

  • 0:空单元格(墙壁),橘子无法穿过,腐烂也不会通过这里传播。
  • 1:新鲜橘子,这是我们要被“感染”的目标。
  • 2:腐烂橘子,这是感染源。

传播规则: 每一个腐烂橘子(值为 2)在每一分钟内,都会使其上下左右相邻(4连通)的新鲜橘子(值为 1)变成腐烂橘子。我们需要计算的是,直到再也没有新鲜橘子存在时,经过了多少分钟。

这个问题本质上是一个多源广度优先搜索问题。你可以把所有的初始腐烂橘子看作是图中的多个“起点”,它们同时向外扩散,寻找“终点”(新鲜橘子)。扩散的距离(层数)就是我们所需的时间。

方法一:朴素模拟法

让我们先从最直观的方法开始。正如我们在日常思维中模拟的那样,我们让时间一秒一秒地流逝,每一秒我们检查整个网格,看看哪些橘子在这一秒被腐烂了。

算法思路:

  • 我们维护一个计时器 elapsedTime,初始为 0。
  • 我们遍历整个矩阵。如果在第 INLINECODEc40bfcfd 分钟,某个单元格的值是 INLINECODE8535805a(例如初始腐烂橘子是 2,即 0+2),那么它在这一分钟具有传染性。
  • 当我们找到具有传染性的橘子时,我们检查它的四个邻居。如果邻居是新鲜的(值为 1),我们将邻居的值更新为 mat[i][j] + 1(即变成下一分钟的腐烂源)。这种标记法非常巧妙,它不仅标记了橘子已腐烂,还记录了它是在哪一分钟被腐烂的,从而防止了它在同一分钟内进行二次传播。
  • 如果在某次遍历中,没有任何新鲜橘子被腐烂,说明扩散停止,我们可以退出循环。
  • 最后,再次检查网格。如果还有值为 1 的橘子,说明存在无法被感染的区域,返回 -1;否则,返回 elapsedTime

虽然这个方法逻辑清晰,但它的时间复杂度较高。在最坏情况下,我们可能需要遍历整个网格多次,复杂度达到了 O((n x m)^2)。

#### C++ 实现:朴素迭代法

#include 
#include 
using namespace std;

// 辅助函数:判断坐标是否安全
bool isSafe(int i, int j, int n, int m) {
    // 确保不越界
    return 0 <= i && i < n && 0 <= j && j < m;
}

int orangesRot(vector<vector>& mat) {
    int n = mat.size();
    int m = mat[0].size();

    // 标记位:用于检查在一次迭代中是否发生了腐烂
    bool changed;
    // 经过时间的计数器
    int elapsedTime = 0;

    // 四个方向:下、右、上、左
    vector<vector> directions = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};

    // 持续迭代直到没有新的腐烂发生
    while (true) {
        changed = false;
        // 遍历每一个单元格
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                
                // 关键逻辑:如果该橘子是在 elapsedTime 时刻腐烂的(值为 elapsedTime + 2)
                // 它将在当前这一轮(代表这一分钟)进行感染
                if (mat[i][j] == elapsedTime + 2) {
                    
                    // 检查四个方向的邻居
                    for (auto& dir : directions) {
                        int x = i + dir[0];
                        int y = j + dir[1];
                        
                        // 如果邻居安全且是新鲜橘子(值为1)
                        if (isSafe(x, y, n, m) && mat[x][y] == 1) {
                            // 将其腐烂,并标记时间戳
                            // 例如:elapsedTime=0时,初始2感染1,使其变为3
                            // 这意味着它在t=1时具有传染性
                            mat[x][y] = mat[i][j] + 1; 
                            changed = true;
                        }
                    }
                }
            }
        }
        
        // 如果没有任何橘子被腐烂,说明传染结束
        if (!changed) break;
        
        // 时间增加
        elapsedTime++;
    }

    // 最终检查:是否还有新鲜橘子残留?
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            if (mat[i][j] == 1) {
                return -1; // 无法全部腐烂
            }
        }
    }

    return elapsedTime;
}

// Driver Code
int main() {
    vector<vector> mat = {
        {2, 1, 0, 2, 1}, 
        {1, 0, 1, 2, 1}, 
        {1, 0, 0, 2, 1}
    };
    cout << "Minimum time to rot: " << orangesRot(mat) << endl;
    return 0;
}

方法二:广度优先搜索(BFS)—— 期望的高效解法

你可能会觉得上面的方法有点“笨”,因为它每秒钟都要重新扫描整个网格。有没有一种方法,能够精确地控制每一分钟只处理当前被腐烂的那一批橘子呢?答案是肯定的,那就是使用队列实现的广度优先搜索。

BFS 是处理此类“层级扩散”问题的利器。想象一下,所有初始的腐烂橘子是“第一波”感染源。它们同时向外扩散,感染到的橘子组成“第二波”,以此类推。

优化思路:

  • 初始化阶段:首先遍历一次矩阵。将所有初始状态为腐烂橘子(2)的坐标加入队列。同时,统计新鲜橘子(1)的总数。如果队列是空的但仍有新鲜橘子,直接返回 -1;如果没有新鲜橘子,直接返回 0。

n2. 处理阶段:我们开始处理队列中的元素。这里有一个技巧:我们可以使用 INLINECODE55c09df8 循环,但在内部增加一个基于当前队列长度的 INLINECODEafb1be53 循环。这能让我们区分“当前分钟”和“下一分钟”的橘子。

  • 时间推进:每当我们处理完当前队列中的所有元素(即当前波次的所有腐烂源都完成了感染工作),我们就将时间 +1。如果在这一分钟内有新鲜橘子被腐烂并加入了队列,它们将在下一轮被处理。
  • 终止条件:当队列为空时,说明没有新的感染源了。此时检查新鲜橘子计数是否为 0。

这种方法的时间复杂度是 O(n x m),因为我们每个单元格最多被访问一次(入队一次)。这是解决此问题的最优时间复杂度。

#### C++ 实现:广度优先搜索

#include 
#include 
#include 
using namespace std;

// 定义坐标结构体,方便操作
struct Coordinate {
    int r, c;
    Coordinate(int r, int c) : r(r), c(c) {}
};

int orangesRottingBFS(vector<vector>& mat) {
    int n = mat.size();
    int m = mat[0].size();
    queue q;
    int fresh = 0; // 新鲜橘子计数器

    // 1. 初始化队列
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            if (mat[i][j] == 2) {
                // 将所有初始腐烂橘子入队
                q.push(Coordinate(i, j));
            } else if (mat[i][j] == 1) {
                // 统计新鲜橘子数量
                fresh++;
            }
        }
    }
    
    // 如果没有新鲜橘子,不需要时间
    if (fresh == 0) return 0;
    
    // 如果有新鲜橘子但没有腐烂橘子,不可能完成,返回 -1
    if (q.empty()) return -1;

    int time = 0;
    // 四个方向数组
    int dr[] = {-1, 1, 0, 0};
    int dc[] = {0, 0, -1, 1};

    // 2. 开始 BFS
    while (!q.empty()) {
        // 关键:记录当前层的节点数量
        // 这代表当前这一分钟内有多少个橘子在同时进行感染
        int size = q.size();
        bool rottedAny = false;

        for (int i = 0; i < size; i++) {
            Coordinate curr = q.front();
            q.pop();

            // 检查四个方向
            for (int k = 0; k = 0 && nr = 0 && nc < m && mat[nr][nc] == 1) {
                    // 腐烂它
                    mat[nr][nc] = 2;
                    fresh--; // 减少新鲜橘子数量
                    rottedAny = true;
                    // 将新腐烂的橘子加入队列,作为下一波的感染源
                    q.push(Coordinate(nr, nc));
                }
            }
        }

        // 只有在当前层确实腐烂了橘子的情况下,时间才增加
        if (rottedAny) {
            time++;
        }
    }

    // 3. 最终检查
    // 如果新鲜橘子数量为0,说明全部腐烂,返回累计时间
    // 否则,说明有无法到达的橘子,返回 -1
    return fresh == 0 ? time : -1;
}

int main() {
    vector<vector> mat = {
        {2, 1, 0, 2, 1}, 
        {0, 0, 1, 2, 1}, 
        {1, 0, 0, 2, 1}
    };
    int result = orangesRottingBFS(mat);
    if (result != -1)
        cout << "Minimum time to rot: " << result << endl;
    else 
        cout << "All oranges cannot rot." << endl;
    return 0;
}

#### Python 实现:同样的逻辑,更简洁的语法

为了让你更好地理解,我们也提供一个 Python 的版本。Python 的 collections.deque 是实现 BFS 队列的最佳选择,因为它的 pop(0) 操作是 O(1) 的。

from collections import deque

def orangesRottingBFS(mat):
    if not mat: return 0
    
    rows, cols = len(mat), len(mat[0])
    queue = deque()
    fresh_count = 0
    
    # 初始化:统计新鲜橘子,并将腐烂橘子入队
    for r in range(rows):
        for c in range(cols):
            if mat[r][c] == 2:
                queue.append((r, c))
            elif mat[r][c] == 1:
                fresh_count += 1
                
    # 如果没有新鲜橘子,直接返回0
    if fresh_count == 0:
        return 0
    
    # 四个方向
    directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
    time = 0
    
    while queue and fresh_count > 0:
        # 每次处理当前层的所有节点
        for _ in range(len(queue)):
            r, c = queue.popleft()
            
            for dr, dc in directions:
                nr, nc = r + dr, c + dc
                
                # 如果是新鲜橘子
                if 0 <= nr < rows and 0 <= nc  0,说明进入了下一分钟
        if queue:
            time += 1
            
    return time if fresh_count == 0 else -1

# 测试
mat = [[2,1,0,2,1], [1,0,1,2,1], [1,0,0,2,1]]
print(f"Minimum time: {orangesRottingBFS(mat)}")

常见陷阱与调试技巧

在实际编码中,你可能会遇到一些棘手的情况。让我们来看看如何处理它们:

  • 孤立的新鲜橘子

场景*:有些新鲜橘子被 0(空单元格)完全包围,没有任何腐烂橘子能接触到它们。
解决方案*:这就是为什么我们在 BFS 结束后(或每次迭代后)必须检查 INLINECODEe87e48e7 或遍历矩阵查找残留的 INLINECODE17f7f368。如果存在这样的橘子,必须返回 -1。

  • 时间计数偏差

场景*:在 BFS 中,什么时候让 time 加 1 很容易出错。如果你在每次循环加 1,可能会多算。
解决方案*:按照我们上面的代码,处理完“当前这一层”的所有节点后,再加 1。因为“当前这一层”是同时发生的,只代表过了 1 单位时间。或者记录橘子变成腐烂时的值 dist[nr][nc] = dist[r][c] + 1,最后取矩阵中的最大值减 2。

  • 空网格或无橘子网格

场景*:输入是 INLINECODE1d448a3e 或者全是 INLINECODEb711778a。
解决方案*:在代码开头做防御性检查,避免数组越界或无意义的计算。

实际应用场景

虽然这听起来像是一个抽象的数学谜题,但这种算法模型在现实世界中有着广泛的应用:

  • 网络病毒传播模拟:计算一个计算机病毒在局域网中感染所有设备所需的时间。这里的橘子就是设备,腐烂就是感染。
  • 图像处理:在某些图像分割算法中,从种子点开始填充区域,计算填充到整个连通域所需的步长。
  • 游戏开发:计算游戏中毒素或法力光环的扩散范围和持续时间。

总结与关键要点

我们深入探讨了腐烂橘子问题,从简单的迭代模拟到高效的广度优先搜索。通过对比我们可以看到,虽然朴素方法容易想到,但在处理大规模数据时效率低下。BFS 利用队列的先进先出特性,完美地模拟了并行传染的过程,将时间复杂度优化到了 O(n x m)。

核心记忆点:

  • 这是一个多源 BFS 问题。
  • 初始化时将所有“起始点”入队。
  • 使用“层级遍历”的概念来控制时间的流逝(处理完一层节点,时间+1)。
  • 最后一定要检查是否还有剩余的“新鲜”节点。

下次遇到类似的网格扩散问题,你可以自信地拿出这套 BFS 模板来解决了!希望这篇文章能帮助你更好地理解图论算法的魅力。继续练习,保持好奇!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。如需转载,请注明文章出处豆丁博客和来源网址。https://shluqu.cn/47450.html
点赞
0.00 平均评分 (0% 分数) - 0