#!/usr/bin/env python

import dataclasses
import enum
import sys
from collections import defaultdict, deque
from collections.abc import Iterator
from typing import NamedTuple


class Point(NamedTuple):
    x: int
    y: int

    def __add__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return Point(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return Point(self.x - other.x, self.y - other.y)


class Direction(str, enum.Enum):
    UP = "^"
    DOWN = "v"
    LEFT = "<"
    RIGHT = ">"

    def to_delta(self) -> Point:
        match self:
            case Direction.UP:
                return Point(-1, 0)
            case Direction.DOWN:
                return Point(1, 0)
            case Direction.LEFT:
                return Point(0, -1)
            case Direction.RIGHT:
                return Point(0, 1)


@dataclasses.dataclass
class ValleyMap:
    start: Point
    goal: Point
    valley_corners: tuple[Point, Point]
    tornadoes: dict[Point, Direction]

    @classmethod
    def from_input(cls, input: list[str]) -> "ValleyMap":
        tornadoes: dict[Point, Direction] = {}
        for x, line in enumerate(input, start=1):
            for y, c in enumerate(line, start=1):
                if c in ("#", "."):
                    continue
                tornadoes[Point(x, y)] = Direction(c)
        return cls(
            # Start position is always above the upper left corner of valley
            start=Point(1, 2),
            # Goal position is always under the lower left corner of valley
            goal=Point(len(input), len(input[0]) - 1),
            # Valley is surrounded by walls, except entrance and exit
            valley_corners=(Point(2, 2), Point(len(input) - 1, len(input[0]) - 1)),
            tornadoes=tornadoes,
        )

    def _is_in_valley(self, p: Point) -> bool:
        # Valley also includes start/end
        if p in (self.start, self.goal):
            return True
        # Otherwise, just do a bounds check for inside the walls
        ((minx, miny), (maxx, maxy)) = self.valley_corners
        return (minx <= p.x <= maxx) and (miny <= p.y <= maxy)

    def _wrap_tornado(self, p: Point) -> Point:
        if self._is_in_valley(p):
            return p
        x, y = p
        h = self.valley_corners[1].x - self.valley_corners[0].x + 1
        w = self.valley_corners[1].y - self.valley_corners[0].y + 1
        if x == 1:
            x += h
        if y == 1:
            y += w
        if x > self.valley_corners[1].x:
            x -= h
        if y > self.valley_corners[1].y:
            y -= w
        return Point(x, y)

    def navigate(self) -> int:
        TornadoesMap = dict[Point, list[Direction]]

        def move_tornadoes(map: TornadoesMap) -> dict[Point, list[Direction]]:
            res: dict[Point, list[Direction]] = defaultdict(list)
            for p, tornadoes in map.items():
                for t in tornadoes:
                    new_pos = self._wrap_tornado(p + t.to_delta())
                    res[new_pos].append(t)
            return dict(res)

        def moves(p: Point) -> Iterator[Point]:
            yield p
            for dx, dy in ((-1, 0), (1, 0), (0, -1), (0, 1)):
                yield p + Point(dx, dy)

        # Do a BFS to find the fastest route
        queue: deque[tuple[int, Point]] = deque([(0, self.start)])
        seen: set[tuple[int, Point]] = set()
        tornado_history = [{p: [t] for p, t in self.tornadoes.items()}]
        while queue:
            dist, pos = queue.popleft()
            # If goal found, return total distance
            if pos == self.goal:
                return dist
            # Check that we don't do redundant work
            if (dist, pos) in seen:
                continue
            seen.add((dist, pos))
            if len(tornado_history) <= (dist + 1):
                tornado_history.append(move_tornadoes(tornado_history[-1]))
            for new_pos in moves(pos):
                # Can't move into the walls, but can move in start/end
                if not self._is_in_valley(new_pos):
                    continue
                # Can't occupy same space as tornadoes
                if new_pos in tornado_history[dist + 1]:
                    continue
                # Enqueue this move to the search space
                queue.append((dist + 1, new_pos))
        assert False  # Sanity check


def solve(input: list[str]) -> int:
    valley = ValleyMap.from_input(input)
    return valley.navigate()


def main() -> None:
    input = sys.stdin.read().splitlines()
    print(solve(input))


if __name__ == "__main__":
    main()