题目一
思路
网格类问题,我们普遍可以分成两大类问题,第一:单纯的将之看作成为网格,一切的操作都基于网格,用较为直观且直接的方法来解决问题;第二:网格上的一个个点坐标,可以将之抽象成为图上的点,点和点之间都可以连接边,每个点在不越界的情况下最多连接4条边,所以就将之抽象成为了图论问题。
最短路径解法
1、首先,我们将之抽象成为图论问题。那么问题就转化成为了,从(0,0)到(m-1,n-1)之间求一条路径并且消耗最小,这一看就知道是最短路径问题,这里我们采用了堆优化Dijkstra法,详细解法请见图论最短路径专题(力扣743、5888)
2、其实我们对于最短路径问题,必须得知道各个算法,他们的核心思想是什么,核心部分咋实现的就行,还有就是得会将原问题抽象处理,抽象成为最短路径问题,只要熟练掌握这些足以。就拿堆优化版本Dijkstra而言,其实每次新的题变化的只是在更新dis数组的方式上是不一致的,但是别的其余思路是不会变的。
并查集
1、我们首先先把网格问题抽象成为图论问题。
2、现在想从(0,0)到(m-1,n-1)之间求一条路径,这两个点之间存在路径,也就是说这两个点需要连通。
3、再看需求,不仅要连通,还要求路径要最短,我们用贪心的想法,一条路径有若干条边组成,每条边我都从当前未选择的边中选择权值最小的,按 那么整体就是最小的。
4、所以最开始两个目标点不连通,每次按照贪心策略寻找最短边连接,当连接某条边时候,恰好连通,为所求最短路径,这种维护连通性操作,用并查集,详见力扣684冗余连接
二分查找
1、我们单纯地站在网格类问题的角度来看。
2、题目的意思就是水深必须得大于或等于某一个坐标位置水深才能游过去。我们还知道最少水深和最大水深。
3、所以我们可以每次都枚举一个水深,看在这个水深下能不能到终点(用BFS判断),到了说明临时的答案可能是这个,还要继续向下枚举更浅的(原因见题意),反之说明当前水深不是答案,继续向上枚举。
4、因为知道最少水深和最大水深,所以枚举时候用一个技巧来提速,就是二分法。
代码
最短路径解法(堆优化Dijkstra)
1、因为一个点最多延申四条边(不越界的情况时候),所以没有必要构造出图。只需要在用某个点扩展的时候现枚举出可能的四个边即可,就不用费时间去构造了。
2、因为想从一点游到另一点,必须水深>=自己待得点&&>=对面点水深,所以每条边权值=max(点1深度,点2深度),保证水足够深以至于能游过去。
3、每次更新时候,想到新的点point,水必须深于这条边的规定深度,所以就要看,到tmp.x的深度和dist边权规定深度,谁更大,必须深于最大的才能通过。
4、不同最短路径问题,其实算法本质不变,变得只是更新方法!
class Node1 {
public:
int x;//代表从源点到目标点x
int dis;//代表从源点到目标点x的距离
bool operator < (const Node1 & node) {
return dis < node.dis;
}
};
class pile {//二叉堆
public:
bool empty() {
if (size == 0) {
return true;
}
return false;
}
Node1 peak() {
return heap[1];
}
void push(Node1 k) {
heap[++size] = k;
up(size);
}
void pop() {
heap[1] = heap[size--];
down(1);
}
private:
int size = 0;
Node1 heap[10005];
void up(int k) {
int fa = k >> 1;
while (fa >= 1) {
if (heap[fa] < heap[k]) {
break;
}
swap(heap[fa], heap[k]);
k = fa;
fa >>= 1;
}
}
void down(int k) {
int son = k << 1;
while (son <= size) {
if (son + 1 <= size && heap[son + 1] < heap[son]) {
son++;
}
if (heap[k] < heap[son]) {
break;
}
swap(heap[son], heap[k]);
k = son;
son <<= 1;
}
}
};
class Solution {
public:
int m;//长宽
int n;
int x1[4] = { 1,-1,0,0 };//用这两个数组枚举出四个方向
int y1[4] = { 0,0,1,-1 };
const int inf = 1 << 28;
int swimInWater(vector<vector<int>>& grid) {
m = grid.size();
n = grid[0].size();
bool visit[2505] = { false };//用来维护走过的点和未走过的点
vector<int> dis(2505, inf);//维护距离源点水深。
pile p;
dis[0] = 0;//初始化,代表从0到0需要水深0
p.push({ 0,0 });
while (!p.empty()) {
Node1 tmp = p.peak();//找到距离最小的
p.pop();
if (visit[tmp.x]) {//如果已经访问过,则略,这也实现了将所有状态加入堆,最小的第一个出来,标志了这个点走过了
//后续的值大的再出来时已无意义了。
continue;
}
visit[tmp.x] = true;
for (int k = 0; k < 4; k++) {//枚举出所有可能的四个边,每次只可能在这四个点之内更新,所以采取直接枚举法。
int i = tmp.x / n;//由点的编号算坐标
int j = tmp.x%n;
int x = i + x1[k];//本次扩展点坐标
int y = j + y1[k];
if (x >= 0 && x < m&&y >= 0 && y < n) {//越界检测
int point = x * n + y;
if (visit[point]) {//走过的不要继续更新了
continue;
}
int dist = max(grid[x][y], grid[i][j]);//这个是边权计算,计算方法在上
dis[point] = max(dis[tmp.x], dist);//想到新的点point,水必须深于这条边的规定深度
//所以就要看,到tmp.x的深度和dist边权规定深度,谁更大,必须深于最大的才能通过
p.push({ point, dis[point] });//更新
}
}
}
return dis[m*n - 1];
}
};
所有代码均以通过力扣测试
(经过多次测试最短时间为):
二分法
1、判断在某个水深时候,首先判断起点位置,如果水深低于起点位置,一定哪里都去不了,所以第一就是要走出源点。
2、每心出队一个点,枚举可能的四条边,第一不能越界,第二不能已经走过了,第三该点高度<=提供的当前水深,这样才能走过去,第四看到没到终点,到了返回true,没到对新的点做好到过标志,入队
3、一定要有visit数组,防止死循环,一定记住走过的点不能再走。
class Solution {
public:
vector<vector<int>> grid;
int x1[4] = { 1,-1,0,0 };
int y1[4] = { 0,0,1,-1 };
int m, n;
bool check(int mid) {
bool visit[2505] = { false };//存某个点是否经过了
queue<int> item;
if (mid < grid[0][0]) {//能否走出原点
return false;
}
item.push(0);//初始化
visit[0] = true;
while (!item.empty()) {
int point = item.front();
item.pop();
for (int k = 0; k < 4; k++) {//四个可能的边
int i = point / n;
int j = point % n;
int x = i + x1[k];
int y = j + y1[k];
if (x >= 0 && x < m&&y >= 0 && y < n) {//越界判定
int p = x * n + y;
if (visit[p]) {//不能提前走过
continue;
}
if (grid[x][y] <= mid) {//能走过去
if (x == m - 1 && y == n - 1) {//终点判断
return true;
}
item.push(p);
visit[p] = true;
}
}
}
}
return false;
}
int swimInWater(vector<vector<int>>& grid) {
this->grid = grid;//初始化
m = n = grid.size();
int left = 0;//初始值读题
int right = m * n - 1;
int ans = 0;
while (left <= right) {//二分法
int mid = (left + right) / 2;
if (check(mid)) {
ans = mid;//如果当前深度可以达到,就更新ans
right = mid - 1;
}
else {
left = mid + 1;
}
}
return ans;
}
};
所有代码均以通过力扣测试
(经过多次测试最短时间为):
并查集
1、因为每次挑选最短边,所以必须提前准备好算好所有可能边,对边进行排序,从小往大取用,随后一次取得边,恰好让两个目标连通了,这个边的dis就是答案,原因是我们想走通这个路径,水深必须>=最高点处,也就是最后一次出现的dis
2、已经在一个集合的两个点不要重复merge
class Node {//边类
public:
int x;//起点
int y;//终点
int dis;//距离
bool operator < (const Node & node) {
return dis < node.dis;
}
};
class union_set {//并查集
public:
union_set(int n) {
this->n = n;
for (int i = 0; i <= n; i++) {
father[i] = i;
}
}
int find(int k) {
if (k <= 0 || k > n) {
return 0;
}
if (father[k] == k) {
return father[k];
}
father[k] = find(father[k]);
return father[k];
}
void merge(int i, int j) {
father[find(i)] = find(j);
}
private:
int n;
int father[3000];
};
class Solution {
public:
int m;
int n;
int ans = 0;
int swimInWater(vector<vector<int>>& grid) {
vector<Node> edge;
m = grid.size();
n = grid[0].size();
int x1[4] = { 1,-1,0,0 };
int y1[4] = { 0,0,1,-1 };
for (int i = 0; i < m; i++) {//获得边
for (int j = 0; j < n; j++) {
for (int k = 0; k < 4; k++) {
int x = i + x1[k];
int y = j + y1[k];
if (x >= 0 && x < m&&y >= 0 && y < n) {
int dis = max(grid[x][y], grid[i][j]);//dis必须是最大深度
edge.push_back({ i*n + j,x*n + y,dis });
}
}
}
}
sort(edge.begin(), edge.end());
int n1 = edge.size();
union_set us(m*n);
for (int i = 0; i < n1; i++) {
Node tmp = edge[i];
int x = tmp.x + 1;
int y = tmp.y + 1;
int dis = tmp.dis;
if (us.find(x) != us.find(y)) {//这两个点必须是没连接过的
us.merge(x, y);
ans = dis;
if (us.find(1) == us.find(m*n)) {
break;
}
}
}
return ans;
}
};