所以在Hackerrank的一个名为“非循环图”的编程竞赛中遇到了这个挑战,它基本上归结为计算“定向非循环图”中每个节点可到达的节点数。例如,假设你有一个这样的图表:
[ 1 ] ---->[ 2 ]--->[ 4 ]--->[ 5 ]
[ 3 ] ------/
可达性计数(包括原点节点):
Node 1: 4
Node 2: 3
Node 3: 4
Node 4: 2
Node 5: 1
我的方法是通过备忘录进行“深度优先”遍历。看了很多,但似乎运行时间不能进一步改善,因为在这种情况下会发生过度计数:
[ 1 ] ---->[ 2 ]--->[ 4 ]--->[ 5 ]
[ 3 ] ------/--------/
第三个节点将计算第四个节点,即使第二个节点已计入第四个节点。为了让事情变得更糟,我只用JavaScript解决了这些挑战。它是我的主要语言,我从推动其界限中获得了快感。领导板上没有人用JavaScript解决它,但我认为它是可能的。比赛结束后,我设法通过以下代码传递了24个测试用例中的13个:
function Solution( graph, nodes ) {
var memory = new Array( nodes + 1 )
, result = 0;
graph.forEach( ( a, v ) => DepthFirstSearch( graph, v, memory ) );
// challenge asks for an output variation, but the accurate
// reachability count of every node will be contained in "d.length".
memory.forEach( ( d, i ) => { if ( i && ( 2 * d.length ) >= nodes ) result++; } );
return result;
}
function DepthFirstSearch( graph, v, memory ) {
if ( memory[ v ] ) return memory[ v ];
var descendants = new Uint16Array( [ v ] );
graph[ v ].forEach( u => {
descendants = MergeTypedArrays(
DepthFirstSearch( graph, u, memory ),
descendants
);
} );
// make elements unique
// to avoid over counting
return memory[ v ] = Uint16Array.from( new Set( descendants ) );
}
function MergeTypedArrays(a, b) {
var c = new a.constructor( a.length + b.length );
c.set( a );
c.set( b, a.length );
return c;
}
// adjacency list
var graph = [
[], // 0
[ 2 ], // 1
[ 4 ], // 2
[ 2 ], // 3
[ 5 ], // 4
[] // 5
];
var nodes = 5;
Solution( graph, nodes );
对于大于50kb的所有输入都失败,可能是输入有大量节点和边缘(即50,000个节点和40,000个边缘)。如果没有识别或设想更快,更高效的内存算法,那么接下来要尝试的内容完全无法实现。考虑使DFS迭代,但我认为记忆数千个阵列的内存消耗将使这相形见绌,这似乎是主要问题。我对Hackerrank的“Abort Called”和“Runtime Error”进行了11次失败的测试(与“Timeout”相反)。还尝试使用“union”来“bitSets”,但由于bitSets数组需要足够大以存储高达50,000的数字,因此内存消耗变得更糟。
约束:
1 ≤ n,m ≤ 5×10^4
1 ≤ a(i),b(i) ≤ n and a(i) ≠ b(i)
It is guaranteed that graph G does not contain cycles.
只是想明确表示,由于此挑战被锁定,我不会获得任何通过所有测试的积分,这是出于教育目的,主要是优化。我知道相关的SO帖子指向拓扑排序,但据我所知,拓扑排序仍然会超过上述情况,因此不是一个可行的解决方案。如果我误解了,请赐教。提前感谢您的时间。
问题:如何进一步优化?有更有效的方法吗?
答案 0 :(得分:2)
深度优先搜索(DFS)是解决此问题的一种好方法。另一种方法是广度优先搜索(BFS),它也可以并行运行并且可以很好地进行优化 - 但所有代价都要高得多。所以我的建议是坚持使用DFS。
首先我必须支持,但我的JavaScript技能不是很好(即它们不存在)所以我的解决方案使用的是Java,但这些想法应该很容易移植。
您的初始问题缺少一个非常重要的细节:我们只需找到可达节点数大于或等于|V| / 2
为什么重要?计算每个节点的可到达节点的数量是昂贵的,因为我们必须从图中的每个节点开始执行DFS或BFS。但是如果我们只需要找到具有上述属性的节点,那就容易多了。
让后继者(n)成为可从 n 到达的所有节点,祖先(n)是可以到达 n的所有节点。 我们可以使用以下观察来大幅减少搜索空间:
我们如何使用它?
false
>= |V| / 2
(见下文)使用迭代DFS的解决方案
public int countReachable(int root, boolean[] visited, boolean[] ignored, Graph graph) {
if (ignored[root]) {
return 0;
}
Stack<Integer> stack = new Stack<>();
stack.push(root);
int count = 0;
while (stack.empty() == false) {
int node = stack.pop();
if (visited[node] == false) {
count++;
visited[node] = true;
for (int neighbor : graph.getNeighbors(node)) {
if (visited[neighbor] == false) {
stack.push(neighbor);
}
}
}
}
if (count * 2 >= graph.numNodes()) {
return markAndCountAncestors(root, visited, ignored, graph);
} else {
return markSuccessors(root, visited, ignored, graph);
}
}
标记祖先的功能
这只是另一个DFS,但使用了转置图。请注意,我们可以重用visited
数组,因为我们将使用的所有值都是false
,因为这是一个非循环图。
public int markAndCountAncestors(int root, boolean[] visited, boolean[] ignored, Graph graph) {
Stack<Integer> stack = new Stack<>();
stack.push(root);
visited[root] = false;
int count = 0;
while (stack.empty() == false) {
int node = stack.pop();
if (visited[node] == false && ignored[node] == false) {
count++;
visited[node] = true;
ignored[node] = true;
for (int neighbor : graph.transposed.getNeighbors(node)) {
if (visited[neighbor] == false && ignored[node] == false) {
stack.push(neighbor);
}
}
}
}
return count;
}
标记后续者的功能
请注意,我们已经有了后继者,因为它们只是我们将visited
设置为true的节点。
public int markSuccessors(int root, boolean[] visited, boolean[] ignored, Graph graph) {
for(int node = 0; node < graph.numNodes(); node++) {
if (visited[node)) {
ignored[node] = true;
}
}
return 0;
}
计算结果的功能
public void solve(Graph graph) {
int count = 0;
boolean[] visited = new boolean[graph.numNodes()];
boolean[] ignored = new boolean[graph.numNodes()];
for (int node = 0; node < graph.numNodes(); node++) {
Arrays.fill(visited, false); // reset visited array
count += countReachable(node, visited, ignored, graph);
}
System.out.println("Result: " + count);
}
在你发布的大型测试用例中,这对我来说只需7.5秒。如果您反转迭代顺序(即在solve
中以最大的节点ID开始),它会下降到4秒,但这有点像欺骗^^