单词接龙
题目描述
字典 wordList 中从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列:
- 序列中第一个单词是 beginWord 。
- 序列中最后一个单词是 endWord 。
- 每次转换只能改变一个字母。
- 转换过程中的中间单词必须是字典 wordList 中的单词。
- 给你两个单词 beginWord 和 endWord 和一个字典 wordList ,找到从 beginWord 到 endWord 的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0。
示例 1:
- 输入:beginWord = “hit”, endWord = “cog”, wordList = [“hot”,“dot”,“dog”,“lot”,“log”,“cog”]
- 输出:5
- 解释:一个最短转换序列是 “hit” -> “hot” -> “dot” -> “dog” -> “cog”, 返回它的长度 5。
示例 2:
- 输入:beginWord = “hit”, endWord = “cog”, wordList = [“hot”,“dot”,“dog”,“lot”,“log”]
- 输出:0
- 解释:endWord “cog” 不在字典中,所以无法进行转换。
解题思路
以示例1为例,从这个图中可以看出 hit 到 cog的路线,不止一条,有三条,一条是最短的长度为5,两条长度为6。
本题只需要求出最短路径的长度就可以了,不用找出路径。
所以这道题要解决两个问题:
- 图中的线是如何连在一起的
- 起点和终点的最短路径长度
首先题目中并没有给出点与点之间的连线,而是要我们自己去连,条件是字符只能差一个,所以判断点与点之间的关系,要自己判断是不是差一个字符,如果差一个字符,那就是有链接。
然后就是求起点和终点的最短路径长度,这里无向图求最短路,广搜最为合适,广搜只要搜到了终点,那么一定是最短的路径。因为广搜就是以起点中心向四周扩散的搜索。
本题如果用深搜,会比较麻烦,要在到达终点的不同路径中选则一条最短路。 而广搜只要达到终点,一定是最短路。
另外需要有一个注意点:
- 本题是一个无向图,需要用标记位,标记着节点是否走过,否则就会死循环!
- 本题给出集合是数组型的,可以转成set结构,查找更快一些
java代码如下:(详细注释)
广搜 标记位
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
HashSet<String> wordSet = new HashSet<>(wordList);// 将 List 转换成 HashSet,提高查询速度
// 如果 endWord 没有在 wordSet 中出现,直接返回 0
if (!wordSet.contains(endWord))
return 0;
// 记录单词是否访问过
Map<String, Integer> visitMap = new HashMap<>(); // <word, 查询到这个 word 路径长度>
// 初始化队列
Queue<String> queue = new LinkedList<>();
queue.add(beginWord);
// 初始化 visitMap
visitMap.put(beginWord, 1);
while (!queue.isEmpty()) {
String word = queue.poll();
int path = visitMap.get(word); // 这个 word 的路径长度
char[] wordChars = word.toCharArray();
for (int i = 0; i < wordChars.length; i++) {
char originalChar = wordChars[i];
for (char c = 'a'; c <= 'z'; c++) {
if (c == originalChar)
continue;
wordChars[i] = c;
String newWord = new String(wordChars);// 用一个新单词替换word,因为每次置换一个字母
if (newWord.equals(endWord))
return path + 1; // 找到了 endWord,返回 path+1
// wordSet 中出现了 newWord,并且 newWord 没有被访问过
if (wordSet.contains(newWord) && !visitMap.containsKey(newWord)) {
// 添加访问信息
visitMap.put(newWord, path + 1);
queue.add(newWord);
}
}
wordChars[i] = originalChar; // 恢复原单词
}
}
return 0;
}
}
参考解题
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
HashSet<String> wordSet = new HashSet<>(wordList); //转换为hashset 加快速度
if (wordSet.size() == 0 || !wordSet.contains(endWord)) { //特殊情况判断
return 0;
}
Queue<String> queue = new LinkedList<>(); //bfs 队列
queue.offer(beginWord);
Map<String, Integer> map = new HashMap<>(); //记录单词对应路径长度
map.put(beginWord, 1);
while (!queue.isEmpty()) {
String word = queue.poll(); //取出队头单词
int path = map.get(word); //获取到该单词的路径长度
for (int i = 0; i < word.length(); i++) { //遍历单词的每个字符
char[] chars = word.toCharArray(); //将单词转换为char array,方便替换
for (char k = 'a'; k <= 'z'; k++) { //从'a' 到 'z' 遍历替换
chars[i] = k; //替换第i个字符
String newWord = String.valueOf(chars); //得到新的字符串
if (newWord.equals(endWord)) { //如果新的字符串值与endWord一致,返回当前长度+1
return path + 1;
}
if (wordSet.contains(newWord) && !map.containsKey(newWord)) { //如果新单词在set中,但是没有访问过
map.put(newWord, path + 1); //记录单词对应的路径长度
queue.offer(newWord);//加入队尾
}
}
}
}
return 0; //未找到
}
841.钥匙和房间
题目描述
有 N 个房间,开始时你位于 0 号房间。每个房间有不同的号码:0,1,2,…,N-1,并且房间里可能有一些钥匙能使你进入下一个房间。
在形式上,对于每个房间 i 都有一个钥匙列表 rooms[i],每个钥匙 rooms[i] [j] 由 [0,1,…,N-1] 中的一个整数表示,其中 N = rooms.length。 钥匙 rooms[i] [j] = v 可以打开编号为 v 的房间。
最初,除 0 号房间外的其余所有房间都被锁住。
你可以自由地在房间之间来回走动。
如果能进入每个房间返回 true,否则返回 false。
示例 1:
- 输入: [[1],[2],[3],[]]
- 输出: true
- 解释: 我们从 0 号房间开始,拿到钥匙 1。 之后我们去 1 号房间,拿到钥匙 2。 然后我们去 2 号房间,拿到钥匙 3。 最后我们去了 3 号房间。 由于我们能够进入每个房间,我们返回 true。
示例 2:
- 输入:[[1,3],[3,0,1],[2],[0]]
- 输出:false
- 解释:我们不能进入 2 号房间。
解题思路
本题其实给我们是一个有向图, 意识到这是有向图很重要!
图中给我的两个示例: [[1],[2],[3],[]]
[[1,3],[3,0,1],[2],[0]]
,画成对应的图如下:
我们可以看出图1的所有节点都是链接的,而图二中,节点2 是孤立的。
这就很容易让我们想起岛屿问题,只要发现独立的岛,就是不能进入所有房间。
此时也容易想到用并查集的方式去解决。
但本题是有向图,在有向图中,即使所有节点都是链接的,但依然不可能从0出发遍历所有边。 给大家举一个例子:
图3:[[5], [], [1, 3], [5]] ,如图:
在图3中,大家可以发现,节点0只能到节点5,然后就哪也去不了了。
所以本题是一个有向图搜索全路径的问题。 只能用深搜(DFS)或者广搜(BFS)来搜。
关于DFS的理论,如果大家有困惑,可以先看我这篇题解: DFS理论基础
以下dfs分析 大家一定要仔细看,本题有两种dfs的解法,很多题解没有讲清楚。 看完之后 相信你对dfs会有更深的理解。
深搜三部曲:
1.确认递归函数,参数
需要传入二维数组rooms来遍历地图,需要知道当前我们拿到的key,以至于去下一个房间。
同时还需要一个数组,用来记录我们都走过了哪些房间,这样好知道最后有没有把所有房间都遍历的,可以定义一个一维数组。
所以 递归函数参数如下:
// key 当前得到的可以
// visited 记录访问过的房间
void dfs(List<List<Integer>> rooms, int key, boolean[] visited) {
2.确认终止条件
遍历的时候,什么时候终止呢?
这里有一个很重要的逻辑,就是在递归中,我们是处理当前访问的节点,还是处理下一个要访问的节点。
这决定 终止条件怎么写。
首先明确,本题中什么叫做处理,就是 visited数组来记录访问过的节点,该节点默认 数组里元素都是false,把元素标记为true就是处理 本节点了。
如果我们是处理当前访问的节点,当前访问的节点如果是 true ,说明是访问过的节点,那就终止本层递归,如果不是true,我们就把它赋值为true,因为这是我们处理本层递归的节点。
代码就是这样:
// 写法一:处理当前访问的节点
void dfs(List<List<Integer>> rooms, int key, boolean[] visited) {
if (visited[key]) { // 本层递归是true,说明访问过,立刻返回
return;
}
visited[key] = true; // 给当前遍历的节点赋值true
List<Integer> keys = rooms.get(key);
for (int nextKey : keys) {
// 深度优先搜索遍历
dfs(rooms, nextKey, visited);
}
}
如果我们是处理下一层访问的节点,而不是当前层。那么就要在 深搜三部曲中第三步:处理目前搜索节点出发的路径的时候对 节点进行处理。
这样的话,就不需要终止条件,而是在 搜索下一个节点的时候,直接判断 下一个节点是否是我们要搜的节点。
代码就是这样的:
// 写法二:处理下一个要访问的节点
void dfs(List<List<Integer>> rooms, int key, boolean[] visited) {
// 这里没有终止条件,而是在处理下一层节点的时候来判断
List<Integer> keys = rooms.get(key);
for (int nextKey : keys) {
if (!visited[nextKey]) { // 处理下一层节点,判断是否要进行递归
visited[nextKey] = true;
dfs(rooms, nextKey, visited);
}
}
}
可以看出,如果看待 我们要访问的节点,直接决定了两种不一样的写法,很多录友对这一块很模糊,可能做过这道题,但没有思考到这个维度上。
3.处理目前搜索节点出发的路径
其实在上面,深搜三部曲 第二部,就已经讲了,因为终止条件的两种写法, 直接决定了两种不一样的递归写法。
这里还有细节:
看上面两个版本的写法中, 好像没有发现回溯的逻辑。
我们都知道,有递归就有回溯,回溯就在递归函数的下面, 那么之前我们做的dfs题目,都需要回溯操作,例如:797.所有可能的路径, 为什么本题就没有回溯呢?
代码中可以看到dfs函数下面并没有回溯的操作。
此时就要在思考本题的要求了,本题是需要判断 0节点是否能到所有节点,那么我们就没有必要回溯去撤销操作了,只要遍历过的节点一律都标记上。
那什么时候需要回溯操作呢?
当我们需要搜索一条可行路径的时候,就需要回溯操作了,因为没有回溯,就没法“调头”, 如果不理解的话,去看我写的 797.所有可能的路径 的题解。
以上分析完毕,DFS整体实现java代码如下:
// 写法一:处理当前访问的节点
class Solution {
private void dfs(List<List<Integer>> rooms, int key, boolean[] visited) {
if (visited[key]) {
return;
}
visited[key] = true;
List<Integer> keys = rooms.get(key);
for (int nextKey : keys) {
// 深度优先搜索遍历
dfs(rooms, nextKey, visited);
}
}
public boolean canVisitAllRooms(List<List<Integer>> rooms) {
boolean[] visited = new boolean[rooms.size()];
dfs(rooms, 0, visited);
// 检查是否都访问到了
for (boolean visit : visited) {
if (!visit)
return false;
}
return true;
}
}
//写法二:处理下一个要访问的节点
class Solution {
private void dfs(List<List<Integer>> rooms, int key, boolean[] visited) {
List<Integer> keys = rooms.get(key);
for (int nextKey : keys) {
if (!visited[nextKey]) {
visited[nextKey] = true;
dfs(rooms, nextKey, visited);
}
}
}
public boolean canVisitAllRooms(List<List<Integer>> rooms) {
boolean[] visited = new boolean[rooms.size()];
visited[0] = true; // 0 节点是出发节点,一定被访问过
dfs(rooms, 0, visited);
// 检查是否都访问到了
for (boolean visit : visited) {
if (!visit)
return false;
}
return true;
}
}
岛屿的周长
给定一个 row x col 的二维网格地图 grid ,其中:grid[i] [j] = 1 表示陆地, grid[i] [j] = 0 表示水域。
网格中的格子 水平和垂直 方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。
岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。计算这个岛屿的周长。
- 输入:grid = [[0,1,0,0],[1,1,1,0],[0,1,0,0],[1,1,0,0]]
- 输出:16
- 解释:它的周长是上面图片中的 16 个黄色的边
示例 2:
- 输入:grid = [[1]]
- 输出:4
示例 3:
- 输入:grid = [[1,0]]
- 输出:4
解法一:
遍历每一个空格,遇到岛屿,计算其上下左右的情况,遇到水域或者出界的情况,就可以计算边了。
如图:
java代码如下:(详细注释)
class Solution {
int[][] direction = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}};
public int islandPerimeter(int[][] grid) {
int result = 0;
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[0].length; j++) {
if (grid[i][j] == 1) {
for (int k = 0; k < 4; k++) { // 上下左右四个方向
int x = i + direction[k][0];
int y = j + direction[k][1]; // 计算周边坐标x,y
if (x < 0 // i在边界上
|| x >= grid.length // i在边界上
|| y < 0 // j在边界上
|| y >= grid[0].length // j在边界上
|| grid[x][y] == 0) { // x,y位置是水域
result++;
}
}
}
}
}
return result;
}
}
解法二:
计算出总的岛屿数量,因为有一对相邻两个陆地,边的总数就减2,那么在计算出相邻岛屿的数量就可以了。
result = 岛屿数量 * 4 - cover * 2;
如图:
java代码如下:(详细注释)
class Solution {
public int islandPerimeter(int[][] grid) {
int sum = 0; // 陆地数量
int cover = 0; // 相邻数量
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[0].length; j++) {
if (grid[i][j] == 1) {
sum++;
// 统计上边相邻陆地
if (i - 1 >= 0 && grid[i - 1][j] == 1)
cover++;
// 统计左边相邻陆地
if (j - 1 >= 0 && grid[i][j - 1] == 1)
cover++;
// 为什么没统计下边和右边? 因为避免重复计算
}
}
}
return sum * 4 - cover * 2;
}
}
ps:部分图片和代码来自代码随想录和Leetcode官网