枚举排列和子集生成的方法

在暴力法中经常会遇到需要枚举排列和获取所有子集的需要,下面给出几种常用的方法。

枚举排列

1. 生成1~n的排列

使用递归的方式即可生成,注意因为不能有重复的元素,所以在选取元素时需要先判断数组中是否已存在该元素,如果存在则不能进入下一层递归。

public void printPermutation(int n, int A[], int k) {
		if(k == n) {
			for(int i = 0 ; i < n; i++) {
				System.out.print(A[i]);
			}
			System.out.println("");
			return;
		}
		
		// 遍历所有取值
		for(int i = 1; i <= n ; i++) {
			int ok = 1;
			for(int j = 0; j < k; j++) {
				if(A[j] == i)
					ok = 0;
			}
			if(ok == 1) {
				A[k] = i;
				printPermutation(n, A, k + 1);
			}
		}
	}

2. 存在重复值的排列

当需要从数组P中选取元素生成排列时,需要考虑重复项的问题,首先,如果还是判断A[0:k]中是否存在P[i]的话,那么当P = {1,1,1}的时候就无法获得结果,因为k是无法等于n的。

考虑用数组A和P中等于P[i]的值的个数,如果A中更少,则包括两种情况:
① A中不存在P[i]
② P[i]在P中有多个,A并没有全部选中
所以这种情况下可以使得A[k] = P[i]

另一方面,考虑P={1,1,2}时,会出现多个112、211等重复的项,这是因为k=0时,选中第一个1和第二个1的情况完全是相同的,因为此时还剩下{1,2},k=1时再次选择1就会出现多个112,其他重复的项同理。那么如何解决?由于P数组的有序的,所以只需要保证P[i]!=P[i-1]即可,这样的话就不会进行多余的递归。
对于P={1,1,2}这个例子,由于P[0]=P[1],所以i=1时不执行,所以k=0时可以选择1或2,进而生成112、121、211这三个排列。

public void printPermutation(int n, int P[], int A[], int k) {
		if(k == n) {
			for(int i = 0 ; i < n; i++) {
				System.out.print(A[i]);
			}
			System.out.println("");
			return;
		}
		
		// 遍历所有取值
		for(int i = 0; i < n ; i++) {
			// 避免生成重复的排列 i=0时也可以进入
			if(i == 0 || P[i] != P[i - 1]) {
				// 计算数组P和A中P[i]的个数
				int c1 = 0, c2 = 0;
				for(int j = 0; j < k; j++) if(P[i] == A[j]) c1++;
				for(int j = 0; j < n; j++) if(P[i] == P[j]) c2++;
				
				// 如果数组A中更少 说明还可以选取该项
				if(c1 < c2) {
					A[k] = P[i];
					printPermutation2(n, P, A, k + 1);
				}
			}
		}
	}

子集生成

1. 增量构造法

增量构造法在每一层递归中都会输出一个子集,集合的子集总共有2^n个(包括空集),所以解答树的结点为 2^n个

值得一提的在for循环中的起始需要保证比前一个子集的最后一个元素大,从而不会出现{1, 2}和{2, 1}这样重复的子集出现,每个自己的元素都是递增的。

public void printSubset(int n, int A[], int k) {
		// 因为是子集 所以不需要k=n的时候再输出
		for(int i = 0 ; i < k; i ++) {
			System.out.print(A[i]);
		}
		System.out.println("");
		
		// 这里使得子集必须是有序的 防止了重复子集的出现
		int s = (k > 0) ? A[k - 1] + 1 : 0;
		for(int i = s; i < n; i++) {
			A[k] = i;
			printSubset(n, A, k + 1);
		}
	}

// 调用方式 printSubset(n, A, 0);

2. 位向量法

位向量法相当于用一个0-1向量表示该子集,比如[1,1,1]表示{0,1,2},当递归深度达到向量长度时输出。

public void printSubset(int n, int B[], int k) {
		// 当向量的每一位的状态确定后输出
		if(k == n) {
			for(int i = 0 ; i < k; i ++) {
				if(B[i] == 1)
					System.out.print(i);
			}
			System.out.println("");
			return;
		}
		
		B[k] = 1;
		printSubset2(n, B, k + 1);
		B[k] = 0;
		printSubset2(n, B, k + 1);
	}

3. 二进制法

二进制法是三种方法中最简单的一种,比如1111B可以表示为子集{0, 1, 2, 3},对应位为1表示选中该元素,这一点和位向量法相同。所以我们可以用一个二进制数来表示一个子集,当元素取0~n-1时,子集对应的二进制数可以取0 ~(1<

public void printSubset(int n) {
		// 遍历2^n个子集
		for(int i = 0; i < (1<<n); i++) {
			// 打印结果
			for(int j = 0; j < n; j++) {
				if((i & (1<<j)) != 0) {
					System.out.print(j);
				}
			}
			System.out.println("");
		}
	}

你可能感兴趣的:(算法,算法,数据结构,java)