【数据结构与算法之面试题】穷举法和回溯法系列面试题
【数据结构与算法之面试题】穷举法和回溯法系列面试题
文章目录
- 【数据结构与算法之面试题】穷举法和回溯法系列面试题
- 1 数组元素之差的最小值
- 2 数的分组问题
- 3 最佳的碰面地点
- 4 多点共线问题
- 5 复原IP地址
- 6 矩阵中的相邻数
- 7 被包围的区域
穷举法和回溯法:
相似点
- 首先建立问题的解空间树
- 再在解空间树中进行搜索,直到找到问题答案
不同点
不同之处在于它们对于解空间树的搜索方式
- 穷举法简单粗暴
- 回溯则深搜 + 剪枝
1 数组元素之差的最小值
【题目描述】有一个整数数组,请求出该数组中不同元素两两之差绝对值的最小值,注意主要计算出最小值即可,不需要给出具体的元素。
本题可以用穷举法求解,步骤如下:
- 首先设定一个最小值初值minVal,这个值最好等于一个较大的数,要确保出它比计算出的最小值大。
- 计算出数组中每两个元素之差的绝对值difference,并将difference 与 minVal 进行比较,如果difference 更小,则将difference 赋值给 minVal,使得minVal 成为新的最小值。
- 重复2,直到比较完数组中两两元素之差的绝对值。
- 返回结果minVal
其实这道题最大的难点在于如何穷举出数组中元素的两两组合,并计算这两个元素之差的绝对值。在实际应用中,数组可能会包含很多元素,因此我们必须按照一定的规则来遍历解空间,穷举出所有数组元素的两两组合。
假设数组a 中包含n 个元素,分别为 a 1 , a 2 , . . . , a i , a i + 1 , . . . , a n a_1,a_2,…,a_i,a_{i+1},…,a_n a1,a2,…,ai,ai+1,…,an,那么这个数组中的元素的两两组合可以与下面矩阵中元素一一对应。
排除掉矩阵对角线上的元素组合一共是 n 2 − n n^2 - n n2−n 种组合,而且又因为 数组元素对 ( a i , a j ) 和 ( a j , a i ) (a_i,a_j) 和 (a_j,a_i) (ai,aj)和(aj,ai)沿对角线对称,因为取绝对值后 |ai - aj| = |aj - ai|,所以我们我们只要穷举出矩阵的上三角或者下三角区域,一共是 $(n^2 - n)/ 2 $ 种组合。
当然我们并不需要真的搞一个这样的矩阵出来,可以通过一个二重循环模拟访问矩阵上三角区域中元素的过程。
Java 实现:
/**
* @Projectname: JavaData_StructureAndAlgorithm
* @Classname: TheMinimumDifferenceBetweenTheElementsOfAnArray
* @Author: Ding Jiaxiong
* @Data:2023/3/18 15:10
*/
public class TheMinimumDifferenceBetweenTheElementsOfAnArray {
public static int getMinDifference(int[] array) {
int minVal = 32767;
int row = 0;
int column = 0;
for (row = 0; row < array.length; row++) {
for (column = row + 1; column < array.length; column++) {
if (Math.abs(array[column] - array[row]) < minVal) {
minVal = Math.abs(array[column] - array[row]);
}
}
}
return minVal;
}
public static void main(String[] args) {
int[] array = {
1, 12, 23, 4, 25, 20, 122};
System.out.println(getMinDifference(array));
}
}
运行结果
2 数的分组问题
【题目描述】有10个任意的正整数,将其分为组A和组B,要求组A 中的每个数据的和 与组B中每个数据的和 之差
的绝对值最小。设计算法实现这样的分组。
举个栗子:数组array[10] = {2,3,6,8,9,10,0,23,16,65},输出的结果应该是 A:{65,6}、B:{16,23,0,10,9,8,3,2} 。因为组A 中每个数据的和等于 65 + 6 = 71,组B中每个数据的和等于 16 + 23 + 0 + 10 + 9 + 8 + 3 + 2 = 71,差为0.
其实解决这道题最直观的方法就是穷举法了。我们可以将这10个数任意分为组A 和 组B,然后计算组A 中每个数字的和S1,组B的和S2,|S1 - S2| 最小的方案就是答案。
首先确定解空间的 大小,【二进制位码】将10个任意的整数分为两组有 2 10 − 2 2^{10} - 2 210−2 = 1022种方案。
同样借助位码来遍历解空间中的每一种分组方案。
Java 实现:
/**
* @Projectname: JavaData_StructureAndAlgorithm
* @Classname: TheProblemOfGroupingNumbers
* @Author: Ding Jiaxiong
* @Data:2023/3/18 15:35
*/
public class TheProblemOfGroupingNumbers {
public static int getBestGrouping(int[] array) {
int scheme, bitmask, index;
int difference = 0;
int sumA = 0, sumB = 0;
int res = 0;
for (int i = 0; i < 10; i++) {
difference = difference + array[i]; // 初始化差值
}
for (scheme = 0x0001; scheme <= 0x03fe; scheme++) {
// 在2^10 - 2 的范围内搜索, 0x03fe十进制是1022
for (bitmask = 0x0001, index = 9; bitmask <= 0x0200; bitmask = bitmask * 2, index--) {
if ((bitmask & scheme) != 0) {
sumA = sumA + array[index]; // 组A 中的元素和
} else {
sumB = sumB + array[index]; // 组B 中的元素和
}
}
if (Math.abs(sumA - sumB) < difference) {
res = scheme;
difference = Math.abs(sumA - sumB);
}
sumA = 0;
sumB = 0;
}
return res;
}
public static void main(String[] args) {
int[] array = {
2, 3, 6, 8, 9, 10, 0, 23, 16, 65};
int bitmask, index;
int res = getBestGrouping(array);
System.out.println(Integer.toBinaryString(res));
System.out.println("Group A: ");
for (bitmask = 0x0001, index = 9; bitmask <= 0x0200; bitmask = bitmask * 2, index--) {
if ((bitmask & res) != 0) {
System.out.println(array[index]);
}
}
System.out.println("Group B: ");
for (bitmask = 0x0001, index = 9; bitmask <= 0x0200; bitmask = bitmask * 2, index--) {
if ((bitmask & res) == 0) {
System.out.println(array[index]);
}
}
}
}
运行结果
二进制位上 = 1表示分到组A,=0 表示分到组B
没问题。
3 最佳的碰面地点
【题目描述】有一队人(两人或以上) 要在一个地方碰面,他们希望最小化总行走距离。
给定一个二维的网格,格子里面的值要么是0,要么是1,其中1表示某人所处的位置,规定这些人每次只能横向走一格或者纵向走一格。
编写一个程序,计算这一队人在网格中的最佳碰面地点。
举个栗子:
最终得到的结果应该是(0,2),3人的总行走距离为 2+ 2+ 2 =6.
【穷举法】方法就是计算网格中的每个 “1点”( 可能有多个,记作 p p p)到达网格中的某个点(记作 q i q_i qi)的距离之和(记作 d i d_i di), q i q_i qi 要穷举到 m x n 的每一个格子,这样就可以得到一组距离之和 d 1 , d 2 , . . . , d m n d_1,d_2,…,d_{mn} d1,d2,…,dmn,其中最小的距离和就是答案。对应的 q i q_i qi 即碰面地点。
其实只要不刻意“绕路”,网格中两个点的距离可以直接用曼哈顿距离公式进行计算。
Java 解法:
import java.util.ArrayList;
import java.util.Arrays;
/**
* @Projectname: JavaData_StructureAndAlgorithm
* @Classname: ThePerfectMeetingPlace
* @Author: Ding Jiaxiong
* @Data:2023/3/18 16:25
*/
class position {
int row;
int col;
public position(int row, int col) {
this.row = row;
this.col = col;
}
}
public class ThePerfectMeetingPlace {
// 二维穷举法
public static position getBestMeetingPlace(ArrayList<position> p, int gridRows, int gridColumns) {
int distance = 0;
int minDistance = 10000;
position pr = new position(0, 0);
for (int row = 0; row < gridRows; row++) {
for (int col = 0; col < gridColumns; col++) {
for (int i = 0; i < p.size(); i++) {
distance = distance + Math.abs(p.get(i).row - row) + Math.abs(p.get(i).col - col);
}
if (minDistance > distance) {
minDistance = distance;
pr.row = row;
pr.col = col;
}
distance = 0;
}
}
return pr;
}
public static position getBestMeetingPlace(int[][] grid) {
ArrayList<position> p = new ArrayList<position>();
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[0].length; j++) {
if (grid[i][j] == 1) {
p.add(new position(i, j));
}
}
}
return getBestMeetingPlace(p, grid.length, grid[0].length);
}
public static void main(String[] args) {
int[][] grid = {
{
1, 0, 0, 0, 1},
{
0, 0, 0, 0, 0},
{
0, 0, 1, 0, 0}
};
position bestMeetingPlace = getBestMeetingPlace(grid);
System.out.println(bestMeetingPlace.row + " " + bestMeetingPlace.col);
}
}
运行结果
还有一种改进后的一维穷举法的实现:
import java.util.ArrayList;
/**
* @Projectname: JavaData_StructureAndAlgorithm
* @Classname: BestMeetingPlace
* @Author: Ding Jiaxiong
* @Data:2023/3/18 16:37
*/
class position {
int row;
int col;
public position(int row, int col) {
this.row = row;
this.col = col;
}
}
public class BestMeetingPlace {
// 一维穷举法
public static int getBestMeetingPlace(ArrayList<Integer> p, int length) {
int distance = 0;
int minDistance = 10000;
int res = 0;
for (int i = 0; i < length; i++) {
for (int j = 0; j < p.size(); j++) {
distance = distance + Math.abs(p.get(j) - i);
}
if (minDistance > distance) {
minDistance = distance;
res = i;
}
distance = 0;
}
return res;
}
public static position getBestMeetingPlace(int[][] grid) {
ArrayList<Integer> prow = new ArrayList<Integer>();
ArrayList<Integer> pcolumn = new ArrayList<Integer>();
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[0].length; j++) {
if (grid[i][j] == 1) {
prow.add(i);
pcolumn.add(j);
}
}
}
// 计算纵向最佳碰面地点的行号
int row = getBestMeetingPlace(prow, grid.length);
// 计算横向最佳碰面地点的列号
int col = getBestMeetingPlace(pcolumn, grid[0].length);
return new position(row, col);
}
public static void main(String[] args) {
int[][] grid = {
{
1, 0, 0, 0, 1},
{
0, 0, 0, 0, 0},
{
0, 0, 1, 0, 0}
};
position bestMeetingPlace = getBestMeetingPlace(grid);
System.out.println(bestMeetingPlace.row + " " + bestMeetingPlace.col);
}
}
运行结果
对于一个m x n 的网格,采用二维数组穷举的时间复杂度为 O ( m n ) O(mn) O(mn) ,而使用一维数组穷举的时间复杂度为 O ( m ) + O ( n ) O(m) + O(n) O(m)+O(n)
4 多点共线问题
【题目描述】给定n (n > 2) 个位于多一平面的点,求最多有多少个点位于同一条直线上。
举个栗子,给定点{1,1}、{2,2}、{3,3},输出结果为3
因为给定的n 个点相互独立,所以只能通过穷举法求解。
“两点确定一直线”,因为平面内存在无数条直线,不可能全部一一列举出来判断,我们只需要穷举那些经过了平面中两个点的直线,然后判断其他点是否位于该直线上。
计算多点共线问题的步骤:
- 通过平面中的两点确定一条直线
- 如果该直线被访问过,则直接返回步骤1 枚举下一条直线;否则判断平面上的其他点是否也位于该直线上,并记录下该直线上点的数量,然后返回步骤1 继续枚举下一条直线。
当平面上由两点确定的直线全部被访问后,记录中每条直线对应的点的数量最大的即为所求。
Java 实现:
import java.util.HashMap;
/**
* @Projectname: JavaData_StructureAndAlgorithm
* @Classname: Multi_pointCollinearProblem
* @Author: Ding Jiaxiong
* @Data:2023/3/18 19:25
*/
class point {
int x;
int y;
public point(int x, int y) {
this.x = x;
this.y = y;
}
}
class line {
int a;
int b;
int cb;
public line(int a, int b, int cb) {
this.a = a;
this.b = b;
this.cb = cb;
}
public int hashCode() {
return 0;
}
public boolean equals(Object anObject) {
if (anObject instanceof line) {
line anLine = (line) anObject;
// 判断两条直线是否重合的条件1
if (anLine.a == this.a && anLine.b == this.b && anLine.cb == this.cb) {
return true;
}
// 判断两条直线是否重合的条件2
if (anLine.a == -this.a && anLine.b == -this.b && anLine.cb == -this.cb) {
return true;
}
// 判断两条直线是否重合的条件3
if ((double) anLine.a * (double) this.b == (double) this.a * (double) anLine.b && (double) anLine.b * (double) this.cb == (double) this.b * (double) anLine.cb && (double) anLine.a * (double) this.cb == (double) this.a * (double) anLine.cb) {
return true;
}
return false;
}
return false;
}
}
public class Multi_pointCollinearProblem {
public static HashMap<line, Integer> map = new HashMap<line, Integer>();
public static boolean hasThisLine(line l) {
if (map.containsKey(l)) {
return true;
}
map.put(l, 2);
return false;
}
public static int getMaxSameLinePoints(point[] points) {
for (int i = 0; i < points.length; i++) {
for (int j = i + 1; j < points.length; j++) {
// 根据点point[i] 和 point[j] 确定一条直线
int a = points[i].y - points[j].y;
int b = points[i].x - points[j].x;
int cb = b * points[i].y - a * points[i].x;
line l = new line(a, b, cb);
// 如果没有访问过这条直线, 则开始计算这条直线上点的数量,并用pointsNum 记录
if (!hasThisLine(l)) {
for (int k = 0; k < points.length; k++) {
if (k != i && k != j) {
if (b * points[k].y == a * points[k].x + cb) {
// k点在直线上
int pointsNum = map.get(l);
map.put(l, ++pointsNum);
}
}
}
}
}
}
// 遍历找出最大值
int maxPoints = 0;
for (Integer numbers : map.values()) {
if (numbers > maxPoints) {
maxPoints = numbers;
}
}
return maxPoints;
}
public static void main(String[] args) {
point[] points = new point[4];
// 第1个点坐标为(1,0)
points[0] = new point(1, 0);
// 第2个点坐标为(1,2)
points[1] = new point(1, 2);
// 第3个点坐标为(3,0)
points[2] = new point(3, 0);
// 第4个点坐标为(0,3)
points[3] = new point(0, 3);
System.out.println(getMaxSameLinePoints(points));
}
}
运行结果
答案是:{3,0}、{1,2}、{0,3}这三个点最多共线。
5 复原IP地址
【题目描述】给定一个只包含数字的字符串,打印出由该字符串构成的所有可能的IP地址。
举个例子:
- 如果给定”25525511135”,则应该打印出 255.255.11.135 和 255.255.111.35
- 如果给定”1111”,则应该打印出 1.1.1.1
- 当然也有可能无法构成IP地址
这道题最容易想到的还是穷举,在一串由数字构成的字符串中间添加3 个点. ,使其构成一个合法的IP地址。
每两个数字之间的间隙都可以尝试放置一个点。
假设有n 个这样的间隙,将这3 个点随意放置到这n 个间隙中,则共有 C(n , 3)种方法。
这样虽然直观,但是其实效率不高,因为存在冗余计算。比如333.33.3.3 一定不是IP地址,因为333 > 255,同理333.3.33.3 也不会是IP地址,还有333.3.3.33这种,所以这样就浪费时间。
所以为了避免冗余,提升算法效率,这里推荐使用回溯法 + 剪枝。
Java 实现:
/**
* @Projectname: JavaData_StructureAndAlgorithm
* @Classname: RecoveryIPAddress
* @Author: Ding Jiaxiong
* @Data:2023/3/18 19:56
*/
public class RecoveryIPAddress {
/**
* @param s 指定的字符串
* @param index 本次递归调用中所要判定的子串在s 中的起始位置下标
* @param pointNumber 本次递归调用要放置第几个分割点
*/
public static void printValidIPAddress(StringBuffer s, int index, int pointNumber) {
if (pointNumber == 3) {
if (isValid(s.substring(index, s.length()))) {
// 得到一个结果, 打印
System.out.println(s);
}
return;
}
for (int i = index + 1; i < s.length(); i++) {
if (isValid(s.substring(index, i))) {
// 如果从index到 i - 1的子串满足IP地址格式要求,则在第i - 1个数字后面加分割点,然后pointNumber ++
s.insert(i, '.');
pointNumber++;
printValidIPAddress(s, i + 1, pointNumber);
// 回溯到上一层,继续探索下一个分支
pointNumber--;
s.deleteCharAt(i);
}
}
}
// 判断s 是否符合IP地址的格式要求
public static boolean isValid(String s) {
if (s.length() == 0) {
return false;
}
int ans = 0;
for (int i = 0; i < s.length(); i++) {
// 包含非数字字符
if (s.charAt(i) - '0' > 9 || s.charAt(i) - '0' < 0) {
return false;
}
ans = ans * 10 + (s.charAt(i) - '0');
// 大于255
if (ans > 255) {
return false;
}
}
// 由0开头
if (s.charAt(0) == '0' && s.length() > 1) {
return false;
}
return true;
}
public static void main(String[] args) {
StringBuffer ip = new StringBuffer("25525511135");
printValidIPAddress(ip,0,0);
StringBuffer ip2 = new StringBuffer("3333333");
printValidIPAddress(ip2,0,0);
}
}
运行结果
6 矩阵中的相邻数
【题目描述】给定一个包含正整数1 和 5 的矩阵,同时给定其中一个数字5 在矩阵中的坐标(row, column),要求计算出该矩阵中与(row, column)位置上的元素5 相邻的5 的数量,并打印出这些相邻元素5 在矩阵中的坐标(行号和列号)。【注意】所谓相邻是指该元素的上下左右4 个方向上的元素,而且相邻具有可传递性,即相邻的相邻也算相邻。
举个栗子:
这道题比较简单的方法就是回溯法。
假定规给定元素5 的下标为(1,0),那么可以采用深搜策略,逐步查找出于矩阵中第1行第0列的元素5相邻的元素5.
具体步骤:
- 定义变量count 计算矩阵中相邻元素5 的数量,初始0
从第1行第0列的元素5出发,依次访问与其相邻的 4个元素。
- 如果当前访问到的相邻元素是5,则count++,然后将这个5 作为新起点继续深搜
- 如果当前访问到的相邻元素不是5,则【剪枝】回退到上一次访问的元素5,访问下一个相邻元素。
这个过程中一定要着重考虑到两个问题:
- 重复搜索:利用HashSet 辅助查重
- 矩阵越界:矩阵中的元素可能并没有上下左右四个相邻元素。
Java 实现描述:
import java.util.HashSet;
/**
* @Projectname: JavaData_StructureAndAlgorithm
* @Classname: AdjacentNumber
* @Author: Ding Jiaxiong
* @Data:2023/3/18 20:33
*/
public class AdjacentNumber {
public static int count = 0;
public static HashSet<String> set = new HashSet<String>();
public static int getAdjacentNumber(int row, int column, int[][] matrix) {
// 上下左右4个元素的行号
int[] rowIndex = {
row + 1, row, row - 1, row};
// 上下左右4个元素的列号
int[] columnIndex = {
column, column + 1, column, column - 1};
String index = row + "," + column;
set.add(index); // 将当前访问过的5的坐标加入HashSet, 以便查重
for (int i = 0; i < 4; i++) {
// 依次从上下左右四个方向深搜
if (isValidPosition(rowIndex[i], columnIndex[i], matrix)) {
/**
* 满足条件
* 1. 没有越界
* 2. 元素为5
* 3. 未被访问过
*/
System.out.println("row = " + rowIndex[i] + " column = " + columnIndex[i]);
count++;
getAdjacentNumber(rowIndex[i], columnIndex[i], matrix); // 递归深搜
}
}
return count;
}
public static boolean isValidPosition(int row, int column, int[][] matrix) {
// 判断是否越界
if (row < 0 || row >= matrix.length || column < 0 || column >= matrix[0].length) {
return false;
}
// 判断是否为5
if (matrix[row][column] != 5) {
return false;
}
// 判断是否被访问过
String index = row + "," + column;
if (set.contains(index)) {
return false;
}
return true;
}
public static void main(String[] args) {
int[][] matrix = {
{
1, 1, 5, 5, 1},
{
5, 5, 5, 1, 1},
{
1, 1, 5, 5, 1},
{
1, 1, 5, 1, 1},
{
5, 1, 1, 1, 5}
};
System.out.println(getAdjacentNumber(1, 0, matrix));
}
}
运行结果
没毛病。
7 被包围的区域
【题目描述】给定一个二维矩阵,包含字母X 和 O。请找到所谓被X 围绕的区域,并将这些区域里所有的 O 用X 替换。
举个栗子:
其实不难发现,这个问题可以转换成在矩阵中所有边界上的O 以及与它们相连的O 都没有被替换,而其他的O 就被替换了。
深搜深搜。
Java 实现:
import com.sun.javafx.image.PixelUtils;
/**
* @Projectname: JavaData_StructureAndAlgorithm
* @Classname: TheAreaUnderSiege
* @Author: Ding Jiaxiong
* @Data:2023/3/18 20:48
*/
public class TheAreaUnderSiege {
public static void surroundAreas(char[][] matrix) {
int maxX = matrix.length - 1; // 矩阵行数 -1
int maxY = matrix[0].length - 1; // 矩阵列数 -1
for (int j = 0; j <= maxY; j++) {
if (matrix[0][j] == 'o') {
expand(0, j, matrix, maxX, maxY);
}
}
for (int j = 0; j <= maxY; j++) {
if (matrix[maxX][j] == 'o') {
expand(maxX, j, matrix, maxX, maxY);
}
}
for (int i = 1; i < maxX; i++) {
if (matrix[i][0] == 'o') {
expand(i, 0, matrix, maxX, maxY);
}
}
for (int i = 1; i < maxX; i++) {
if (matrix[i][maxY] == 'o') {
expand(i, maxY, matrix, maxX, maxY);
}
}
for (int i = 0; i <= maxX; i++) {
for (int j = 0; j <= maxY; j++) {
if (matrix[i][j] == 'o') {
matrix[i][j] = 'x';
} else if (matrix[i][j] == '#') {
matrix[i][j] = 'o';
}
}
}
}
public static void expand(int x, int y, char[][] matrix, int maxX, int maxY) {
if (x > maxX || x < 0 || y > maxY || y < 0) {
return;
}
if (matrix[x][y] == 'x') {
return;
}
if (matrix[x][y] == '#') {
return;
}
if (matrix[x][y] == 'o') {
matrix[x][y] = '#';
}
expand(x + 1, y, matrix, maxX, maxY);
expand(x, y + 1, matrix, maxX, maxY);
expand(x - 1, y, matrix, maxX, maxY);
expand(x, y - 1, matrix, maxX, maxY);
}
public static void main(String[] args) {
char[][] matrix = {
{
'x', 'x', 'o', 'o', 'x', 'x', 'x', 'o'},
{
'x', 'o', 'o', 'x', 'x', 'x', 'x', 'x'},
{
'x', 'o', 'o', 'o', 'o', 'x', 'x', 'x'},
{
'x', 'o', 'x', 'x', 'x', 'o', 'x', 'x'},
{
'x', 'x', 'x', 'o', 'x', 'o', 'o', 'x'},
{
'x', 'o', 'o', 'x', 'x', 'o', 'o', 'x'},
{
'x', 'o', 'o', 'x', 'x', 'x', 'o', 'x'},
{
'x', 'x', 'x', 'x', 'x', 'x', 'o', 'x'},
};
for (int i = 0; i <= 7; i++) {
for (int j = 0; j <= 7; j++) {
System.out.print(matrix[i][j] + " ");
}
System.out.println();
}
System.out.println("===================================================");
surroundAreas(matrix);
for (int i = 0; i <= 7; i++) {
for (int j = 0; j <= 7; j++) {
System.out.print(matrix[i][j] + " ");
}
System.out.println();
}
}
}
运行结果
还没有评论,来说两句吧...