Java&C++题解与拓展——leetcode675.为高尔夫比赛砍树【复习启发式搜索和并查集、二维容器初始化、pair、tuple学习】

155 阅读9分钟
每日一题做题记录,参考官方和三叶的题解

题目要求

在这里插入图片描述

理解

  • 树的高度唯一,那其实只能沿着从低到高依次砍,也就是说路线唯一;
  • 而两棵树之间的最短距离是确定的(有树也可通过),所以求相邻高度两棵树的最短距离加起来就好了。

思路一:BFS

  • 数据范围只有50,所以可以直接BFS。
  • 把所有的树找出来,排序得到砍树路径,依次遍历找当前树(x,y)(x,y)和下一棵树(nx,ny)(nx,ny)之间的最短距离:
    • 用一个队列存代遍历的点;
    • 对每个点cur=(cx,cy)cur=(cx,cy)向四个方向走,找下一棵树。

Java

class Solution {
    int N = 50;
    int[][] g = new int[N][N];
    int m, n;
    List<int[]> tree = new ArrayList<>();
    public int cutOffTree(List<List<Integer>> forest) {
        n = forest.size();
        m = forest.get(0).size();
        // forest转存为g,所有树存入tree并按高度排序
        for(int i = 0; i < n; i++) {
            for(int j = 0; j < m; j++) {
                g[i][j] = forest.get(i).get(j);
                if(g[i][j] > 1)
                    tree.add(new int[]{g[i][j], i , j});
            }
        }
        if(g[0][0] == 0)
            return -1;
        Collections.sort(tree, (a, b) -> a[0] - b[0]);
        int x = 0, y = 0, res = 0;
        for(int[] ne : tree) {
            int nx = ne[1], ny = ne[2];
            int dis = BFS(x, y, nx, ny); // 砍下一高度的树
            if(dis == -1)
                return -1;
            res += dis;
            x = nx;
            y = ny;
        }
        return res;
    }
    int[][] dirs = new int[][]{{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
    int BFS(int x, int y, int nx, int ny) {
        if(x == nx && y == ny)
            return 0;
        boolean[][] vis = new boolean[n][m];
        Deque<int[]> que = new ArrayDeque<>(); // 待走的路
        que.addLast(new int[]{x, y});
        vis[x][y] = true;
        int res = 0;
        while(!que.isEmpty()) {
            int sz = que.size();
            while(sz-- > 0) {
                int[] cur = que.pollFirst();
                int cx = cur[0], cy = cur[1];
                for(int[] d : dirs) {
                    int ncx = cx + d[0], ncy = cy + d[1];
                    if(ncx < 0 || ncx >= n || ncy < 0 || ncy >= m) // 超出边界
                        continue;
                    if(g[ncx][ncy] == 0 || vis[ncx][ncy]) // 障碍或已遍历
                        continue;
                    if(ncx == nx && ncy == ny) // 下一步路刚好是下一棵待砍的树
                        return res + 1;
                    que.addLast(new int[]{ncx, ncy});
                    vis[ncx][ncy] = true;
                }
            }
            res++;
        }
        // 到不了下一棵要砍的树
        return -1;
    }
}
  • 时间复杂度:O(m2×n2)O(m^2\times n^2)
  • 空间复杂度:O(m×n)O(m\times n)

C++

【要注意初始化布尔容器,二维容器初始化每次都要查一下……】

const int N = 50;
class Solution {
    int g[N][N];
    int m, n;
    vector<vector<int>> tree;
public:
    int cutOffTree(vector<vector<int>>& forest) {
        n = forest.size();
        m = forest[0].size();
        // forest转存为g,所有树存入tree并按高度排序
        for(int i = 0; i < n; i++) {
            for(int j = 0; j < m; j++) {
                g[i][j] = forest[i][j];
                if(g[i][j] > 1)
                    tree.push_back({g[i][j], i , j});
            }
        }
        if(g[0][0] == 0)
            return -1;
        sort(tree.begin(), tree.end());
        int x = 0, y = 0, res = 0;
        for(auto ne : tree) {
            int nx = ne[1], ny = ne[2];
            int dis = BFS(x, y, nx, ny); // 砍下一高度的树
            if(dis == -1)
                return -1;
            res += dis;
            x = nx;
            y = ny;
        }
        return res;
    }

    int dirs[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
    int BFS(int x, int y, int nx, int ny) {
        if(x == nx && y == ny)
            return 0;
        vector<vector<bool>> vis(n, vector<bool>(m, false));
        queue<pair<int, int>> que; // 待走的路
        que.emplace(x, y);
        vis[x][y] = true;
        int res = 0;
        while(!que.empty()) {
            int sz = que.size();
            while(sz-- > 0) {
                auto [cx, cy] = que.front();
                que.pop();
                for(auto d : dirs) {
                    int ncx = cx + d[0], ncy = cy + d[1];
                    if(ncx < 0 || ncx >= n || ncy < 0 || ncy >= m) // 超出边界
                        continue;
                    if(g[ncx][ncy] == 0 || vis[ncx][ncy]) // 障碍或已遍历
                        continue;
                    if(ncx == nx && ncy == ny) // 下一步路刚好是下一棵待砍的树
                        return res + 1;
                    que.emplace(ncx, ncy);
                    vis[ncx][ncy] = true;
                }
            }
            res++;
        }
        // 到不了下一棵要砍的树
        return -1;
    }
};
  • 时间复杂度:O(m2×n2)O(m^2\times n^2)
  • 空间复杂度:O(m×n)O(m\times n)

STL pair

思路二:Dijstra算法

  • DIjstra算法也可以求最短距离,但是本题存在障碍物设置,所以选择的最短路径节点可能不是最优的,时间复杂度反而增加。【反正后面就是A*所以就不写了、不是偷懒、绝不是】
  • 时间复杂度:O(m2×n2×log(m×n))O(m^2\times n^2\times\log(m\times n))
  • 空间复杂度:O(m×n)O(m\times n)

思路三:A*

【半个月前学的、还记得那天每种方法都卡、433.最小基因变化(A*学习)

  • BFS向四个方向找最短距离,但其实根据两点的相对关系可以简化这个过程:
    • 向该点方向找;
    • 找不到再换其他方向绕。
  • 用最小理论步数作为启发式函数:
    • 用曼哈顿距离作为两点的最小理论步数,即走直线的距离;
    • 两棵树之间的距离估算距离为(x,y)(cx,cy)之间所走的实际步数+(cx,cy)(nx,ny)之间的理论步数(x,y)到(cx,cy)之间所走的实际步数+(cx,cy)到(nx,ny)之间的理论步数,按这个结果的大小压入队列,依次遍历计算。
  • 注意该路径上点改变造成的其他点的最小步数的变化。

Java

class Solution {
    int N = 50;
    int[][] g = new int[N][N];
    int m, n;
    List<int[]> tree = new ArrayList<>();
    public int cutOffTree(List<List<Integer>> forest) {
        n = forest.size();
        m = forest.get(0).size();
        for(int i = 0; i< n; i++) {
            for(int j = 0; j< m; j++) {
                g[i][j] = forest.get(i).get(j);
                if(g[i][j] > 1)
                    tree.add(new int[]{g[i][j], i, j});
            }
        }
        if(g[0][0] == 0)
            return -1;
        Collections.sort(tree, (a, b) -> a[0] - b[0]);
        int x = 0, y = 0, res = 0;
        for(int[] ne : tree) {
            int nx = ne[1], ny = ne[2];
            int dis = Astar(x, y, nx, ny);
             // 砍下一高度的树
            if(dis == -1)
                return -1;
            res += dis;
            x = nx;
            y = ny;
        }
        return res;
    }
    int[][] dirs = new int[][]{{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
    
    int Astar(int x, int y, int nx, int ny) {
        if(x == nx && y == ny)
            return 0;
        Map<Integer, Integer> step = new HashMap<>();
        // 待走的路,按理论步数排序
        PriorityQueue<int[]> que = new PriorityQueue<>((a,b) -> a[0] - b[0]);
        que.add(new int[]{distance(x, y, nx, ny), x, y});
        step.put(getIdx(x, y), 0);

        while(!que.isEmpty()) {
            int[] cur = que.poll();
            int cx = cur[1], cy = cur[2];
            int res = step.get(getIdx(cx, cy)); // 实际步数
            for(int[] d : dirs) {
                int ncx = cx + d[0], ncy = cy + d[1], nidx = getIdx(ncx, ncy);
                if(ncx < 0 || ncx >= n || ncy < 0 || ncy >= m) // 超出边界
                    continue;
                if(g[ncx][ncy] == 0) // 障碍
                    continue;
                if(ncx == nx && ncy == ny) // 下一步路刚好是下一棵待砍的树
                    return res + 1;
                // 更新nidx点的最短路径
                if(!step.containsKey(nidx) || step.get(nidx) > res + 1) {
                    que.add(new int[]{res + 1 + distance(ncx, ncy, nx, ny), ncx, ncy});
                    step.put(nidx, res + 1);
                }
            }
        }
        // 到不了下一棵要砍的树
        return -1;
    }
    int getIdx(int x, int y) {
        // 转一维编号
        return x * m + y;
    }
    int distance(int x, int y, int nx, int ny) {
        // 曼哈顿距离,作为两点之间的理论步数
        return Math.abs(x - nx) + Math.abs(y - ny);
    }    
}
  • 启发式搜索不讨论时空复杂度。

C++

const int N = 50;
class Solution {
    int g[N][N];
    int m, n;
    vector<vector<int>> tree;
public:
    int cutOffTree(vector<vector<int>>& forest) {
        n = forest.size();
        m = forest[0].size();
        // forest转存为g,所有树存入tree并按高度排序
        for(int i = 0; i < n; i++) {
            for(int j = 0; j < m; j++) {
                g[i][j] = forest[i][j];
                if(g[i][j] > 1)
                    tree.push_back({g[i][j], i , j});
            }
        }
        if(g[0][0] == 0)
            return -1;
        sort(tree.begin(), tree.end());
        int x = 0, y = 0, res = 0;
        for(auto ne : tree) {
            int nx = ne[1], ny = ne[2];
            int dis = Astar(x, y, nx, ny); // 砍下一高度的树
            if(dis == -1)
                return -1;
            res += dis;
            x = nx;
            y = ny;
        }
        return res;
    }

    int dirs[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
    int Astar(int x, int y, int nx, int ny) {
        if(x == nx && y == ny)
            return 0;
        unordered_map<int, int> step;
        priority_queue<tuple<int, int, int>, vector<tuple<int, int, int>>, greater<tuple<int, int, int>>> que;
        que.emplace(distance(x, y, nx, ny), x, y);
        step[getIdx(x, y)] = 0;

        while(!que.empty()) {
            auto [dis, cx, cy] = que.top();
            que.pop();
            int res = step[getIdx(cx, cy)]; // 实际步数
            for(auto d : dirs) {
                int ncx = cx + d[0], ncy = cy + d[1], nidx = getIdx(ncx, ncy);
                if(ncx < 0 || ncx >= n || ncy < 0 || ncy >= m) // 超出边界
                    continue;
                if(g[ncx][ncy] == 0) // 障碍
                    continue;
                if(ncx == nx && ncy == ny) // 下一步路刚好是下一棵待砍的树
                    return res + 1;
                // 更新nidx点的最短路径
                if(!step.count(nidx) || step[nidx] > res + 1) {
                    que.emplace(res + 1 + distance(ncx, ncy, nx, ny), ncx, ncy);
                    step[nidx] = res + 1;
                }
            }
        }
        // 到不了下一棵要砍的树
        return -1;
    }
    int getIdx(int x, int y) {
        // 转一维编号
        return x * m + y;
    }
    int distance(int x, int y, int nx, int ny) {
        // 曼哈顿距离,作为两点之间的理论步数
        return abs(x - nx) + abs(y - ny);
    }
};
  • 启发式搜索不讨论时空复杂度。

tuple

  • 学习参考链接
  • 实例化的对象可以存储任意数量、任意类型的数据。

思路四:A*+并查集

417.太平洋大西洋水流问题(并查集的学习)

  • 并查集和Dijstra相同的问题,在『到下一棵树要反向绕一圈』的情况中,因为优先队列要反而多log\log的复杂度;
  • 所以用并查集优化,预处理森林,看是不是所有树都和起点连通,来消除后续对于无解结果的大量无效搜索。

Java

class Solution {
    int N = 50;
    int[][] g = new int[N][N];
    int m, n;
    List<int[]> tree = new ArrayList<>();

    int[] pre = new int[N * N + 10];
    // 并查集
    void union(int a, int b) { // 连通
        pre[find(a)] = pre[find(b)];
    }
    boolean query(int a, int b) {
        return find(a) == find(b);
    }
    int find(int x) {
        if(pre[x] != x)
            pre[x] = find(pre[x]);
        return pre[x];
    }
    
    public int cutOffTree(List<List<Integer>> forest) {
        n = forest.size();
        m = forest.get(0).size();
        // 预处理
        for(int i = 0; i < n * m; i++)
            pre[i] = i;
        int[][] tmp = new int[][]{{0, -1}, {-1, 0}}; // 上和左
        for(int i = 0; i < n; i++) {
            for(int j = 0; j < m; j++) {
                g[i][j] = forest.get(i).get(j);
                if(g[i][j] > 1)
                    tree.add(new int[]{g[i][j], i, j});
                if(g[i][j] == 0)
                    continue;
                for(int[] d : tmp) {
                    int nx = i + d[0], ny = j + d[1];
                    if(nx < 0 || nx >= n || ny < 0 || ny >= m)
                        continue;
                    if(g[nx][ny] != 0)
                        union(getIdx(i, j), getIdx(nx, ny));
                }
            }
        }
        // 不与起点连通直接无解
        for(int[] info : tree) {
            int x = info[1], y = info[2];
            if(!query(getIdx(0, 0), getIdx(x, y)))
                return -1;
        }

        Collections.sort(tree, (a, b) -> a[0] - b[0]);
        int x = 0, y = 0, res = 0;
        for(int[] ne : tree) {
            int nx = ne[1], ny = ne[2];
            int dis = Astar(x, y, nx, ny);
             // 砍下一高度的树
            if(dis == -1)
                return -1;
            res += dis;
            x = nx;
            y = ny;
        }
        return res;
    }
    int[][] dirs = new int[][]{{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
    
    int Astar(int x, int y, int nx, int ny) {
        if(x == nx && y == ny)
            return 0;
        Map<Integer, Integer> step = new HashMap<>();
        // 待走的路,按理论步数排序
        PriorityQueue<int[]> que = new PriorityQueue<>((a,b) -> a[0] - b[0]);
        que.add(new int[]{distance(x, y, nx, ny), x, y});
        step.put(getIdx(x, y), 0);

        while(!que.isEmpty()) {
            int[] cur = que.poll();
            int cx = cur[1], cy = cur[2];
            int res = step.get(getIdx(cx, cy)); // 实际步数
            for(int[] d : dirs) {
                int ncx = cx + d[0], ncy = cy + d[1], nidx = getIdx(ncx, ncy);
                if(ncx < 0 || ncx >= n || ncy < 0 || ncy >= m) // 超出边界
                    continue;
                if(g[ncx][ncy] == 0) // 障碍
                    continue;
                if(ncx == nx && ncy == ny) // 下一步路刚好是下一棵待砍的树
                    return res + 1;
                // 更新nidx点的最短路径
                if(!step.containsKey(nidx) || step.get(nidx) > res + 1) {
                    que.add(new int[]{res + 1 + distance(ncx, ncy, nx, ny), ncx, ncy});
                    step.put(nidx, res + 1);
                }
            }
        }
        // 到不了下一棵要砍的树
        return -1;
    }
    int getIdx(int x, int y) {
        // 转一维编号
        return x * m + y;
    }
    int distance(int x, int y, int nx, int ny) {
        // 曼哈顿距离,作为两点之间的理论步数
        return Math.abs(x - nx) + Math.abs(y - ny);
    }    
}
  • 启发式搜索不讨论时空复杂度。

C++

const int N = 50;
class Solution {
    int g[N][N];
    int m, n;
    vector<vector<int>> tree;
    int pre[N * N + 10];
public:
    // 并查集
    void UNION(int a, int b) { // 连通
        pre[find(a)] = pre[find(b)];
    }
    bool query(int a, int b) {
        return find(a) == find(b);
    }
    int find(int x) {
        if(pre[x] != x)
            pre[x] = find(pre[x]);
        return pre[x];
    }

    int cutOffTree(vector<vector<int>>& forest) {
        n = forest.size();
        m = forest[0].size();
        // 预处理
        for(int i = 0; i < n * m; i++)
            pre[i] = i;
        int tmp[2][2] = {{0, -1}, {-1, 0}}; // 上和左
        for(int i = 0; i < n; i++) {
            for(int j = 0; j < m; j++) {
                g[i][j] = forest[i][j];
                if(g[i][j] > 1)
                    tree.push_back({g[i][j], i, j});
                if(g[i][j] == 0)
                    continue;
                for(auto d : tmp) {
                    int nx = i + d[0], ny = j + d[1];
                    if(nx < 0 || nx >= n || ny < 0 || ny >= m)
                        continue;
                    if(g[nx][ny] != 0)
                        UNION(getIdx(i, j), getIdx(nx, ny));
                }
            }
        }
        // 不与起点连通直接无解
        for(auto info : tree) {
            int x = info[1], y = info[2];
            if(!query(getIdx(0, 0), getIdx(x, y)))
                return -1;
        }

        sort(tree.begin(), tree.end());
        int x = 0, y = 0, res = 0;
        for(auto ne : tree) {
            int nx = ne[1], ny = ne[2];
            int dis = Astar(x, y, nx, ny); // 砍下一高度的树
            if(dis == -1)
                return -1;
            res += dis;
            x = nx;
            y = ny;
        }
        return res;
    }

    int dirs[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
    int Astar(int x, int y, int nx, int ny) {
        if(x == nx && y == ny)
            return 0;
        unordered_map<int, int> step;
        priority_queue<tuple<int, int, int>, vector<tuple<int, int, int>>, greater<tuple<int, int, int>>> que;
        que.emplace(distance(x, y, nx, ny), x, y);
        step[getIdx(x, y)] = 0;

        while(!que.empty()) {
            auto [dis, cx, cy] = que.top();
            que.pop();
            int res = step[getIdx(cx, cy)]; // 实际步数
            for(auto d : dirs) {
                int ncx = cx + d[0], ncy = cy + d[1], nidx = getIdx(ncx, ncy);
                if(ncx < 0 || ncx >= n || ncy < 0 || ncy >= m) // 超出边界
                    continue;
                if(g[ncx][ncy] == 0) // 障碍
                    continue;
                if(ncx == nx && ncy == ny) // 下一步路刚好是下一棵待砍的树
                    return res + 1;
                // 更新nidx点的最短路径
                if(!step.count(nidx) || step[nidx] > res + 1) {
                    que.emplace(res + 1 + distance(ncx, ncy, nx, ny), ncx, ncy);
                    step[nidx] = res + 1;
                }
            }
        }
        // 到不了下一棵要砍的树
        return -1;
    }
    int getIdx(int x, int y) {
        // 转一维编号
        return x * m + y;
    }
    int distance(int x, int y, int nx, int ny) {
        // 曼哈顿距离,作为两点之间的理论步数
        return abs(x - nx) + abs(y - ny);
    }
};
  • 启发式搜索不讨论时空复杂度。

总结

哦困难的图论、启发式搜索确定了启发函数感觉可以靠修改BFS代码逻辑顺下来、不过要注意中间点的更新……

C++用生往二位容器里填充内容会疯狂超时,还没有琢磨清楚原因,但要学会用pair和tuple,避免二维容器吧。

复习了高级版Dijstra的A*和找爸的并查集,启发式搜索感觉要专门针对性学习,这样在题目里的间歇性学习不太够。


欢迎指正与讨论!