diff --git a/articles/minimum-knight-moves.md b/articles/minimum-knight-moves.md new file mode 100644 index 000000000..294880328 --- /dev/null +++ b/articles/minimum-knight-moves.md @@ -0,0 +1,323 @@ +## 1. BFS (Breadth-First Search) + +::tabs-start + +```python +class Solution: + def minKnightMoves(self, x: int, y: int) -> int: + # the offsets in the eight directions + offsets = [(1, 2), (2, 1), (2, -1), (1, -2), + (-1, -2), (-2, -1), (-2, 1), (-1, 2)] + + def bfs(x, y): + visited = set() + queue = deque([(0, 0)]) + steps = 0 + + while queue: + curr_level_cnt = len(queue) + # iterate through the current level + for i in range(curr_level_cnt): + curr_x, curr_y = queue.popleft() + if (curr_x, curr_y) == (x, y): + return steps + + for offset_x, offset_y in offsets: + next_x, next_y = curr_x + offset_x, curr_y + offset_y + if (next_x, next_y) not in visited: + visited.add((next_x, next_y)) + queue.append((next_x, next_y)) + + # move on to the next level + steps += 1 + + return bfs(x, y) +``` + +```java +class Solution { + public int minKnightMoves(int x, int y) { + // the offsets in the eight directions + int[][] offsets = {{1, 2}, {2, 1}, {2, -1}, {1, -2}, + {-1, -2}, {-2, -1}, {-2, 1}, {-1, 2}}; + + // - Rather than using the inefficient HashSet, we use the bitmap + // otherwise we would run out of time for the test cases. + // - We create a bitmap that is sufficient to cover all the possible + // inputs, according to the description of the problem. + boolean[][] visited = new boolean[607][607]; + + Deque queue = new LinkedList<>(); + queue.addLast(new int[]{0, 0}); + int steps = 0; + + while (queue.size() > 0) { + int currLevelSize = queue.size(); + // iterate through the current level + for (int i = 0; i < currLevelSize; i++) { + int[] curr = queue.removeFirst(); + if (curr[0] == x && curr[1] == y) { + return steps; + } + + for (int[] offset : offsets) { + int[] next = new int[]{curr[0] + offset[0], curr[1] + offset[1]}; + // align the coordinate to the bitmap + if (!visited[next[0] + 302][next[1] + 302]) { + visited[next[0] + 302][next[1] + 302] = true; + queue.addLast(next); + } + } + } + steps++; + } + // move on to the next level + return steps; + } +} +``` + +::tabs-end + +### Time & Space Complexity + +- Time complexity: $O\left(\left(\max(|x|, |y|)\right)^2\right)$ +- Space complexity: $O\left(\left(\max(|x|, |y|)\right)^2\right)$ + +> Where $(x,y)$ is the coordinate of the target. + +--- + +## 2. Bidirectional BFS + +::tabs-start + +```python +class Solution: + def minKnightMoves(self, x: int, y: int) -> int: + # the offsets in the eight directions + offsets = [(1, 2), (2, 1), (2, -1), (1, -2), + (-1, -2), (-2, -1), (-2, 1), (-1, 2)] + + # data structures needed to move from the origin point + origin_queue = deque([(0, 0, 0)]) + origin_distance = {(0, 0): 0} + + # data structures needed to move from the target point + target_queue = deque([(x, y, 0)]) + target_distance = {(x, y): 0} + + while True: + # check if we reach the circle of target + origin_x, origin_y, origin_steps = origin_queue.popleft() + if (origin_x, origin_y) in target_distance: + return origin_steps + target_distance[(origin_x, origin_y)] + + # check if we reach the circle of origin + target_x, target_y, target_steps = target_queue.popleft() + if (target_x, target_y) in origin_distance: + return target_steps + origin_distance[(target_x, target_y)] + + for offset_x, offset_y in offsets: + # expand the circle of origin + next_origin_x, next_origin_y = origin_x + offset_x, origin_y + offset_y + if (next_origin_x, next_origin_y) not in origin_distance: + origin_queue.append((next_origin_x, next_origin_y, origin_steps + 1)) + origin_distance[(next_origin_x, next_origin_y)] = origin_steps + 1 + + # expand the circle of target + next_target_x, next_target_y = target_x + offset_x, target_y + offset_y + if (next_target_x, next_target_y) not in target_distance: + target_queue.append((next_target_x, next_target_y, target_steps + 1)) + target_distance[(next_target_x, next_target_y)] = target_steps + 1 +``` + +```java +class Solution { + public int minKnightMoves(int x, int y) { + // the offsets in the eight directions + int[][] offsets = {{1, 2}, {2, 1}, {2, -1}, {1, -2}, + {-1, -2}, {-2, -1}, {-2, 1}, {-1, 2}}; + + // data structures needed to move from the origin point + Deque originQueue = new LinkedList<>(); + originQueue.addLast(new int[]{0, 0, 0}); + Map originDistance = new HashMap<>(); + originDistance.put("0,0", 0); + + // data structures needed to move from the target point + Deque targetQueue = new LinkedList<>(); + targetQueue.addLast(new int[]{x, y, 0}); + Map targetDistance = new HashMap<>(); + targetDistance.put(x + "," + y, 0); + + while (true) { + // check if we reach the circle of target + int[] origin = originQueue.removeFirst(); + String originXY = origin[0] + "," + origin[1]; + if (targetDistance.containsKey(originXY)) { + return origin[2] + targetDistance.get(originXY); + } + + // check if we reach the circle of origin + int[] target = targetQueue.removeFirst(); + String targetXY = target[0] + "," + target[1]; + if (originDistance.containsKey(targetXY)) { + return target[2] + originDistance.get(targetXY); + } + + for (int[] offset : offsets) { + // expand the circle of origin + int[] nextOrigin = new int[]{origin[0] + offset[0], origin[1] + offset[1]}; + String nextOriginXY = nextOrigin[0] + "," + nextOrigin[1]; + if (!originDistance.containsKey(nextOriginXY)) { + originQueue.addLast(new int[]{nextOrigin[0], nextOrigin[1], origin[2] + 1}); + originDistance.put(nextOriginXY, origin[2] + 1); + } + + // expand the circle of target + int[] nextTarget = new int[]{target[0] + offset[0], target[1] + offset[1]}; + String nextTargetXY = nextTarget[0] + "," + nextTarget[1]; + if (!targetDistance.containsKey(nextTargetXY)) { + targetQueue.addLast(new int[]{nextTarget[0], nextTarget[1], target[2] + 1}); + targetDistance.put(nextTargetXY, target[2] + 1); + } + } + } + } +} +``` + +::tabs-end + +### Time & Space Complexity + +- Time complexity: $O\left(\left(\max(|x|, |y|)\right)^2\right)$ +- Space complexity: $O\left(\left(\max(|x|, |y|)\right)^2\right)$ + +> Where $(x,y)$ is the coordinate of the target. + +--- + +## 3. DFS (Depth-First Search) with Memoization + +::tabs-start + +```python +class Solution: + def minKnightMoves(self, x: int, y: int) -> int: + + @lru_cache(maxsize=None) + def dfs(x, y): + if x + y == 0: + # base case: (0, 0) + return 0 + elif x + y == 2: + # base case: (1, 1), (0, 2), (2, 0) + return 2 + else: + return min(dfs(abs(x - 1), abs(y - 2)), dfs(abs(x - 2), abs(y - 1))) + 1 + + return dfs(abs(x), abs(y)) +``` + +```java +class Solution { + + private Map memo = new HashMap<>(); + + private int dfs(int x, int y) { + String key = x + "," + y; + if (memo.containsKey(key)) { + return memo.get(key); + } + + if (x + y == 0) { + return 0; + } else if (x + y == 2) { + return 2; + } else { + Integer ret = Math.min(dfs(Math.abs(x - 1), Math.abs(y - 2)), + dfs(Math.abs(x - 2), Math.abs(y - 1))) + 1; + memo.put(key, ret); + return ret; + } + } + + public int minKnightMoves(int x, int y) { + return dfs(Math.abs(x), Math.abs(y)); + } +} +``` + +```cpp +class Solution { +private: + unordered_map memo; + + int dfs(int x, int y) { + string key = to_string(x) + "," + to_string(y); + if (memo.find(key) != memo.end()) { + return memo[key]; + } + + if (x + y == 0) { + return 0; + } else if (x + y == 2) { + return 2; + } else { + int ret = min(dfs(abs(x - 1), abs(y - 2)), + dfs(abs(x - 2), abs(y - 1))) + 1; + memo[key] = ret; + return ret; + } + } + +public: + int minKnightMoves(int x, int y) { + return dfs(abs(x), abs(y)); + } +}; +``` + +```javascript +class Solution { + /** + * @param {number} x + * @param {number} y + * @return {number} + */ + minKnightMoves(x, y) { + const memo = new Map(); + + const dfs = (x, y) => { + const key = `${x},${y}`; + if (memo.has(key)) { + return memo.get(key); + } + + if (x + y === 0) { + return 0; + } else if (x + y === 2) { + return 2; + } else { + const ret = Math.min(dfs(Math.abs(x - 1), Math.abs(y - 2)), + dfs(Math.abs(x - 2), Math.abs(y - 1))) + 1; + memo.set(key, ret); + return ret; + } + }; + + return dfs(Math.abs(x), Math.abs(y)); + } +} +``` + +::tabs-end + +### Time & Space Complexity + +- Time complexity: $O(|x \cdot y|)$ +- Space complexity: $O(|x \cdot y|)$ + +> Where $(x,y)$ is the coordinate of the target. diff --git a/articles/the-maze-iii.md b/articles/the-maze-iii.md new file mode 100644 index 000000000..e90460ea4 --- /dev/null +++ b/articles/the-maze-iii.md @@ -0,0 +1,336 @@ +## 1. Dijkstra's + +::tabs-start + +```python +class Solution: + def findShortestWay(self, maze: List[List[int]], ball: List[int], hole: List[int]) -> str: + def valid(row, col): + return 0 <= row < m and 0 <= col < n and maze[row][col] == 0 + + def get_neighbors(row, col): + directions = [(0, -1, 'l'), (-1, 0, 'u'), (0, 1, 'r'), (1, 0, 'd')] + neighbors = [] + + for dy, dx, direction in directions: + curr_row = row + curr_col = col + dist = 0 + + while valid(curr_row + dy, curr_col + dx): + curr_row += dy + curr_col += dx + dist += 1 + if [curr_row, curr_col] == hole: + break + + neighbors.append((curr_row, curr_col, dist, direction)) + + return neighbors + + m = len(maze) + n = len(maze[0]) + heap = [(0, "", ball[0], ball[1])] + seen = set() + + while heap: + curr_dist, path, row, col = heapq.heappop(heap) + + if (row, col) in seen: + continue + + if [row, col] == hole: + return path + + seen.add((row, col)) + + for next_row, next_col, dist, direction in get_neighbors(row, col): + heapq.heappush(heap, (curr_dist + dist, path + direction, next_row, next_col)) + + return "impossible" +``` + +```java +class State { + int row; + int col; + int dist; + String path; + + public State(int row, int col, int dist, String path) { + this.row = row; + this.col = col; + this.dist = dist; + this.path = path; + } +} + +class Solution { + int[][] directions = new int[][]{{0, -1}, {-1, 0}, {0, 1}, {1, 0}}; + String[] textDirections = new String[]{"l", "u", "r", "d"}; + int m; + int n; + + public String findShortestWay(int[][] maze, int[] ball, int[] hole) { + m = maze.length; + n = maze[0].length; + + PriorityQueue heap = new PriorityQueue<>((a, b) -> { + int distA = a.dist; + int distB = b.dist; + + if (distA == distB) { + return a.path.compareTo(b.path); + } + + return distA - distB; + }); + + boolean[][] seen = new boolean[m][n]; + heap.add(new State(ball[0], ball[1], 0, "")); + + while (!heap.isEmpty()) { + State curr = heap.remove(); + int row = curr.row; + int col = curr.col; + + if (seen[row][col]) { + continue; + } + + if (row == hole[0] && col == hole[1]) { + return curr.path; + } + + seen[row][col] = true; + + for (State nextState: getNeighbors(row, col, maze, hole)) { + int nextRow = nextState.row; + int nextCol = nextState.col; + int nextDist = nextState.dist; + String nextChar = nextState.path; + heap.add(new State(nextRow, nextCol, curr.dist + nextDist, curr.path + nextChar)); + } + } + + return "impossible"; + } + + private boolean valid(int row, int col, int[][] maze) { + if (row < 0 || row >= m || col < 0 || col >= n) { + return false; + } + + return maze[row][col] == 0; + } + + private List getNeighbors(int row, int col, int[][] maze, int[] hole) { + List neighbors = new ArrayList<>(); + + for (int i = 0; i < 4; i++) { + int dy = directions[i][0]; + int dx = directions[i][1]; + String direction = textDirections[i]; + + int currRow = row; + int currCol = col; + int dist = 0; + + while (valid(currRow + dy, currCol + dx, maze)) { + currRow += dy; + currCol += dx; + dist++; + if (currRow == hole[0] && currCol == hole[1]) { + break; + } + } + + neighbors.add(new State(currRow, currCol, dist, direction)); + } + + return neighbors; + } +} +``` + +```cpp +class Solution { +private: + // State: (distance, path, row, col) + // Priority queue will sort by distance first, then path lexicographically + using State = tuple; + + vector> directions = {{0, -1}, {-1, 0}, {0, 1}, {1, 0}}; + vector textDirections = {"l", "u", "r", "d"}; + int m; + int n; + + bool valid(int row, int col, vector>& maze) { + if (row < 0 || row >= m || col < 0 || col >= n) { + return false; + } + return maze[row][col] == 0; + } + + vector getNeighbors(int row, int col, vector>& maze, vector& hole) { + vector neighbors; + + for (int i = 0; i < 4; i++) { + int dy = directions[i][0]; + int dx = directions[i][1]; + string direction = textDirections[i]; + + int currRow = row; + int currCol = col; + int dist = 0; + + while (valid(currRow + dy, currCol + dx, maze)) { + currRow += dy; + currCol += dx; + dist++; + + if (currRow == hole[0] && currCol == hole[1]) { + break; + } + } + + neighbors.push_back(make_tuple(dist, direction, currRow, currCol)); + } + + return neighbors; + } + +public: + string findShortestWay(vector>& maze, vector& ball, vector& hole) { + m = maze.size(); + n = maze[0].size(); + + // Min heap: sorts by distance, then path lexicographically + priority_queue, greater> heap; + vector> seen(m, vector(n, false)); + + heap.push(make_tuple(0, "", ball[0], ball[1])); + + while (!heap.empty()) { + State curr = heap.top(); + heap.pop(); + + int dist = get<0>(curr); + string path = get<1>(curr); + int row = get<2>(curr); + int col = get<3>(curr); + + if (seen[row][col]) { + continue; + } + + if (row == hole[0] && col == hole[1]) { + return path; + } + + seen[row][col] = true; + + for (State& nextState : getNeighbors(row, col, maze, hole)) { + int nextDist = get<0>(nextState); + string nextChar = get<1>(nextState); + int nextRow = get<2>(nextState); + int nextCol = get<3>(nextState); + + heap.push(make_tuple(dist + nextDist, path + nextChar, nextRow, nextCol)); + } + } + + return "impossible"; + } +}; +``` + +```javascript +class Solution { + /** + * @param {number[][]} maze + * @param {number[]} ball + * @param {number[]} hole + * @return {string} + */ + findShortestWay(maze, ball, hole) { + const m = maze.length; + const n = maze[0].length; + + const valid = (row, col) => { + return row >= 0 && row < m && col >= 0 && col < n && maze[row][col] === 0; + }; + + const getNeighbors = (row, col) => { + const directions = [[0, -1, 'l'], [-1, 0, 'u'], [0, 1, 'r'], [1, 0, 'd']]; + const neighbors = []; + + for (const [dy, dx, direction] of directions) { + let currRow = row; + let currCol = col; + let dist = 0; + + while (valid(currRow + dy, currCol + dx)) { + currRow += dy; + currCol += dx; + dist++; + + if (currRow === hole[0] && currCol === hole[1]) { + break; + } + } + + neighbors.push([currRow, currCol, dist, direction]); + } + + return neighbors; + }; + + const pq = new PriorityQueue((a, b) => { + if (a.dist !== b.dist) { + return a.dist - b.dist; + } + return a.path.localeCompare(b.path); + }); + + pq.enqueue({ dist: 0, path: "", row: ball[0], col: ball[1] }); + + const seen = new Set(); + + while (!pq.isEmpty()) { + const { dist: currDist, path, row, col } = pq.dequeue(); + + const key = `${row},${col}`; + if (seen.has(key)) { + continue; + } + + if (row === hole[0] && col === hole[1]) { + return path; + } + + seen.add(key); + + for (const [nextRow, nextCol, dist, direction] of getNeighbors(row, col)) { + pq.enqueue({ + dist: currDist + dist, + path: path + direction, + row: nextRow, + col: nextCol + }); + } + } + + return "impossible"; + } +} +``` + +::tabs-end + +### Time & Space Complexity + +- Time complexity: $O(n \cdot \log n)$ +- Space complexity: $O(n)$ + +> Where $n$ is the number of squares in `maze`.