在数学中排列组合是两个经典的数学问题,排列组合是组合学最基本的概念。所谓排列,就是指从给定个数的元素中取出指定个数的元素进行排序。组合则是指从给定个数的元素中仅仅取出指定个数的元素,不考虑排序。
排列组合的中心问题是研究给定要求的排列和组合可能出现的情况总数。 排列组合与古典概率论关系密切。
排列的定义:从n个不同元素中,任取m(m≤n,m与n均为自然数,下同)个不同的元素按照一定的顺序排成一列,叫做从n个不同元素中取出m个元素的一个排列;从n个不同元素中取出m(m≤n)个元素的所有排列的个数,叫做从n个不同元素中取出m个元素的排列数,用符号 A(n,m)表示。
计算公式:
A n m = n ( n − 1 ) ( n − 2 ) . . . . ( n − m + 1 ) = n ! ( n − m ) ! A_n^m=n(n-1)(n-2)....(n-m+1)=\frac{n!}{(n-m)!} Anm=n(n−1)(n−2)....(n−m+1)=(n−m)!n!
组合的定义:从n个不同元素中,任取m(m≤n)个元素并成一组,叫做从n个不同元素中取出m个元素的一个组合;从n个不同元素中取出m(m≤n)个元素的所有组合的个数,叫做从n个不同元素中取出m个元素的组合数。用符号 C(n,m) 表示。
计算公式:
C n m = A n m m ! = n ! m ! ( n − m ) ! C_n^m=\frac{A_n^m}{m!}=\frac{n!}{m!(n-m)!} Cnm=m!Anm=m!(n−m)!n!
此外规定0! = 1,也就是说,对于组合,即使取出0个,也算作一种方法
排列组合作为古典数学中经典的数学问题,也经常被应用于编程实际中。
在编程中,我们共有两种方法可以实现:
一、按照数学公式进行编程。
二、依照排列组合的自身规律,用递归实现。
在此,仅介绍第二种方式。
例题:请列出"ABC"的所有排列顺序方式。
思路:
方法一:使用List,循环套递归的方式,将字符串拆解。外循环相当于“试探”,将所有字母作为第一,内嵌套递归相当于“拆解”,将除排在第一的字母外传入递归,排列第二字母,最后排列所有字母后返回最后一个字母,与前面的字母拼接并存入list,完成所有情况递归,实现全排列。
方法一:试探回溯法,将第 i 个字母与指定下标的字母进行交换,并将除第 i 个的所有字母拼接,送入下一层递归,下一层递归会做同样的事,直到指定下标变成最后一个字母,最后一个字母不可交换,于是打印此时交换完成的字符,返回上层,这时必须回溯,不然字符数组就混乱了,在切换第 i 字母时就会紊乱。
import java.util.LinkedList;
import java.util.List;
public class fullPermutationString {
public static List fullP(String s) {
List list = new LinkedList();
if(s.length() == 1) {
list.add(s);
return list;
}
for(int i = 0;i<s.length();i++) {
char locat = s.charAt(i);
List t = fullP(s.substring(0,i)+s.substring(i+1));
for(int j = 0;j<t.size();j++) {
list.add(""+locat+t.get(j));
}
}
return list;
}
public static void main(String[] args) {
String s = "ABC";
List l = fullP(s);
for(int i =0;i<l.size();i++) {
System.out.println(l.get(i));
}
}
}
方法二:
//字符串全排列无List实现
public class fullPermutationString_2 {
public static void fullP(char[] srr,int index) {
if(index == srr.length-1) {
System.out.println(String.valueOf(srr));
return;
}
for(int i = index;i<srr.length;i++) {
char temp = srr[index];//试探
srr[index] = srr[i];
srr[i] = temp;
fullP(srr,index+1);
temp = srr[index];//回溯
srr[index] = srr[i];
srr[i] = temp;
}
}
public static void main(String[] args) {
String s = "ABCD";
fullP(s.toCharArray(),0);
}
}
例题:从n个元素中取m个元素,有多少种取法?
思路:首先遇到排列组合,首先想到递归和公式,公式记不起来,首选递归。
那么我们如何转化问题为符合递归规律的问题呢?
我们可以假设n个元素中,有一个特殊的元素,不将其算入n个种,那么当取m个出来时,就会有两种情况:
一、特殊的没有被取出,不将其算入,则递归问题转化为n-1与m
二、特殊的被取出,不将其算入,则递归问题转化为n-1与m-1
此时,递归所要考虑的两个问题,已经解决一个了。
再考虑什么时候需要结束递归:
此时有两种极端情况:
一、当排除特殊的,全部取了不特殊的元素,m等于n时将无法再取,此时返回值为1(当m=n时只有一种取法)
二、当该取得m个全部取完,此时m=0,返回值为1(对于组合,即使取出0个,也算作一种方法)
public class rankAndGet {
public static int Get(int sum,int value) {
if(sum == value) return 1;
if(value == 0) return 1;
return Get(sum - 1,value)+Get(sum - 1,value - 1);
}
public static void main(String[] args) {
System.out.println(Get(5,3));
}
}
例题:从“ABCDE”中取3个,有多少种取法?
对于字符类组合选取,是有规律可循的,凡是从字符串中选取几个的组合类题目,都可以使用以下方法解读:
如题,使用List,循环套递归的方式,将字符串拆解。外循环相当于“试探”,将所有字母作为第一,内嵌套递归相当于“拆解”,由n计算拆解取出多少个,最后逐一返还拼接为一个3字母的字符串,才进入下一层循环。
import java.util.LinkedList;
import java.util.List;
public class combination {
public static List<String> Get(String s,int n) {
List<String> list = new LinkedList();
if(n == 0) {
list.add("");
return list;
}
for(int i = 0;i<s.length();i++) {
char locat = s.charAt(i);
List<String> temp = Get(s.substring(i+1),n-1);
for(int j = 0;j<temp.size();j++) {
list.add(""+locat+temp.get(j));
}
}
return list;
}
public static void main(String[] args) {
String s = "ABCDE";
List<String> list = Get(s,3);
for(int i = 0;i<list.size();i++) {
System.out.println(list.get(i));
}
}
}
例题:从“AABBBC”中取3个,有多少种取法?
同样是字符组合问题,却有一个特例,那就是组合重复。
如果按照字符组合的方式取,就会出现重复情况,因为使用List方法取得字符是逐位取得的,如果后面与前面字符有重复,就会有重叠的情况。
我们可以使用数字标记的方式,用数组记下各个字母出现的次数,用试探回溯法,用递归的参数下标作为标志,标记每种字母的所在下标,for循环的 i 作为要取得个数,将可以实现的组合存在一个新数组中,设置一个打印,打印出相应的组合内容即可。
//从“AABBBCCD”中取3个,有多少取法?
public class stringGetString {
public static void show(int[] time) {
for(int i = 0;i<time.length;i++) {
for(int j = 0;j<time[i];j++) {
System.out.print((char)('A'+i));
}
}
System.out.println();
}
public static void Get(int[] data,int[] time,int index,int goal) {
if(index == time.length) {
if(goal == 0)
show(time);
return;
}
for(int i = 0;i<= Math.min(data[index], goal);i++) {
time[index] = i;
Get(data,time,index+1,goal-i);
time[index] = 0;
}
}
public static void main(String[] args) {
int[] a = {2,3,2,1};
int[] time = new int[a.length];
Get(a,time,0,3);
}
}
排列组合问题,都可以套用两个方法进行解答:
一、使用List,循环拆解再归并的方式。
二、使用循环试探回溯,每次完成一个就打印一个。
【问题描述】 小明最近喜欢搭数字积木。一共有10块积木,每个积木上有一个数字,0~9。 搭积木规则:
每个积木放到其它两个积木的上面,并且一定比下面的两个积木数字小。 最后搭成4层的金字塔形,必须用完所有的积木。 下面是两种合格的搭法:0 1 2 3 4 5 6 7 8 9 0 3 1 7 5 2 9 8 6 4
请你计算这样的搭法一共有多少种?
思路:
首先对问题简化会发现,问题的要求就是对一串塔形数字,重新排列后,满足一定条件即可实现。
也就是说,我们不必要纠结与在实际编程中是否为金字塔型,可以设置为一维数组甚至为一个字符串。
由于数组输出时更易摆成金字塔型,在此使用数组,又是排列问题,而数组适用于试探回溯法,完成实现如下:
public class buildingBlocks {
static int SUM = 0;
public static void show(int[] block) {
System.out.println(" "+" "+" "+block[0]);
System.out.println(" "+" "+block[1]+" "+block[2]);
System.out.println(" "+block[3]+" "+block[4]+" "+block[5]);
System.out.println(block[6]+" "+block[7]+" "+block[8]+" "+block[9]);
}
public static void jurge(int[] block) {
if(block[0]>block[1]) return;
if(block[0]>block[2]) return;
if(block[1]>block[3]) return;
if(block[1]>block[4]) return;
if(block[2]>block[4]) return;
if(block[2]>block[5]) return;
if(block[3]>block[6]) return;
if(block[3]>block[7]) return;
if(block[4]>block[7]) return;
if(block[4]>block[8]) return;
if(block[5]>block[8]) return;
if(block[5]>block[9]) return;
show(block);
SUM++;
}
public static void building(int[] block,int index) {
if(index == block.length-1) {
jurge(block);
return;
}
for(int i = index;i<block.length;i++) {
int temp = block[index];
block[index] = block[i];
block[i] = temp;
building(block,index+1);
temp = block[index];
block[index] = block[i];
block[i] = temp;
}
}
public static void main(String[] args) {
int[] a = {0,1,2,3,4,5,6,7,8,9};
building(a,0);
System.out.println("sum is"+SUM);
}
}
【问题描述】X星球要派出一个5人组成的观察团前往W星。
其中:
A国最多可以派出4人。
B国最多可以派出2人。
C国最多可以派出2人。
D国最多可以派出1人。
E国最多可以派出1人。
F国最多可以派出3人。
那么最终派往W星的观察团会有多少种国别的不同组合呢?
思路:典型的组合重复问题,按照套路走即可。
public class delegationVisit {
public static void Show(int[] value) {
for(int i = 0;i<value.length;i++) {
System.out.print((char)('A'+i)+"国:"+value[i]+" ");
}
System.out.println();
}
public static void Get(int[] data,int[] value,int index,int goal) {
if(index == data.length) {
if(goal == 0)
Show(value);
return;
}
for(int i = 0;i<=Math.min(data[index], goal);i++) {
value[index] = i;
Get(data,value,index+1,goal-i);
}
value[index] = 0;
}
public static void main(String[] args) {
int[] data = {4,2,2,1,1,3};
int[] value = new int[data.length];
Get(data,value,0,5);
}
}
【问题描述】A A 2 2 3 3 4 4, 一共4对扑克牌。请你把它们排成一行。
要求:两个A中间有1张牌,两个2之间有2张牌,两个3之间有3张牌,两个4之间有4张牌。
请填写出所有符合要求的排列中,字典序最小的那个。
例如:22AA3344 比 A2A23344 字典序小。当然,它们都不是满足要求的答案。
思路:典型的排列问题,按全排列形式模板编程,if 筛选出需要的排列。
由于存在重复的问题在这里使用set,可以去除重复部分。
在此用的是List方法,也可以选用试探回溯法。
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
public class pokerSequence {
static Set set = new TreeSet();
public static List<String> Rank(String s){
List list = new LinkedList();
if(s.length() == 1) {
list.add(s);
return list;
}
for(int i = 0;i<s.length();i++) {
char c = s.charAt(i);
List<String> temp = Rank(s.substring(0,i)+s.substring(i+1));
for(int j = 0;j<temp.size();j++) {
list.add(""+c+temp.get(j));
}
}
return list;
}
public static void main(String[] args) {
String s = "AA223344";
List<String> list = Rank(s);
for(int i = 0;i<list.size();i++) {
if(list.get(i).lastIndexOf('A')-list.get(i).indexOf('A') == 2) {
if(list.get(i).lastIndexOf('2')-list.get(i).indexOf('2') == 3) {
if(list.get(i).lastIndexOf('3')-list.get(i).indexOf('3') == 4) {
if(list.get(i).lastIndexOf('4')-list.get(i).indexOf('4') == 5) {
set.add(list.get(i));
}
}
}
}
}
for(Object a:set) {
System.out.println(a);
}
}
}