面试题12.矩阵中的路径
本题使用回溯算法。
-
DFS:通过递归,先朝一个方向搜到底,再回溯至上个节点,沿另一个方向搜索,以此类推
-
剪枝:在搜索中,遇到这条路不可能和目标字符串匹配成功的情况(例如:此矩阵元素和目标字符不同、此元素已被访问过),则应立即返回。
-
递归参数:当前元素在矩阵 board 中的行列索引 i 和 j,当前目标字符word中的索引 k
-
终止条件:
- 1.返回false:(1)行或列索引越界;(2)当前矩阵元素与目标字符不同;(3)当前矩阵元素已访问过;
- 2.返回true: k = len(word) - 1,即字符全部匹配完成。
-
递推工作:
- 1.标记当前矩阵元素:将 board[i][j] 修改为字符 ‘.’,代表此元素已被访问过防止之后重复访问。
- 2.搜索下一单元格:朝当前元素的上、下、左、右四个方向开始下层递归,使用或连接
public boolean exist(char[][] board, String word) {
char[] words = word.toCharArray();
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
//从[i,j]这个坐标开始查找
if (dfs(board, words, i, j, 0))
return true;
}
}
return false;
}
public boolean dfs(char[][] board, char[] words, int i, int j, int k) {
//边界的判断,如果越界直接返回false。index表示的是查找到字符串word的第几个字符,
//如果这个字符不等于board[i][j],说明验证这个坐标路径是走不通的,直接返回false
if(i < 0 || i >= board.length || j < 0 || j >= board[0].length || board[i][j] != words[k])
return false;
//如果word的每个字符都查找完了,直接返回true
if(k == words.length - 1) return true;
//把当前坐标的值保存下来,为了在最后复原
char temp = board[i][j];
//然后修改当前坐标的值
board[i][j] = '.';
//走递归,沿着当前坐标的上下左右4个方向查找
boolean res = dfs(board, words, i + 1, j, k + 1) ||
dfs(board, words, i - 1, j, k + 1) ||
dfs(board, words, i, j + 1, k + 1) ||
dfs(board, words, i, j - 1, k + 1);
//递归之后再把当前的坐标复原
board[i][j] = temp;
return res;
}
代码二:
class Solution {
public int m;
public int n;
//偏移量数组,代表走的方向
// x-1,y
// x,y-1 x,y x,y+1
// x+1,y
int[][] direction = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};
//标记该点是否已被选择过,true表示被选择过,false表示还没被选择
boolean[][] marked;
public boolean exist(char[][] board, String word) {
m = board.length;
if(m == 0) return false;
n = board[0].length;
marked = new boolean[m][n];
//遍历所有的点,并找到其中符合条件的点
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
if(dfs(board, i, j, 0, word)) return true;
}
}
return false;
}
//判断网格中是否有该字符串
//start 表示当前是第几个字符,从0开始
public boolean dfs(char[][] board, int row, int col, int start, String word) {
//如果start表示最后一个字符,且也和字符串的最后一个字符一致,则返回true;
if(start == word.length() - 1) return board[row][col] == word.charAt(start);
if(board[row][col] == word.charAt(start)) {
marked[row][col] = true; //表示这个字符已被选择
for(int i = 0; i < 4; i++) {
int newX = row + direction[i][0]; //新的横坐标
int newY = col + direction[i][1]; //新的纵坐标
if(inArea(newX, newY) && !marked[newX][newY]) {
if(dfs(board, newX, newY, start + 1, word) return true;
}
}
marked[row][col] = false; //撤销选择
}
return false;
}
//判断这个(x,y)这个点是否越界
public boolean inArea(int x, int y) {
return x >= 0 && x < m && y >= 0 && y < n;
}
}
—————————————————————————————
面试题13.机器人的运动范围
方法一:DFS
-
递归参数:当前元素在矩阵中的行列索引 i 和 j。
-
终止条件:当(1)行或列索引越界;(2)数位和超过目标值 k;(3)当前元素已访问过,返回0
-
递推工作:
- 1.标记当前单元格
- 2.计算当前元素的下、右两个方向元素的数位和,并开启下层递归
-
回溯返回值:返回 1+右方搜索的可达解总数+下方搜索的可达解总数,代表从本单元格递归搜索的可达解总数。
class Solution {
boolean[][] flag;
int row;
int col;
public int movingCount(int m, int n, int k) {
flag = new boolean[m][n];
row = m;
col = n;
return dfs(0, 0, k);
}
public int dfs(int i, int j, int k) {
if(i < 0 || i >= row || j < 0 || j >= col ||
getDigitSum(i) + getDigitSum(j) > k || flag[i][j])
return 0;
flag[i][j] = true;
return 1 + dfs(i + 1, j, k) + dfs(i, j + 1, k);
}
//计算一个数字的数位之和
public int getDigitSum(int x) {
int res = 0;
while(x > 0) {
res += x % 10;
x /= 10;
}
return res;
}
}
方法一:BFS
-
初始化:将机器人初始点(0,0)加入队列queuel;
-
迭代终止条件:queue为空,代表已经遍历完所有可达解;
-
迭代工作:
- 1.单元格出队:将队首单元格的索引、数位和 弹出,作为当前搜索单元格;
- 2.判断是否跳过:若(1)索引越界;(2)数位和超过目标值;(3)当前元素已经被访问过,执行 continue;
- 3.标记当前单元格:将单元格索引(i,j)存入flag,代表已经被访问过了;
- 4.单元格入队:将当前元素的下方、右方单元格的索引、数位和加入queue。
class Solution {
public int movingCount(int m, int n, int k) {
boolean[][] flag = new boolean[m][n];
int res = 0;
Queue<int[]> queue = new LinkedList<>();
//4个0依次表示为:行索引,列索引,行的数位和,列的数位和
queue.add(new int[]{0, 0, 0, 0});
while(queue.size() > 0) {
int[] x = queue.poll();
int i = x[0], j = x[1], si = x[2], sj = x[3];
if(i >= m || i < 0 || j >= n || j < 0 ||
si + sj > k || flag[i][j])
continue;
flag[i][j] = true;
res++;
queue.add(new int[]{i + 1, j, getDigitSum(i + 1), sj});
queue.add(new int[]{i, j + 1, si, getDigitSum(j + 1)});
}
return res;
}
//计算一个数字的数位之和
public int getDigitSum(int x) {
int res = 0;
while(x > 0) {
res += x % 10;
x /= 10;
}
return res;
}
}
—————————————————————————————
面试题14.剪绳子
动态规划
-
状态定义:dp[i] 表示将正整数 i 拆分成至少两个正整数的和之后,这些正整数的最大乘积。
-
状态转移方程:把长度为i的绳子拆成两部分,一部分是j,另一部分是i-j,那么会有下面4种情况
- 1.j 和 i-j 都不再拆: dp[i] = j * (i-j);
- 2.j 拆,i-j 不拆:dp[i] = dp[j] * (i-j);
- 3.j 不拆,i-j 拆:dp[i] = j * dp[i-j];
- 4.j 不拆,i-j 不拆:dp[i] = dp[j] * dp[i-j];
由此得到的递推公式为 :
dp[i]=max(dp[i],max(j,dp[j]) * max(i-j,dp[i-j]))。
- 初始化:dp[0]=0,dp[1]=1
class Solution {
public int cuttingRope(int n) {
int[] dp = new int[n + 1];
dp[1] = 1;
for(int i = 2; i <= n; i++) {
for(int j = 1; j < i; j++) {
dp[i] = Math.max(dp[i], Math.max(j, dp[j]) * Math.max(i - j, dp[i - j]));
}
}
return dp[n];
}
}
另外一种状态转移方程:当 i≥2 时,假设对正整数 i 拆分出的第一个正整数是 j(1≤j<i),则有以下两种方案:
- 将 i 拆分成 j 和 i−j 的和,且 i−j 不再拆分成多个正整数,此时的乘积是 j×(i−j);
- 将 i 拆分成 j 和 i−j 的和,且 i−j 继续拆分成多个正整数,此时的乘积是 j×dp[i−j]。
因此状态转移方程为:dp[i]=max(j×(i−j),j×dp[i−j])
class Solution {
public int cuttingRope(int n) {
int[] dp = new int[n + 1];
for (int i = 2; i <= n; i++) {
for (int j = 1; j < i; j++) {
dp[i]= Math.max(dp[i], Math.max(j * (i - j), j * dp[i - j]));
}
}
return dp[n];
}
}
贪心算法
核心思路:尽可能把绳子分成长度为3的小段,这样乘积最大
- 1.如果 n == 2,返回1,如果 n == 3,返回2,两个可以合并成n小于4的时候返回n - 1
- 2.如果 n == 4,返回4
- 3.如果 n > 4,分成尽可能多的长度为3的小段,每次循环长度n减去3,乘积res乘以3;最后返回时乘以小于等于4的最后一小段
证明:原文地址
class Solution {
public int cuttingRope(int n) {
if(n < 4) return n - 1;
int res = 1;
while(n > 4) {
n -= 3;
res *= 3;
}
return res * n;
}
}
—————————————————————————————
面试题15.二进制中1的个数
方法一:逐位判断
-
根据与运算定义,设二进制数字 n,则有:
- 若 n&1 = 0,则 n 二进制最右一位为 0
- 若 n&1 = 1,则 n 二进制最右一位为 1
-
作以下循环判断:
- 1.判断 n 最右一位是否为1,根据结果计数
- 2.将 n 右移一位
public class Solution {
public int hammingWeight(int n) {
int count = 0;
while(n != 0){
count += n & 1;
n = n >>> 1;
}
return count;
}
}
优化:另一种代码
当输入为负数时,这种代码也可以正常运行
public class Solution {
public int hammingWeight(int n) {
int count = 0;
for (int i = 0; i < 32; i++) {
if ((n & (1 << i)) != 0) {
count++;
}
return count;
}
}
方法二:巧用 n&(n-1)
- (n-1)解析:二进制数字 n 最右边的 1 变成 0,这个 1 右边的 0 都变成 1;
- n&(n-1)解析:二进制数字 n 最右边的 1 变成 0,其余不变。
从以上可知:一个整数的二进制表示中有多少个1,就可以进行多少次这样的操作。
public class Solution {
public int hammingWeight(int n) {
int count = 0;
while(n != 0) {
count++;
n = (n & (n - 1));
}
return count;
}
}