Navigate text with arrow keys Background

Most (halfway decent) text editors allow you to navigate text using the arrow keys.  Up and down allow you to navigate lines, while left and right move across a line but also wrap around.  Furthermore, if the line is shorter than your cursor's X position, the cursor appears at the end of the line but returns to the same X position if you keep moving up or down.  Perhaps the following visual explanation will help:

Examples Of Motion

A simple sample of text could look like this.  The cursor will be inserted between two characters in this text, or at an end.

-----
---
------

let's put the cursor here:

X-----
---
------

move down (v):

-----
X---
------

move left (&lt;):

-----X
---
------

v

-----
---X
------

v (notice how the X position of the cursor has been maintained)

-----
---
-----X-

^

-----
---X
------

&gt;  (more line wrapping)

-----
---
X------

&lt;

-----
---X
------

^ (the X-position from earlier is no longer maintained due to the left-right motion)

---X--
---
------


The Challenge

Given several lines of ASCII test, find the shortest path from the start location to the end location.  The start location is represented by ^ and the end location is represented by $, and there will only be one of each.  These aren't considered part of the text and don't contribute to that line's "length."

The input will consist of several non-blank lines of text.  Output will be a series of ^v&lt;&gt; characters that show one of the shortest paths.  You may optionally assume an additional newline at the end of each, but that is not included as part of the navigable text.

You can write a program or named function.  The winner will be the shortest submission, measured in bytes.

Example I/O

^Squares
are
fun$ny

vv&lt;v  (which is better than the naive vv&gt;&gt;&gt;)

Squares
^are
funny$

&lt;vv

Alp$habet^
Song

v&lt;^

Mary had a little lamb,
His fleece was white as snow,
And$ everywhere that ^Mary went,
The lamb was sure to go.

^^&gt;&gt;&gt;&gt;v&gt;&gt;&gt;

$^degenerate case

(no output)
CJam, 139 bytes

Well this took many hours to come to something that feels done. It feels like the time it takes to aggressively optimize CJam code is something larger than O(n) with respect to the size of the code...

You can try it online, but for any input for which the best path is at least 6 operations or so, you should probably try it offline with a faster interpreter.

Squished:

q_'$-_'^-:T;'^#\'^-'$#W{)2$5Y$5b+{:D[L"_T&lt;W%_N#)_@&gt;N+N#X-Ue&gt;+-"_"W%-U"--2'&lt;t2'&gt;t'++'(')]=~0e&gt;T,e&lt;D3/1$T&lt;N\+W%N#X?:X;}/2$-}g5b{" ^v&lt;&gt;"=}%]W=


Expanded and commented:

q               "Read the input";
_'$-            "Remove the end marker";
_'^-:T;         "Remove the start marker and save the text";
'^#             "With only the end marker removed, locate the start marker";
\'^-'$#         "With only the start marker removed, locate the end marker";
W               "Initialize the path number to -1";
{               "Do...";
  )               "Increment the path number";
  2$              "Initialize the cursor position to that of the start marker";
  5Y$5b+          "Convert the path number to base 5, then add a leading 5
                   (the leading 5 will act to initialize the column memory)";
  {:D             "For each digit in the path digit string:";
    [               "Begin cases:";
      L               "0: Do nothing";
      "_T&lt;W%_N#)_@&gt;N+N#X-Ue&gt;+-"
"REFS: [   1   ][  2  ][ 3 ]45
                       1: [1] Calculate the distance to the end of the previous
                              line (0 if no such line)
                          [2] Calculate the length of the previous line (0 if
                              no such line)
                          [3] Calculate the distance to move backwards in the
                              previous line as the maximum of the length of the
                              previous line minus the column memory and 0
                          [4] Calculate the total distance to move as the sum 
                              of [1] and [3]
                          [5] Subtract [4] from the cursor position";
      _"W%-U"-        "2: Start with a base of the logic of case 1, but with a
                          few operations adjusted.";
      -2'&lt;t2'&gt;t       "   [1] Calculate the distance to the *start* of the
                              *next* line (0 if no such line)
                          [2] Calculate the length of the *next* line (0 if no
                              such line)
                          [3] Calculate the distance to move *forwards* in the
                              *next* line as the *minimum* of the length of the
                              *next line* and *the column memory*
                          [4] Calculate the total distance to move as the sum 
                              of [1] and [3]";
      '++             "   [5] *Add* [4] *to* the cursor position";
      '(              "3: Decrement the cursor position";
      ')              "4: Increment the cursor position";
    ]=~             "Execute the case corresponding to the path digit mod 5";
    0e&gt;T,e&lt;         "Clamp the cursor position to [0, text length]";
    D3/             "Check if the path digit is not 0, 1, or 2...";
    1$T&lt;N\+W%N#     "Calculate the current column";
    X?:X;           "If the above check succeeded, update the column memory";
  }/              "End for each";
  2$-             "Subtract the end marker position from the cursor position";
}g              "... While the above subtraction is nonzero";
5b              "Convert the path number to base 5";
{" ^v&lt;&gt;"=}%     "Map each digit in the path string to its operation symbol";
]W=             "Clean up";


Overall, this is a pretty straightforward solution. It "executes" the digits of the base-5 representation of a path number that is incremented every iteration, starting with 0, until a path works. The digits 1-4 map to the operations up, down, left, and right, and 0 does nothing. The first iteration using a path of just 0 catches the degenerate case. All other paths that contain a 0 are never selected because they're just versions of already-tested paths with added no-ops.

The state is modeled in as minimalistic of a way as possible: the text with start and end markers removed, the cursor position in the text, and the "column memory." Newlines are mostly treated like any other character, so there's no concept of a row, and the cursor position is just an index. This makes moving left and right dead simple, which are just implemented as decrement and increment (with clamping to the size of the text). Moving up and down is a bit trickier, but still manageable.

Code reuse was a pretty vital optimization tactic. Examples of this include:


Writing the code for moving up in a way such that it's smaller to generate the code for moving down at runtime than to write its own code. This is done by copying the code for moving up and removing/replacing a few characters.
Updating the "column memory" is done conditionally based on the path digit divided by 3 instead of it being coded into the logic of the operation. This also allows initialization of the column memory by adding a dummy 5 operation to the start of the path string, which also just so happens to use the 0 no-op logic due to circular array indexing and there being only 5 defined operations.


Overall, I'm very happy with how this came out. This is definitely the most work I've put into a code golf answer to date (for something that fits into a tweet!?). The run time is pretty abysmal, though. CJam isn't exactly the fastest language to begin with and this algorithm has a complexity of something like O(m*5n), where m is the size of the input and n is the size of the output. Good thing speed doesn't count!