排列与组合问题


一、排列

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和第一个字符交换之前,先要把ba交换回来。在交换ba之后,再拿c和处在第一位置的a进行交换,得到cba。我们再次固定第一个字符c,求后面两个字符ba的排列。(参考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),如果它的箭头所指的方向的邻点小于它本身。例如

  

635都是可移的。显然1永远不可移,n除了以下两种情形外,它都是可移的:

(1) n是第一个数,且其方向指向左侧

(2) n是最后一个数,且其方向指向右侧

于是,我们可由

按如下算法产生所有排列

算法

1开始时:


2,当存在可移数时

(a) 找最大的可移数 m
(b) m 与其箭头所指的邻数互换位置
(c) 将所得排列中比 m 大的数 p 的方向调整,即改为相反方向。
根据这种方法的到的全排列不是按照字符排序的,代码如下:
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;
    }
}



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值