排列组合算法
排列:从n个不同元素中,任取m(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)! 此外规定0!=1
组合:从n个不同元素中,任取m(m<=n)个元素并成一组,叫做从n个不同元素中取出m个元素的一个组合;从n个不同元素中取出m(m<=n)个元素的所有组合的个数,叫做从n个不同元素中取出m个元素的组合数。用符号C(n,m) 表示。 C(n,m)=A(n,m)/m!=n!/((n-m)!*m!); C(n,m)=C(n,n-m)。
排列组合是组合数学中的基础。排列就是指从给定个数的元素中取出指定个数的元素进行排序;组合则是指从给定个数的元素中仅仅取出指定个数的元素,不考虑排序。排列组合的中心问题是研究给定要求的排列和组合可能出现的情况总数。排列组合与古典概率论关系密切。
在高中初等数学中,排列组合多是利用列表、枚举等方法解题
以下一些实例作为讲解:
排列
给定一组不同的数字,返回所有可能的排列。 举个例子, [1,2,3]有以下排列:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
思路
对于nums数组中的每一个数,都依次放入结果集中,如果结果集中已经包含这个数,就继续下一次循环。
以数组[1,2,3]为例,每次循环的结果是:
[1,2,3]
[1,3,2]
[2,1,3]
[2,3,1]
[3,1,2]
[3,2,1]
代码:
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
backtrack(list, new ArrayList<>(), nums);
return list;
}
private void backtrack(List<List<Integer>> list, List<Integer> tempList, int [] nums){
if(tempList.size() == nums.length){
list.add(new ArrayList<>(tempList));
} else{
for(int i = 0; i < nums.length; i++){
if(tempList.contains(nums[i])) continue; // element already exists, skip
tempList.add(nums[i]);
backtrack(list, tempList, nums);
tempList.remove(tempList.size() - 1);
}
}
}
排列II 给定一个可能包含重复项的数字集合,返回所有可能的唯一排列。
举个例子, [1,1,2]有以下独特的排列:
[
[1,1,2],
[1,2,1],
[2,1,1]
]
思路
这道题比上一道题多了一个条件,即数组中有重复的数。有两种思路:
- 仍然按照上一道题的解法,但是把结果用set保存,最终转换成list。
- 考虑数组中有相同的数,规定必须按照从前到后的顺序使用数字,即数组[1,1],在组合时,必须先使用第一个1,才能再使用第二个1,这样就避免了结果集重复的情况。
代码:
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
Arrays.sort(nums);
backtrack(list, new ArrayList<>(), nums, new boolean[nums.length]);
return list;
}
private void backtrack(List<List<Integer>> list, List<Integer> tempList, int [] nums, boolean [] used){
if(tempList.size() == nums.length){
list.add(new ArrayList<>(tempList));
} else{
for(int i = 0; i < nums.length; i++){
if(used[i] || i > 0 && nums[i] == nums[i-1] && !used[i - 1]) continue;
used[i] = true;
tempList.add(nums[i]);
backtrack(list, tempList, nums, used);
used[i] = false;
tempList.remove(tempList.size() - 1);
}
}
}
子集
给定一组不同的整数,nums,返回所有可能的子集。 注意:解决方案集不能包含重复的子集。 举个例子, 如果nums = [1,2,3],一个解是:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
代码:
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
Arrays.sort(nums);
backtrack(list, new ArrayList<>(), nums, 0);
return list;
}
private void backtrack(List<List<Integer>> list , List<Integer> tempList, int [] nums, int start){
list.add(new ArrayList<>(tempList));
for(int i = start; i < nums.length; i++){
tempList.add(nums[i]);
backtrack(list, tempList, nums, i + 1);
tempList.remove(tempList.size() - 1);
}
}
子集II
给定一个可能包含重复项的整数集合,num返回所有可能的子集。 注意:解决方案集不能包含重复的子集。 举个例子, 如果nums = [1,2,2],一个解是:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]
思路
处理重复的数,和上面是一个思路,即只允许用前面的数字。
代码:
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
Arrays.sort(nums);
backtrack(list, new ArrayList<>(), nums, 0);
return list;
}
private void backtrack(List<List<Integer>> list, List<Integer> tempList, int [] nums, int start){
list.add(new ArrayList<>(tempList));
for(int i = start; i < nums.length; i++){
if(i > start && nums[i] == nums[i-1]) continue; // skip duplicates
tempList.add(nums[i]);
backtrack(list, tempList, nums, i + 1);
tempList.remove(tempList.size() - 1);
}
}
组合和
给定一组候选数©(没有重复)和一个目标数(T),找出C中所有候选数总和为T的唯一组合。 相同的重复数可以从C中无限次选择。 注意: 所有数字(包括target)都是正整数。 解决方案集不得包含重复的组合。 例如,给定候选集[2,3,6,7]和目标7, 解决方案集是:
[
[7],
[2, 2, 3]
]
思路
和Subsets是同一个思路,只不过这次不是求子集,而是加上了限制条件:和为指定的值。
代码:
public List<List<Integer>> combinationSum(int[] nums, int target) {
List<List<Integer>> list = new ArrayList<>();
Arrays.sort(nums);
backtrack(list, new ArrayList<>(), nums, target, 0);
return list;
}
private void backtrack(List<List<Integer>> list, List<Integer> tempList, int [] nums, int remain, int start){
if(remain < 0) return;
else if(remain == 0) list.add(new ArrayList<>(tempList));
else{
for(int i = start; i < nums.length; i++){
tempList.add(nums[i]);
backtrack(list, tempList, nums, remain - nums[i], i); // not i + 1 because we can reuse same elements
tempList.remove(tempList.size() - 1);
}
}
}
C++经典算法:
将一组数字、字母或符号进行排列,以得到不同的组合顺序,例如1 2 3这三个数的排列组合有:
1 2 3、1 3 2、2 1 3、2 3 1、3 1 2、3 2 1。
解法
可以使用递回将问题切割为较小的单元进行排列组合,例如1 2 3 4的排列可以分为
1 [2 3 4]、2 [1 3 4]、3 [1 2 4]、4 [1 2 3]
进行排列,这边利用旋转法,先将旋转间隔设为0,将最右边的数字旋转至最左边,并逐步增加旋转的间隔,例如:
1 2 3 4 -> 旋转1 -> 继续将右边2 3 4进行递回处理
2 1 3 4 -> 旋转1 2 变为 2 1-> 继续将右边1 3 4进行递回处理
3 1 2 4 -> 旋转1 2 3变为 3 1 2 -> 继续将右边1 2 4进行递回处理
4 1 2 3 -> 旋转1 2 3 4变为4 1 2 3 -> 继续将右边1 2 3进行递回处理
代码示例
#include <stdio.h>
#include <stdlib.h>
#define N 4
void perm(int*, int); int main(void) {
int num[N+1], i;
for(i = 1; i <= N; i++) num[i] = i;
perm(num, 1);
return 0;
}
void perm(int* num, int i) { int j, k, tmp;
if(i < N) {
for(j = i; j <= N; j++) { tmp = num[j];
// 旋转该区段最右边数字至最左边
for(k = j; k > i; k--)
num[k] = num[k-1]; num[i] = tmp; perm(num, i+1);
// 还原
for(k = i; k < j; k++) num[k] = num[k+1];
num[j] = tmp;
}
}
else { // 显示此次排列
for(j = 1; j <= N; j++) printf("%d ", num[j]);
printf("\n");
}
}
python排列组合解决:
def c(n,m,out):
if(m==0):
return 1
x=n
while x>=m:
out.append(x)
if(c(x-1,m-1,out)):
print out
out.pop()
x-=1
return 0
c(10,8,out=[])
def permutation(listobj, length):
assert listobj != None and 0 < length <= len(listobj)
if(length == 1):
return [ [x] for x in listobj ]
result = []
for i in range(len(listobj)):
cp = list(listobj)
cur = cp[i]
del cp[i]
result.extend( [cur] + x for x in permutation(cp, length-1) )
return result
n个取m个数的组合数问题
C#实现排列组合算法:
using System;
using System.Collections.Generic;
namespace Test
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(P1(6, 3));
Console.WriteLine(P2(6, 3));
Console.WriteLine(C(6, 2));
}
/// <summary>
/// 排列循环方法
/// </summary>
/// <param name="N"></param>
/// <param name="R"></param>
/// <returns></returns>
static long P1(int N, int R)
{
if (R > N || R <= 0 || N <= 0 ) throw new ArgumentException("params invalid!");
long t = 1;
int i = N;
while (i!=N-R)
{
try
{
checked
{
t *= i;
}
}
catch
{
throw new OverflowException("overflow happens!");
}
--i;
}
return t;
}
/// <summary>
/// 排列堆栈方法
/// </summary>
/// <param name="N"></param>
/// <param name="R"></param>
/// <returns></returns>
static long P2(int N, int R)
{
if (R > N || R <= 0 || N <= 0 ) throw new ArgumentException("arguments invalid!");
Stack<int> s = new Stack<int>();
long iRlt = 1;
int t;
s.Push(N);
while ((t = s.Peek()) != N - R)
{
try
{
checked
{
iRlt *= t;
}
}
catch
{
throw new OverflowException("overflow happens!");
}
s.Pop();
s.Push(t - 1);
}
return iRlt;
}
/// <summary>
/// 组合
/// </summary>
/// <param name="N"></param>
/// <param name="R"></param>
/// <returns></returns>
static long C(int N, int R)
{
return P1(N, R) / P1(R, R);
}
}
}
字符串排列组合
问题1 :输入一个字符串,打印出该字符串中字符的所有排列。例如输入字符串abc,则输出由字符a、b、c所能排列出来的所有字符串abc、acb、bac、bca、cab和cba。
思路:这是个递归求解的问题。递归算法有四个特性:(1)必须有可达到的终止条件,否则程序将陷入死循环;(2)子问题在规模上比原问题小;(3)子问题可通过再次递归调用求解;(4)子问题的解应能组合成整个问题的解。
对于字符串的排列问题。如果能生成n - 1个元素的全排列,就能生成n个元素的全排列。对于只有1个元素的集合,可以直接生成全排列。全排列的递归终止条件很明确,只有1个元素时。下面这个图很清楚的给出了递归的过程。
参考代码:解法1通过Permutation_Solution1(str, 0, n); 解法2通过调用Permutation_Solution2(str, str)来求解问题。
//函数功能 : 求一个字符串某个区间内字符的全排列
//函数参数 : pStr为字符串,begin和end表示区间
//返回值 : 无
void Permutation_Solution1(char *pStr, int begin, int end)
{
if(begin == end - 1) //只剩一个元素
{
for(int i = 0; i < end; i++) //打印
cout<<pStr[i];
cout<<endl;
}
else
{
for(int k = begin; k < end; k++)
{
swap(pStr[k], pStr[begin]); //交换两个字符
Permutation_Solution1(pStr, begin + 1, end);
swap(pStr[k],pStr[begin]); //恢复
}
}
}
//函数功能 : 求一个字符串某个区间内字符的全排列
//函数参数 : pStr为字符串,pBegin为开始位置
//返回值 : 无
void Permutation_Solution2(char *pStr, char *pBegin)
{
if(*pBegin == '\0')
{
cout<<pStr<<endl;
}
else
{
char *pCh = pBegin;
while(*pCh != '\0')
{
swap(*pBegin, *pCh);
Permutation_Solution2(pStr, pBegin + 1);
swap(*pBegin, *pCh);
pCh++;
}
}
}
//提供的公共接口
void Permutation(char *pStr)
{
Permutation_Solution1(pStr, 0, strlen(pStr));
//Permutation_Solution2(pStr,pStr);
}
问题2:输入一个字符串,输出该字符串中字符的所有组合。举个例子,如果输入abc,它的组合有a、b、c、ab、ac、bc、abc。
思路:同样是用递归求解。可以考虑求长度为n的字符串中m个字符的组合,设为C(n,m)。原问题的解即为C(n, 1), C(n, 2),…C(n, n)的总和。对于求C(n, m),从第一个字符开始扫描,每个字符有两种情况,要么被选中,要么不被选中,如果被选中,递归求解C(n-1, m-1)。如果未被选中,递归求解C(n-1, m)。不管哪种方式,n的值都会减少,递归的终止条件n=0或m=0。
//函数功能 : 从一个字符串中选m个元素
//函数参数 : pStr为字符串, m为选的元素个数, result为选中的
//返回值 : 无
void Combination_m(char *pStr, int m, vector<char> &result)
{
if(pStr == NULL || (*pStr == '\0'&& m != 0))
return;
if(m == 0) //递归终止条件
{
for(unsigned i = 0; i < result.size(); i++)
cout<<result[i];
cout<<endl;
return;
}
//选择这个元素
result.push_back(*pStr);
Combination_m(pStr + 1, m - 1, result);
result.pop_back();
//不选择这个元素
Combination_m(pStr + 1, m, result);
}
//函数功能 : 求一个字符串的组合
//函数参数 : pStr为字符串
//返回值 : 无
void Combination(char *pStr)
{
if(pStr == NULL || *pStr == '\0')
return;
int number = strlen(pStr);
for(int i = 1; i <= number; i++)
{
vector<char> result;
Combination_m(pStr, i, result);
}
}