剑指Offer.13:机器人的运动范围
一、题目描述
地上有一个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。请问该机器人能够到达多少个格子?
二、问题分析
单从问题来看,场景还是很好理解的,是典型的搜索&回溯问题。在写回溯之前,先写一下在题解中看到的数位之和运算,这个内容可以记住,毕竟很多题中都有所体现。
回溯,即递归回溯,在本质上可以说就是搜索,一种可以避免不必要搜索的穷举暴力搜索,在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯。(其实回溯法就是对隐式图的深度优先搜索算法)。 若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。 而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。
而回溯和枚举的不同之处还在于在搜索过程中用剪枝函数避免无效搜索。
剪枝:在搜索中,遇到 这条路不可能和目标字符串匹配成功 的情况(例如:此矩阵元素和目标字符不同、此元素已被访问),则应立即返回,称之为 可行性剪枝 。
剪枝的两种类型:
- 用约束函数在扩展结点处剪去不满足约束的子树;
- 用限界函数剪去得不到最优解的子树。
数位之和运算
设一数字 x,向下取整除法符号 // ,求余符号 ⊙ ,则有:
- x⊙10 :得到 x 的个位数字;
- x//10 : 令x的十进制数向右移动一位,即删除个位数字。
数位和运算封装为API为:
public int sums(int n){
int s = 0;
while(n != 0){
s += n % 10;
n /= 10;
}
return s;
}
由于机器人每次只能移动一格(即只能从 x 运动至 x±1),因此每次只需计算 x 到 x±1 的数位和增量。本题说明1≤n,m≤100 ,以下公式仅在此范围适用。
数位和公式:
- 当 (n+1)⊙10 = 0时,当x或者y从n时的9进位成(n+1)0时,数位和减小了8即Sn+1 - Sn = 8
设进位后数的数位和为s,差值为x,得出方程s=(s-1)+9+x,即x=-8 - 当 (n+1)⊙10 != 0时,常识可得差值为1即Sn+1 - Sn = 8.
问题解决
//DFS
//避免递归传参,定义全局变量
int x, y, k;
//是否已经访问过
boolean[][] visited;
public int movingCount(int m, int n, int k) {
//初始化
this.x = m;
this.y = n;
this.k = k;
this.visited = new boolean[m][n];
return dfs(0, 0, 0, 0);
}
public int dfs(int x, int y, int sumX, int sumY) {
//下标越界,数位和大于指定值以及已经访问过都会直接返回
if (x >= this.x || y >= this.y || sumX + sumY > this.k || visited[x][y]) return 0;
//标记此坐标访问过
visited[x][y] = true;
//数位和运算,适用范围是[0,100)
//至于为什么只向右和向下扩张是因为不用回溯已走过的路径,扩张的路径只会向下或者向右
return 1 + dfs(x + 1, y, (x + 1) % 10 == 0 ? sumX - 8 : sumX + 1, sumY)
+ dfs(x, y + 1, sumX, (y + 1) % 10 == 0 ? sumY - 8 : sumY + 1);
}
一般的DFS算法:
int m,n,k;
boolean[][] visited;//定义全局变量可以避免方法传参过多,我的理解哈哈
public int movingCount(int m, int n, int k) {
this.m = m;
this.n = n;
this.k = k;
this.visited = new boolean[m][n];
return dfs(0,0,0,0);
}
public int dfs(int i,int j,int si,int sj){
if(i>m-1||j>n-1||visited[i][j]||si+sj>k){//数组越界,不满足数位和或者遍历重复
return 0;
}
visited[i][j] = true;//标记,防止遍历重复
return 1+dfs(i,j+1,sum(i),sum(j+1))+dfs(i+1,j,sum(i+1),sum(j));
}
public int sum(int n){
return n%10+n/10;//因为1<n,m<100,这是一个两位数
}