You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

判断添加边后有向图是否存在环的高效实现问题(CS50 Tideman)

问题描述

现有一个含N(约150个)节点的有向无环图,节点用连续整数0到N-1表示,图的边通过布尔数组locked存储:当存在边n→m时,locked[n, m] = true。现在需要判断添加指定边后图是否会形成环,待添加的边存在pairs数组中,每个元素是包含winner(源节点)和loser(目标节点)的结构体。

当前实现的lockPairs方法和环检测函数hasCycle运行效率极低(部分边检测时nestedLevels调用次数超2000万),而且无法正确检测环,需要高效的环检测实现。

当前代码实现

lockPairs方法

private void lockPairs()
{
    for (int i = 0; i < Program.pairCount-1; i++)
    {
        // don't add an edge if it will make the graph form a cycle
        Console.WriteLine(Program.nestedLevels);
        Program.nestedLevels = 0;
        if (!hasCycle(Program.pairs[i].winner, Program.pairs[i].loser))
        {
            Program.locked[Program.pairs[i].winner , Program.pairs[i].loser] = true;
            Console.WriteLine("Locked");
        }
        else
        {
            Console.WriteLine("NotLocked");
        }
    }
}

hasCycle检测函数

private bool hasCycle(int winner, int loser)
{
    Program.nestedLevels++;
    if (Program.locked[loser,winner])
    {
        return true;
    }
    for (int i = 0; i < Program.numCandidate; i++)
    {
        if (Program.locked[loser,i] && hasCycle(winner, i))
        {
            return true;
        }
    }
    return false;
}
高效环检测实现方案

问题根源

原hasCycle函数的核心问题:

  1. 没有标记已访问节点,递归遍历会重复访问同一节点,导致调用次数指数级增长,效率极低
  2. 逻辑方向是对的(检查loser能否到达winner),但无限制的递归导致大量重复计算

优化思路

添加边winner→loser后形成环的等价条件是:当前图中已经存在从loser到winner的路径。我们可以用DFS/BFS结合访问标记来实现路径检测,避免重复遍历,时间复杂度降到O(N+E),对于150个节点的规模完全够用。

优化代码实现

方案1:DFS带访问标记(递归实现)

// 辅助数组,标记DFS过程中已访问的节点,避免重复遍历
private bool[] visited;

private bool hasCycle(int winner, int loser)
{
    visited = new bool[Program.numCandidate];
    // 检查从loser出发能否走到winner
    return Dfs(loser, winner);
}

private bool Dfs(int current, int target)
{
    if (current == target)
    {
        return true;
    }
    visited[current] = true;
    for (int i = 0; i < Program.numCandidate; i++)
    {
        // 如果有current→i的边,且i未被访问过,继续递归检查
        if (Program.locked[current, i] && !visited[i])
        {
            if (Dfs(i, target))
            {
                return true;
            }
        }
    }
    return false;
}

方案2:BFS实现(非递归,避免栈溢出)

如果递归深度太大(比如节点链很长),可以用BFS非递归实现,更安全:

private bool hasCycle(int winner, int loser)
{
    bool[] visited = new bool[Program.numCandidate];
    Queue<int> queue = new Queue<int>();
    queue.Enqueue(loser);
    visited[loser] = true;

    while (queue.Count > 0)
    {
        int current = queue.Dequeue();
        if (current == winner)
        {
            return true;
        }
        for (int i = 0; i < Program.numCandidate; i++)
        {
            if (Program.locked[current, i] && !visited[i])
            {
                visited[i] = true;
                queue.Enqueue(i);
            }
        }
    }
    return false;
}

进阶优化:预处理传递闭包(适合大量检测场景)

如果需要频繁进行环检测,可以预先用Floyd-Warshall算法计算所有节点的可达性(传递闭包),之后每次检测环只需要O(1)查询:

// 传递闭包矩阵:reachable[a,b]表示a能否到达b
private bool[,] reachable;

// 初始化传递闭包
private void InitReachability()
{
    int nodeCount = Program.numCandidate;
    reachable = new bool[nodeCount, nodeCount];
    
    // 初始化:自己到自己可达,直接边也标记为可达
    for (int i = 0; i < nodeCount; i++)
    {
        reachable[i, i] = true;
        for (int j = 0; j < nodeCount; j++)
        {
            if (Program.locked[i, j])
            {
                reachable[i, j] = true;
            }
        }
    }
    
    // Floyd-Warshall算法计算所有节点对的可达性
    for (int k = 0; k < nodeCount; k++)
    {
        for (int i = 0; i < nodeCount; i++)
        {
            for (int j = 0; j < nodeCount; j++)
            {
                if (reachable[i, k] && reachable[k, j])
                {
                    reachable[i, j] = true;
                }
            }
        }
    }
}

// 环检测直接查传递闭包
private bool hasCycle(int winner, int loser)
{
    return reachable[loser, winner];
}

// 添加边后更新传递闭包
private void UpdateReachability(int from, int to)
{
    int nodeCount = Program.numCandidate;
    if (!reachable[from, to])
    {
        reachable[from, to] = true;
        // 更新所有通过from→to形成的新可达路径
        for (int i = 0; i < nodeCount; i++)
        {
            for (int j = 0; j < nodeCount; j++)
            {
                if (reachable[i, from] && reachable[to, j])
                {
                    reachable[i, j] = true;
                }
            }
        }
    }
}

使用这种方式,初始化一次后,每次环检测都是O(1),添加边后更新传递闭包的代价也很低,适合需要处理大量边检测的场景。

内容的提问来源于stack exchange,提问作者Spektre

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 22:10:27