困难题来了!
1、读题
今天的困难题属于从来没有接触过的类型。
虽然解法还是基于bfs的,但思路上则是完全木有的船新体验。
2、审题
题目属于比较简洁的,而且类型属于图的遍历。
让你在两点之间寻找是否能连通的题,之前貌似也接触过。
辣么难点在哪呢?
一个 106 x 106 的网格中
这个图太大了。
在这个范围内无限制地使用dfs或bfs遍历无疑问都是超时的。
辣么怎么判断两个点是否能联通呢?
不急,先说说题目给的些许信息吧。
- 首先自然是图的边界:106,这个量级对于图来说不可谓不大了。
0 <= blocked.length <= 200
,看似平平无奇的条件,却是此题的突破点。- 起点
source
或是终点target
都不会出现在blocked
中 - 路径仅能从上下左右四个方向延伸。然后当然,不能越界。
3、思路
在整个思路历程中,我的思路大概变了三次。 并且,我也很惊讶我能想到最后也是最终我做出来的这种思路。
首先,当然是朴素的暴力搜索了,由于范围过大,我企图用dfs+优化剪枝来实现。
但最终,全都是超时。
当暴力的搜索毫无建树的时候,我开始注意到了blocked
的长度:0 <= blocked.length <= 200
。
就算是最长的200个点,在106 x 106的图中也显得过于渺小了。
你很容易就能发现,就算是200个点完全相连形成一条直线,也无法横贯整个图。
这意味着若想要在图中阻隔两个点,辣么这条封锁线,将其中一点围成一个圈,或是依靠边界围成一个圈。
但是,就算判断一些点能否收尾相连很容易,我也不清楚如何判断起点和终点是否在这个包围圈内。
于是这个思路也很快夭折了。
但新的思路又很快在此至上诞生。
即是围棋。
稍微接触过一些围棋的人都知道,一个孤零零的棋子有四个气,即是其上下左右。当其上下左右都被包围时,这个子就气绝了。
而相连的同色棋子是共用气的,棋子连得越多,气也会相对增加。
于是我想到,若blocked
形成的圈,能包围住某一个点,则必定会限制住这个点延伸出来的一系列点的气。
而完美情况下,blocked
的长度,就是这一系列点的气的数量。
反过来想,如果某一点延伸出来的一系列点的气的数量超过了blocked
的长度,辣么blocked
就绝对无法限制这个点向外延伸,也无法限制它连通到终点了。
思路成形,动手吧。
4、开工!
class Solution {
private static Integer BOARD = 1000000;
public boolean isEscapePossible(int[][] blocked, int[] source, int[] target) {
Set<Point> walls = new HashSet<>();
for (int[] p : blocked) {
walls.add(new Point(p[0], p[1]));
}
//无任何阻隔时,一定可以到达
if (walls.isEmpty()) {
return true;
}
Point s = new Point(source[0], source[1]);
Point t = new Point(target[0], target[1]);
return !isSurrounded(walls, blocked.length, s, t)
&& !isSurrounded(walls, blocked.length, t, s);
}
private boolean isSurrounded(Set<Point> blocked, int blockSize, Point point, Point target) {
Set<Point> visited = new HashSet<>();
Deque<Point> air = new ArrayDeque<>();
air.add(point);
while (!air.isEmpty() && air.size() <= blockSize) {
//取出一个气,并判断是否能继续延伸
Point current = air.poll();
//标记当前点已访问
visited.add(current);
//抵达目标点时
if (current.equals(target)) {
return false;
}
//向四周延伸气,但是要判断是否可以算入气内
Point left = new Point(current.x - 1, current.y);
if (!air.contains(left) && canSpread(blocked, left, visited)) {
air.add(left);
}
Point right = new Point(current.x + 1, current.y);
if (!air.contains(right) && canSpread(blocked, right, visited)) {
air.add(right);
}
Point top = new Point(current.x, current.y + 1);
if (!air.contains(top) && canSpread(blocked, top, visited)) {
air.add(top);
}
Point bottom = new Point(current.x, current.y - 1);
if (!air.contains(bottom) && canSpread(blocked, bottom, visited)) {
air.add(bottom);
}
}
//判断气的大小是否超过blocked的长度
return air.size() <= blockSize;
}
private boolean canSpread(Set<Point> blocked, Point point, Set<Point> visit) {
//已访问
if (visit.contains(point)) {
return false;
}
//越界
if (point.x < 0 || point.x >= BOARD
|| point.y < 0 || point.y >= BOARD) {
return false;
}
//碰壁
if (blocked.contains(point)) {
return false;
}
return true;
}
class Point {
int x;
int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
} else if (!(o instanceof Solution.Point)) {
return false;
} else {
Solution.Point other = (Solution.Point) o;
if (!other.canEqual(this)) {
return false;
} else if (this.x != other.x) {
return false;
} else {
return this.y == other.y;
}
}
}
protected boolean canEqual(Object other) {
return other instanceof Solution.Point;
}
@Override
public int hashCode() {
int resultx = 1;
int result = resultx * 59 + this.x;
result = result * 59 + this.y;
return result;
}
}
5、解读
大家可以发现我实现了一个叫做Point的类,其作用是为了记录每一个点是否有访问,也方便记录延伸出去的气。为此,我还专门
有请lambok实现了它的equals()和hashCode()方法……
在结合了大牛们的做法后,我发现这确实多此一举了。
这次,除了主函数外我还实现了另外两个方法。
其一是canSpread()
,其作用是判断某一点能否访问。
限制无非是是否越界、是否已访问 和 是否遇到blocked点。
private boolean canSpread(Set<Point> blocked, Point point, Set<Point> visit) {
//已访问
if (visit.contains(point)) {
return false;
}
//越界
if (point.x < 0 || point.x >= BOARD
|| point.y < 0 || point.y >= BOARD) {
return false;
}
//碰壁
if (blocked.contains(point)) {
return false;
}
return true;
}
然后是isSurrounded()
方法,其作用是从某一点出发作bfs遍历,并判断其延伸的气的数量是否超过了blocked点的数量。
队列的数量即为气的数量,而延伸的起点也是队列中的点。
在围棋中想连接一系列点,则会占用其一个气,但同时,自身也将气延续了出去。
private boolean isSurrounded(Set<Point> blocked, int blockSize, Point point, Point target) {
Set<Point> visited = new HashSet<>();
Deque<Point> air = new ArrayDeque<>();
air.add(point);
while (!air.isEmpty() && air.size() <= blockSize) {
//取出一个气,并判断是否能继续延伸
Point current = air.poll();
//标记当前点已访问
visited.add(current);
//抵达目标点时
if (current.equals(target)) {
return false;
}
//向四周延伸气,但是要判断是否可以算入气内
Point left = new Point(current.x - 1, current.y);
if (!air.contains(left) && canSpread(blocked, left, visited)) {
air.add(left);
}
Point right = new Point(current.x + 1, current.y);
if (!air.contains(right) && canSpread(blocked, right, visited)) {
air.add(right);
}
Point top = new Point(current.x, current.y + 1);
if (!air.contains(top) && canSpread(blocked, top, visited)) {
air.add(top);
}
Point bottom = new Point(current.x, current.y - 1);
if (!air.contains(bottom) && canSpread(blocked, bottom, visited)) {
air.add(bottom);
}
}
//判断气的大小是否超过blocked的长度
return air.size() <= blockSize;
}
当然,还有一种情况是source
和target
都在包围圈内,在bfs遍历中便相遇后,则可以快速剪枝了。
这就是为什么我在isSurrounded()
方法中将两个点都传入了。
//抵达目标点时
if (current.equals(target)) {
return false;
}
最后是主函数,其逻辑反而没有isSurrounded()
复杂。
主要是将blocked
数组转化为所有的点。然后从source
和target
两个点分别发起bfs遍历。
由于blocked
只要将source
和target
中任一一点包围,则两点必然无法连通,所以得全都进行一次bfs。
当然 ,如果两点都在一个圈内的话,后续的遍历就属于多余的了。 但我没有进行这段剪枝,懒得了。不想破坏这么完整的逻辑。
6、提交
困难题,做出来就好,不多求了……
时间复杂度和空间复杂度都不会分析,直接看大牛的吧……
7、学习大牛们
我的思路与这位大牛的一致。
不过就我之前说的,看了大牛们将二维转化为一维的方法后,我通过定义类来实现的做法就真的显得弱智了。
以及,这位大牛提供了java中耗时排名100%的解法,但老实说……让我融会贯通还需要些时间。
8、总结
虽然我的解法并不算优质,但在整个题解过程中的思路变化,确认让我自己也有些惊讶。
就方法是超越了自我一样的进化,这就是这些天来刷题的收获吗?终于能有些反馈了吗?