Pacific Atlantic Water Flow (LC 417): Reverse BFS from Both Oceans

Find every grid cell whose rainwater can reach both the Pacific and Atlantic edges. Solve LeetCode 417 by searching uphill from each ocean's border and intersecting the results, in O(m*n) time and space.

Language Selection

Choose your preferred programming language

Showing: Python

Pacific Atlantic Water Flow

Problem Statement

You are given an m x n grid heights describing the elevation of an island. The Pacific Ocean touches the island’s top edge and left edge. The Atlantic Ocean touches the bottom edge and right edge.

Rain falling on a cell can flow to any of its four neighbors (up, down, left, right) whose height is less than or equal to the current cell’s height. Water on a cell in the first row or first column can flow straight into the Pacific, and water on a cell in the last row or last column can flow straight into the Atlantic.

Return the coordinates [r, c] of every cell from which water can reach both oceans. The cells may be returned in any order.

Examples

Example 1:

Input: heights = [[1,2,3],
                  [8,9,4],
                  [7,6,5]]
Output: [[0,2],[1,0],[1,1],[1,2],[2,0],[2,1],[2,2]]
Explanation: (0,0) and (0,1) are on the top edge, so they reach the Pacific,
but every path toward the bottom or right edge goes uphill, so they never
reach the Atlantic. Every other cell can flow down to both coasts, for
example 9 -> 8 reaches the left edge and 9 -> 6 reaches the bottom edge.

Example 2:

Input: heights = [[5,1,5],
                  [1,4,1],
                  [5,1,5]]
Output: [[0,2],[1,1],[2,0]]
Explanation: The center (height 4) flows up to (0,1) for the Pacific and down
to (2,1) for the Atlantic. Corners (0,2) and (2,0) touch both oceans directly.
Corner (0,0) only touches the Pacific, and its lower neighbors are walled in by
higher cells, so it cannot reach the Atlantic. The same holds for (2,2) and
the Pacific.

Example 3:

Input: heights = [[7]]
Output: [[0,0]]
Explanation: A single cell touches all four edges, so it reaches both oceans.

Constraints

  • 1 <= m, n <= 200
  • 0 <= heights[r][c] <= 10^5

Approach 1: Search Downhill from Every Cell

Algorithm

The direct reading of the problem: for each cell, simulate where its water can go and check whether it touches both oceans.

  1. For each cell (r, c), run a BFS that moves to a neighbor only if the neighbor’s height is less than or equal to the current height.
  2. While searching, record whether any visited cell lies on the top or left edge (Pacific) and whether any lies on the bottom or right edge (Atlantic).
  3. Stop early once both flags are set. If both are set, add (r, c) to the answer.

Each search uses its own visited grid, so the work is repeated many times. This is fine for small grids and is a useful baseline, but on a 200 x 200 grid it can do up to 40,000 searches of 40,000 cells each.

Implementation

Python:

from collections import deque
from typing import List


class Solution:
    def pacificAtlantic(self, heights: List[List[int]]) -> List[List[int]]:
        m, n = len(heights), len(heights[0])
        result = []

        for sr in range(m):
            for sc in range(n):
                seen = [[False] * n for _ in range(m)]
                seen[sr][sc] = True
                queue = deque([(sr, sc)])
                pacific = atlantic = False
                while queue and not (pacific and atlantic):
                    r, c = queue.popleft()
                    if r == 0 or c == 0:
                        pacific = True
                    if r == m - 1 or c == n - 1:
                        atlantic = True
                    # water only moves to neighbors that are not higher
                    for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
                        if (0 <= nr < m and 0 <= nc < n and not seen[nr][nc]
                                and heights[nr][nc] <= heights[r][c]):
                            seen[nr][nc] = True
                            queue.append((nr, nc))
                if pacific and atlantic:
                    result.append([sr, sc])
        return result

Java:

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;

class Solution {
    private static final int[][] DIRS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};

    public List<List<Integer>> pacificAtlantic(int[][] heights) {
        int m = heights.length, n = heights[0].length;
        List<List<Integer>> result = new ArrayList<>();

        for (int sr = 0; sr < m; sr++) {
            for (int sc = 0; sc < n; sc++) {
                boolean[][] seen = new boolean[m][n];
                ArrayDeque<int[]> queue = new ArrayDeque<>();
                seen[sr][sc] = true;
                queue.add(new int[]{sr, sc});
                boolean pacific = false, atlantic = false;

                while (!queue.isEmpty() && !(pacific && atlantic)) {
                    int[] cell = queue.poll();
                    int r = cell[0], c = cell[1];
                    if (r == 0 || c == 0) pacific = true;
                    if (r == m - 1 || c == n - 1) atlantic = true;
                    // water only moves to neighbors that are not higher
                    for (int[] d : DIRS) {
                        int nr = r + d[0], nc = c + d[1];
                        if (nr >= 0 && nr < m && nc >= 0 && nc < n && !seen[nr][nc]
                                && heights[nr][nc] <= heights[r][c]) {
                            seen[nr][nc] = true;
                            queue.add(new int[]{nr, nc});
                        }
                    }
                }
                if (pacific && atlantic) result.add(List.of(sr, sc));
            }
        }
        return result;
    }
}

Go:

func pacificAtlantic(heights [][]int) [][]int {
    m, n := len(heights), len(heights[0])
    dirs := [4][2]int{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}
    result := [][]int{}

    for sr := 0; sr < m; sr++ {
        for sc := 0; sc < n; sc++ {
            seen := make([][]bool, m)
            for i := range seen {
                seen[i] = make([]bool, n)
            }
            seen[sr][sc] = true
            queue := [][2]int{{sr, sc}}
            pacific, atlantic := false, false

            for head := 0; head < len(queue) && !(pacific && atlantic); head++ {
                r, c := queue[head][0], queue[head][1]
                if r == 0 || c == 0 {
                    pacific = true
                }
                if r == m-1 || c == n-1 {
                    atlantic = true
                }
                // water only moves to neighbors that are not higher
                for _, d := range dirs {
                    nr, nc := r+d[0], c+d[1]
                    if nr >= 0 && nr < m && nc >= 0 && nc < n && !seen[nr][nc] &&
                        heights[nr][nc] <= heights[r][c] {
                        seen[nr][nc] = true
                        queue = append(queue, [2]int{nr, nc})
                    }
                }
            }
            if pacific && atlantic {
                result = append(result, []int{sr, sc})
            }
        }
    }
    return result
}

JavaScript:

/**
 * @param {number[][]} heights
 * @return {number[][]}
 */
var pacificAtlantic = function (heights) {
    const m = heights.length, n = heights[0].length;
    const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];
    const result = [];

    for (let sr = 0; sr < m; sr++) {
        for (let sc = 0; sc < n; sc++) {
            const seen = Array.from({ length: m }, () => new Array(n).fill(false));
            seen[sr][sc] = true;
            const queue = [[sr, sc]];
            let pacific = false, atlantic = false;

            // array + head index works as a queue without O(n) shift()
            for (let head = 0; head < queue.length && !(pacific && atlantic); head++) {
                const [r, c] = queue[head];
                if (r === 0 || c === 0) pacific = true;
                if (r === m - 1 || c === n - 1) atlantic = true;
                // water only moves to neighbors that are not higher
                for (const [dr, dc] of dirs) {
                    const nr = r + dr, nc = c + dc;
                    if (nr >= 0 && nr < m && nc >= 0 && nc < n && !seen[nr][nc]
                        && heights[nr][nc] <= heights[r][c]) {
                        seen[nr][nc] = true;
                        queue.push([nr, nc]);
                    }
                }
            }
            if (pacific && atlantic) result.push([sr, sc]);
        }
    }
    return result;
};

C#:

using System.Collections.Generic;

public class Solution {
    private static readonly int[][] Dirs = {
        new[] { 1, 0 }, new[] { -1, 0 }, new[] { 0, 1 }, new[] { 0, -1 }
    };

    public IList<IList<int>> PacificAtlantic(int[][] heights) {
        int m = heights.Length, n = heights[0].Length;
        var result = new List<IList<int>>();

        for (int sr = 0; sr < m; sr++) {
            for (int sc = 0; sc < n; sc++) {
                var seen = new bool[m, n];
                var queue = new Queue<(int r, int c)>();
                seen[sr, sc] = true;
                queue.Enqueue((sr, sc));
                bool pacific = false, atlantic = false;

                while (queue.Count > 0 && !(pacific && atlantic)) {
                    var (r, c) = queue.Dequeue();
                    if (r == 0 || c == 0) pacific = true;
                    if (r == m - 1 || c == n - 1) atlantic = true;
                    // water only moves to neighbors that are not higher
                    foreach (var d in Dirs) {
                        int nr = r + d[0], nc = c + d[1];
                        if (nr >= 0 && nr < m && nc >= 0 && nc < n && !seen[nr, nc]
                            && heights[nr][nc] <= heights[r][c]) {
                            seen[nr, nc] = true;
                            queue.Enqueue((nr, nc));
                        }
                    }
                }
                if (pacific && atlantic) result.Add(new List<int> { sr, sc });
            }
        }
        return result;
    }
}

C++:

#include <queue>
#include <utility>
#include <vector>
using namespace std;

class Solution {
public:
    vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {
        int m = heights.size(), n = heights[0].size();
        const int dirs[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
        vector<vector<int>> result;

        for (int sr = 0; sr < m; sr++) {
            for (int sc = 0; sc < n; sc++) {
                vector<vector<bool>> seen(m, vector<bool>(n, false));
                queue<pair<int, int>> q;
                seen[sr][sc] = true;
                q.push({sr, sc});
                bool pacific = false, atlantic = false;

                while (!q.empty() && !(pacific && atlantic)) {
                    auto [r, c] = q.front();
                    q.pop();
                    if (r == 0 || c == 0) pacific = true;
                    if (r == m - 1 || c == n - 1) atlantic = true;
                    // water only moves to neighbors that are not higher
                    for (const auto& d : dirs) {
                        int nr = r + d[0], nc = c + d[1];
                        if (nr >= 0 && nr < m && nc >= 0 && nc < n && !seen[nr][nc]
                            && heights[nr][nc] <= heights[r][c]) {
                            seen[nr][nc] = true;
                            q.push({nr, nc});
                        }
                    }
                }
                if (pacific && atlantic) result.push_back({sr, sc});
            }
        }
        return result;
    }
};

Complexity Analysis

  • Time: O((m * n)^2). There are m * n starting cells, and each BFS can visit all m * n cells.
  • Space: O(m * n) for one visited grid and queue at a time.

Approach 2: Reverse BFS from Each Ocean (Optimal)

Algorithm

Instead of asking “where can this cell’s water go?” for every cell, ask the reverse question once per ocean: “which cells can drain into this ocean?” Water flows from high to equal-or-lower ground, so walking backwards means climbing from the coast to neighbors of equal or greater height.

  1. Put every Pacific border cell (row 0 and column 0) into a queue and mark it reachable for the Pacific.
  2. Run a BFS. From cell (r, c), move to an unvisited neighbor (nr, nc) only if heights[nr][nc] >= heights[r][c]. Every cell reached this way can send water down the path you just climbed.
  3. Repeat with the Atlantic border cells (last row and last column) into a second visited grid.
  4. Return every cell marked in both grids.

Each BFS visits each cell at most once, so the whole thing is linear in the grid size. The searches are iterative, so a 200 x 200 grid cannot overflow the call stack the way a recursive DFS might.

Implementation

Python:

from collections import deque
from typing import List


class Solution:
    def pacificAtlantic(self, heights: List[List[int]]) -> List[List[int]]:
        m, n = len(heights), len(heights[0])

        def climb(starts):
            # reverse flow: from the coast, move to neighbors that are not lower
            reach = [[False] * n for _ in range(m)]
            for r, c in starts:
                reach[r][c] = True
            queue = deque(starts)
            while queue:
                r, c = queue.popleft()
                for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
                    if (0 <= nr < m and 0 <= nc < n and not reach[nr][nc]
                            and heights[nr][nc] >= heights[r][c]):
                        reach[nr][nc] = True
                        queue.append((nr, nc))
            return reach

        pacific = climb([(0, c) for c in range(n)] + [(r, 0) for r in range(1, m)])
        atlantic = climb([(m - 1, c) for c in range(n)] + [(r, n - 1) for r in range(m - 1)])
        return [[r, c] for r in range(m) for c in range(n)
                if pacific[r][c] and atlantic[r][c]]

Java:

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;

class Solution {
    private static final int[][] DIRS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};

    public List<List<Integer>> pacificAtlantic(int[][] heights) {
        int m = heights.length, n = heights[0].length;
        ArrayDeque<int[]> pacQueue = new ArrayDeque<>(), atlQueue = new ArrayDeque<>();
        boolean[][] pacific = new boolean[m][n], atlantic = new boolean[m][n];

        // seed each ocean with its border cells
        for (int r = 0; r < m; r++) {
            mark(pacific, pacQueue, r, 0);
            mark(atlantic, atlQueue, r, n - 1);
        }
        for (int c = 0; c < n; c++) {
            mark(pacific, pacQueue, 0, c);
            mark(atlantic, atlQueue, m - 1, c);
        }
        climb(heights, pacific, pacQueue);
        climb(heights, atlantic, atlQueue);

        List<List<Integer>> result = new ArrayList<>();
        for (int r = 0; r < m; r++)
            for (int c = 0; c < n; c++)
                if (pacific[r][c] && atlantic[r][c]) result.add(List.of(r, c));
        return result;
    }

    private void mark(boolean[][] reach, ArrayDeque<int[]> queue, int r, int c) {
        if (!reach[r][c]) {   // corners are seeded twice; enqueue once
            reach[r][c] = true;
            queue.add(new int[]{r, c});
        }
    }

    // reverse flow: from the coast, move to neighbors that are not lower
    private void climb(int[][] heights, boolean[][] reach, ArrayDeque<int[]> queue) {
        int m = heights.length, n = heights[0].length;
        while (!queue.isEmpty()) {
            int[] cell = queue.poll();
            int r = cell[0], c = cell[1];
            for (int[] d : DIRS) {
                int nr = r + d[0], nc = c + d[1];
                if (nr >= 0 && nr < m && nc >= 0 && nc < n && !reach[nr][nc]
                        && heights[nr][nc] >= heights[r][c]) {
                    reach[nr][nc] = true;
                    queue.add(new int[]{nr, nc});
                }
            }
        }
    }
}

Go:

func pacificAtlantic(heights [][]int) [][]int {
    m, n := len(heights), len(heights[0])
    dirs := [4][2]int{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}

    // climb runs a reverse-flow BFS from the given border cells
    climb := func(starts [][2]int) [][]bool {
        reach := make([][]bool, m)
        for i := range reach {
            reach[i] = make([]bool, n)
        }
        queue := make([][2]int, 0, m*n)
        for _, s := range starts {
            if !reach[s[0]][s[1]] {
                reach[s[0]][s[1]] = true
                queue = append(queue, s)
            }
        }
        for head := 0; head < len(queue); head++ {
            r, c := queue[head][0], queue[head][1]
            for _, d := range dirs {
                nr, nc := r+d[0], c+d[1]
                // move uphill (or level): water could flow back down this step
                if nr >= 0 && nr < m && nc >= 0 && nc < n && !reach[nr][nc] &&
                    heights[nr][nc] >= heights[r][c] {
                    reach[nr][nc] = true
                    queue = append(queue, [2]int{nr, nc})
                }
            }
        }
        return reach
    }

    var pacStarts, atlStarts [][2]int
    for r := 0; r < m; r++ {
        pacStarts = append(pacStarts, [2]int{r, 0})
        atlStarts = append(atlStarts, [2]int{r, n - 1})
    }
    for c := 0; c < n; c++ {
        pacStarts = append(pacStarts, [2]int{0, c})
        atlStarts = append(atlStarts, [2]int{m - 1, c})
    }
    pacific, atlantic := climb(pacStarts), climb(atlStarts)

    result := [][]int{}
    for r := 0; r < m; r++ {
        for c := 0; c < n; c++ {
            if pacific[r][c] && atlantic[r][c] {
                result = append(result, []int{r, c})
            }
        }
    }
    return result
}

JavaScript:

/**
 * @param {number[][]} heights
 * @return {number[][]}
 */
var pacificAtlantic = function (heights) {
    const m = heights.length, n = heights[0].length;
    const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];

    // reverse flow: from the coast, move to neighbors that are not lower
    const climb = (starts) => {
        const reach = Array.from({ length: m }, () => new Array(n).fill(false));
        const queue = [];
        for (const [r, c] of starts) {
            if (!reach[r][c]) {
                reach[r][c] = true;
                queue.push([r, c]);
            }
        }
        for (let head = 0; head < queue.length; head++) {
            const [r, c] = queue[head];
            for (const [dr, dc] of dirs) {
                const nr = r + dr, nc = c + dc;
                if (nr >= 0 && nr < m && nc >= 0 && nc < n && !reach[nr][nc]
                    && heights[nr][nc] >= heights[r][c]) {
                    reach[nr][nc] = true;
                    queue.push([nr, nc]);
                }
            }
        }
        return reach;
    };

    const pacStarts = [], atlStarts = [];
    for (let r = 0; r < m; r++) {
        pacStarts.push([r, 0]);
        atlStarts.push([r, n - 1]);
    }
    for (let c = 0; c < n; c++) {
        pacStarts.push([0, c]);
        atlStarts.push([m - 1, c]);
    }
    const pacific = climb(pacStarts), atlantic = climb(atlStarts);

    const result = [];
    for (let r = 0; r < m; r++)
        for (let c = 0; c < n; c++)
            if (pacific[r][c] && atlantic[r][c]) result.push([r, c]);
    return result;
};

C#:

using System.Collections.Generic;

public class Solution {
    private static readonly int[][] Dirs = {
        new[] { 1, 0 }, new[] { -1, 0 }, new[] { 0, 1 }, new[] { 0, -1 }
    };

    public IList<IList<int>> PacificAtlantic(int[][] heights) {
        int m = heights.Length, n = heights[0].Length;
        var pacStarts = new List<(int, int)>();
        var atlStarts = new List<(int, int)>();
        for (int r = 0; r < m; r++) {
            pacStarts.Add((r, 0));
            atlStarts.Add((r, n - 1));
        }
        for (int c = 0; c < n; c++) {
            pacStarts.Add((0, c));
            atlStarts.Add((m - 1, c));
        }
        bool[,] pacific = Climb(heights, pacStarts);
        bool[,] atlantic = Climb(heights, atlStarts);

        var result = new List<IList<int>>();
        for (int r = 0; r < m; r++)
            for (int c = 0; c < n; c++)
                if (pacific[r, c] && atlantic[r, c]) result.Add(new List<int> { r, c });
        return result;
    }

    // reverse flow: from the coast, move to neighbors that are not lower
    private static bool[,] Climb(int[][] heights, List<(int, int)> starts) {
        int m = heights.Length, n = heights[0].Length;
        var reach = new bool[m, n];
        var queue = new Queue<(int r, int c)>();
        foreach (var (r, c) in starts) {
            if (!reach[r, c]) {   // corners appear twice; enqueue once
                reach[r, c] = true;
                queue.Enqueue((r, c));
            }
        }
        while (queue.Count > 0) {
            var (r, c) = queue.Dequeue();
            foreach (var d in Dirs) {
                int nr = r + d[0], nc = c + d[1];
                if (nr >= 0 && nr < m && nc >= 0 && nc < n && !reach[nr, nc]
                    && heights[nr][nc] >= heights[r][c]) {
                    reach[nr, nc] = true;
                    queue.Enqueue((nr, nc));
                }
            }
        }
        return reach;
    }
}

C++:

#include <queue>
#include <utility>
#include <vector>
using namespace std;

class Solution {
public:
    vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {
        int m = heights.size(), n = heights[0].size();
        vector<pair<int, int>> pacStarts, atlStarts;
        for (int r = 0; r < m; r++) {
            pacStarts.push_back({r, 0});
            atlStarts.push_back({r, n - 1});
        }
        for (int c = 0; c < n; c++) {
            pacStarts.push_back({0, c});
            atlStarts.push_back({m - 1, c});
        }
        vector<vector<bool>> pacific = climb(heights, pacStarts);
        vector<vector<bool>> atlantic = climb(heights, atlStarts);

        vector<vector<int>> result;
        for (int r = 0; r < m; r++)
            for (int c = 0; c < n; c++)
                if (pacific[r][c] && atlantic[r][c]) result.push_back({r, c});
        return result;
    }

private:
    // reverse flow: from the coast, move to neighbors that are not lower
    vector<vector<bool>> climb(const vector<vector<int>>& heights,
                               const vector<pair<int, int>>& starts) {
        int m = heights.size(), n = heights[0].size();
        const int dirs[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
        vector<vector<bool>> reach(m, vector<bool>(n, false));
        queue<pair<int, int>> q;
        for (const auto& [r, c] : starts) {
            if (!reach[r][c]) {   // corners appear twice; enqueue once
                reach[r][c] = true;
                q.push({r, c});
            }
        }
        while (!q.empty()) {
            auto [r, c] = q.front();
            q.pop();
            for (const auto& d : dirs) {
                int nr = r + d[0], nc = c + d[1];
                if (nr >= 0 && nr < m && nc >= 0 && nc < n && !reach[nr][nc]
                    && heights[nr][nc] >= heights[r][c]) {
                    reach[nr][nc] = true;
                    q.push({nr, nc});
                }
            }
        }
        return reach;
    }
};

Complexity Analysis

  • Time: O(m * n). Each of the two searches visits every cell at most once and checks four neighbors per cell, and the final scan is also O(m * n).
  • Space: O(m * n) for the two reachability grids and the BFS queues.

Key Insights

  • Reverse the direction of flow: searching downhill from every cell repeats work. Searching uphill from the coast answers the question for all cells at once.
  • Multi-source BFS: an entire border is one starting set. All border cells go into the queue before the search begins.
  • Two independent passes, then intersect: a cell qualifies exactly when it is reachable in both reverse searches.
  • Equal heights count: water flows to neighbors of the same height, so the uphill test is >=, not >.

Edge Cases

  1. Single cell: it touches both oceans, so the answer is [[0,0]].
  2. Single row or single column: every cell touches both oceans directly, so every cell is in the answer.
  3. Flat grid: all heights equal, so water moves everywhere and every cell qualifies.
  4. Corners: the top-right and bottom-left corners always qualify because they touch both oceans. The answer is never empty.
  5. Strict ridges and basins: low interior cells surrounded by higher walls reach neither ocean.

Common Mistakes

  1. Using > instead of >= in the reverse search: this drops paths across flat ground.
  2. Flipping the comparison: the reverse search climbs (neighbor >= current). Using <= there simulates forward flow from the coast, which is meaningless.
  3. Mixing up the coasts: the Pacific is the top and left edges, the Atlantic the bottom and right edges.
  4. One shared visited grid for both oceans: each ocean needs its own grid so you can intersect them.
  5. Recursive DFS on a large grid: a 200 x 200 grid with a snake-shaped path can recurse 40,000 levels deep and overflow the stack in some languages. An iterative BFS avoids that.

Interview Tips

  1. Start by stating the brute force (search from every cell) and its O((m * n)^2) cost, then explain why reversing the flow removes the repetition.
  2. Say clearly which comparison you use and why it is >= in the reverse direction.
  3. Mention that BFS and DFS both work for the reverse search. BFS with an explicit queue avoids recursion depth limits.
  4. Relate it to multi-source BFS problems such as rotting oranges or walls-and-gates.

Follow-up Questions

  1. Three or more oceans: run one reverse search per ocean and intersect all of them. The cost is O(k * m * n) for k oceans.
  2. Water that reaches at least one ocean: take the union of the two grids instead of the intersection.
  3. Diagonal flow: add the four diagonal directions. The algorithm is unchanged.
  4. Streaming height updates: a single cell change can flip reachability for a large region. Recomputing the affected ocean’s search is the simple answer, and incremental approaches are much harder.