Skip to main content
Commonmark migration
Source Link

###Notes

Notes

###Notes

Notes

deleted 238 characters in body
Source Link
rici
  • 12.1k
  • 22
  • 39

Let's recall that the basic breadth-first algorithm involves the use of a queue of transition tree nodes. We initialise the queue with the root node ($[start, ""]]$$<start, "">$) and then, as long as the queue is non-empty and we have not found enough solutions, we do the following:

With this modification, we can count the accepting node when we push it onto the next-level queue, rather than when we pop if off the current queue. Counting the accepting node early means that we can terminate the algorithm earlier.

Let's recall that the basic breadth-first algorithm involves the use of a queue of transition tree nodes. We initialise the queue with the root node ($[start, ""]]$) and then, as long as the queue is non-empty and we have not found enough solutions, we do the following:

With this modification, we can count the accepting node when we push it onto the next-level queue, rather than when we pop if off the current queue. Counting the accepting node early means that we can terminate the algorithm earlier.

Let's recall that the basic breadth-first algorithm involves the use of a queue of transition tree nodes. We initialise the queue with the root node ($<start, "">$) and then, as long as the queue is non-empty and we have not found enough solutions, we do the following:

A less hand-wavey answer saying roughly the same thing
Source Link
rici
  • 12.1k
  • 22
  • 39

You should just useThe essence of this problem is to search the possibly-infinite transition tree induced by the DFA. Each node in this tree consists of a breadthstate and a string (the prefix of a recognised sentence). For each possible transition from the state, there is a child node consisting of the target state and the string augmented with the transition character. The target strings are those belonging to nodes whose states are accepting states.

I think this assignment is probably intended to get you to understand recursion with back-tracking, since there is a pretty simple solution: recursively (depth-first search) walk the transition tree, failing if the recursion depth reaches the limit on string length. (If strings can be long, you might want to use an explicit stack rather than recursion to avoid stack overflow.)

One potential problemThat is thatnot, in my opinion, the BFS might accumulatebest solution to this problem. I'd use a hugebreadth-first search limited to the number of short non-accepted strings before it generates useful stringsyou need to find. ButHowever, if it is OKyou do choose to assume thatimplement the DFA has norecursive backtracking solution, I do recommend that you do useless state elimination as suggested by [Note 1]; otherwise, a self-looping dead statesstate -(with at least two loops) will produce exponential slow-down during the DFS.

Let's recall that the basic breadth-first algorithm involves the use of a queue of transition tree nodes. We initialise the queue with the root node ($[start, ""]]$) and then, as long as the queue is non-empty and we have not found enough solutions, for every state therewe do the following:

1. Remove the first node from the queue
2. If that node has an accepting state, report the string as a solution.
3. Append all possible transitions from that node to the end of the queue.

The result of this algorithm is that solutions will be generated in order by length, shortest strings first.

The problem with breadth-first search, as I gather you discovered through experimentation, is that if the some sequence ofbranching factor is high and several transitions which will reachare required before an accepting state --is found, then you can reduce the damage by limiting thequeue expands to an exponential size (and that takes exponential time).

However, not all of the setqueue is necessary, since the number of target strings is known in advance. If we assume that the DFA has no useless states [Note 1], then every node in the transition tree is either a level tosolution or has at least one solution in its subtree. Consequently, the totalmaximum useful size of the queue is the number of strings you want to generatesolutions we require. When doing thatthe queue has reached this size, you might want to makewe just don't do the append. When we find a solution, we can reduce the size limit by one.

Imposing this limit on the queue size removes the possibility of exponential time blow-up, but a naïve implementation no longer guarantees that the solutions found are the shortest solutions. This introduces an attempt to keep as many different states asinefficiency, and even a possible error (because in pathological cases we might reach the string size limit before finding enough solutions, because if alleven though shorter solutions exist).

As a fix, we can track the strings you keep arelevels during the BFS walk, reordering the traverse of each level. In this version, we use two queues, one for the current level, from which we pop nodes, and one statefor the next level, onto which we push newly found nodes. Every time the result set mightcurrent queue becomes empty we've finished a level; we then swap the two queues. If both queues are empty, there's nothing more to be highly biaseddone. Unless you have

Now we're not limited to processing the queue for a huge numberlevel from left-to-right; we can process it as we see fit, as long as we do all of statesit before moving on to the next node.

For a first approximation, we can ensure that should be fairly simplewe don't lose any accepting states. When we push a new node onto the next level:

  • If it is an accepting node, we push it onto the beginning. If the queue is full, we push the new node anyway, popping the last node.

  • If it is not an accepting node and the queue is not full, we push it at the end.

A more sophisticated heuristicWith this modification, we can count the accepting node when we push it onto the next-level queue, rather than just keeping a random collection ofwhen we pop if off the current queue. Counting the accepting node early means that we can terminate the algorithm earlier.

That still won't guarantee that all shortest strings at each levelare produced, wouldbut it should be to startsufficient in most cases. It can be improved by computingsorting the shortest pathnext-level queue by the minimum distance from each non-accepting state to somean accepting state, using the standard all-shortest-paths algorithm. Then atThe minimum distances for each level, youstate can selectbe precomputed with a simple breadth-first search over the set of candidatesDFA, which are closestwill also serve to being acceptedeliminate useless states


###Notes

  1. Here I'm using the phrase "useless state" in a possibly non-standard way. By "useless state" I mean a state which does not appear in any transition sequence ending at an accepting state. In other words, for a state to be useful, it must be

    • reachable from the start state

    • accepting, or able to reach an accepting state

If that's not guaranteed in the DFAs you're begin given, it is easy to guarantee by detecting and removing useless states.

You should just use a breadth-first search.

One potential problem is that the BFS might accumulate a huge number of short non-accepted strings before it generates useful strings. But if it is OK to assume that the DFA has no dead states -- that is, for every state there is some sequence of transitions which will reach an accepting state -- then you can reduce the damage by limiting the size of the set of strings in a level to the total number of strings you want to generate. When doing that, you might want to make an attempt to keep as many different states as possible, because if all the strings you keep are from one state, the result set might be highly biased. Unless you have a huge number of states, that should be fairly simple.

A more sophisticated heuristic, rather than just keeping a random collection of strings at each level, would be to start by computing the shortest path from each non-accepting state to some accepting state, using the standard all-shortest-paths algorithm. Then at each level, you can select the set of candidates which are closest to being accepted.

The essence of this problem is to search the possibly-infinite transition tree induced by the DFA. Each node in this tree consists of a state and a string (the prefix of a recognised sentence). For each possible transition from the state, there is a child node consisting of the target state and the string augmented with the transition character. The target strings are those belonging to nodes whose states are accepting states.

I think this assignment is probably intended to get you to understand recursion with back-tracking, since there is a pretty simple solution: recursively (depth-first) walk the transition tree, failing if the recursion depth reaches the limit on string length. (If strings can be long, you might want to use an explicit stack rather than recursion to avoid stack overflow.)

That is not, in my opinion, the best solution to this problem. I'd use a breadth-first search limited to the number of strings you need to find. However, if you do choose to implement the recursive backtracking solution, I do recommend that you do useless state elimination as suggested by [Note 1]; otherwise, a self-looping dead state (with at least two loops) will produce exponential slow-down during the DFS.

Let's recall that the basic breadth-first algorithm involves the use of a queue of transition tree nodes. We initialise the queue with the root node ($[start, ""]]$) and then, as long as the queue is non-empty and we have not found enough solutions, we do the following:

1. Remove the first node from the queue
2. If that node has an accepting state, report the string as a solution.
3. Append all possible transitions from that node to the end of the queue.

The result of this algorithm is that solutions will be generated in order by length, shortest strings first.

The problem with breadth-first search, as I gather you discovered through experimentation, is that if the branching factor is high and several transitions are required before an accepting state is found, then the queue expands to an exponential size (and that takes exponential time).

However, not all of the queue is necessary, since the number of target strings is known in advance. If we assume that the DFA has no useless states [Note 1], then every node in the transition tree is either a solution or has at least one solution in its subtree. Consequently, the maximum useful size of the queue is the number of solutions we require. When the queue has reached this size, we just don't do the append. When we find a solution, we can reduce the size limit by one.

Imposing this limit on the queue size removes the possibility of exponential time blow-up, but a naïve implementation no longer guarantees that the solutions found are the shortest solutions. This introduces an inefficiency, and even a possible error (because in pathological cases we might reach the string size limit before finding enough solutions, even though shorter solutions exist).

As a fix, we can track the levels during the BFS walk, reordering the traverse of each level. In this version, we use two queues, one for the current level, from which we pop nodes, and one for the next level, onto which we push newly found nodes. Every time the current queue becomes empty we've finished a level; we then swap the two queues. If both queues are empty, there's nothing more to be done.

Now we're not limited to processing the queue for a level from left-to-right; we can process it as we see fit, as long as we do all of it before moving on to the next node.

For a first approximation, we can ensure that we don't lose any accepting states. When we push a new node onto the next level:

  • If it is an accepting node, we push it onto the beginning. If the queue is full, we push the new node anyway, popping the last node.

  • If it is not an accepting node and the queue is not full, we push it at the end.

With this modification, we can count the accepting node when we push it onto the next-level queue, rather than when we pop if off the current queue. Counting the accepting node early means that we can terminate the algorithm earlier.

That still won't guarantee that all shortest strings are produced, but it should be sufficient in most cases. It can be improved by sorting the next-level queue by the minimum distance from each state to an accepting state. The minimum distances for each state can be precomputed with a simple breadth-first search over the DFA, which will also serve to eliminate useless states


###Notes

  1. Here I'm using the phrase "useless state" in a possibly non-standard way. By "useless state" I mean a state which does not appear in any transition sequence ending at an accepting state. In other words, for a state to be useful, it must be

    • reachable from the start state

    • accepting, or able to reach an accepting state

If that's not guaranteed in the DFAs you're begin given, it is easy to guarantee by detecting and removing useless states.

added 355 characters in body
Source Link
rici
  • 12.1k
  • 22
  • 39
Loading
Source Link
rici
  • 12.1k
  • 22
  • 39
Loading