【问题标题】:Efficient code for finding route between nodes of a data tree查找数据树节点之间路径的高效代码
【发布时间】:2020-01-23 17:06:56
【问题描述】:

我有一个格式如下的文件:

Y1DP480P T FDVII005 ID=000
Y1DPMS7M T Y1DP480P ID=000
Y1DPMS7M T Y1DP4860 ID=000
Y1DPMS7M T Y1ENDCYP ID=000
Y1DPMS6M T Y1DPMS7M ID=000
Y1DPMS5M T VPY1CM28 ID=000
Y1DPMS5M T Y1DPMS6M ID=000
Y1DPAS21 T Y1DPMS5M ID=000
Y1DPMS4M T FDRBC004 ID=000
Y1DPMS4M T FDYBL004 ID=000

等等。等等

仅使用1-8列和12-19列的数据,可以认为是:

node1 -> node2
node1 -> node3
node3 -> node5
node2 -> node4
node4 -> node5
node5 -> node7

我需要一种有效的方法来映射从给定起始节点到给定结束节点的路径。

例如,如果我想要从 node1 到 node7 的路径,该函数将返回 node1->node3, node3->node5, node5->node7。

目前的做法:

我将文件读入一个数组,将前 19 个字符作为键和值,例如

$data[Y1DP480P T FDVII005] = 'Y1DP480P T FDVII005'  

(我使用值作为键,因为输入文件可能包含重复项,因为这会将它们过滤掉 - 我不认为 PHP 具有“设置”数据结构)。

我有一个递归子例程,可以从给定节点中找到下一个“n”个依赖项,如下所示:

(入口时,$path[]是一个空数组,节点数据在$data中,开始搜索的节点是$job,依赖的深度是$depth)

function createPathFrom($data, $job, $depth) {
    global $path, $maxDepth, $timeStart;
    $job = trim($job);
    // echo "Looking for $job\n";
    if ( $depth > $maxDepth ) {return;} // Search depth exceeded
    // if ( (microtime(true) - $timeStart) > 70 ) {return;} //Might not be needed as we have the same further down
    // $depth += 1;
    // Get the initial list of predecessors for this job.
    // echo __FUNCTION__."New iteration at depth $depth for $job\n";
    $dependents = array_filter($data, function($dataLine) use($job){
        // preg_match('/'.JOB_SPLIT_MASK.'/', $dataLine, $result);
        // $dependent = trim($result[1]);
        $dependent = explode(" ", $dataLine)[0];
        return ( $dependent == $job );
        // return ( preg_match('/'.$job.'/', $dependent) );
    });

    if (count($dependents) == 0) {
        return;
    } else {
        // print_r($predecessors);
        $elapsedTime = microtime(true) - $timeStart;
        // print $elapsedTime." : Searching ".count($dependents)." at depth ".$depth.NL;

        $path = array_merge($path, $dependents);
        foreach($dependents as $dependency) {
            // preg_match('/'.JOB_SPLIT_MASK.'/', $dependency, $result);
            // $dependent = trim($result[3]);
            $dependent = explode(" ", $dependency)[2];
            if ( (microtime(true) - $timeStart) > 85 ) {return;} // Let's get out if running out of time... (90s in HTTPD/conf)
            createPathFrom($data, $dependent, $depth+1);
        }
    }
}

我有一个几乎相同的函数,它为我的终端节点建立了前辈,称为createPathTo

时间限制(70 秒和 85 秒,是的 - 绝对是多余的)和深度限制是为了避免我的 cgi 脚本超时。

如果我以足够的“深度”调用这两个例程,我可以查看它们是否连接,但有很多死胡同。

我认为我正在进行广度优先搜索,而我认为我应该进行深度优先搜索并丢弃未到达目标节点的搜索。

问题:

给定一个开始节点和一个结束节点,是否有高效的搜索算法将返回最少的节点以建立连接或指示未找到路径的某个值?

这个问题来自Recursive function in PHP to find path between arbitrary nodes。我有通往(现在从)我的目标节点的节点,但现在我想将它修剪为仅 2 个节点之间的路径。

编辑:我确定答案已经在 SO 上,但我对 PHP 和这类算法还很陌生,所以一直找不到。

【问题讨论】:

    标签: php algorithm depth-first-search breadth-first-search


    【解决方案1】:

    这样的结构会更好:

    $data =[
        "Y1DP480P" => ["FDVII005" => true],
        "Y1DPMS7M" => ["Y1DP480P" => true, "Y1DP4860" => true, "Y1ENDCYP" => true],
        // ...etc
    ];
    

    因此,每个键都有一组子键,从第一个键开始一步即可到达。尽管集合不存在,但您通常会这样模仿:使用具有 true 值(或您喜欢的任何值)的关联数组。这也将忽略您在输入中可能存在的重复条目。

    那么,标准的 BFS 将非常有效:

    $input = "aaaaaaaa T bbbbbbbb ID=000
    aaaaaaaa T cccccccc ID=000
    cccccccc T eeeeeeee ID=000
    bbbbbbbb T dddddddd ID=000
    dddddddd T eeeeeeee ID=000
    eeeeeeee T gggggggg ID=000";
    
    // Convert input to the data structure:
    $data = [];
    foreach (explode("\n", $input) as $line) {
        list($a, $b) = explode(" T ", substr($line, 0, 19));
        $data[$a][$b] = true;
        if (!isset($data[$b])) $data[$b] = [];
    }
    
    function shortestPath($data, $source, $target) { // Perform a BFS
        $comeFrom[$source] = null;
        $frontier = [$source];
        while (count($frontier)) {
            $nextFrontier = [];
            foreach ($frontier as $key) {
                if ($key == $target) {
                    $path = [];
                    while ($key) { // unwind the comeFrom info into a path
                        $path[] = $key;
                        $key = $comeFrom[$key];
                    }
                    return array_reverse($path); // the path needs to go from source to target
                }
                foreach ($data[$key] as $neighbor => $_) {
                    if (isset($comeFrom[$neighbor])) continue;
                    $comeFrom[$neighbor] = $key;
                    $nextFrontier[] = $neighbor;
                }
            }
            $frontier = $nextFrontier;
        }
    }
    
    $path = shortestPath($data, "aaaaaaaa", "gggggggg");
    
    print_r($path); // ["aaaaaaaa", "cccccccc", "eeeeeeee", "gggggg"]
    

    【讨论】:

    • 谢谢 - 我看看明天有没有时间试试这个。
    • 我的输入文件有超过 1m 行 (1,040,500),大小为 84Mb,但只有大约 100,0000 个唯一节点,因为每个节点可以有许多依赖/前任,每个节点都在我的输入文件的单独一行。所以我当前的数据数组每行输入文件有一个 19 字节的元素。我刚刚尝试了你的例程来构建新数组(这个例程应该在每条记录中调用一次,首先定义 $data ?)我用完了存储但是 - 我已经知道我想要的开始和结束节点,所以我可以修改您的解析器以简单地拒绝任何其他会使其保持较小的内容是吗?
    • 当然。你遇到过内存问题吗?
    • 抱歉 - 不小心发布了 1/2 条评论。但是是的,我滴滴 :-) 请查看编辑后的版本。但是,由于我事先知道我的起始节点,我将更新您的函数以简单地拒绝该行 if $a $startNode
    • 非常感谢 - 现在正在运行。我可能遇到的唯一问题与数据量有关,但您的代码正确识别了路径。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-01-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-12-06
    • 1970-01-01
    相关资源
    最近更新 更多