网格 DFS 遍历的框架代码
如何避免这样的重复遍历呢?答案是标记已经遍历过的格子。以岛屿问题为例,我们需要在所有值为 1 的陆地格子上做 DFS 遍历。每走过一个陆地格子,就把格子的值改为 2,这样当我们遇到 2 的时候,就知道这是遍历过的格子了。
复制 void dfs( int [][] grid , int r , int c) {
// 判断 base case
// 如果坐标 (r, c) 超出了网格范围,直接返回
if ( ! inArea(grid , r , c) ) {
return ;
}
// 如果这个格子不是岛屿,直接返回
if (grid[r][c] != 1 ) {
return ;
}
grid[r][c] = 2 ; // 将格子标记为「已遍历过」
// 访问上、下、左、右四个相邻结点
dfs(grid , r - 1 , c) ;
dfs(grid , r + 1 , c) ;
dfs(grid , r , c - 1 ) ;
dfs(grid , r , c + 1 ) ;
}
// 判断坐标 (r, c) 是否在网格中
boolean inArea( int [][] grid , int r , int c) {
return 0 <= r && r < grid . length
&& 0 <= c && c < grid[ 0 ] . length ;
}
给你一个大小为 m x n
的二进制矩阵 grid
。
岛屿 是由一些相邻的 1
(代表土地) 构成的组合,这里的「相邻」要求两个 1
必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid
的四个边缘都被 0
(代表水)包围着。
岛屿的面积是岛上值为 1
的单元格的数目。
计算并返回 grid
中最大的岛屿面积。如果没有岛屿,则返回面积为 0
。
复制 public int maxAreaOfIsland( int [][] grid) {
int res = 0 ;
for ( int r = 0 ; r < grid . length ; r ++ ) {
for ( int c = 0 ; c < grid[ 0 ] . length ; c ++ ) {
if (grid[r][c] == 1 ) {
int a = area(grid , r , c) ;
res = Math . max (res , a);
}
}
}
return res;
}
int area( int [][] grid , int r , int c) {
if ( ! inArea(grid , r , c) ) {
return 0 ;
}
if (grid[r][c] != 1 ) {
return 0 ;
}
grid[r][c] = 2 ;
return 1
+ area(grid , r - 1 , c)
+ area(grid , r + 1 , c)
+ area(grid , r , c - 1 )
+ area(grid , r , c + 1 ) ;
}
boolean inArea( int [][] grid , int r , int c) {
return 0 <= r && r < grid . length
&& 0 <= c && c < grid[ 0 ] . length ;
}
给你一个大小为 n x n
二进制矩阵 grid
。最多 只能将一格 0
变成 1
。
返回执行此操作后,grid
中最大的岛屿面积是多少?
岛屿 由一组上、下、左、右四个方向相连的 1
形成。
大致的思路我们不难想到,我们先计算出所有岛屿的面积,在所有的格子上标记出岛屿的面积。然后搜索哪个海洋格子相邻的两个岛屿面积最大。
这种做法可能遇到一个问题。如下图中红色方框内的海洋格子,它的上边、左边都与岛屿相邻。
那么,我们不能在方格中标记岛屿的面积,而应该标记岛屿的索引(下标),另外用一个数组记录每个岛屿的面积。
可以看到,这道题实际上是对网格做了两遍 DFS:第一遍 DFS 遍历陆地格子,计算每个岛屿的面积并标记岛屿;第二遍 DFS 遍历海洋格子,观察每个海洋格子相邻的陆地格子。
复制 class Solution {
// 方向数组,用于表示上、下、左、右四个方向的坐标偏移
static int [] d = { 0 , - 1 , 0 , 1 , 0 };
// 主函数,计算最大人工岛的面积
public int largestIsland ( int [][] grid) {
int n = grid . length , res = 0 ;
int [][] tag = new int [n][n]; // 用于标记岛屿的标签
Map < Integer , Integer > area = new HashMap < Integer , Integer >(); // 存储每个岛屿的面积
// 遍历整个grid,标记每个岛屿并计算面积
for ( int i = 0 ; i < n; i ++ ) {
for ( int j = 0 ; j < n; j ++ ) {
if (grid[i][j] == 1 && tag[i][j] == 0 ) {
//相邻岛屿的tag都设置为同一个t值
int t = i * n + j + 1 ; // 计算岛屿的唯一标签
area . put (t , dfs(grid , i , j , tag , t) ); // 计算并存储岛屿面积
res = Math . max (res , area . get (t)); // 更新最大岛屿面积
}
}
}
// 遍历grid中的海洋格子,计算将海洋格子变成陆地后的最大人工岛面积
for ( int i = 0 ; i < n; i ++ ) {
for ( int j = 0 ; j < n; j ++ ) {
if (grid[i][j] == 0 ) {
int z = 1 ;
Set < Integer > connected = new HashSet < Integer >(); // 用于记录与当前海洋格子相邻的岛屿标签
for ( int k = 0 ; k < 4 ; k ++ ) {
int x = i + d[k] , y = j + d[k + 1 ];
// 判断相邻的岛屿是否有效并且未被记录
if ( ! valid(n , x , y) || tag[x][y] == 0 || connected . contains (tag[x][y])) {
continue ;
}
z += area . get (tag[x][y]); // 计算当前岛屿面积
connected . add (tag[x][y]); // 记录当前岛屿标签
}
res = Math . max (res , z); // 更新最大人工岛面积
}
}
}
return res;
}
// 深度优先搜索,标记并计算岛屿面积
public int dfs ( int [][] grid , int x , int y , int [][] tag , int t) {
int n = grid . length , res = 1 ;
tag[x][y] = t; // 标记当前坐标
// 遍历当前坐标的四个相邻坐标
for ( int i = 0 ; i < 4 ; i ++ ) {
int x1 = x + d[i] , y1 = y + d[i + 1 ];
// 判断相邻坐标是否有效、是否是陆地、是否未被标记
if ( valid(n , x1 , y1) && grid[x1][y1] == 1 && tag[x1][y1] == 0 ) {
res += dfs(grid , x1 , y1 , tag , t) ; // 递归搜索并计算岛屿面积
}
}
return res;
}
// 判断坐标是否有效
public boolean valid ( int n , int x , int y) {
return x >= 0 && x < n && y >= 0 && y < n;
}
}
给定一个 row x col
的二维网格地图 grid
,其中:grid[i][j] = 1
表示陆地, grid[i][j] = 0
表示水域。
网格中的格子 水平和垂直 方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。
岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。计算这个岛屿的周长。
复制 public int islandPerimeter( int [][] grid) {
for ( int r = 0 ; r < grid . length ; r ++ ) {
for ( int c = 0 ; c < grid[ 0 ] . length ; c ++ ) {
if (grid[r][c] == 1 ) {
// 题目限制只有一个岛屿,计算一个即可
return dfs(grid , r , c) ;
}
}
}
return 0 ;
}
int dfs( int [][] grid , int r , int c) {
// 函数因为「坐标 (r, c) 超出网格范围」返回,对应一条黄色的边
if ( ! inArea(grid , r , c) ) {
return 1 ;
}
// 函数因为「当前格子是海洋格子」返回,对应一条蓝色的边
if (grid[r][c] == 0 ) {
return 1 ;
}
// 函数因为「当前格子是已遍历的陆地格子」返回,和周长没关系
if (grid[r][c] != 1 ) {
return 0 ;
}
grid[r][c] = 2 ;
return dfs(grid , r - 1 , c)
+ dfs(grid , r + 1 , c)
+ dfs(grid , r , c - 1 )
+ dfs(grid , r , c + 1 ) ;
}
// 判断坐标 (r, c) 是否在网格中
boolean inArea( int [][] grid , int r , int c) {
return 0 <= r && r < grid . length
&& 0 <= c && c < grid[ 0 ] . length ;
}
给你一个由 '1'
(陆地)和 '0'
(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
复制 class Solution {
int ans = 0 ;
public int numIslands ( char [][] grid) {
for ( int i = 0 ; i < grid . length ; i ++ ){
for ( int j = 0 ; j < grid[ 0 ] . length ;j ++ ){
if (grid[i][j] == '1' ){
ans ++ ;
dfs(grid , i , j) ;
}
}
}
return ans;
}
void dfs ( char [][] grid , int r , int c) {
// 判断 base case
// 如果坐标 (r, c) 超出了网格范围,直接返回
if ( ! inArea(grid , r , c) ) {
return ;
}
// 如果这个格子不是岛屿,直接返回
if (grid[r][c] != '1' ) {
return ;
}
grid[r][c] = '2' ; // 将格子标记为「已遍历过」
// 访问上、下、左、右四个相邻结点
dfs(grid , r - 1 , c) ;
dfs(grid , r + 1 , c) ;
dfs(grid , r , c - 1 ) ;
dfs(grid , r , c + 1 ) ;
}
// 判断坐标 (r, c) 是否在网格中
boolean inArea ( char [][] grid , int r , int c) {
return 0 <= r && r < grid . length
&& 0 <= c && c < grid[ 0 ] . length ;
}
}
给你一个 m x n
的矩阵 board
,由若干字符 'X'
和 'O'
,找到所有被 'X'
围绕的区域,并将这些区域里所有的 'O'
用 'X'
填充。
复制 被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。 任何不在边界上,或不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。
反向寻找,找在边界上的O
复制 class Solution {
int n , m;
public void solve ( char [][] board) {
n = board . length ;
if (n == 0 ) {
return ;
}
m = board[ 0 ] . length ;
//遍历第一列和最后一列
for ( int i = 0 ; i < n; i ++ ) {
dfs(board , i , 0 ) ;
dfs(board , i , m - 1 ) ;
}
//遍历第一行和最后一行(i需要排除已经遍历过的地方)
for ( int i = 1 ; i < m - 1 ; i ++ ) {
dfs(board , 0 , i) ;
dfs(board , n - 1 , i) ;
}
for ( int i = 0 ; i < n; i ++ ) {
for ( int j = 0 ; j < m; j ++ ) {
if (board[i][j] == 'A' ) {
board[i][j] = 'O' ;
} else if (board[i][j] == 'O' ) {
board[i][j] = 'X' ;
}
}
}
}
public void dfs ( char [][] board , int x , int y) {
if (x < 0 || x >= n || y < 0 || y >= m || board[x][y] != 'O' ) {
return ;
}
board[x][y] = 'A' ;
dfs(board , x + 1 , y) ;
dfs(board , x - 1 , y) ;
dfs(board , x , y + 1 ) ;
dfs(board , x , y - 1 ) ;
}
}
给你无向 连通 图中一个节点的引用,请你返回该图的 深拷贝 (克隆)。
图中的每个节点都包含它的值 val
(int
) 和其邻居的列表(list[Node]
)。
复制 class Node {
public int val;
public List < Node > neighbors;
}
解法:递归,用map保存是否已经克隆过
复制 class Solution {
private HashMap < Node , Node > visited = new HashMap <> ();
public Node cloneGraph ( Node node) {
if (node == null ) {
return node;
}
// 如果该节点已经被访问过了,则直接从哈希表中取出对应的克隆节点返回
if ( visited . containsKey (node)) {
return visited . get (node);
}
// 克隆节点,注意到为了深拷贝我们不会克隆它的邻居的列表
Node cloneNode = new Node( node . val , new ArrayList()) ;
// 哈希表存储
visited . put (node , cloneNode);
// 遍历该节点的邻居并更新克隆节点的邻居列表
for ( Node neighbor : node . neighbors ) {
cloneNode . neighbors . add ( cloneGraph(neighbor) );
}
return cloneNode;
}
}
给你一个变量对数组 equations
和一个实数值数组 values
作为已知条件,其中 equations[i] = [Ai, Bi]
和 values[i]
共同表示等式 Ai / Bi = values[i]
。每个 Ai
或 Bi
是一个表示单个变量的字符串。
另有一些以数组 queries
表示的问题,其中 queries[j] = [Cj, Dj]
表示第 j
个问题,请你根据已知条件找出 Cj / Dj = ?
的结果作为答案。
返回 所有问题的答案 。如果存在某个无法确定的答案,则用 -1.0
替代这个答案。如果问题中出现了给定的已知条件中没有出现的字符串,也需要用 -1.0
替代这个答案。
**注意:**输入总是有效的。你可以假设除法运算中不会出现除数为 0 的情况,且不存在任何矛盾的结果。
**注意:**未在等式列表中出现的变量是未定义的,因此无法确定它们的答案。
TODO ---并查集
复制 import java . util . HashMap ;
import java . util . List ;
import java . util . Map ;
public class Solution {
public double [] calcEquation ( List < List < String >> equations , double [] values , List < List < String >> queries) {
int equationsSize = equations . size ();
UnionFind unionFind = new UnionFind( 2 * equationsSize) ;
// 第 1 步:预处理,将变量的值与 id 进行映射,使得并查集的底层使用数组实现,方便编码
Map < String , Integer > hashMap = new HashMap <>( 2 * equationsSize);
int id = 0 ;
for ( int i = 0 ; i < equationsSize; i ++ ) {
List < String > equation = equations . get (i);
String var1 = equation . get ( 0 );
String var2 = equation . get ( 1 );
if ( ! hashMap . containsKey (var1)) {
hashMap . put (var1 , id);
id ++ ;
}
if ( ! hashMap . containsKey (var2)) {
hashMap . put (var2 , id);
id ++ ;
}
unionFind . union ( hashMap . get (var1) , hashMap . get (var2) , values[i]);
}
// 第 2 步:做查询
int queriesSize = queries . size ();
double [] res = new double [queriesSize];
for ( int i = 0 ; i < queriesSize; i ++ ) {
String var1 = queries . get (i) . get ( 0 );
String var2 = queries . get (i) . get ( 1 );
Integer id1 = hashMap . get (var1);
Integer id2 = hashMap . get (var2);
if (id1 == null || id2 == null ) {
res[i] = - 1.0d ;
} else {
res[i] = unionFind . isConnected (id1 , id2);
}
}
return res;
}
private class UnionFind {
private int [] parent;
/**
* 指向的父结点的权值
*/
private double [] weight;
public UnionFind ( int n) {
this . parent = new int [n];
this . weight = new double [n];
for ( int i = 0 ; i < n; i ++ ) {
parent[i] = i;
weight[i] = 1.0d ;
}
}
public void union ( int x , int y , double value) {
int rootX = find(x) ;
int rootY = find(y) ;
if (rootX == rootY) {
return ;
}
parent[rootX] = rootY;
// 关系式的推导请见「参考代码」下方的示意图
weight[rootX] = weight[y] * value / weight[x];
}
/**
* 路径压缩
*
* @param x
* @return 根结点的 id
*/
public int find ( int x) {
if (x != parent[x]) {
int origin = parent[x];
parent[x] = find(parent[x]) ;
weight[x] *= weight[origin];
}
return parent[x];
}
public double isConnected ( int x , int y) {
int rootX = find(x) ;
int rootY = find(y) ;
if (rootX == rootY) {
return weight[x] / weight[y];
} else {
return - 1.0d ;
}
}
}
}
你这个学期必须选修 numCourses
门课程,记为 0
到 numCourses - 1
。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites
给出,其中 prerequisites[i] = [ai, bi]
,表示如果要学习课程 ai
则 必须 先学习课程 bi
。
例如,先修课程对 [0, 1]
表示:想要学习课程 0
,你需要先完成课程 1
。
请你判断是否可能完成所有课程的学习?如果可以,返回 true
;否则,返回 false
。
示例 1:
复制 输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
示例 2:
复制 输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。
构建图:就是构建每个节点和其他节点之间的关系。可以用edges = new ArrayList<List>()这个二维list 来表示。
解法1:拓扑排序,对图进行一遍深度优先搜索。当每个节点进行回溯的时候,我们把该节点放入栈中。最终从栈顶到栈底的序列就是一种拓扑排序。
由于我们只需要判断是否存在一种拓扑排序,而栈的作用仅仅是存放最终的拓扑排序结果,因此我们可以只记录每个节点的状态,而省去对应的栈。
对于图中的任意一个节点,它在搜索的过程中有三种状态,即:
「未搜索」:我们还没有搜索到这个节点;
「搜索中」:我们搜索过这个节点,但还没有回溯到该节点,即该节点还没有入栈,还有相邻的节点没有搜索完成);
「已完成」:我们搜索过并且回溯过这个节点,即该节点已经入栈,并且所有该节点的相邻节点都出现在栈的更底部的位置,满足拓扑排序的要求。
通过上述的三种状态,我们就可以给出使用深度优先搜索得到拓扑排序的算法流程,在每一轮的搜索搜索开始时,我们任取一个「未搜索」的节点开始进行深度优先搜索。
我们将当前搜索的节点 u 标记为「搜索中」,遍历该节点的每一个相邻节点 v:
如果 v 为「未搜索」,那么我们开始搜索 v,待搜索完成回溯到 u;
如果 v 为「搜索中」,那么我们就找到了图中的一个环,因此是不存在拓扑排序的;
如果 v 为「已完成」,那么说明 v 已经在栈中了,而 u 还不在栈中,因此 u 无论何时入栈都不会影响到 (u,v) 之前的拓扑关系,以及不用进行任何操作。
当 u 的所有相邻节点都为「已完成」时,我们将 u 放入栈中,并将其标记为「已完成」。
复制 class Solution {
List < List < Integer >> edges;
int [] visited;
boolean valid = true ;
public boolean canFinish ( int numCourses , int [][] prerequisites) {
//每门课都有一条边
edges = new ArrayList < List < Integer >>();
for ( int i = 0 ; i < numCourses; ++ i) {
edges . add ( new ArrayList < Integer >());
}
//存储课程是否被访问过
visited = new int [numCourses];
for ( int [] info : prerequisites) {
//添加课程之间的关联关系
edges . get (info[ 1 ]) . add (info[ 0 ]);
}
for ( int i = 0 ; i < numCourses && valid; ++ i) {
if (visited[i] == 0 ) {
//遍历每一条edges边
dfs(i) ;
}
}
return valid;
}
public void dfs ( int u) {
//搜索中
visited[u] = 1 ;
for ( int v : edges . get (u)) {
if (visited[v] == 0 ) {
dfs(v) ;
if ( ! valid) {
return ;
}
} else if (visited[v] == 1 ) {
valid = false ;
return ;
}
}
//搜索完成
visited[u] = 2 ;
}
}
解法2:广度优先
我们使用一个队列来进行广度优先搜索。初始时,所有入度为 0 的节点都被放入队列中,它们就是可以作为拓扑排序最前面的节点,并且它们之间的相对顺序是无关紧要的。
在广度优先搜索的每一步中,我们取出队首的节点 u:
我们将 u 放入答案中;
我们移除 u 的所有出边,也就是将 u 的所有相邻节点的入度减少 1。如果某个相邻节点 v 的入度变为 0,那么我们就将 v 放入队列中。
在广度优先搜索的过程结束后。如果答案中包含了这 n 个节点,那么我们就找到了一种拓扑排序,否则说明图中存在环,也就不存在拓扑排序了。
复制 class Solution {
List < List < Integer >> edges;
int [] indeg;
public boolean canFinish ( int numCourses , int [][] prerequisites) {
edges = new ArrayList < List < Integer >>();
for ( int i = 0 ; i < numCourses; ++ i) {
edges . add ( new ArrayList < Integer >());
}
indeg = new int [numCourses];
for ( int [] info : prerequisites) {
edges . get (info[ 1 ]) . add (info[ 0 ]);
++ indeg[info[ 0 ]];
}
Queue < Integer > queue = new LinkedList < Integer >();
for ( int i = 0 ; i < numCourses; ++ i) {
if (indeg[i] == 0 ) {
queue . offer (i);
}
}
int visited = 0 ;
while ( ! queue . isEmpty ()) {
++ visited;
int u = queue . poll ();
for ( int v : edges . get (u)) {
-- indeg[v];
if (indeg[v] == 0 ) {
queue . offer (v);
}
}
}
return visited == numCourses;
}
}
现在你总共有 numCourses
门课需要选,记为 0
到 numCourses - 1
。给你一个数组 prerequisites
,其中 prerequisites[i] = [ai, bi]
,表示在选修课程 ai
前 必须 先选修 bi
。
例如,想要学习课程 0
,你需要先完成课程 1
,我们用一个匹配来表示:[0,1]
。
返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。
解法: 1. 构建图(二维列表),2.遍历图,3.返回之后的结果。
深度优先
复制 class Solution {
// 存储有向图
List < List < Integer >> edges;
// 标记每个节点的状态:0=未搜索,1=搜索中,2=已完成
int [] visited;
// 用数组来模拟栈,下标 n-1 为栈底,0 为栈顶
int [] result;
// 判断有向图中是否有环
boolean valid = true ;
// 栈下标
int index;
public int [] findOrder ( int numCourses , int [][] prerequisites) {
edges = new ArrayList < List < Integer >>();
for ( int i = 0 ; i < numCourses; ++ i) {
edges . add ( new ArrayList < Integer >());
}
visited = new int [numCourses];
result = new int [numCourses];
index = numCourses - 1 ;
for ( int [] info : prerequisites) {
edges . get (info[ 1 ]) . add (info[ 0 ]);
}
// 每次挑选一个「未搜索」的节点,开始进行深度优先搜索
for ( int i = 0 ; i < numCourses && valid; ++ i) {
if (visited[i] == 0 ) {
dfs(i) ;
}
}
if ( ! valid) {
return new int [ 0 ];
}
// 如果没有环,那么就有拓扑排序
return result;
}
public void dfs ( int u) {
// 将节点标记为「搜索中」
visited[u] = 1 ;
// 搜索其相邻节点
// 只要发现有环,立刻停止搜索
for ( int v : edges . get (u)) {
// 如果「未搜索」那么搜索相邻节点
if (visited[v] == 0 ) {
dfs(v) ;
if ( ! valid) {
return ;
}
}
// 如果「搜索中」说明找到了环
else if (visited[v] == 1 ) {
valid = false ;
return ;
}
}
// 将节点标记为「已完成」
visited[u] = 2 ;
// 将节点入栈,因为是递归,所以从最后面开始放
result[index -- ] = u;
}
}
广度优先
复制 class Solution {
// 存储有向图
List < List < Integer >> edges;
// 存储每个节点的入度
int [] indeg;
// 存储答案
int [] result;
// 答案下标
int index;
public int [] findOrder ( int numCourses , int [][] prerequisites) {
edges = new ArrayList < List < Integer >>();
for ( int i = 0 ; i < numCourses; ++ i) {
edges . add ( new ArrayList < Integer >());
}
indeg = new int [numCourses];
result = new int [numCourses];
index = 0 ;
for ( int [] info : prerequisites) {
edges . get (info[ 1 ]) . add (info[ 0 ]);
++ indeg[info[ 0 ]];
}
Queue < Integer > queue = new LinkedList < Integer >();
// 将所有入度为 0 的节点放入队列中
for ( int i = 0 ; i < numCourses; ++ i) {
if (indeg[i] == 0 ) {
queue . offer (i);
}
}
while ( ! queue . isEmpty ()) {
// 从队首取出一个节点
int u = queue . poll ();
// 放入答案中,广度优先,从前面开始放
result[index ++ ] = u;
for ( int v : edges . get (u)) {
-- indeg[v];
// 如果相邻节点 v 的入度为 0,就可以选 v 对应的课程了
if (indeg[v] == 0 ) {
queue . offer (v);
}
}
}
if (index != numCourses) {
return new int [ 0 ];
}
return result;
}
}
给你一个大小为 n x n
的整数矩阵 board
,方格按从 1
到 n2
编号,编号遵循 转行交替方式 ,从左下角开始 (即,从 board[n - 1][0]
开始)每一行交替方向。
玩家从棋盘上的方格 1
(总是在最后一行、第一列)开始出发。
每一回合,玩家需要从当前方格 curr
开始出发,按下述要求前进:
选定目标方格
,目标方格的编号符合范围
复制 [curr + 1, min(curr + 6, n2)]
。
该选择模拟了掷 六面体骰子 的情景,无论棋盘大小如何,玩家最多只能有 6 个目的地。
传送玩家:如果目标方格 next
处存在蛇或梯子,那么玩家会传送到蛇或梯子的目的地。否则,玩家传送到目标方格 next
。
返回达到编号为 n2
的方格所需的最少移动次数,如果不可能,则返回 -1
。
r
行 c
列的棋盘,按前述方法编号,棋盘格中可能存在 “蛇” 或 “梯子”;如果 board[r][c] != -1
,那个蛇或梯子的目的地将会是 board[r][c]
。编号为 1
和 n2
的方格上没有蛇或梯子。
注意,玩家在每回合的前进过程中最多只能爬过蛇或梯子一次:就算目的地是另一条蛇或梯子的起点,玩家也 不能 继续移动。
举个例子,假设棋盘是 [[-1,4],[-1,3]]
,第一次移动,玩家的目标方格是 2
。那么这个玩家将会顺着梯子到达方格 3
,但 不能 顺着方格 3
上的梯子前往方格 4
。
返回达到编号为 n2
的方格所需的最少移动次数,如果不可能,则返回 -1
。
解法:广度优先搜索:将节点编号和到达该节点的移动次数作为搜索状态,顺着该节点的出边扩展新状态,直至到达终点 N^2 ,返回此时的移动次数。若无法到达终点则返回 −1。
代码实现时,我们可以用一个队列来存储搜索状态,初始时将起点状态 (1,0) 加入队列,表示当前位于起点 1,移动次数为 0。然后不断取出队首,每次取出队首元素时扩展新状态,即遍历该节点的出边,若出边对应节点未被访问,则将该节点和移动次数加一的结果作为新状态,加入队列。如此循环直至到达终点或队列为空。
复制 class Solution {
public int snakesAndLadders ( int [][] board) {
int n = board . length ;
boolean [] vis = new boolean [n * n + 1 ];
Queue < int []> queue = new LinkedList < int []>();
//表示当前位于起点 1,移动次数为 0
queue . offer ( new int []{ 1 , 0 });
while ( ! queue . isEmpty ()) {
int [] p = queue . poll ();
for ( int i = 1 ; i <= 6 ; ++ i) {
int nxt = p[ 0 ] + i;
if (nxt > n * n) { // 超出边界
break ;
}
int [] rc = id2rc(nxt , n) ; // 得到下一步的行列
if (board[rc[ 0 ]][rc[ 1 ]] > 0 ) { // 存在蛇或梯子
nxt = board[rc[ 0 ]][rc[ 1 ]];
}
if (nxt == n * n) { // 到达终点
return p[ 1 ] + 1 ;
}
if ( ! vis[nxt]) {
vis[nxt] = true ;
queue . offer ( new int []{nxt , p[ 1 ] + 1 }); // 扩展新状态
}
}
}
return - 1 ;
}
public int [] id2rc ( int id , int n) {
int r = (id - 1 ) / n , c = (id - 1 ) % n;
if (r % 2 == 1 ) {
c = n - 1 - c;
}
return new int []{n - 1 - r , c};
}
}
基因序列可以表示为一条由 8 个字符组成的字符串,其中每个字符都是 'A'
、'C'
、'G'
和 'T'
之一。
假设我们需要调查从基因序列 start
变为 end
所发生的基因变化。一次基因变化就意味着这个基因序列中的一个字符发生了变化。
例如,"AACCGGTT" --> "AACCGGTA"
就是一次基因变化。
另有一个基因库 bank
记录了所有有效的基因变化,只有基因库中的基因才是有效的基因序列。(变化后的基因必须位于基因库 bank
中)
给你两个基因序列 start
和 end
,以及一个基因库 bank
,请你找出并返回能够使 start
变化为 end
所需的最少变化次数。如果无法完成此基因变化,返回 -1
。
注意:起始基因序列 start
默认是有效的,但是它并不一定会出现在基因库中。
解法:广度搜索
复制 class Solution {
public int minMutation ( String start , String end , String [] bank) {
Set < String > cnt = new HashSet < String >();
Set < String > visited = new HashSet < String >();
char [] keys = { 'A' , 'C' , 'G' , 'T' };
for ( String w : bank) {
cnt . add (w);
}
if ( start . equals (end)) {
return 0 ;
}
if ( ! cnt . contains (end)) {
return - 1 ;
}
Queue < String > queue = new ArrayDeque < String >();
queue . offer (start);
//作用是判断在变化过程中有没有循环
visited . add (start);
int step = 1 ;
while ( ! queue . isEmpty ()) {
int sz = queue . size ();
for ( int i = 0 ; i < sz; i ++ ) {
String curr = queue . poll ();
//8 *4种变化
for ( int j = 0 ; j < 8 ; j ++ ) {
for ( int k = 0 ; k < 4 ; k ++ ) {
if (keys[k] != curr . charAt (j)) {
StringBuffer sb = new StringBuffer(curr) ;
sb . setCharAt (j , keys[k]);
String next = sb . toString ();
if ( ! visited . contains (next) && cnt . contains (next)) {
if ( next . equals (end)) {
return step;
}
queue . offer (next);
visited . add (next);
}
}
}
}
}
step ++ ;
}
return - 1 ;
}
}
字典 wordList
中从单词 beginWord
和 endWord
的 转换序列 是一个按下述规格形成的序列 beginWord -> s1 -> s2 -> ... -> sk
:
对于 1 <= i <= k
时,每个 si
都在 wordList
中。注意, beginWord
不需要在 wordList
中。
给你两个单词 beginWord
和 endWord
和一个字典 wordList
,返回 从 beginWord
到 endWord
的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0
。
看到最短首先想到的就是广度优先搜索 。想到广度优先搜索 自然而然的就能想到图。
图就是一个二维列表。
解法1.广度优先,从开始单词开始,一个字符一个字符进行判断是否在字段里面,如果不在则添加到queue中。然后继续遍历。
复制 public class Solution {
public int ladderLength ( String beginWord , String endWord , List < String > wordList) {
// 第 1 步:先将 wordList 放到哈希表里,便于判断某个单词是否在 wordList 里
Set < String > wordSet = new HashSet <>(wordList);
if ( wordSet . size () == 0 || ! wordSet . contains (endWord)) {
return 0 ;
}
wordSet . remove (beginWord);
// 第 2 步:图的广度优先遍历,必须使用队列和表示是否访问过的 visited 哈希表
Queue < String > queue = new LinkedList <>();
queue . offer (beginWord);
Set < String > visited = new HashSet <>();
visited . add (beginWord);
// 第 3 步:开始广度优先遍历,包含起点,因此初始化的时候步数为 1
int step = 1 ;
while ( ! queue . isEmpty ()) {
int currentSize = queue . size ();
for ( int i = 0 ; i < currentSize; i ++ ) {
// 依次遍历当前队列中的单词
String currentWord = queue . poll ();
// 如果 currentWord 能够修改 1 个字符与 endWord 相同,则返回 step + 1
if ( changeWordEveryOneLetter(currentWord , endWord , queue , visited , wordSet) ) {
return step + 1 ;
}
}
step ++ ;
}
return 0 ;
}
/**
* 尝试对 currentWord 修改每一个字符,看看是不是能与 endWord 匹配
*
* @param currentWord
* @param endWord
* @param queue
* @param visited
* @param wordSet
* @return
*/
private boolean changeWordEveryOneLetter ( String currentWord , String endWord ,
Queue < String > queue , Set < String > visited , Set < String > wordSet) {
char [] charArray = currentWord . toCharArray ();
for ( int i = 0 ; i < endWord . length (); i ++ ) {
// 先保存,然后恢复
char originChar = charArray[i];
for ( char k = 'a' ; k <= 'z' ; k ++ ) {
if (k == originChar) {
continue ;
}
charArray[i] = k;
String nextWord = String . valueOf (charArray);
if ( wordSet . contains (nextWord)) {
if ( nextWord . equals (endWord)) {
return true ;
}
if ( ! visited . contains (nextWord)) {
queue . add (nextWord);
// 注意:添加到队列以后,必须马上标记为已经访问
visited . add (nextWord);
}
}
}
// 恢复
charArray[i] = originChar;
}
return false ;
}
}
解法2.双向广度优先遍历
已知目标顶点的情况下,可以分别从起点和目标顶点(终点)执行广度优先遍历,直到遍历的部分有交集。这种方式搜索的单词数量会更小一些;
也就是先从起点开始遍历,然后再从终点开始遍历。再继续遍历下一层。因为起点和终点只有一个,所以这样可以减少数目。
复制 分别用左边和右边扩散的哈希表代替单向 BFS 里的队列,它们在双向 BFS 的过程中交替使用
复制 import java . util . ArrayList ;
import java . util . Collections ;
import java . util . HashSet ;
import java . util . List ;
import java . util . Set ;
public class Solution {
public int ladderLength ( String beginWord , String endWord , List < String > wordList) {
// 第 1 步:先将 wordList 放到哈希表里,便于判断某个单词是否在 wordList 里
Set < String > wordSet = new HashSet <>(wordList);
if ( wordSet . size () == 0 || ! wordSet . contains (endWord)) {
return 0 ;
}
// 第 2 步:已经访问过的 word 添加到 visited 哈希表里
Set < String > visited = new HashSet <>();
// 分别用左边和右边扩散的哈希表代替单向 BFS 里的队列,它们在双向 BFS 的过程中交替使用
Set < String > beginVisited = new HashSet <>();
beginVisited . add (beginWord);
Set < String > endVisited = new HashSet <>();
endVisited . add (endWord);
// 第 3 步:执行双向 BFS,左右交替扩散的步数之和为所求
int step = 1 ;
while ( ! beginVisited . isEmpty () && ! endVisited . isEmpty ()) {
// 优先选择小的哈希表进行扩散,考虑到的情况更少
if ( beginVisited . size () > endVisited . size ()) {
Set < String > temp = beginVisited;
beginVisited = endVisited;
endVisited = temp;
}
// 逻辑到这里,保证 beginVisited 是相对较小的集合,nextLevelVisited 在扩散完成以后,会成为新的 beginVisited
Set < String > nextLevelVisited = new HashSet <>();
for ( String word : beginVisited) {
if ( changeWordEveryOneLetter(word , endVisited , visited , wordSet , nextLevelVisited) ) {
return step + 1 ;
}
}
// 原来的 beginVisited 废弃,从 nextLevelVisited 开始新的双向 BFS
beginVisited = nextLevelVisited;
step ++ ;
}
return 0 ;
}
/**
* 尝试对 word 修改每一个字符,看看是不是能落在 endVisited 中,扩展得到的新的 word 添加到 nextLevelVisited 里
*
* @param word
* @param endVisited
* @param visited
* @param wordSet
* @param nextLevelVisited
* @return
*/
private boolean changeWordEveryOneLetter ( String word , Set < String > endVisited ,
Set < String > visited ,
Set < String > wordSet ,
Set < String > nextLevelVisited) {
char [] charArray = word . toCharArray ();
for ( int i = 0 ; i < word . length (); i ++ ) {
char originChar = charArray[i];
for ( char c = 'a' ; c <= 'z' ; c ++ ) {
if (originChar == c) {
continue ;
}
charArray[i] = c;
String nextWord = String . valueOf (charArray);
if ( wordSet . contains (nextWord)) {
if ( endVisited . contains (nextWord)) {
return true ;
}
if ( ! visited . contains (nextWord)) {
nextLevelVisited . add (nextWord);
visited . add (nextWord);
}
}
}
// 恢复,下次再用
charArray[i] = originChar;
}
return false ;
}
}