zoukankan      html  css  js  c++  java
  • 算法 | 【分治策略 || 排列树 & 子集树】——全排列、求子集问题...

    全排列问题

    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 背包等经典算法。

    最后,如果觉得我的文章对你有帮助的话请帮忙点个赞,你的鼓励就是我学习的动力。如果文章中有错误的地方欢迎指正,有不同意见的同学也欢迎在评论区留言,互相学习。

    ——学习路上,你我共勉
  • 相关阅读:
    C++结构体内重载、this指针和友元函数(初步了解)
    数据结构—造树计划—二叉搜索树
    PTA顺序的分数
    PTA兼容任务
    PTA航船
    UML-基于GRASP对象设计步骤
    UML-设计对象时涉及的制品有哪些?
    UML-什么是用例实现(场景实现)?
    UML-如何使用GRASP进行对象设计?
    日志总结
  • 原文地址:https://www.cnblogs.com/TaoR320/p/12680090.html
Copyright © 2011-2022 走看看