搜索:
BFS:广度优先
每一层遍历的节点都与根节点距离相同。设 di 表示第 i 个节点与根节点的距离,推导出一个结论:对于先遍历的节点 i 与后遍历的节点 j,有 di <= dj。利用这个结论,可以求解最短路径等 最优解 问题:第一次遍历到目的节点,其所经过的路径为最短路径。应该注意的是,使用 BFS 只能求解无权图的最短路径。
在程序实现 BFS 时需要考虑以下问题:
- 队列:用来存储每一轮遍历得到的节点;
- 标记:对于遍历过的节点,应该将它标记,防止重复遍历。
1) 每一层遍历的节点都与根节点距离相同:决定了什么时候path++:就是每遍历一层之前保留queue.size,这个size就是这一层的大小
2) 什么时候置flag?已经从队列中poll了之后
3) 最优解问题:因为每次遍历的都是和根节点距离相同的节点,所以最先到达终点的一定是最短路径
1091. 二进制矩阵中的最短路径(m)(经典BFS+visited)
重点:注意只有{0}一个数的情况
public int shortestPathBinaryMatrix(int[][] grid) {
if(grid[0][0]==1 || grid[grid.length-1][grid[0].length-1]==1)
return -1;
if (grid.length == 1 && grid[0][0] == 0) {
return 1;
}
// 顺时针
int[][] xOy = {{-1,0},{-1,-1},{0,-1},{1,-1},{1,0},{1,1},{0,1},{-1,1}};
// int[][] xOy = {{1, 0}, {1, 1}, {1,-1}, {0, 1}, {0, -1}, {-1, 0},{-1, -1}, {-1, 1}};
int n = grid.length;
boolean[][] visited = new boolean[n][n];
visited[0][0] = true;
Queue<int[]> queue = new LinkedList<>();
queue.add(new int[] {0,0});
int pathLength = 0;
while (!queue.isEmpty()) {
// 这一层节点的个数
int size = queue.size();
pathLength++;
while (size-- > 0) {
int[] node = queue.poll();
int x = node[0];
int y = node[1];
// grid[x][y] = 1;
for (int[] d : xOy) {
int nx = x + d[0];
int ny = y + d[1];
if (nx == n-1 && ny == n-1) {
return pathLength+1;
}
if (nx < 0 || nx >= n || ny < 0 || ny >= n || visited[nx][ny] || grid[nx][ny] == 1){
continue;
}
visited[nx][ny] = true;
queue.add(new int[] {nx,ny});
}
}
}
return -1;
}
279. 完全平方数(m)
思路:
可以将每个整数看成图中的一个节点,如果两个整数之差为一个平方数,那么这两个整数所在的节点就有一条边。
要求解最小的平方数数量,就是求解从节点 n 到节点 0 的最短路径。
本题也可以用动态规划求解,在之后动态规划部分中会再次出现。
完全平方数的规律:1,4,9,16.。。中间刚好相差(3,5,7,9....),即n^2-(n-1)^2 = 2n-1
public int numSquares(int n) {
List<Integer> list = generateSquares(n);
Queue<Integer> queue = new LinkedList<>();
boolean[] visited = new boolean[n+1];
queue.add(n);
visited[n] = true;
int level = 0;
while (!queue.isEmpty()) {
int size = queue.size();
level++;
while (size-- > 0) {
int cur = queue.poll();
for (int i: list
) {
int next = cur - i;
// 因为LinkedList是按顺序读,我们存的时候就是(1,4,9...)
if (next < 0) {
break;
}
if (next == 0) {
return level;
}
visited[next] = true;
queue.add(next);
}
}
}
return n;
}
/**
* 生成小于 n 的平方数序列
* @return 1,4,9,...
*/
private List<Integer> generateSquares(int n) {
List<Integer> squares = new ArrayList<>();
int square = 1;
int diff = 3;
while (square <= n) {
squares.add(square);
square += diff;
diff += 2;
}
return squares;
}
127. 单词接龙(m)(重点!!)
重点:
1. 如何比较两个String中是不是有一位字符不同?
所以需要建立一个图(graph),单词是节点,如果两个单词之间只有一位不同则有一条边
2. 如何做visited[]标记数组?因为wordList中是String类型,如何按下标记录标记?
建立graph的时候使用下标而不是单词作为节点
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
if (!wordList.contains(endWord)) {
return 0;
}
// 便于建图
wordList.add(beginWord);
// Queue<String> queue = new LinkedList<>();
Queue<Integer> queue = new LinkedList<>();
List<Integer>[] graph = buildGraph(wordList);
int n = wordList.size();
boolean[] visited = new boolean[n+1];
int end = 0;
for (int i = 0 ; i < wordList.size() ;i ++) {
if (!endWord.equals(wordList.get(i))) {
end++;
}
else {
break;
}
}
queue.add(n-1);
visited[n-1] = true;
int step = 0;
while (!queue.isEmpty()) {
int size = queue.size();
step++;
while (size-- > 0) {
int cur = queue.poll();
// graph[cur]是个list<Integer>
for (int next : graph[cur]) {
if (next == end) {
return step+1;
}
if (visited[next]) {
continue;
}
visited[next] = true;
queue.add(next);
}
}
}
return 0;
}
public boolean isConnected(String s1,String s2) {
int diffCnt = 0;
for (int i = 0; i < s1.length() && diffCnt <= 1; i++) {
if (s1.charAt(i) != s2.charAt(i)) {
diffCnt++;
}
}
return diffCnt == 1;
}
public List<Integer>[] buildGraph(List<String> wordList) {
int n = wordList.size();
List<Integer>[] graph = new List[n];
for (int i = 0 ; i < n ; i++) {
graph[i] = new LinkedList<>();
for (int j = 0 ; j < n; j++) {
if (isConnected(wordList.get(i),wordList.get(j))) {
graph[i].add(j);
}
}
}
return graph;
}
DFS(深度优先,重点!!!)
从一个节点出发,使用 DFS 对一个图进行遍历时,能够遍历到的节点都是从初始节点可达的,DFS 常用来求解这种 可达性 问题。
在程序实现 DFS 时需要考虑以下问题:
- 栈:用栈来保存当前节点信息,当遍历新节点返回时能够继续遍历当前节点。可以使用递归栈。
- 标记:和 BFS 一样同样需要对已经遍历过的节点进行标记。
695. 岛屿的最大面积(m)
重点:为了方便起见,本题没有采用visited数组,而是直接将遍历过的置为0。注意找到什么情况需要恢复visited数组,什么情况不需要(本题不需要,剑指offer66题机器人路径规划也不需要),像剑指offer中65题找矩阵中的路径就需要(因为是要找序列)
int[][] direction = {{0,1},{0,-1},{1,0},{-1,0}};
public int maxAreaOfIsland(int[][] grid) {
if (grid == null || grid.length <= 0) {
return 0;
}
// 几行
int m = grid.length;
// 几列
int n = grid[0].length;
int maxArea = 0;
for (int i = 0 ; i < m ; i++) {
for (int j = 0 ; j < n; j++) {
maxArea = Math.max(maxArea,dfs(grid,i,j));
}
}
return maxArea;
}
private int dfs(int[][] grid, int i, int j) {
if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] == 0) {
return 0;
}
// 本来应该是visited数组来标记,但是这里为了方便直接将访问过的置为0
grid[i][j] = 0;
int area = 1;
for (int[] d : direction) {
area += dfs(grid,i+d[0],j+d[1]);
}
return area;
}
200. 岛屿的数量(m)
重点:矩阵看做有向图,就是有向图求连通分量的问题
和上题差不多,但是这里不需要统计大小,所以重点是遍历过的都改为0即可,所以直接用一个返回值为void的dfs。
int[][] direction = {{0,1},{0,-1},{1,0},{-1,0}};
public int numIslands(char[][] grid) {
...
int count = 0;
for (int i = 0; i < m ;i ++) {
for (int j = 0; j < n ;j++) {
if (grid[i][j] != '0')
{
dfs(grid,i,j);
count++;
}
}
}
return count;
}
private void dfs(char[][] grid, int i, int j) {
if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] == '0') {
return ;
}
grid[i][j] = '0';
for (int[] d : direction) {
dfs(grid,i+d[0],j+d[1]);
}
}
547. 朋友圈(m)
重点:矩阵看做无向图
就是无向图求连通分量的问题
不用两层循环,一层即可,因为必有m[i][j]=m[j][i]
就是先找0的朋友,假设1是,再找1的朋友。。。
public int findCircleNum(int[][] M) {
if (M == null || M.length <= 0) {
return 0;
}
int count = 0;
boolean[] visited = new boolean[M.length];
for (int i = 0; i < M.length; i++) {
if (!visited[i]) {
dfs(M,i,visited);
count++;
}
}
return count;
}
private void dfs(int[][] M,int i,boolean[] visited) {
visited[i] = true;
for (int j = 0; j < M.length ; j++) {
if (M[i][j] == 1 && !visited[j]) {
dfs(M,j,visited);
}
}
}
130. 被围绕的区域(m)
重点:找到边界和与边界相连的O,这些O不会被更改(因为边界处的不可能被包围),把这些O做上特殊标记#,然后剩下的O全部改为X即可。
找边界和边界相连的,相当于求最大岛屿面积,只是只能是有边界的地方,即求#的最大面积
417. 太平洋和大西洋水流问题(m)
重点:
1. 思路:逆流,看能不能从海洋流回去
2. Arrays.asList(),该方法是将数组转化为list,但是长度不可变(没有add、remove等方法)
List<List<Integer>> res = new ArrayList<>();
res.add(Arrays.asList(i, j));
回溯:
Backtracking(回溯)属于 DFS。
- 普通 DFS 主要用在 可达性问题 ,这种问题只需要执行到特点的位置然后返回即可。
- 而 Backtracking 主要用于求解 排列组合 问题,例如有 { 'a','b','c' } 三个字符,求解所有由这三个字符排列得到的字符串,这种问题在执行到特定的位置返回之后还会继续执行求解过程。
因为 Backtracking 不是立即返回,而要继续求解,因此在程序实现时,需要注意对元素的标记问题:
- 在访问一个新元素进入新的递归调用时,需要将新元素标记为已经访问,这样才能在继续递归调用时不用重复访问该元素;
- 但是在递归返回时,需要将元素标记为未访问,因为只需要保证在一个递归链中不同时访问一个元素,可以访问已经访问过但是不在当前递归链中的元素,所以标记可能要复位。
[39,40],[46,47],[78,90]都是相同问题但是存在重复与否的差异,解题方法能够大同小异,仔细记住。
17. 电话号码的字母组合(m)
backtrack(digits,0,"");
private void backtrack(String digits, int index, String s) {
if (index == digits.length()) {
res.add(s);
return;
}
char ch = digits.charAt(index);
String letters = KEYS[ch-'0'];
for (int i = 0 ; i < letters.length(); i++) {
backtrack(digits,index+1,s+letters.charAt(i));
}
return;
}
93. 复原IP地址(m)
我们要知道IP
的格式,IP地址总共有四段,每一段可能有一位,两位或者三位,范围是[0, 255]
当某段是三位时,我们要判断其是否越界(>255),还有一点很重要的是,当只有一位时,0可以成某一段,如果有两位或三位时,像 00, 01, 001, 011, 000等都是不合法的,所以我们还是需要有一个判定函数来判断某个字符串是否合法。
我们用k来表示当前分的段数,如果k = 4,则表示三个点已经加入完成,四段已经形成,若这时字符串刚好为空,则将当前分好的结果保存。若k != 4, 则对于每一段,我们分别用一位,两位,三位来尝试,分别判断其合不合法,如果合法,则调用递归继续分剩下的字符串,最终和求出所有合法组合
79. 单词搜索(m)——经典题
经典回溯!!!!有visited,有复位
private final static int[][] direction = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
private int m;
private int n;
public boolean exist(char[][] board, String word) {
if (word == null) {
return true;
}
if (board == null || board.length <= 0 || board[0].length <= 0) {
return false;
}
m = board.length;
n = board[0].length;
boolean[][] visited = new boolean[m][n];
for (int i = 0 ; i < m ; i++) {
for (int j = 0 ;j < n; j++) {
if (backTracking(0,i,j,visited,board,word)) {
return true;
}
}
}
return false;
}
private boolean backTracking(int curLen, int i, int j, boolean[][] visited, char[][] board, String word) {
if (curLen == word.length()) {
return true;
}
if (i < 0 || i >= m || j < 0 || j >= n || visited[i][j] || board[i][j] != word.charAt(curLen)) {
return false;
}
visited[i][j] = true;
for (int[] d : direction) {
if (backTracking(curLen+1,i+d[0],j+d[1],visited,board,word)) {
return true;
}
}
// 标记一定要复位(回溯)
visited[i][j] = false;
return false;
}
257. 二叉树的所有路径(e)——经典题
public List<String> binaryTreePaths(TreeNode root) {
List<String> ans = new ArrayList<>();
if (root == null) {
return ans;
}
List<Integer> path = new ArrayList<>();
backtracking(root,path,ans);
return ans;
}
private void backtracking(TreeNode root, List<Integer> path, List<String> ans) {
if (root == null){
return;
}
path.add(root.val);
if (root.left == null && root.right == null) {
StringBuilder tmp = new StringBuilder();
for (int i = 0; i < path.size(); i++) {
tmp.append(path.get(i));
if (i != path.size()-1) {
tmp.append("->");
}
}
ans.add(tmp.toString());
}
backtracking(root.left,path,ans);
backtracking(root.right,path,ans);
// 回溯
path.remove(path.size()-1);
}
}
46. 全排列(m)
public List<List<Integer>> permute(int[] nums) {
List<Integer> list = new ArrayList<>();
List<List<Integer>> res = new ArrayList<>();
boolean[] visited = new boolean[nums.length];
backtracking(nums,list,res,visited);
return res;
}
private void backtracking(int[] nums, List<Integer> list, List<List<Integer>> res, boolean[] visited) {
if (list.size() == nums.length) {
// 必须要重新new一个list
res.add(new ArrayList<>(list));
return;
}
for (int i = 0 ; i < nums.length ;i ++) {
if (visited[i]) {
continue;
}
visited[i] = true;
list.add(nums[i]);
backtracking(nums,list,res,visited);
// 回溯
list.remove(list.size()-1);
visited[i] = false;
}
}
47. 全排列2(m)
数组中可能有重复,但是最后的结果不能有重复的组合
重点:两步走1. 排序;2. !visited[i]剪枝
public List<List<Integer>> permuteUnique(int[] nums) {
List<Integer> list = new ArrayList<>();
List<List<Integer>> res = new ArrayList<>();
boolean[] visited = new boolean[nums.length];
// 修改1:排序
Arrays.sort(nums);
backtracking(nums,list,res,visited);
return res;
}
private void backtracking(int[] nums, List<Integer> list, List<List<Integer>> res, boolean[] visited) {
if (list.size() == nums.length) {
res.add(new ArrayList<>(list));
return;
}
for (int i = 0;i < nums.length;i ++) {
if (visited[i]) {
continue;
}
// 修改2:在 used[i - 1] 刚刚被撤销的时候剪枝,说明接下来会被选择,搜索一定会重复,故"剪枝"
if (i > 0 && nums[i-1] == nums[i] && !visited[i-1]) {
continue;
}
visited[i] = true;
list.add(nums[i]);
backtracking(nums,list,res,visited);
visited[i] = false;
list.remove(list.size()-1);
}
}
77. 组合(m)
重点:i的遍历不需要到n,只需要到n-k+1,用来剪枝,也是为了防止重复
这是一个回溯法函数,它将第一个添加到组合中的数和现有的组合作为参数。 backtrack(first, curr)
若组合完成- 添加到输出中。
遍历从 first t到 n的所有整数。
将整数 i 添加到现有组合 curr中。
继续向组合中添加更多整数 :
backtrack(i + 1, curr).
将 i 从 curr中移除,实现回溯。
public List<List<Integer>> combine(int n, int k) {
List<Integer> list = new ArrayList<>();
List<List<Integer>> res = new ArrayList<>();
backtracking(n,k,1,list,res);
return res;
}
private void backtracking(int n, int k, int start, List<Integer> list, List<List<Integer>> res) {
if (list.size() == k) {
res.add(new ArrayList<>(list));
}
for (int i = start; i <= n; i++) {
list.add(i);
backtracking(n,k,i+1,list,res);
list.remove(list.size()-1);
}
}
39. 组合求和(m)
剑指中和为S的序列的变种
for (int i = start; i < candidates.length; i++) {
if (candidates[i] <= target) {
list.add(candidates[i]);
// 不能是i+1,因为可以重复
backtracking(list,res,i,target-candidates[i],candidates);
list.remove(list.size()-1);
}
}
40. 含有相同元素的组合求和(m)
看47题
“解集不能包含重复的组合”,就提示我们得对数组先排个序(“升序”或者“降序”均可,下面示例中均使用“升序”)。
“candidates 中的每个数字在每个组合中只能使用一次”: 那就按照顺序依次减去数组中的元素(target-camdidate[i]),递归求解即可:遇到 0 就结算且回溯,遇到负数也回溯。
candidates 中的数字可以重复: 遇到(i != 0 && candidates[i-1] == candidates[i] && !visited[i-1]) 就continue(剪枝)
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<Integer> list = new ArrayList<>();
List<List<Integer>> res = new ArrayList<>();
boolean[] visited = new boolean[candidates.length];
// 排序
Arrays.sort(candidates);
backtracking(list, res, 0, visited, target, candidates);
return res;
}
private void backtracking(List<Integer> list, List<List<Integer>> res, int start, boolean[] visited, int target, int[] candidates) {
if (target == 0 ){
res.add(new ArrayList<>(list));
return;
}
for (int i = start;i < candidates.length; i++) {
if (i != 0 && candidates[i-1] == candidates[i] && !visited[i-1]) {
continue;
}
if (candidates[i] <= target) {
list.add(candidates[i]);
visited[i] = true;
backtracking(list,res,i+1,visited,target-candidates[i],candidates);
visited[i] = false;
list.remove(list.size()-1);
}
}
}
78. 子集(m)
public List<List<Integer>> subsets(int[] nums) {
List<Integer> temSub = new ArrayList<>();
List<List<Integer>> res = new ArrayList<>();
boolean[] visited = new boolean[nums.length];
for (int size = 0; size <= nums.length; size++) {
backtracking(nums,temSub,size,0,res,visited);
}
return res;
}
private void backtracking(int[] nums, List<Integer> temSub, int size,int start,List<List<Integer>> res, boolean[] visited) {
if (temSub.size() == size) {
res.add(new ArrayList<>(temSub));
return;
}
for (int i = start; i < nums.length; i ++) {
if (visited[i]) {
continue;
}
visited[i] = true;
temSub.add(nums[i]);
backtracking(nums,temSub,size,i+1,res,visited);
visited[i] = false;
temSub.remove(temSub.size()-1);
}
}
90. 子集2
有重复元素
加上两步:1.排序;2.!visited[i-1]