题目
地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?
示例 1: 输入:m = 2, n = 3, k = 1 输出:3 示例 2:
输入:m = 3, n = 1, k = 0 输出:1
提示: 1 <= n,m <= 100 0 <= k <= 20
当作回溯算法
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
Main main = new Main();
main.movingCount(7, 2, 3);
}
int m = 0;
int n = 0;
int k = 0;
List<int []> finalList = new ArrayList<>();
public int movingCount(int m, int n, int k) {
this.m = m;
this.n = n;
this.k = k;
List<int []> pathList = new ArrayList<>();
pathList.add(new int[] {0, 0});
huisu(pathList, new int[] {0, 0});
return finalList.size();
}
public void huisu(List<int []> pathList, int [] position) {
// 由position的到选择列表, 同时剔除非法选择
List<int []> optionList = getOptionList(position, pathList);
if (optionList.size() == 0) {
for (int [] path: pathList) {
if (!contains(finalList, path)) {
finalList.add(path);
}
}
return;
}
for (int i = 0; i < optionList.size(); i ++) {
int [] option = optionList.get(i);
// 记录当前路径
List<int []> newPathList = new ArrayList<>();
newPathList.addAll(pathList);
newPathList.add(option);
// 作出选择
huisu(newPathList, option);
}
}
public List<int []> getOptionList(int [] position, List<int []> pathList) {
List<int []> optionList = new ArrayList<>();
int row = position[0];
int col = position[1];
int [] rowOffset = new int [] {1, -1, 0, 0};
int [] colOffset = new int [] {0, 0, 1, -1};
for (int i = 0; i < 4; i++) {
int targetRow = row + rowOffset[i];
int targetCol = col + colOffset[i];
int [] option = new int[]{targetRow, targetCol};
if (targetRow < 0 || targetRow >= m || targetCol < 0 || targetCol >= n || !isLessK(targetRow, targetCol) || contains(pathList, option)) {
continue;
}
optionList.add(option);
}
return optionList;
}
// 判断行 列各位数之和是否小宇k
public boolean isLessK(int row, int col) {
int sum = row % 10;
while(row / 10 != 0) {
row = row / 10;
sum = sum + row % 10;
}
sum = sum + col % 10;
while(col / 10 != 0) {
col = col / 10;
sum = sum + col % 10;
}
if (sum > k) {
return Boolean.FALSE;
}
return Boolean.TRUE;
}
public Boolean contains(List<int []> pathList, int [] position) {
for (int [] path: pathList) {
if (path[0] == position[0] && path[1] == position[1]) {
return Boolean.TRUE;
}
}
return Boolean.FALSE;
}
}
分析
-
每条递归到底的路径通过一个List<int[]> 来记录.
-
通过遍历list来判断某个格子是否走过
-
最后的结果通过合并所有的list, 得到一个没有重复的list, 然后通过其大小得到所有可能走到的格子.
优点:
- 该算法本质就是DFS, 通过递归和回退来探索每一条可能的路径, 也设置了相应的备忘录来防止走回头路.
缺点:
-
采用的备忘录方式太笨重, 没必要记录每一条递归的具体路径, 因为我只是想判断该格子是否走过
-
统计最后结果的方式也太愚蠢, 因为题目不需要知道到底是哪些格子被走到了, 只需要一个数量
-
具体执行的时候, 当格子多了之后, 算法运行很慢, 会超时
改良版
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
Main main = new Main();
main.movingCount(1, 2, 1);
}
int m = 0;
int n = 0;
int k = 0;
boolean [][] visited;
int count = 1;
public int movingCount(int m, int n, int k) {
this.m = m;
this.n = n;
this.k = k;
this.visited = new boolean[m][n];
visited[0][0] = Boolean.TRUE;
huisu(new int[] {0, 0});
return this.count;
}
public void huisu(int [] position) {
// 由position的到选择列表, 同时剔除非法选择
List<int []> optionList = getOptionList(position);
if (optionList.size() == 0) {
return;
}
for (int i = 0; i < optionList.size(); i ++) {
int [] option = optionList.get(i);
// 作出选择
huisu(option);
}
}
public List<int []> getOptionList(int [] position) {
List<int []> optionList = new ArrayList<>();
int row = position[0];
int col = position[1];
int [] rowOffset = new int [] {1, -1, 0, 0};
int [] colOffset = new int [] {0, 0, 1, -1};
for (int i = 0; i < 4; i++) {
int targetRow = row + rowOffset[i];
int targetCol = col + colOffset[i];
int [] option = new int[]{targetRow, targetCol};
if (targetRow < 0 || targetRow >= m || targetCol < 0 || targetCol >= n || !isLessK(targetRow, targetCol) || visited[targetRow][targetCol]) {
continue;
}
visited[targetRow][targetCol] = Boolean.TRUE;
count ++;
optionList.add(option);
}
return optionList;
}
// 判断行 列各位数之和是否小宇k
public boolean isLessK(int row, int col) {
int sum = row % 10;
while(row / 10 != 0) {
row = row / 10;
sum = sum + row % 10;
}
sum = sum + col % 10;
while(col / 10 != 0) {
col = col / 10;
sum = sum + col % 10;
}
if (sum > k) {
return Boolean.FALSE;
}
return Boolean.TRUE;
}
}
-
备忘录采用二维布尔类型的数组来记录某个格子是否走过
-
最后的结果只通过简单的计数完成
-
不会超时
BFS的写法
import java.util.LinkedList;
import java.util.Queue;
public class Main {
public static void main(String[] args) {
Main main = new Main();
main.movingCount(38, 15, 9);
}
public int movingCount(int m, int n, int k) {
if (k == 0) {
return 1;
}
Queue<int[]> queue = new LinkedList<int[]>();
// 记录格子
boolean [][] visited = new boolean[m][n];
// 添加起点
queue.offer(new int[] {0, 0});
visited[0][0] = Boolean.TRUE;
int count = 1;
int [] rowOffset = new int [] {1, -1, 0, 0};
int [] colOffset = new int [] {0, 0, 1, -1};
while(!queue.isEmpty()) {
int [] position = queue.poll();
// 扩散节点
int row = position[0];
int col = position[1];
for (int i = 0; i < 4; i ++) {
int targetRow = row + rowOffset[i];
int targetCol = col + colOffset[i];
if (get(targetRow) + get(targetCol) != sum(targetRow, targetCol)) {
System.out.println();
}
if (targetRow < 0 || targetRow >= m || targetCol < 0 || targetCol >= n || get(targetRow) + get(targetCol) > k || visited[targetRow][targetCol]) {
continue;
}
queue.offer(new int[]{targetRow, targetCol});
visited[targetRow][targetCol] = Boolean.TRUE;
count ++;
}
}
return count;
}
private int get(int x) {
int res = 0;
while (x != 0) {
res += x % 10;
x /= 10;
}
return res;
}
}
DFS的写法
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
Main main = new Main();
main.movingCount(11, 8, 16);
}
int m = 0;
int n = 0;
int k = 0;
boolean [][] visited;
public int movingCount(int m, int n, int k) {
this.m = m;
this.n = n;
this.k = k;
visited = new boolean[m][n];
return dfs(0, 0);
}
public int dfs(int row, int col) {
if (row < 0 || row >= m || col < 0 || col >= n || get(row) + get(col) > k || visited[row][col]) {
return 0;
}
visited[row][col] = Boolean.TRUE;
return 1 + dfs(row + 1, col) + dfs(row - 1, col) + dfs(row, col + 1) + dfs(row, col - 1);
}
private int get(int x) {
int res = 0;
while (x != 0) {
res += x % 10;
x /= 10;
}
return res;
}
}
2.24日回溯算法重写
int m;
int n;
int k;
boolean [][] visited;
int maxCount = 1;
public int movingCount(int m, int n, int k) {
this.m = m;
this.n = n;
this.k = k;
// visited数组用于记录机器人是否到达过该方格
this.visited = new boolean[m][n];
visited[0][0] = Boolean.TRUE;
digui(new int[]{0, 0});
return maxCount;
}
private void digui(int [] position) {
// 获得选择列表
List<int []> optionList = getOptionList(position);
if (optionList.size() == 0) {
// 当没有选择的时候, 记录一下结果
// maxCount = Math.max(maxCount, count);
return;
}
for(int [] option : optionList) {
// 做出选择
visited[option[0]][option[1]] = Boolean.TRUE;
digui(option);
}
}
// 根据当前位置, 得到下一步可以走的选择列表
private List<int []> getOptionList(int [] position) {
List<int []> optionList = new ArrayList<>();
int row = position[0];
int col = position[1];
// 移动有上下左右四种可能
int [] rowOffset = new int[] {1, -1, 0, 0};
int [] colOffset = new int[] {0, 0, 1, -1};
for (int i = 0; i < 4; i ++) {
int targetRow = row + rowOffset[i];
int targetCol = col + colOffset[i];
if (targetRow >=0 && targetRow < m && targetCol >= 0 && targetCol < n && judge(targetRow, targetCol, k) && !visited[targetRow][targetCol]) {
maxCount ++;
visited[targetRow][targetCol] = Boolean.TRUE;
optionList.add(new int[] {targetRow, targetCol});
}
}
return optionList;
}
private boolean judge(int row, int col, int k) {
// 判断row和col位数之和是否大于k
int sum = 0;
while (row != 0) {
sum += row % 10;
row = row / 10;
}
while (col != 0) {
sum += col % 10;
col = col / 10;
}
if (sum <= k) {
return Boolean.TRUE;
}
return Boolean.FALSE;
}
错误的地方
- 没有正确的设置初始值
visited[0][0] = Boolean.TRUE;
int maxCount = 1;
- 累加变量以及visited数组赋值位置的问题
即下面两行代码的位置
maxCount ++;
visited[targetRow][targetCol] = Boolean.TRUE;
如果我们是在获取选择列表的时候, 来判断哪些选择不能选的时候, 这两个代码就应该放在获取选择列表的里面, 这样保证了总体上不会获得重复的选择, 因为你如果放在进行选择的地方才赋值的话, 就会出现如下的情况, 有一个选择列表是1, 2, 3然后因为回溯是类似DFS的算法, 那么选择了1进行之后的选择, 就可能又生成了3这个选择(因为第一个选择列表还没遍历到3, 因此visited是没有记录3的), 这样就会产生重复选择(两个3), maxCount也是同理
如果我们是在选择列表那里剔除访问过的选择的话, 那么就可以把这两个代码放到选择列表那里, 因为还是上文那种情况如果从选择1出发, 到了选择3, 那么下次在第一个循环里的选择列表那里, 遇见3就会直接跳过了, 也不会出现重复选择