排列组合问题回溯法、递归法解决

排列组合问题

问题描述

📕用户输入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)
解题思路

🌷递归方法只要找到递归口,还有想清楚当前阶段所要完成的事情就可以很容易的写出代码。
🌷当前阶段完成的事情:

  1. 为当前阶段赋值
  2. 判断当前取值是否合法
  3. 如果合法并且达到阶段m,输出方案
  4. 如果合法并且未达到阶段m,则为下一个阶段赋值,即调用该函数。
  5. 如果不合法当前阶段取值加一。

🌷递归出口: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) 。
🌂向上走👆,本层函数执行完毕,会自动回调,知道回到第一层函数并且执行完第一次函数递归函数才会结束。如果此处不是很明白,可以通过调试程序,来看一下函数的执行过程。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值