數據結構與算法 | 【分治策略 || 排列樹 & 子集樹】——全排列、求子集問題...

全排列問題

R={r1,r2,... rn}R=\{r_1,r_2,... \ r_n\} 是要進行排列的n個元素, Ri=R{ri}R_i=R-\{r_i\} 。集合X中元素的全排列記爲 perm(X)perm(X)(r1)perm(X)(r_1)perm(X) 表示在全排列 perm(X)perm(X) 的每一個排列前加上前綴 rir_i ,得到的排列。RR 的全排列可歸納定義如下:

  • n=ln=l 時,perm(R)=(r)perm(R)=(r),其中 r 是集合 R 中唯一的元素;
  • n>1n>1 時,perm(R)perm(R)(r1)perm(R1),(r2)perm(R2),...,(rn)perm(Rn)(r_1)perm(R_1) , (r_2)perm(R_2) , ... , (r_n)perm(R_n) 構成。

分析:

對於 Ri = R-{ri}分析:
設 R = {1,2,3}   n = 3, 則有:
R1 = r-{r1} = {2,3}   R2 = R-{r2} = {1,3}   R3 = R-{r3} = {1,2}

對於 {1,2,3}的全排列有:
	1 2 3 
	1 3 2
	2 1 3
	2 3 1
	3 2 1
	3 1 2

依此遞歸定義,可設計產生 perm(R) 的遞歸過程:

						{ 1 , 2 , 3 }						初始集合 {1,2,3}
					   /      |      \
					 /        |        \
			 (1)p{2,3}    (2)p{1,3}   (3)p{1,2}				每次從中取一個數據
	       /      |        |     |       |      \
	      /       |        |     |       |       \  
	  (2)p{3}  (3)p{2} (1)p{3} (3)p{1}  (1)p{2}  (2)p{1} 	再次在前一次的基礎上取一個數據
	     |        |        |     |       |         |
	     |        |        |     |       |         |
	     3        2        3     1       2         1		直至該集合只剩一個元素
	     ↓        ↓        ↓     ↓       ↓         ↓
	     ↓        ↓        ↓     ↓       ↓         ↓
	【1,2,3】 【1,3,2】【2,1,3】【2,3,1】【3,1,2】【3,2,1】	按照每次取出的數據順序,形成排列

遞歸算法設計:

設有 ar = {1,2,3} ,設計遞歸函數 Perm(ar,i,m) ,其中 i 待提取元素的下標,m 爲集合下標的最大值max_index。

  • 第一層遞歸,提取 (ri)Perm{Ri} 。格式爲 (ar[0])Perm(ar,0,2)
  • 第二層遞歸,提取 (ri)Perm{Ri} 。格式爲 (ar[1])Perm(ar,1,2)
  • 第三層遞歸,提取 (ri)Perm{Ri} 。格式爲 (ar[2])Perm(ar,2,2)
  • 得到序列 {ar[0],ar[1],ar[2]}\{ar[0],ar[1],ar[2]\}

其中,我們規定,ar[i]ar[i] 爲每次遞歸提取的數,(i,m](i, m]區間內爲集合剩餘元素 。核心算法:在遞歸內使用 循環+交換 的方式,在每次遞歸時分別把每個元素提取到 ar[i]ar[i] 位置,使(i,m](i, m]區間內的元素繼續下一次遞歸,直至集合內只剩一個元素。

#include<iostream>
using namespace std;

void Swap(int& a, int& b)
{
	int c = a;
	a = b;
	b = c;
}

void Perm(int *ar,int i,int m)
{
	if (i == m)	// 只剩一個元素,打印{ar[0],ar[1],ar[2]}
	{
		for (int k = 0; k <= m; ++k)
		{
			cout << ar[k] << " ";
		}
		cout << endl;
	}
	else
	{
		for (int k = i; k <= m; ++k)	// 使用循環,保證 1,2,3 都被提取一次
		{
			/*
				ar[i] 的位置是被提取的位置
				在第一次遞歸時,提取ar[0],第二次ar[1],第 ...
				因此,分別把集合中的每個元素放在提取位,使之被提取出集合
			*/
			Swap(ar[i], ar[k]);
			Perm(ar, i + 1, m);	// 提取 i~m 之間的元素
			Swap(ar[i], ar[k]);
		}

	}
}


int main()
{
	int ar[] = { 1,2,3 };
	int n = sizeof(ar) / sizeof(ar[0]);
	Perm(ar,0,n-1);
	return 0;
}
求子集問題

基本性質:
非空集合A中含有n個元素,A={1,2,3,... ...n}A=\{1,2,3, ...\ ... n\},則

  • A的子集個數爲2n2^n
  • A的真子集的個數爲2n12^n-1
  • A的非空子集的個數爲2n12^n-1
  • A的非空真子集的個數爲2n22^n-2

舉個栗子:
A={1,2,3},則他的子集有:

  • 特殊元素:φ
  • 一位元素:{1}、{2}、{3}
  • 二位元素:{1,2}、{1,3}、{2,3}
  • 三位元素:{1,2,3}

子集數:23=82^3=8
真子集數:231=72^3-1=7 ,沒有 {1,2,3}
非空子集數:231=72^3-1=7,沒有 φ
非空真子集數:232=62^3-2=6,沒有 {1,2,3} 和 φ

算法分析:
通過觀察子集與集合本身的特點,我們發現子集其實是集合本身某一元素的缺失。

如:

  • 集合{1,2,3}==> 子集{1,2},缺失 3,或者說只存在 1,2
  • 集合{1,2,3}==> 子集{1},缺失 2,3,或者說只存在 1

因此,我們發現集合中每個元素的屬性只用兩種,要麼出現,要麼不出現。

類比我們學過的一種數據結構——二叉樹。二叉樹只有左右結點,其中滿二叉樹除最後一層無任何子節點外,每一層上的所有結點都有兩個子結點的二叉樹。並且,滿二叉樹的最後一層節點個數爲 2n2^n 個,其中 n 爲樹的深度。

結合以上兩者的特點,做出如下分析:

1表示出現,0表示隱藏
0 0 0   		 	    φ
0 0 1					3
0 1 0					2
0 1 1					2 3
1 0 0					1
1 0 1					1 3
1 1 0					1 2
1 1 1					1 2 3

滿二叉樹:

0
1
0
1
0
1
0
1
0
1
0
1
0
1
A
A
B
B
B
B
C
C
C
C
C
C
C
C
000
001
010
011
100
101
110
111

算法設計:

生成滿二叉樹算法。代碼分析請看:【遞歸調用陷阱】

void fun(int i, int n)
{
	if (i >= n)
	{
	}
	else
	{
		fun1(i + 1, n);	// 左子樹
		fun1(i + 1, n);	// 右子樹
	}

}

使用數組 br[] 標記二叉樹的左右的編碼。

代碼實現如下:

#include <iostream>
using namespace std;

void subset(int *ar,int *br,int i, int n)
{
	if (i >= n)
	{
		int i = 0;
		while (i < n)
		{
			if(br[i] == 1)
				cout << ar[i] << " ";
			i++;
		}
		cout << endl;
	}
	else
	{
		br[i] = 0;		/* 左邊記爲0  */
		subset(ar, br, i + 1, n);	/* 進入左孩子 */
		br[i] = 1;		/* 右邊記爲1 */
		subset(ar, br, i + 1, n);	/* 進入右孩子 */

	}
}

int main()
{
	int ar[] = { 1,2,3 };	
	int br[] = { 0,0,0 };
	subset(ar, br, 0, 3);
	return 0;
}

本次我們使用遞歸的方式完成了全排列,和求子集的問題。如果,爲了追求效率我們還可以使用循環的方式去設計算法。

在設計全排列遞歸實現時,我們使用了排列樹進行實現。在設計子集問題的遞歸實現時,我們使用了子集樹進行實現。其中排列樹和子集樹正如他們的命名一般,前者是對不同元素的排列組合,後者是對不同元素的取捨

排列樹和子集樹在很多金典算法中都有涉及。如,排列數可以用來解決圖的最短路徑問題,子集樹可以用來解決如01揹包的n個物品中若干取值的最優解問題。

本章通過全排列問題和求子集問題粗淺的瞭解了排列樹和子集樹,後續我將繼續分享兩種問題的非遞歸實現方法,以及 01 揹包等經典算法。

最後,如果覺得我的文章對你有幫助的話請幫忙點個贊,你的鼓勵就是我學習的動力。如果文章中有錯誤的地方歡迎指正,有不同意見的同學也歡迎在評論區留言,互相學習。

——學習路上,你我共勉
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章