题目
CC21 包围区域
描述
现在有一个仅包含‘X’和‘O’的二维板,请捕获所有的被‘X’包围的区域
捕获一个被包围区域的方法是将被包围区域中的所有‘O’变成‘X’
例如:
X X X X
X O O X
X X O X
O X X X
执行完你给出的函数以后,这个二维板应该变成:
X X X X
X X X X
X X X X
O X X X
思考
这道题,有两个思路
思路1:
我们找到处在边缘的 O,和这些 O 相连的 O ,就都不是符合条件的 O
我们将它们排除之后,剩下的 O 就是符合条件的 O,我们再将这些符合条件的 O 改成 X
思路2:
对于所有 O,直接进行 DFS,如果其子遍历符合条件,说明当前位置也符合条件,将当前位置修改后,返回
基于思路二,我写出的代码如下:
public void solve(char[][] board) {
int R = board.length;
if (R==0) return;
int C = board[0].length;
if (C==0) return;
boolean[][] visited = new boolean[R][C];
for (int i = 0; i <R; i++) {
for (int j = 0; j < C; j++) {
// 当前位置为 O 且还没有被访问过
// (没访问是因为我们之前的 dfs 操作,可能已经让改点被访问过了)
if (board[i][j]=='O' && !visited[i][j]) {
dfs(visited,board,i,j);
}
}
}
}
public boolean dfs(boolean[][]visited,char[][]board,int r,int c) {
int R = board.length;
int C = board[0].length;
// 如果 O 界限的边界触碰到 board 边缘,说明不符合条件
if (r<0 || r>=R || c<0 || c>=C) return false;
// 如果是 X 或者是已经访问过了,那么认为这个位置是可行的
if (board[r][c]=='X' || visited[r][c]) return true;
visited[r][c]=true;
int[] rs = {0,0,1,-1};
int[] cs = {1,-1,0,0};
boolean res = true;
for (int i = 0; i < 4; i++) {
//这里要保证所有方向都符合条件
res = res && dfs(visited,board,r+rs[i],c+cs[i]);
}
if (res) {
board[r][c]='X';
}
return res;
}
however ,这段代码跑不通力扣的所有用例。
比如说下面这个用例
我们可以发现,这个位置,是某次 dfs 的最底层
依照我们的代码逻辑
// 如果 O 界限的边界触碰到 board 边缘,说明不符合条件
if (r<0 || r>=R || c<0 || c>=C) return false;
// 如果是 X 或者是已经访问过了,那么认为这个位置是可行的
if (board[r][c]=='X' || visited[r][c]) return true;
visited[r][c]=true;
int[] rs = {0,0,1,-1};
int[] cs = {1,-1,0,0};
boolean res = true;
for (int i = 0; i < 4; i++) {
//这里要保证所有方向都符合条件
res = res && dfs(visited,board,r+rs[i],c+cs[i]);
}
if (res) {
board[r][c]='X';
}
我们可以发现,这个位置是完全符合的,我们便会将其修改为 X,然后返回
但是,这个位置的父位置不符合,所以按理来说,这个位置不应该被修改
西卡西 dfs 没法做到反向回溯,它被修改了,就是被修改了
变相说明,这个方法行不通
所以只能考虑思路 1
题解
这里只有思路1可以解决
// 这里我们采用思路2
// 对所有处于边间的 O 进行深度遍历
public void solve(char[][] board) {
if (board==null) return;
int R = board.length;
if (R==0) return;
int C = board[0].length;
boolean[][] visited = new boolean[R][C];
for (int i = 0; i < R; i++) {
for (int j = 0; j < C; j++) {
char currSim = board[i][j];
if (currSim=='X') visited[i][j]=true;
else {
// 发现了处于边界的 O
if (i==0 || i==R-1 || j==0 || j==C-1) {
dfs(visited,board,i,j);
}
}
}
}
// 将符合要求的 O,修改为 X
for (int i = 0; i < R; i++) {
for (int j = 0; j < C; j++) {
if (!visited[i][j]) {
board[i][j]='X';
}
}
}
}
// 深度遍历,标记所有不符合要求的 O 的位置
public void dfs(boolean[][] visited,char[][]board,int r,int c) {
int R = board.length;
int C = board[0].length;
if (r<0 || r>=R || c<0 || c>=C) return;
// dfs 终点
if (board[r][c]=='X') return;
if (visited[r][c]) return;
visited[r][c]=true;
int[] rs = {0,0,1,-1};
int[] cs = {1,-1,0,0};
for (int i = 0; i < 4; i++) {
dfs(visited,board,r+rs[i],c+cs[i]);
}
}
小结
这道题告诉我们,有时候正过来思考问题发现行不通,那就需要反过来思考一下
还有一点就是,牛客上跑题发现没法AC,可以去力扣上找一样的题再跑一次,因为力扣会显示跑不通的用例
CC22 二叉树根节点到叶子节点的所有路径和
描述
给定一个仅包含数字 0−9 的二叉树,每一条从根节点到叶子节点的路径都可以用一个数字表示。
例如根节点到叶子节点的一条路径是1→2→3,那么这条路径就用 123 来代替。
找出根节点到叶子节点的所有路径表示的数字之和
例如下面这棵树:
这颗二叉树一共有两条路径,
根节点到叶子节点的路径 1→2 用数字 12 代替
根节点到叶子节点的路径 1→3 用数字 13 代替
所以答案为 12+13=25
思考
这里可以说是一个经典的回溯题目了
我们使用 dfs ,存储每个节点的数字,当到达叶子的时候,存储路径的数字
然后使用回溯的方式,遍历另一个分支
结束之后,我们将所有存储的路径的值进行相加
题解
public int sumNumbers (TreeNode root) {
List<String> res = new ArrayList<>();
dfs(root,new StringBuilder(),res);
//System.out.println(res);
int sum = 0;
for (String re : res) {
sum+=Integer.parseInt(re);
}
return sum;
}
public void dfs(TreeNode root, StringBuilder track, List<String> res) {
if (root==null) return;
if (root.left==null && root.right==null) {
track.append(root.val);
res.add(track.toString());
removeLast(track);
return;
}
track.append(root.val);
if (root.left!=null) dfs(root.left,track,res);
if (root.right!=null) dfs(root.right,track,res);
// 回溯
removeLast(track);
}
// 将 StringBuilder 的最后一个元素移除,方便后面的回溯操作
private static void removeLast(StringBuilder sb) {
sb.replace(sb.length()-1,sb.length(),"");
}
小结
不难
CC23 最长的连续元素序列长度
描述
给定一个无序的整数类型数组,求最长的连续元素序列的长度。
例如:
给出的数组为[1000, 4, 2000, 1, 3, 2],
最长的连续元素序列为[1, 2, 3, 4]. 返回这个序列的长度:4
你需要给出时间复杂度在O(n)之内的算法
思考
思路1:
看到这个问题,我们第一反应,就是先排序,然后再遍历
虽然排序可以到 nlogn 的复杂度,但是遍历的复杂度是 n^2
思路2:
我们可以对上面的做法进行优化
我们可以发现,上面的操作有许多重复的判断
比如 1,2,3,4,7;我们 判断从 1 开始,长度为四
但是下一次,我们又从 2 开始了,这是完全没有必要的,我们完全可以从 7 开始判断,这就要求我们记录之前判断的长度,然后跳着进行判断,这样可以大幅优化时间
思路3:
However,题目要求时间复杂度在 O(n)了,我们上面的方法因为用到了排序,再怎么优化都到不了 O(n)
所以,我们必须另辟蹊径
这里,我们可以使用 set 来存储我们的数
题解
思路3
public int longestConsecutive (int[] num) {
Set<Integer> set = new HashSet<>();
for (int n : num) {
set.add(n);
}
int res = Integer.MIN_VALUE;
for (Integer n : set) {
// 保证该元素之前的元素没有进行过判断
// 避免重复劳动
int count = 1;
if (!set.contains(n-1)) {
while (set.contains(++n)) count++;
}
res = Math.max(res,count);
}
return res;
}
小结
题目不难,但要持续思考怎么进行优化