所以问题如下:我提供了一个未加权的树,允许我从任何节点开始,我希望只访问已经在数组中提供的某些节点。我的目标是找到前往所需节点所需的时间。每一条边都需要一分钟才能过去。
我已经尝试实现Dijkstra的算法,以便从我想要访问的节点开始并尝试从那里形成最短路径。 但我的问题是,尽管提供了解决方案,但它可能不是最有效的解决方案,因为我不知道如何强制Dijkstra算法考虑两次在同一边缘上行进。
上图中的此问题的一个示例。假设我希望访问节点[90,50,20,75]并且我从节点90开始并遍历到节点50然后到达节点20我将如何使Dijkstra算法考虑到达节点20之前到节点50的返回行程时间?
答案 0 :(得分:1)
让我详细说明一下我的评论:首先,我们将修复树中的任意根,以便树被植根(也许你已经有了一个有根的树)。然后,第一步是找到一个最小长度循环,它从根开始并在根处结束并通过所有所需节点。
这可以通过分而治之的方法来完成。如果您在任何节点,则可以检查是否需要在路径中包含该节点。如果是这样,你做到了。然后,对于每个子树,只需使用相同的方法并在必要时扩展路径。最后,确保子路径返回到当前子树的根(代码如下)。
找到循环后,需要删除最长的子路径,以便最终得到非循环路径。这可以通过循环遍历线性时间来完成。在我的实现中,我让循环提取不仅发出节点序列,还发出一个标志,确定路径是否只是通过一个节点(并且不访问节点)。因此,此步骤只是查找实际访问的任意两个节点之间的路径段。
为了确定最优性,仍然缺少一步。但是,让我告诉你到目前为止的代码。我已经在JavaScript中实现了它,因为你可以在SO上运行它。实施的目的是可理解性而非效率。
//the tree from your example
var tree = { value: 90, children: [{ value: 50, children: [{ value: 20, children: [{ value: 5 }, { value: 25 }] }, { value: 75, children: [{ value: 66 }, { value: 80 }] }] }, { value: 150, children: [{ value: 95, children: [{ value: 92 }, { value: 111 }] }, { value: 175, children: [{ value: 166 }, { value: 200 }] }] }] };
var nodesToVisit = [90, 50, 20, 75];
//var nodesToVisit = [92, 111, 166];
function findCycle(treeNode, nodesToVisit) {
var subPath = [];
var currentNodeIncluded = false;
if(nodesToVisit.indexOf(treeNode.value) != -1) {
//this node should be visited
subPath.push({node: treeNode, passThrough: false});
currentNodeIncluded = true;
}
//find the subpath for all subtrees
if(treeNode.children) {
for(var i = 0; i < treeNode.children.length; ++i) {
var subTreePath = findCycle(treeNode.children[i], nodesToVisit);
if(subTreePath.length > 0) {
if(!currentNodeIncluded) {
subPath.push({node: treeNode, passThrough: true});
currentNodeIncluded = true;
}
//if we need to visit this subtree, merge it to the current path
subPath = subPath.concat(subTreePath);
subPath.push({node: treeNode, passThrough: true}); //go back to the current node
}
}
}
return subPath;
}
function removeLongestPassThroughSegment(cycle) {
var longestSegmentStart = -1;
var longestSegmentEnd = -1;
//the start of the current pass-through segment between non-pass-through nodes
var currentStart = -1;
var segmentLengthAtStart = -1;
for(var i = 0; i < cycle.length; ++i) {
if(!cycle[i].passThrough) {
//we have found a node that we need to visit
if(currentStart != -1) {
var length = i - currentStart;
if(length > longestSegmentEnd - longestSegmentStart) {
longestSegmentStart = currentStart;
longestSegmentEnd = i;
}
} else
segmentLengthAtStart = i;
currentStart = i;
}
}
//check the path segment that wraps around
if(cycle.length - currentStart + segmentLengthAtStart > longestSegmentEnd - longestSegmentStart) {
longestSegmentStart = currentStart;
longestSegmentEnd = segmentLengthAtStart;
}
//build the final path by cutting the longest segment
var path = [];
var i = longestSegmentEnd;
do {
path.push(cycle[i]);
i++;
if(i >= cycle.length)
i = 0;
} while(i != longestSegmentStart);
path.push(cycle[longestSegmentStart]);
return path;
}
function printPath(path) {
for(var i = 0; i < path.length; ++i)
if(path[i].passThrough)
console.log("Pass through " + path[i].node.value);
else
console.log("Visit " + path[i].node.value);
}
var cycle = findCycle(tree, nodesToVisit);
console.log("Cycle:");
printPath(cycle);
var path = removeLongestPassThroughSegment(cycle);
console.log("Final Path:");
printPath(path);
&#13;
您会发现此代码已找到最佳解决方案并打印:
Final Path:
Visit 90
Visit 50
Visit 20
Pass through 50
Visit 75
即使对于一组更具挑战性的节点,这也会达到最佳路径(var nodesToVisit = [92, 111, 166];
):
Final Path:
Visit 92
Path through 95
Visit 111
Pass through 95
Pass through 150
Pass through 175
Visit 166
现在,找到最佳解决方案的关键是,最终切割的路径段实际上是最长的路径段。在上面的代码中不一定是这种情况,因为您可以自由选择处理子树的顺序,如果您在一个应该访问的节点,您可以自由地进行实际访问(与传递相反) )访问过的子树之间的任何地方。
为此,找到所有所需节点之间的距离(可以在树上有效地完成)。距离最大的对将是起始节点和结束节点。因此,您需要确保它们在循环中的访问随后发生(即,它们之间没有访问过其他节点)。您可以通过在递归调用返回的路径段的开头和结尾处强制执行特定的访问节点来执行此操作。例如,如果子路径包含开始或结束节点,则递归调用也会返回。在调用函数中,以正确的顺序放置这些子路径。这也将简化removeLongestPassThroughSegment()
函数,因为您已经知道最长的路径段是什么。