一、排列
1.1 生成全排列
所谓排列,就是指从给定个数的元素中取出指定m个数的元素进行排序,其中0<=m<=n,当m=n时,则为称之为全排列。对于一个元素个数为n的集合,它的全排列个数T=n*(n-1)*(n-2)*...*2*1=n!。那么我们该如何编程输出这n!个排列呢。
方案1:
首先最容易想到的便是按照排列的定义进行枚举,即用一个大小为n的布尔数组标识集合中对应下表的元素是否已经占用。假设当前排列的1到k的位置的元素已经确定,那么在确定k+1位置上的元素时,根据布尔数组的标识情况,选择一个尚未在前k个元素中出现的元素作为当前元素,然后设置标志位为true。这样依次遍历所有的可能性。根据这个思路,代码则很容易实现。然而这种暴力的方法的时间复杂度是指数级的,空间复杂度是O(n)。
方案2:
利用递归的思想,假设我们已经有了1..n-1的全排列,那么当我们需要求1..n的全排列时,我们只需要将n一次插入1..n-1的每一个排列的所有位置即可。这种递归的思想实现步骤如下:
1.固定第一个元素,递归求2..n的全排列
2.将第一个元素与第二个元素交换,递归求1,3..n的全排列
3.将第一个元素与第三个元素交换,递归求1,2,4..n的全排列
.....直到将第一个元素与第n个元素交换,递归求1..n-1的全排列
我们以三个字符abc为例来分析一下求字符串排列的过程。首先我们固定第一个字符a,求后面两个字符bc的排列。当两个字符bc的排列求好之后,我们把第一个字符a和后面的b交换,得到bac,接着我们固定第一个字符b,求后面两个字符ac的排列。现在是把c放到第一位置的时候了。记住前面我们已经把原先的第一个字符a和后面的b做了交换,为了保证这次c仍然是和原先处在第一位置的a交换,我们在拿c和第一个字符交换之前,先要把b和a交换回来。在交换b和a之后,再拿c和处在第一位置的a进行交换,得到cba。我们再次固定第一个字符c,求后面两个字符b、a的排列。(参考http://zhedahht.blog.163.com/blog/static/254111742007499363479/)
这种思路的代码实现直接上大神的代码了,具体参见http://zhedahht.blog.163.com/blog/static/254111742007499363479/:
void Permutation(char* pStr, char* pBegin);
/
// Get the permutation of a string,
// for example, input string abc, its permutation is
// abc acb bac bca cba cab
/
void Permutation(char* pStr)
{
Permutation(pStr, pStr);
}
/
// Print the permutation of a string,
// Input: pStr - input string
// pBegin - points to the begin char of string
// which we want to permutate in this recursion
/
void Permutation(char* pStr, char* pBegin)
{
if(!pStr || !pBegin)
return;
// if pBegin points to the end of string,
// this round of permutation is finished,
// print the permuted string
if(*pBegin == '\0')
{
printf("%s\n", pStr);
}
// otherwise, permute string
else
{
for(char* pCh = pBegin; *pCh != '\0'; ++ pCh)
{
// swap pCh and pBegin
char temp = *pCh;
*pCh = *pBegin;
*pBegin = temp;
Permutation(pStr, pBegin + 1);
// restore pCh and pBegin
temp = *pCh;
*pCh = *pBegin;
*pBegin = temp;
}
}
}
方案3:
当然我们也可以利用非递归的方式来实现。主要的思想和步骤如下:
考虑{1,2…n}的一个排列,其上每一个整数都给了一个方向,我们称整数k是可移的(Mobile&Active),如果它的箭头所指的方向的邻点小于它本身。例如
中6、3、5都是可移的。显然1永远不可移,n除了以下两种情形外,它都是可移的:
(1) n是第一个数,且其方向指向左侧
(2) n是最后一个数,且其方向指向右侧
于是,我们可由
按如下算法产生所有排列
算法
1,开始时:
2,当存在可移数时
enum Direction
{
LEFT,
RIGHT
};
void printPermutation(int *arr, int n)
{
for(int i = 0; i < n; i++)
{
if(i == 0)
{
cout << arr[i];
}
else
{
cout << " " << arr[i];
}
}
cout << endl;
}
int getNextMovable(int *arr, Direction* direct, int n)
{
int index = -1; //the position of max exchangeable element
int maxMovable = INT_MIN; //the max exchangeable element
for(int i = 0; i < n; i++)
{
if(direct[i] == LEFT)
{
if(i==0 || arr[i-1]>arr[i]) continue;
if(arr[i] > maxMovable)
{
maxMovable = arr[i];
index = i;
}
}
else
{
if(i==n-1 || arr[i+1]>arr[i]) continue;
if(arr[i] > maxMovable)
{
maxMovable = arr[i];
index = i;
}
}
}//fin for
return index;
}
template<class T>
void swap(T& a, T& b)
{
int t = a;
a = b;
b = t;
}
void permutation(int *arr, Direction* direct, int n)
{
int index = -1;
while( (index = getNextMovable(arr, direct, n)) != -1)
{
printPermutation(arr, n);
int m = arr[index];
//swap the element on position index with its neighbor, directed by its arrow
if(direct[index] == LEFT)
{::swap(arr[index], arr[index-1]); ::swap(direct[index], direct[index-1]);}
else
{::swap(arr[index], arr[index+1]); ::swap(direct[index], direct[index+1]);}
//reverse the arrow of all the elements that bigger than m
for(int i = 0; i < n; i++)
{
if(arr[i] > m)
{
if(direct[i] == LEFT) direct[i] = RIGHT;
else direct[i] = LEFT;
}
}//fin for
}//fin while
printPermutation(arr, n);
}
void permutation(int *arr, int n )
{
Direction *direct = new Direction[n];
for(int i = 0; i < n; i++) direct[i] = LEFT;
permutation(arr, direct, n);
delete[] direct;
}
1.2 根据给定的一个排列输出其按字母序的下一个排列
我们知道,在STL中有一个next_permutation函数,它能够返回给定排列的下一个按字母序的下一个排列。它的实现步骤如下:
设P是[1,n]的一个全排列,通过以下步骤获得其下一个排列,
1. P=P1P2…Pn=P1P2…Pj-1PjPj+1…Pk-1PkPk+1…Pn
j=max{i|Pi<Pi+1}, k=max{i|Pi>Pj}
2. 对换Pj,Pk,将Pj+1…Pk-1PjPk+1…Pn翻转,
3. P’= P1P2…Pj-1PkPn…Pk+1PjPk-1…Pj+1即P的下一个。
代码实现如下:
/**
*generate the next permutation from current permutation
*/
bool nextPermutation(int *arr, int n)
{
if(arr == NULL || n == 0) return false;
int k, l, i;
//find the k = max{i|arr[i]<arr[i+1]}
//if k doesn't exist, then current
//permutation will be the last permutation
for(i = n-2; i >= 0; i--)
{
if(arr[i]<arr[i+1]) break;
}
if(i < 0) return false; //this k does not exist
k = i;
//find the l = max{i|arr[i]>arr[k]}
for(i = n-1; i > l; i--)
{
if(arr[i]>arr[k]) break;
}
l = i;
//swap arr[k] and arr[l]
int t = arr[k];
arr[k] = arr[l];
arr[l] = t;
//reverse the order between k+1 to n-1
int low = k+1, high = n-1;
while(low < high)
{
int t = arr[low];
arr[low] = arr[high];
arr[high] = t;
low++,high--;
}
return true;
}
二、组合
组合则是指从给定个数的元素中仅仅取出指定个数的元素,不考虑排序。对于一个包含有n个元素的集合,它的所有子集的个数为2^n个,其中包括空集和其自身。
2.1 组合的指定元素个数的子集个数
给定一个含n个元素的的集合,求其包含r个元素的真子集的个数T。我们知道T=C(n,r)的问题,即从一个函n个元素的集合中选取r个元素出来,有多少种选法。根据组合知识我们知道这个值的求值公式为:
然而我们却不能直接利用这个公式来求T,因为这里涉及到了阶乘的操作。而求阶乘涉及到大量的乘法运算,所以运算效率不高。并且,当n稍微大一些时,会导致数据溢出。根据组合的性质,我们有如下递推公式,
利用这个递推公式我们便能够很容易写出求C(n,r)的代码了,但是由于直接的递归形式会导致大量的重复计算,所以以下给出非递归的形式代码:
long getSubCount(int n, int r)
{
r = (r>n-r)?(n-r):r; //Since C(n,r) = C(n,n-r),so we use the smaller value between r and n-r to speed up
if(r == 0 || r == n) return 1;
if(r == 1) return n;
vector<vector<long> > c(n+1); //C[i][j] = C(i,j)
int i,j;
for(i = 1; i <= n; i++)
{
c[i].resize(r+1);
c[i][0] = 1;
c[i][1] = (long)i;
}
for(i = 2; i <= n; i++)
{
for(j = 2; j <= r; j++)
{
if(j > i) break;
if(j == i) {c[i][j] = 1; break;}
c[i][j] = c[i-1][j] + c[i-1][j-1];
}
}
return c[n][r];
}
2.2 集合的所有子集
给定一个包含n个元素的集合S,要求其所有子集,包括空集和起自身。这个问题有多种求解方式。
方案一:
我们知道集合S的子集个数总共有2^n个。一看到2^n,便很容易联想到包含n位的二进制,因为它正好能够表示2^n个数。这样我们便可以利用一个包含n为的二进制数来求解这个问题,对于一个数,假设对应位为1,则将S中对应位置的元素添加至子集中;否则S中对应位置的元素则不在该子集中。即每一个数都代表S的一个子集所包含的元素。如0则表示空集,所有位为1则代表集合S本身。按照这种思路则很容易能够实现,代码如下:
template<class T>
void printAllSubSet_1(T *arr, int n)
{
if(arr == NULL || n == 0) {cout << "{ }" << endl; return;}
vector<bool> flag(n);
int i;
while(true)
{
bool firstElem = true;
cout << "{ ";
for(i = 0; i < n; i++)
{
if(firstElem && flag.at(i))
{ cout << arr[i]; firstElem = false;}
else if(flag.at(i)) cout << " " << arr[i];
}
cout << "}" << endl;
//simulate the plus operation of binary
for(i = 0; i < n; i++)
{
flag[i] = !flag[i];
if(flag.at(i)) break;
}
if(i == n) break;
}
}
方案二:
利用方案一输出的子集的顺序并不是按字典顺序(Lexical Order)的。若我们要求输出的子集是按字典顺序输出的,则我们则不能利用上述的方法。事实上,这是一个十分简单的程序,除了空集合之外,最“小”的一个集合就是{1}再下一个就是包含1,而且再加上1的下一个元素(1+1=2)的集合,即{1,2};下一个元素 自然就是含有1与2,并且还有2的下一个元素(2+1=3)的集合{1,2,3}了。就这样一直到包含了所有元素为止,亦即{1,2,3,····,n}。下一个集合是谁?绝不是{1,2,3,…,n-1},.因为它 比{1,2,3,…,n}小,事实上应该是{1,2,3,…,n-2,n}。为什么呢?在{1,2,3,…,n-1,n}与 {1,2,3,…,n-2,n}之间,前n-2个元素完全相同,但是n-1<n,这不就证实了以上说法了吗?由于以上原因,因此可以用一个数组set[]来存放集合的元素,一开始时set[0]=1,表 示{1};另外,用一个变量position,指出目前最右边的位置何在,在开始时自然就是1。 接下来,就陆续地产生下一个集合了。注意,目前集合中最右边的元素是set[position],如 果它的值比n小,那就表示还可以加进一个元素,就像是从{1,2,3}加上一个元素变成{1,2,3, 4}一样(n>4)。这倒是容易做,下一个元素在set[position+1],因此在那存入set[position+1] 这个值就行了;同时position也向右移一位。如果目前最右边元素set [position]已经是n, 因而不能再增加元素了。例如,当n=4时,如果有{1,3,4},那自然不能像前面所说的加入一个5。这时看最右边元素的位置,亦即position,是不是在第一位(如果n=6,而现在的集合是{6}),如果不在第一位,那就可以往回移一位,并且把那个位置的值加上1。例如, 如果现在有{1,3,4},而n=4;最右边(4)的位置不是在第一位,因而退回一位,等于是{1,3}; 但这是不对的,因为{1,3}比{1,3,4} “小”,要做得比{1,3,4}大,把3加上1而变成{1,4}就 行了。如果最右边(4)的位置是在第一位,那么程序就完成了。
下面是这种方案的代码实现:
template<class T>
void printAllSubSetLexical(T *s, int n)
{
if(s == NULL || n == 0) {cout << "{}" << endl;}
vector<int> subset(n, 0);
int position = 1; //the element count of current subset
int i;
cout << "{}" << endl;
while(true)
{
cout << "{";
for(i = 0; i < position; i++)
{
if(i == 0) cout << s[subset[i]];
else cout << " " << s[subset[i]];
}
cout << "}" << endl;
if(subset[position-1] != n-1)
{
subset[position] = subset[position-1]+1;
position++;
}
else if(position != 1)
{
position--;
subset[position-1]++;
}
else break;
}
}
2.3 求出包含指定个数
在上一节中我们是一次性输出集合的所有子集,那假如现在我们要求输出包含指定元素个数的子集又该如何呢?最简单的莫过于利用上面的方法,求出集合的所有子集,然后值取元素个数为指定个数的子集。但这样会造成很多不必要的计算,其实我们只需要对上面方案二进行简单的修改即可解决这个问题。我们现在以具体的的例子来说明这个问题,假设集合S包含的字符为abcdef,现在要求所有包含3个元素的子集。按照字典序,第一个子集应该是{abcd},接着是{abce},然后是{abcf},那么接下来的下一个子集应该是{abde}...当子集为{cdef}时,所有的子集便已经输出。那么,当任意给定一个个子集,我们如何求出它的下一个子集呢?假设集合大小为n,要求大小为m的子集。则给定一个子集sub[0..m-1]时,确定下一个子集的步骤如下:
1.找到k = min{i|sub[m-i] != n-i}
2.如果k不存在,则已经输出了所有子集,算法结束
3.sub[m-k] += 1,并且对于所有k< j <m,sub[j] = sub[j]+1
具体代码实现如下:
template<class T>
void printSubSetWithKElems_1(T *s, int n, int k)
{
if(s == NULL || n == 0 || k == 0 || k > n)
{cout << "{}" << endl; return;}
vector<int> subset(k);
int i;
for(i = 0; i < k; i++) subset[i] = i;
while(true)
{
cout << "{";
for(i = 0; i < k; i++)
{
if(i == 0) cout << s[subset[i]];
else cout << " " << s[subset[i]];
}
cout << "}" << endl;
//find the smallest l = min{i|subset[m-i]!=n-i}
i = 1;
for(; i <= k && subset[k-i]==n-i; i++);
if(i == k+1) break; //already got all the subsets
subset[k-i]++;
for(int j = k-i+1; j < k; j++) subset[j] = subset[j-1]+1;
}
}