- 求全排列:全排列值得是:n个元素中取n个元素(全部元素)的所有排列组合情况。
- 求组合:n个元素,取m个元素(m<=n)的所有组合情况
- 求子集:n个元素的所有子集(所有的组合情况)
全排列常用解决办法:邻里交换法和回溯法
回溯法
题目:输入[1,2,3]
输出1,2,3所有的不重复的排列组合:
1 ,2,3 1,3,2
2,1,3 2,3,1
3,1,2 3,2,1
回溯:一般解决搜索问题,全排列也是一种搜索问题。
- 回溯:就是类似枚举的搜索尝试过程。在搜索过程中寻找问题 的解。当发现不满足求解的条件时,就回溯返回,尝试别的路径。
全排列可以使用试探的办法列举所有的可能性。一个长度位n 的序列,所有的排列组合:n!
1.从集合中选取一个元素(n种情况),并标记该元素已经被使用。
2.在第一步的基础上递归到下一层,从剩余的n-1 个元素中,按照第一步的方法再找到一个元素,并标记(n-1)
3.依次类推,所有的元素都被标记,将元素存起来,取对比求解的情况。
邻里交换法
回溯时试探性填充数据,给每个位置都试探性赋值。
邻里交换,也是通过递归实现,但是是一种基于交换的思路。
步骤:
- 将数组分成2个部分:暂时确定部分和未确定部分。刚开始,都是未确定部分。
- 在未确定部分中,让每一个数据都有机会和未确定部分中的第一位交换。然后第一位就变成暂时确定部分。
- 以此类推:每个数据都和未确定部分中的第二位交换(第一位数据除外)…直到确定所有数据
- 将确定好的数据和条件对比,对比结束后,还原数据。
题目
2017年Java组c组第三题:
A,2,3,4,5,6,7,8,9共9张纸牌排成一个正三角形(A按1计算)。要求每个边的和相等。 下图就是一种排法。
图片描述这样的排法可能会有很多。
如果考虑旋转、镜像后相同的算同一种,一共有多少种不同的排法呢?请你计算并提交该数字。 - 分析:同一个三角形,旋转加镜像后会变成6种,所以最终的结果要除6
1.暴力解:
public class MagicSquareCount {
public static void main(String[] args) {
int sum = 0; // 初始化用于计数的变量 sum
// 使用嵌套循环来遍历数字1到9填充3x3方阵的所有可能组合
for (int a = 1; a < 10; a++) {
for (int b = 1; b < 10; b++) {
for (int c = 1; c < 10; c++) {
for (int d = 1; d < 10; d++) {
for (int e = 1; e < 10; e++) {
for (int f = 1; f < 10; f++) {
for (int g = 1; g < 10; g++) {
for (int h = 1; h < 10; h++) {
for (int i = 1; i < 10; i++) {
// 检查是否有重复的数字
if (a == b || a == c || a == d || a == e || a == f || a == g || a == h || a == i
|| b == c || b == d || b == e || b == f || b == g || b == h || b == i
|| c == d || c == e || c == f || c == g || c == h || c == i
|| d == e || d == f || d == g || d == h || d == i
|| e == f || e == g || e == h || e == i
|| f == g || f == h || f == i
|| g == h || g == i || h == i) {
continue; // 如果有重复的数字,则跳过当前循环
}
// 检查是否满足幻方的条件:每行、每列、每条对角线的和相等
if (a + b + c + d == d + e + f + g && d + e + f + g == g + h + i + a) {
sum++; // 如果满足条件,增加 sum 的值
}
}
}
}
}
}
}
}
}
}
// 打印出独特排列的数量,除以6是因为有旋转和镜像导致的重复
System.out.println("一共有 " + sum / 6 + " 种不同的排法");
}
}
2.回溯:
public class MagicSquareCount {
static int num[] = new int[10]; // 存放数据的数组
static int count = 0; // 符合条件的排列组合个数
static boolean bool[] = new boolean[10]; // 标记数字是否已被使用
public static void main(String[] args) {
dfs(1); // 从第1位开始填充
System.out.println(count / 3 / 2); // 要除以6
}
// 深度优先搜索函数
public static void dfs(int step) {
// 当9位数据已经赋值完成时,进行条件判断
if (step == 10) {
// 判断是否满足幻方条件:每行、每列、每条对角线的和相等
if (num[1] + num[2] + num[4] + num[6] == num[6] + num[7] + num[8] + num[9] &&
num[1] + num[2] + num[4] + num[6] == num[1] + num[3] + num[5] + num[9]) {
count++; // 如果满足条件,增加 count 的值
}
return; // 返回上一层递归
}
// 尝试填充当前步数的数字
for (int i = 1; i < 10; i++) {
// 如果数字还没有被使用过
if (!bool[i]) {
bool[i] = true; // 标记该位数字已赋值
num[step] = i; // 赋值
dfs(step + 1); // 递归填充下一位数字
bool[i] = false; // 撤回标记,用于尝试其他数字
}
}
}
}
3.邻里交换法:
public class MagicSquareCount {
static int count; // 符合条件的排列组合个数
static int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; // 用于排列的数组
public static void main(String[] args) {
f(a, 0); // 调用递归函数开始排列
System.out.println(count / 6); // 打印结果,要除以6
}
// 递归函数 f
public static void f(int a[], int step) {
// 当step为a的最后一位索引时,即9位数字都排列完毕
if (step == a.length - 1) {
// 判断是否满足幻方条件:每行、每列、每条对角线的和相等
if (a[0] + a[1] + a[2] + a[3] == a[3] + a[4] + a[5] + a[6] &&
a[0] + a[1] + a[2] + a[3] == a[6] + a[7] + a[8] + a[0]) {
count++; // 如果满足条件,增加 count 的值
}
return; // 返回上一层递归
}
// 尝试交换当前位置和后面位置的数字,进行排列
for (int i = step; i < a.length; i++) {
// 交换当前位置和后面位置的数字
int x = a[i];
a[i] = a[step];
a[step] = x;
f(a, step + 1); // 递归确定下一位数字
// 还原交换,进行回溯
x = a[i];
a[i] = a[step];
a[step] = x;
}
}
}
区别:
当比较这三段代码时,我们可以更详细地分析它们的不同之处,包括实现方法、时间复杂度、空间复杂度等方面:
第一个代码(使用嵌套循环)
实现方法:使用了多层嵌套循环,每层循环对应方阵的一行,通过遍历所有可能的排列组合来判断是否符合条件。
时间复杂度:嵌套循环的时间复杂度为 O(n^6),因为有 9 个数字,6 层循环。
空间复杂度:除了几个整数变量外,没有额外的空间需求。
第二个代码(使用深度优先搜索 DFS)
实现方法:使用了递归和深度优先搜索,从第一个位置开始递归地填充数字,并在每次填充后判断是否符合条件。
时间复杂度:由于是使用深度优先搜索,时间复杂度依赖于符合条件的排列数量。在最坏情况下,会遍历所有可能的排列,因此时间复杂度为 O(n!)。
空间复杂度:使用了一个布尔数组来标记已经使用过的数字,空间复杂度为 O(n),其中 n 是方阵的大小。
第三个代码(使用递归和回溯)
实现方法:同样使用了递归,但在递归函数中采用了回溯的技巧,即在递归过程中交换元素来生成所有可能的排列组合。
时间复杂度:同样依赖于符合条件的排列数量,时间复杂度也是 O(n!),因为在最坏情况下会遍历所有可能的排列。
空间复杂度:同样使用了一个整数数组和一个布尔数组,空间复杂度也是 O(n)。
总体比较
时间复杂度:三段代码的时间复杂度在最坏情况下都是 O(n!),因为需要遍历所有可能的排列。但是第一个代码的嵌套循环方式相对更简单,不涉及递归和回溯,所以在实际执行中可能更快一些。
空间复杂度:三段代码的空间复杂度都是 O(n),因为都使用了一个整数数组和一个布尔数组来存储数字和标记是否使用过。
算法选择
如果方阵的大小较小(如 3x3),并且对性能要求不高,可以选择第一个代码(使用嵌套循环)。
如果方阵的大小较大(如 4x4 或更大),或者需要更高效的算法,可以选择第二个代码(使用深度优先搜索)或第三个代码(使用递归和回溯)。
总的来说,这三段代码解决的问题相同,但实现方法和性能特点不同。选择哪种方法取决于问题的规模和性能要求。