一、全排列初识
从 n 个不同元素中任取 m(m≤n)个元素,按照一定的顺序排列起来,叫做从 n 个不同元素中取出 m 个元素的一个排列。当 m=n 时所有的排列情况叫全排列。
二、问题引入
看了上面的定义还是一脸糊涂?不要紧,先看看下面的例子,你就会对全排列概念有个具体的认识:
对于一个给定的序列 a = [a1, a2, a3, … , an],请设计一个算法,用于输出这个序列的全部排列方式。
例如:a = [1, 2, 3]
输出
[1, 2, 3]
[1, 3, 2]
[2, 1, 3]
[2, 3, 1]
[3, 2, 1]
[3, 1, 2]
这就是一个简单的全排列问题,单纯看上面的例子,貌似看起来很简单,用眼盯着可能都能算对,但是如果数组a的值个数增加到十个百个呢,那手算出来的概率就变得很低很低了,这时候就要利用算法来解决这一类问题。
三、递归实现
我们先考虑两种情况,
1. 数组元素互不相同
如果数组a中所有元素都不相同,每个数值都是唯一的,这时候问题就简单了,只需要进行dfs算法深度优先搜索,就可以实现,看代码:
import java.util.Arrays;
public class 全排列 {
public static void main(String[] args) {
int[] a = {1, 2, 3};//无重复元素
allrange(a, 0, a.length - 1);
}
//全排列(递归回溯)
private static void allrange(int[] a, int cursor, int end) {
// 递归终止条件
// 已经到序列结尾了
if (cursor == end) {
System.out.println(Arrays.toString(a));
}
//初始 i=游标,因为 游标 之前的顺序已经确定了,不需要再排列了
for (int i = cursor; i <= end; i++) {
swap(a, cursor, i);//固定游标,让 i 值不断变化,去输出当前后面的各种顺序的排列
allrange(a, cursor + 1, end);
swap(a, cursor, i);// 回到交换之前的序列,这里可以理解为回溯,保证下一个值不受上一个值的影响(记忆抹去)
}
}
private static void swap(int[] a, int cursor, int i) {
int temp = a[cursor];
a[cursor] = a[i];
a[i] = temp;
}
}
输出
[1, 2, 3]
[1, 3, 2]
[2, 1, 3]
[2, 3, 1]
[3, 2, 1]
[3, 1, 2]
2. 数组内有重复元素
如果数组内有重复元素,我们就要让这几个重复的元素不进行互相交换,如果它们互相交换,可以很容易得知,他们交换后输出的数组的顺序都是一样的,所以交换并没有改变输出顺序,这个是无意义的。所以我们必须排除这种不必要的交换,可以将这个功能封装成一个方法完成,整个代码如下:
import java.util.Arrays;
public class 全排列 {
public static void main(String[] args) {
int[] a = {1, 2, 3,2};//有重复元素
allrange(a, 0, a.length - 1);
}
//全排列(递归回溯)
private static void allrange(int[] a, int cursor, int end) {
// 递归终止条件
// 已经到序列结尾了
if (cursor == end) {
System.out.println(Arrays.toString(a));
}
//初始 i=游标,因为 游标 之前的顺序已经确定了,不需要再排列了
for (int i = cursor; i <= end; i++) {
//如果不进行这一步,则有重复元素的数组可能会出现输出两个相同的情况(比如[1,2,2,3]输出两次)
//但是对于没有重复元素的数组,这一步可有可无
if (!judgeSwap(a, cursor, i)) {//看cursor到i之间是否有两个数组值相同,如果有,进入for让i递增,此i就不进行交换 (有相等,就不交换,就i递增进入下一项)
continue;
}
swap(a, cursor, i);//固定游标,让 i 值不断变化,去输出当前后面的各种顺序的排列
allrange(a, cursor + 1, end);
swap(a, cursor, i);// 回到交换之前的序列,这里可以理解为回溯,保证下一个值不受上一个值的影响(记忆抹去)
}
}
private static void swap(int[] a, int cursor, int i) {
int temp = a[cursor];
a[cursor] = a[i];
a[i] = temp;
}
//判断是否需要进行交换
//看cursor到i之间是否有两个数组值相同,如果有,返回 false,不交换,直接continue
private static boolean judgeSwap(int[] a, int cursor, int i) {
for (int j = cursor; j < i; j++) {
//若同一个数组出现两个相同的值,如果还交换的话,那就相当于会输出两次,也就是说相同的值会输出两次,重复了
if (a[j] == a[i]) { //有相等,就不交换
return false;
}
}
return true;
}
}
输出
[1, 2, 3, 2]
[1, 2, 2, 3]
[1, 3, 2, 2]
[2, 1, 3, 2]
[2, 1, 2, 3]
[2, 3, 1, 2]
[2, 3, 2, 1]
[2, 2, 3, 1]
[2, 2, 1, 3]
[3, 2, 1, 2]
[3, 2, 2, 1]
[3, 1, 2, 2]
上面已经将两种情况都用算法描述好了,看到这里,相信应该对全排列的方法基本掌握了,下面通过一道有关全排列的算法题对这一知识点进行巩固吧。
四、实例分析
问题描述
100 可以表示为带分数的形式:100 = 3 + 69258 / 714。
还可以表示为:100 = 82 + 3546 / 197。
注意特征:带分数中,数字 1~9 分别出现且只出现一次(不包含 0)。
类似这样的带分数,100 有 11 种表示法。
输入格式
从标准输入读入一个正整数 N (N<1000*1000)
输出格式
程序输出该数字用数码 1~9 不重复不遗漏地组成带分数表示的全部种数。
注意:不要求输出每个表示,只统计有多少表示法!
样例输入 1
100
样例输出 1
11
样例输入 2
105
样例输出 2
6
分析:
1)先写出全排列模板;(dfs)
2)再划分 " + 前,/前,/后 ",三个区域,然后对这三个区域进行计算。
- 划分区域需要把数组中的值取出来(
toInt()
) - 计算(从后往前)(注意:
res+=arr[i]*t;
t*=10;
,arr[i] 乘 10的n次方(arr[i]*10, arr[i]*100…) )
具体的解析过程在代码中都有体现,上代码:
public class 带分数 {
static int input;
static int count;
//static int res;错误(教训:少定义全局变量)
public static void main(String[] args) {
int []arr={1,2,3,4,5,6,7,8,9};
Scanner sc = new Scanner(System.in);
input=sc.nextInt();
dfs(arr,0);//因为已知是9位数,所以不用再定义end参数传递进去了
System.out.println(count);
}
static void dfs(int []arr,int k){ //因为已知是9位数,所以不用再定义end参数传递进去了
//因为这里不可重复,所以不用考虑判断是否交换(相当于刚刚的第一种情况:数组元素不重复)
if(k==9) {
check(arr);
//return;
}
for (int i = k; i <9; i++) {//i=k
swap(arr,k,i);//这里必须传递arr进去,否则是值参数,返回过来不改变实际值
dfs(arr,k+1);
swap(arr,k,i); //swap(arr[k],arr[i]);错误写法(必须传入 arr)
}
}
private static void check(int[] arr) {
//+前面的数最多有7个(最少留2位) // 100 = 3 + 69258 / 714
for(int i=0;i<=6;i++){//统一从0开始(避免后面计算数组值时混淆)
int beforeAdd=toInt(arr,0,i+1);//i+1 保证至少 +前 至少有1位
if(beforeAdd>=input) continue;//优化(效率)
// /前面的数 (包括 + 前面的数)( 7-i = 8-i-1 )共8位(0开始算)(最后至少留一位,-1)
for (int j = 0; j <=7-i; j++) {
int beforeMul=toInt(arr,i+1,j);//第三个参数是长度(i+1 ~ i+1 + j)
int afterMul=toInt(arr,i+j+1,8-i-j);
if(beforeAdd+beforeMul/afterMul==input && beforeMul%afterMul==0){ //计算
count++;
}
}
}
}
private static int toInt(int[] arr, int start, int len) {
int t=1;
//教训:少定义全局变量
int res=0;//res必须在这里定义,不能定义成全局static,因为每个数的res都不同,所以每次都要重新初始化
for (int i = start+len-1; i >= start; i--) { //下标,所以-1
res+=arr[i]*t;//arr[i] 乘 10的n次方(arr[i]*10, arr[i]*100, arr[i]*1000...)
t*=10;
}
return res;
}
static void swap(int []arr,int k, int i) {
int t=arr[k];
arr[k]=arr[i];
arr[i]=t;
}
}
到这里,相信对于全排列算法的理解一定有更进一步的提升,全排列算法在算法题中,包括各种竞赛题和面试题,也频繁出现,但全排列有一个相对固定的模板,而具体的案例都是基于模板进一步展开的,所以只要将全排列的模板牢记心中,大部分题目拿起来都会有思路进行求解的。