| 每日一题做题记录,参考官方和三叶的题解 |
题目要求
理解
- 树的高度唯一,那其实只能沿着从低到高依次砍,也就是说路线唯一;
- 而两棵树之间的最短距离是确定的(有树也可通过),所以求相邻高度两棵树的最短距离加起来就好了。
思路一:BFS
- 数据范围只有50,所以可以直接BFS。
- 把所有的树找出来,排序得到砍树路径,依次遍历找当前树和下一棵树之间的最短距离:
- 用一个队列存代遍历的点;
- 对每个点向四个方向走,找下一棵树。
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;
}
}
- 时间复杂度:
- 空间复杂度:
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;
}
};
- 时间复杂度:
- 空间复杂度:
STL pair
- 学习参考链接
- 一个键值对类,放在
vector里放二维数组很好用。
思路二:Dijstra算法
- DIjstra算法也可以求最短距离,但是本题存在障碍物设置,所以选择的最短路径节点可能不是最优的,时间复杂度反而增加。【反正后面就是A*所以就不写了、不是偷懒、绝不是】
- 时间复杂度:
- 空间复杂度:
思路三:A*
【半个月前学的、还记得那天每种方法都卡、433.最小基因变化(A*学习)】
- BFS向四个方向找最短距离,但其实根据两点的相对关系可以简化这个过程:
- 向该点方向找;
- 找不到再换其他方向绕。
- 用最小理论步数作为启发式函数:
- 用曼哈顿距离作为两点的最小理论步数,即走直线的距离;
- 两棵树之间的距离估算距离为,按这个结果的大小压入队列,依次遍历计算。
- 注意该路径上点改变造成的其他点的最小步数的变化。
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*+并查集
- 并查集和Dijstra相同的问题,在『到下一棵树要反向绕一圈』的情况中,因为优先队列要反而多的复杂度;
- 所以用并查集优化,预处理森林,看是不是所有树都和起点连通,来消除后续对于无解结果的大量无效搜索。
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*和找爸的并查集,启发式搜索感觉要专门针对性学习,这样在题目里的间歇性学习不太够。
| 欢迎指正与讨论! |