Conversation
Walking back through optimal parents stopped at a vertex without parents, taking that to mean the start had been reached. An edge costing nothing breaks both halves of that. A vertex reached again at exactly the cost already recorded gains another optimal parent, so across a zero-cost edge a vertex becomes an optimal parent of itself, or of a vertex it forms a zero-cost cycle with. Walking those never ends, and the first solution never arrives: the iterator allocates until the process dies. A zero-cost cycle through the start also gives the start parents of its own, so having no parents no longer identifies it. Stop at the start vertex itself, and where a loop is possible, skip parents already on the path being built, backtracking when that leaves a vertex with nowhere to go. Paths stay simple, which is what astar already promises. Costs are non-negative, so a cycle among optimal parents needs every edge on it to cost nothing. Whether such an edge was ever relaxed is recorded during the search, and when there was none the original walk is used unchanged, which keeps the usual case off the slower path. Fixes evenfurther#837
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #837.
astar_bagnever returns its first solution when the graph has a cycle whose edges all cost zero. It is not slow, it does not terminate: the iterator allocates until the process dies. A single zero-cost self loop is enough.Cause
Two things break at once.
A vertex reached again at exactly the cost already recorded gains another optimal parent. Across an edge costing nothing that is reachable, so a vertex becomes an optimal parent of itself, or of a vertex it forms a zero-cost cycle with.
completewalks back through parents and never finds the end.Less obviously,
completedecided it had reached the start when a vertex had no parents. A zero-cost cycle through the start gives the start parents of its own, so that test stops firing. This is what the smallest random failure looked like, with1 -> 0,0 -> 2,2 -> 1all costing zero:Fix
Stop at the start vertex itself rather than at a vertex without parents, and where a loop is possible, skip parents already on the path being built, backtracking when that leaves a vertex with nowhere to go.
Paths therefore stay simple, which is the position
astaralready takes: "a node will never be included twice in the path". It is also the only finite answer available, since a zero-cost cycle admits infinitely many equally cheap walks.The backtracking matters. Skipping a parent can empty the candidate set at a vertex that is not the start, and the old code read an empty set as "reached the start" and emitted the path. My first attempt did exactly that and turned the hang into
astar_bagreporting a cost while yielding no solutions at all, which the random comparison below caught.Keeping the usual case at full speed
Costs are non-negative, so a cycle among optimal parents needs every edge on it to cost nothing. The search records whether any such edge was ever relaxed, and when there was none the original walk runs unchanged.
That separation is worth having. Selecting between the two inside the loop, rather than choosing the loop up front, costs about 17% on path enumeration even when the guarded branch never runs. Enumerating all 3432 shortest paths across an open 7x7 grid, fastest of 20 runs, alternating which build goes first over 6 rounds:
mainso +3.6% on a deliberately worst-case workload. For reference, two builds of unmodified
mainin separate worktrees measured 0.332-0.348 and 0.334-0.335 on the same harness, so the residual is small but real rather than layout noise.Tests
tests/astar_bag_zero_cost.rs:astar_bagreports must matchastar, and a reported cost must come with at least one solutionAgainst
mainthe first two hang and the random ones hang, so they do detect the bug. On an open 7x7 grid both versions return the same 3432 paths, so nothing changes where there is no zero-cost cycle.