文章目录
排列组合问题
问题描述
📕用户输入n和m,实现排列A(n, m) 和组合C(n, m) ,打印所有排列情况和组合情况。
举例:输入3, 2:
输出:
A (3, 2)
[[1, 2], [1, 3], [2, 1], [2, 3], [3, 1], [3, 2]]
C (3, 2)
[[1, 2], [1, 3], [2, 3]]
解决方法
回溯法
A (n, m)
解题思路
🍀从n个数中取m个数,m个数是不可以重复的,并且这m个数顺序不同,视为不同的方案。
🍀每次取一个数,把他称为一个阶段,每个阶段的取值范围是1倒n,一共又m个阶段。
🍀i表示阶段数,a[ i ]记录阶段i的取值。
🍀每次阶段i都从1开始取值,如果阶段i是第m个阶段,并且阶段i的取值合法,即阶段i的取值与之前阶段的取值不相同,则输出此次方案。
🍀如果阶段i的取值合法,并且没有达到第m个阶段,则为阶段i + 1赋值。
🍀如果阶段i的取值不合法,则阶段i的取值加一,即a[i] + 1。
🍀如果阶段i取值等于n,则该阶段取值结束,需要向上回溯,即对阶段i - 1进行取值。
🍀如果第一个阶段等于n,赋值结束。
代码
//数组a为成员变量
public static void fun(int n, int m) {
//第一阶段初始
int i = 1;
a[i] = 1;
while (true) {
boolean flag = true;
for (int j = 0; j < i; j++) {
//判断当前阶段取值是否合法
if (a[j] == a[i]) {
flag = false;
break;
}
}
//如果达到阶段m,并且取值合法,则输出方案,数组下标从1到m
if (flag && i == m) {
//toString方法为自定义方法
System.out.println(toString(a, 1, m));
sum++;
}
//如果没有达到m阶段,但是取值合法,则对下一个阶段取值
if (flag && i < m) {
i++;
a[i] = 1;//每个阶段赋值从1开始
continue;//赋值之后,必须判断取值是否合法
}
//如果阶段i取值等于n,向上回溯
while (a[i] == n && i > 1) {
i--;
}
//如果阶段i取值为n,循环结束,退出循环
if (i == 1 && a[i] == n) {
break;
}
else {
//阶段取值不合法,取值加一
a[i] += 1;
}
}
}
C (n, m)
解题思路
❀C (n, m)和A (n, m)的解题思路基本相同。不同的是在C(n, m)中,m个数顺序不同视为同种情况。即[1, 2, 3]和[1, 3, 2]为一种情况。所以,只需要按一种顺序去遍历即可,此处选择从小到大的顺序遍历,当然也可以选择从大到小的顺序。
❀当前阶段的取值只要大于之前所有阶段的取值即可,此处也可以简化,每个阶段取值大于前一个阶段取值即可,第一个阶段没有前一个阶段,所以第一阶段的取值一定是合法的。
❀对阶段i的取值,其实可以从i开始,因为阶段i的取值要大于之前阶段的取值,所以当取值小于i时,一定是不合法的(一定不和法,但是不一定合法)。当然也可以从1开始。
代码
public static void fun(int n, int m) {
//第一阶段赋值
int i = 1;
a[i] = 1;
//注意循环的条件,后面有解释
while (i > 0) {
boolean flag = true;
//从第二阶段开始,判断当前阶段取值是否合法
if (i > 1 && a[i] <= a[i - 1]) {
flag = false;
}
if (flag && i == m) {
System.out.println(toString(a, 1, m));
sum++;
}
if (flag && i < m) {
i++;
//这里赋值可以直接从i开始。
a[i] = 1;
continue;
}
//当阶段取值等于n时,该阶段结束,当i取值为1,i会减成0,此时所有情况遍历完成,退出循环,所以while循环的条件时i>0
while (a[i] == n){
i--;
}
a[i]++;
}
}
C (n, m)算法改进
解题思路
接下来,对C (n, m)算法进行改进。尝试缩小每个阶段的取值范围。
🐟除了第一阶段外,每个阶段都得大于前一个阶段的取值,所以每个阶段的取值范围都可以从上一个阶段取值加一开始,即a[i - 1] + 1。
🐟每个阶段的取值都可以取到n吗?显然不是。可以用C (4, 3)来举例,从4个数中选择3个,第一阶段的取值只有1和2,如果第一阶段取3,那么第二阶段就得取4,第三个阶段无法取值。每个阶段的取值范围都是到n - m + i的。
🐟限制了阶段的取值范围,所以每个阶段的取值一直都是合法的,代码中可以省略合法判断。
代码
public static void fun(int n, int m) {
int i = 1;
a[i] = 1;
while(i > 0) {
if (i == m) {
System.out.println(print(a, 1, m));
sum++;
}
if (i < m) {
i++;
a[i] = a[i - 1] + 1;
continue;
}
//每个阶段最大到达n-m+i,该阶段取值结束,向上回溯。
//注意i必须大于0,如果没有这个条件,n=m并且i=0时a[i] = n - n + 0 = 0,满足循环条件,会进入while循环,i--,出现i = -1的情况。
while (a[i] == n - m + i && i > 0) {
i--;
}
a[i]++;
}
}
递归法
A (n, m)
解题思路
🌷递归方法只要找到递归口,还有想清楚当前阶段所要完成的事情就可以很容易的写出代码。
🌷当前阶段完成的事情:
- 为当前阶段赋值
- 判断当前取值是否合法
- 如果合法并且达到阶段m,输出方案
- 如果合法并且未达到阶段m,则为下一个阶段赋值,即调用该函数。
- 如果不合法当前阶段取值加一。
🌷递归出口:i>m时,本层函数结束。
代码
public static void fun(int k, int n, int m) {
//递归出口
if(k > m) {
return;
}
//为当前阶段赋值
for (int i = 1; i <= n; i++) {
a[k] = i;
boolean flag = true;
//取值合法判断
for (int j = 1; j < k; j++) {
if (a[k] == a[j]) {
flag = false;
break;
}
}
if (flag) {
//输出方案
if (k == m) {
System.out.println(toString(a, 1, m));
sum++;
}
else {
//进行下一个阶段
fun(k + 1, n, m);
}
}
}
}
C (n, m)
解题思路
C (n, m)只有判断取值是否合法和A (n, m)不同,其他完全相同。
这里,换一种递归出口,如果阶段i小于等于m,阶段i进行赋值,判断等一系列操作。其实,本质上还和原来的递归出口一样。
代码
public static void fun(int k, int n, int m) {
if (k <= m) {
for (int i = 1; i <= n; i++) {
a[k] = i;
boolean flag = true;
//取值合法判断
if (k > 1 && a[k] <= a[k - 1]) {
flag = false;
}
if (flag) {
//输出方案
if (k == m) {
System.out.println(toString(a, 1, m));
sum++;
}
//下一阶段
else {
fun(k + 1, n, m);
}
}
}
}
}
C (n, m)算法改进
解题思路
这里改进的还是取值范围。
此处解题思路和回溯法的思路相同。
代码
public static void fun(int k, int n, int m) {
if (k > m) {
return;
}
//为阶段i赋值
for (int i = a[k - 1] + 1; i <= n - m + k; i++) {
a[k] = i;
//未达到阶段m,进行下一个阶段
if (k < m) {
fun(k + 1, n, m);
}
if (k == m) {
System.out.println(toString(a, 1, m));
sum++;
}
}
}
总结
对回溯法和递归法解决排列组合问题做一个总结。
回溯法:
💧把阶段i取值的变化称为向右走👉,对应代码a[i]++。
💧把下一阶段取值称为向下走👇,对应代码i++; a[i] = i。
💧把回溯的过程称为向上走👆,对应代码i–。
在递归方法中也执行了这些操作。
🌂向右走👉,刚开始对阶段i赋值时,就是一个范围,通过for循环完成向右走👉的过程,对应代码for (int i = 1; i <= n; i++) { }。
🌂向下走👇,就是对函数递归调用的过程,对应代码fun(k + 1, n, m) 。
🌂向上走👆,本层函数执行完毕,会自动回调,知道回到第一层函数并且执行完第一次函数递归函数才会结束。如果此处不是很明白,可以通过调试程序,来看一下函数的执行过程。