回溯法 求集合全排列、子集


回溯法,参见之前的blog

全排列:

全排列是将一组数按一定顺序进行排列,如果这组数有n个,那么全排列数为n!个。

从集合中依次选出每一个元素,作为排列的第一个元素,然后对剩余的元素进行全排列,如此递归处理,从而得到所有元素的全排列。

以对字符串abc进行全排列为例,我们可以这么做:以abc为例
固定a,求后面bc的排列:abc,acb,求好后,a和b交换,得到bac
固定b,求后面ac的排列:bac,bca,求好后,c放到第一位置,得到cba
固定c,求后面ba的排列:cba,cab。

这个思想和回溯法比较吻合。

代码可如下编写所示

// 回溯法搜索全排列树

#include<stdio.h>

#define M 20
int n;
int a[M];
int cnt = 0;// 记录全排列个数

void swap(int *a, int *b)//交换a,b
{
    char t;
    t = *a;
    *a = *b;
    *b = t;
}

void dfs(int cur)
{
	int i;
	if(cur == n)// 找到 输出全排列
	{
		++cnt;
		for(i=0; i<n; i++)
			printf("%d ", a[i]);
		printf("\n");
	}
	else
	{
		 // 将集合中的所有元素分别与第一个交换,这样就总是在
		// 处理剩下元素的全排列(即用递归)
		for(i=cur; i<n; i++)
		{
			swap(&a[cur], &a[i]);
			dfs(cur+1);
			swap(&a[cur], &a[i]);//回溯
		}
	}
}


int main()
{
	while(scanf("%d", &n) != EOF)
	{
		for(int i=0; i<n; i++)
			a[i] = i+1;// 假设集合S为:1 2 3 ... n
		cnt = 0;
		dfs(0);
		printf("count:%d\n", cnt);
	}
	return 0;
}



或者利用一个vis数组标识每个元素是否已经被访问,代码如下:

#include <stdio.h>

int a[10];
bool vis[10];
int n;//排列个数 n
void dfs(int dep)  //打印所有的全排列,穷举每一种方案
{
    if(dep == n)
    {
        for(int i = 0; i < n; i++)
        {
            printf("%d ",a[i]);
        }
        printf("\n");
        return ;
    }
    for(int i = 0; i < n; i++)// 找一个最小的未标记的数字,保证了字典序最小
    {
        if(!vis[i])
        {
            a[dep] = i+1;
            vis[i] = true;// 找到了就标记掉,继续下一层
            dfs(dep + 1);
            vis[i] = false;
        }
    }
}
int main()
{
    while(scanf("%d",&n) != EOF)
    {
        dfs(0);
    }
    return 0;
}


子集构造:

从n个元素的集合S中找出S满足某种性质的子集时,相应解空间树称为子集树。

求n个元素集合的子集,如A = {1, 2, 3}则A集合的子集有:  
 P(A) = {{1,2,3}, {1,2}, {1,3},{1},{2,3},{2},{3},{}}

采用位向量法,构造一个位向量vis[], vis[i] = 1 表示i在子集A中。

代码如下:

#include<stdio.h>

#define M 20
int n;
int a[M];
int vis[M];
int cnt = 0;// 记录子集个数


void dfs(int cur)
{
	int i;
	if(cur == n)// 找到 输出所有子集
	{
		++cnt;
		int flag =1;
		for(i=0; i<n; i++)
			if(vis[i])
			{
				printf("%d ", a[i]);
				flag = 0;
			}
			if(flag)//子集中的空集
				printf("φ");
		printf("\n");
	}
	else
	{
		for(i=1; i>=0; --i)//vis 中分为 选or不选即 1,0
		{
			vis[cur] = i;
			dfs(cur+1);
		}
	}
}


int main()
{
	while(scanf("%d", &n) != EOF)
	{
		for(int i=0; i<n; i++)
			a[i] = i+1;// 假设集合S为:1 2 3 ... n
		cnt = 0;
		dfs(0);
		printf("count:%d\n", cnt);
	}
	return 0;
}



此外可以采用采用增量构造法,
代码如下:


#include<stdio.h>

#define M 20
int n;
int a[M];
int vis[M];
int cnt = 0;// 记录子集个数


void subset(int cur, int A[])
{
	int i;
	++cnt;
	if(0 == cur)//子集中的空集
		printf("φ");
	for(i=0; i<cur; ++i)// 打印当前集合
	{
		printf("%d ", A[i]);
	}
	printf("\n");
	
	int min  = cur ? A[cur-1]:0;//确定当前元素的最小可能值
	for(i=min; i<n; ++i)
	{
		A[cur] = a[i];
		subset(cur+1, A);//递归构造
	}
}


int main()
{
	while(scanf("%d", &n) != EOF)
	{
		for(int i=0; i<n; i++)
			a[i] = i+1;// 假设集合S为:1 2 3 ... n
		cnt = 0;
		int A[M]={0};
		subset(0, A);
		printf("count:%d\n", cnt);
	}
	return 0;
}



参考资料:

算法竞赛入门经典


  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值