我有一个计算机科学问题,我最近一直在思考这个问题,我很想听听其他人的反馈:
假设你有一个3D blockworld(就像我的世界)。如果对于任何块,从该块到最底部的块行存在实体路径,则世界是“稳定的”。当世界产生时,它保证是稳定的,但随着玩家四处乱窜,他们可能会让世界变得不稳定。例如,玩家可以挖出一棵树的树干,在这种情况下,树的顶部将在半空中盘旋,因此世界不稳定。
目标是快速弄清楚世界是否因为挖掘而变得不稳定,以及应该移除哪些立方体以使世界恢复到稳定状态。该算法应该是快速的,并且在一个正在进行许多挖掘的世界的环境中工作,其中大部分都不会使世界变得不稳定。
一种天真的方法是在挖掘后取出每个相邻的立方体,找到通往地球底部的路径。如果你找不到任何一个邻居的底部路径,那么这个世界现在已经不稳定了(你还可以了解要删除哪些立方体作为过程的一部分)。当通往底部的路径非常长时,这会分崩离析。很容易想出一个计算成本昂贵的例子(想象一下,移除一个大型的蜿蜒塔楼的顶部)。
这个问题可以被认为是一个图形问题,您希望快速检测单个顶点是否可以将图形划分为两个或更多个组件。
我很想知道是否有人有其他方法可以解决这个问题。
答案 0 :(得分:5)
Link/cut tree可能会对您有所帮助。 Daniel Sleator,该结构的作者shared his solution指向similar problem。看his comments on codeforces.ru你会发现它们很有用。
我会在这里写下我的想法。让我们在底层创建一个顶点。这个顶点是你建筑的地下室。将basement_vertex与底层的所有顶点连接起来。从地下室顶点运行深度优先搜索(DFS)。这个dfs将生成root树。这棵树应该是一些链接切割树的基础(初始阶段)。
<强>更新强>
Link-Cut Trees仅在给定图形为树时才起作用。对于任何图表,我们必须解决动态连接问题。 Here有关动态连接问题的更多信息。
答案 1 :(得分:0)
您可以考虑使用所谓的Flood-fill
算法来标记节点并查看成员资格是否相同。
这是一个简单的图表。我们有原始图G,然后我们每个删除一个立方体以获得图H和I.
igraph
包很适合测试图形连通性。在上面的测试示例中,G和H是可以的,但是删除边(3,5)会导致5,6,7的成员资格与节点1(“地球节点”)的成员资格不同。 (因此,在第3个图表中,图表I,您应该删除多维数据集5,6和7。)
在R语言中,使用igraph库
library(igraph)
edges <- c(c(1,2), c(2,3), c(2,4), c(3,5), c(5,6), c(5,7), c(6,7))
g<- graph(edges, directed=FALSE)
clusters(g)$membership # 1 1 1 1 1 1 1
edges <- c(c(1,2), c(2,3), c(2,4), c(3,5), c(5,6), c(5,7)) # drop edge(6,7)
h<- graph(edges, directed=FALSE)
clusters(h)$membership # 1 1 1 1 1 1 1
edges <- c(c(1,2), c(2,3), c(2,4), c(5,6), c(5,7), c(6,7)) # drop (3,5)
i<- graph(edges, directed=FALSE)
clusters(i)$membership # 1 1 1 1 2 2 2
clusters
是igraph包附带的功能。其membership
值显示图表中每个节点的标签。如果任何节点(在您的情况下为立方体)具有与“地球节点”不同的成员资格值,则它不会连接到地面,应该被删除。
答案 2 :(得分:0)
如果每个块都是“unstable-if-dug”,我会考虑存储,也称为 cut-vertex 或 biconnected组件。然后,您可以在单击时以恒定时间查找它。
基本上,我们的想法是能够立即知道它是否稳定。然后在等待下一个用户输入时重新计算图形。用户体验更顺畅,除非有人快速点击非常,否则他们不应该注意到减速。
您可以使用DFS在O(n)时间内找到全部图形的切割顶点。
来自维基百科biconnected components:
我们的想法是在保持以下信息的同时运行深度优先搜索:
- 深度优先搜索树中的每个顶点的深度(一旦被访问)
- 对于每个顶点v,深度优先搜索树中v的所有后代的邻居的最低深度,称为低点。
在访问v ...的所有后代之后,可以计算v的低点作为最小值:
- v的深度
- v的所有邻居的深度(在深度优先搜索树中v的父亲旁边)
- 深度优先搜索树中v的所有子节点的低点。
关键的事实是非根顶点v是一个切割顶点(或关节点),当且仅当有一个
y
<的孩子时才将两个双连通组件分开。强>v
,以lowpoint(y) ≥ depth(v)
。根顶点必须单独处理:当且仅当它至少有两个子节点时,它才是一个切割顶点。
答案 3 :(得分:0)
一个简单的解决方案是使用BFS(广度优先搜索)来检查被删除节点(minecraft块)的所有直接邻居是否属于同一个连接组件。
标记第一个邻居,然后在连接到某个标记节点时检查所有其他邻居。
这是我测试过的代码(C#):
public bool CanRemoveNode(int indexNode)
{
var initialNeighbors = this.nodeNeighbors[indexNode];
if (initialNeighbors.Count() < 2)
{
// leaf node - can be safely removed
return true;
}
HashSet<int> nodesComponent = new HashSet<int>(initialNeighbors.Take(1));
HashSet<int> nodesProcessed = new HashSet<int>();
Queue<int> nodesVisit = new Queue<int>();
foreach (int indexNodeStart in initialNeighbors.Skip(1))
{
nodesProcessed.Clear();
nodesVisit.Clear();
nodesVisit.Enqueue(indexNodeStart);
while (nodesVisit.Any())
{
int indexNodeCurrent = nodesVisit.Dequeue();
nodesProcessed.Add(indexNodeCurrent);
nodesComponent.Add(indexNodeCurrent);
foreach (int indexNodeNeighbor in this.nodeNeighbors[indexNodeCurrent])
{
if (indexNodeNeighbor == indexNode)
{
// do not inspect removed node
continue;
}
if (nodesProcessed.Contains(indexNodeNeighbor))
{
// neighbor node already processed
continue;
}
if (nodesComponent.Contains(indexNodeNeighbor))
{
// neighbor node belongs to the component - we can terminate search
goto NextStartNode;
}
// mark neighbor node for further inspection
nodesVisit.Enqueue(indexNodeNeighbor);
}
}
return false;
NextStartNode:
;
}
return true;
}
&#39; nodeNeighbors&#39; dictionary包含每个节点的邻居索引。
在最坏的情况下,我们需要运行与任意两个直接邻居节点之间的最长路径长度一样多的BFS迭代。
更快的方法是使用唯一的数字标签标记每个直接邻居,然后执行连接组件标签。我们可以为每个标签运行并行BFS搜索,并在两个标签相遇时终止,并在底层的Union-Find结构上执行Union操作。
当执行的Union操作数与初始标签减去1时相同,即所有标签属于同一组件时,整个搜索可以终止。
另一个速度改进是创造多个种子&#34;均匀分布在移除点周围。每个种子都是具有唯一标签的节点。这可以保证组件更快地连接到一个组件中。
您也可以在经过一定数量的迭代(例如10 000)后终止搜索,因为这意味着立方体连接的距离非常远,玩家甚至不会发现断开连接。
此后也可以在后台进行搜索。
答案 4 :(得分:0)
检查以下代码,graphA损坏,而graphB没有损坏,isGraphBroken方法为您检查该情况,甚至告诉您调用堆栈有多少,还有哪些节点被其他节点破坏了
let graphA = [
['A', 'B'],
['C', 'D'],
['D', 'E']
];
let graphB = [
['A', 'B'],
['B', 'C'],
['C', 'D'],
['D', 'E']
];
function isGraphBroken(graph) {
let map = new Map();
let allNodes = new Set();
let callStack = 0;
graph.forEach(([x, y]) => {
allNodes.add(x);
allNodes.add(y);
callStack++;
if (map.has(x)) {
map.get(x).add(y);
} else {
map.set(x, new Set([y]));
}
if (map.has(y)) {
map.get(y).add(x);
} else {
map.set(y, new Set([x]));
}
});
if (allNodes.size <= 1) {
console.log("there is a way between each node pairs");
return false;
}
let traverseNode = (node => {
callStack++;
if (allNodes.size == 0) {
return;
}
if (allNodes.has(node))
allNodes.delete(node);
if (allNodes.size == 0) {
return;
}
let childs = map.get(node);
if (childs && childs.size > 0)
childs.forEach(child => {
if (allNodes.has(child)) {
traverseNode(child);
}
});
});
traverseNode(allNodes.values().next().value);
console.log("callstack length", callStack);
if (allNodes.size == 0) {
console.log("there is a way between each node pairs");
return false;
}
console.log("no way from these nodes to others:", ...allNodes);
return true;
}
console.log("checking ... graphA:")
isGraphBroken(graphA);
console.log("checking ... graphB:")
isGraphBroken(graphB);