全排列问题
设 R = { r 1 , r 2 , . . . r n } R=\{r_1,r_2,... \ r_n\} R={r1,r2,... rn} 是要进行排列的n个元素, R i = R − { r i } R_i=R-\{r_i\} Ri=R−{ri} 。集合X中元素的全排列记为 p e r m ( X ) perm(X) perm(X) 。 ( r 1 ) p e r m ( X ) (r_1)perm(X) (r1)perm(X) 表示在全排列 p e r m ( X ) perm(X) perm(X) 的每一个排列前加上前缀 r i r_i ri ,得到的排列。 R R R 的全排列可归纳定义如下:
- 当 n = l n=l n=l 时, p e r m ( R ) = ( r ) perm(R)=(r) perm(R)=(r),其中 r 是集合 R 中唯一的元素;
- 当 n > 1 n>1 n>1 时, p e r m ( R ) perm(R) perm(R) 由 ( r 1 ) p e r m ( R 1 ) , ( r 2 ) p e r m ( R 2 ) , . . . , ( r n ) p e r m ( R n ) (r_1)perm(R_1) , (r_2)perm(R_2) , ... , (r_n)perm(R_n) (r1)perm(R1),(r2)perm(R2),...,(rn)perm(Rn) 构成。
分析:
对于 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)
- 得到序列 { a r [ 0 ] , a r [ 1 ] , a r [ 2 ] } \{ar[0],ar[1],ar[2]\} {ar[0],ar[1],ar[2]}
其中,我们规定, a r [ i ] ar[i] ar[i] 为每次递归提取的数, ( i , m ] (i, m] (i,m]区间内为集合剩余元素 。核心算法:在递归内使用 循环+交换 的方式,在每次递归时分别把每个元素提取到 a r [ i ] ar[i] ar[i] 位置,使 ( i , m ] (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;
}
2021/8/20更新…
STL库中的全排列函数:
string S("cba");
sort(S.begin(), S.end(), less<char>()); // 注这里需要升序排序 --> abc
do {
cout << S << " ";
} while (next_permutation(S.begin(), S.end()));
cout << endl;
/*
* 输出:abc acb bac bca cab cba
*/
需要注意的是这里如果 S=“cba”,那么只会输出一个结果,所以需要排序。
next_permutation的函数声明:#include <algorithm>
bool next_permutation( iterator start, iterator end);
- next_permutation()函数功能是输出所有比当前排列大的排列,顺序是从小到大。
- prev_permutation()函数功能是输出所有比当前排列小的排列,顺序是从大到小。
求子集问题
基本性质:
非空集合A中含有n个元素,
A
=
{
1
,
2
,
3
,
.
.
.
.
.
.
n
}
A=\{1,2,3, ...\ ... n\}
A={1,2,3,... ...n},则
- A的子集个数为 2 n 2^n 2n。
- A的真子集的个数为 2 n − 1 2^n-1 2n−1
- A的非空子集的个数为 2 n − 1 2^n-1 2n−1
- A的非空真子集的个数为 2 n − 2 2^n-2 2n−2
举个栗子:
A={1,2,3},则他的子集有:
- 特殊元素:φ
- 一位元素:{1}、{2}、{3}
- 二位元素:{1,2}、{1,3}、{2,3}
- 三位元素:{1,2,3}
子集数:
2
3
=
8
2^3=8
23=8
真子集数:
2
3
−
1
=
7
2^3-1=7
23−1=7 ,没有 {1,2,3}
非空子集数:
2
3
−
1
=
7
2^3-1=7
23−1=7,没有 φ
非空真子集数:
2
3
−
2
=
6
2^3-2=6
23−2=6,没有 {1,2,3} 和 φ
算法分析:
通过观察子集与集合本身的特点,我们发现子集其实是集合本身某一元素的缺失。
如:
- 集合{1,2,3}==> 子集{1,2},缺失 3,或者说只存在 1,2
- 集合{1,2,3}==> 子集{1},缺失 2,3,或者说只存在 1
因此,我们发现集合中每个元素的属性只用两种,要么出现,要么不出现。
类比我们学过的一种数据结构——二叉树。二叉树只有左右结点,其中满二叉树除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树。并且,满二叉树的最后一层节点个数为 2 n 2^n 2n 个,其中 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
满二叉树:
算法设计:
生成满二叉树算法。代码分析请看:【递归调用陷阱】
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 背包等经典算法。
最后,如果觉得我的文章对你有帮助的话请帮忙点个赞,你的鼓励就是我学习的动力。如果文章中有错误的地方欢迎指正,有不同意见的同学也欢迎在评论区留言,互相学习。
2021/12/5更新…
上述代码中都使用的是指针传参,如果我们想使用容器也是可以的,我们只需要将函数头部的参数类型修改一下即可。
void Perm(vector<int>& ar, int i, int m);
void subset(const vector<int>& ar, vector<bool>& br, int i, int n)
另外,在C++中的 <algorithm> 库中,有个next_permutation函数,我们可以借助它简化我们的全排列。
template< class BidirIt >
bool next_permutation( BidirIt first, BidirIt last );// 按照operator<
template< class BidirIt, class Compare >
bool next_permutation( BidirIt first, BidirIt last, Compare comp );
Permutes the range [first, last) into the next permutation, where the set of all permutations is ordered lexicographically with respect to operator< or comp. Returns true if such a “next permutation” exists; otherwise transforms the range into the lexicographically first permutation (as if by std::sort(first, last, comp)) and returns false.
——https://en.cppreference.com/w/cpp/algorithm/next_permutation
next_permutation函数会在给定的范围 [first, last) 中,将所有的元素按照 operator< 或 comp 的比较方式以字典序进行重新排列。
- 若新排列按字典序大于旧者则返回 true 。
例如 abc 重排列后为 bac ,bac 的字典序大于 abc - 若抵达最后,重排并重置范围为首个排列则为 false 。
例如 cba 为重排列后字典序最大的,再进行一次next_permutation后,会被重置为初始状态,即 abc。
示例:对字符串进行全排列。
需要注意的是,next_permutation会进行全排列的条件是,存在排列后的字典序大于当前字典序的情况、因此,我们需要保证排列前,我们的字典序最小。
#include <algorithm>
#include <string>
#include <iostream>
int main()
{
std::string s = "cab";
std::sort(s.begin(), s.end(), less<char>());
do {
std::cout << s << '\n';
} while (std::next_permutation(s.begin(), s.end()));
}
/* 输出
abc
acb
bac
bca
cab
cba*/
示例2:如果不存在可能的大于当前字典序的情况,则返回初始状态。
#include <algorithm>
#include <string>
#include <iostream>
int main()
{
std::string s = "cab";
std::sort(s.begin(), s.end(), greater<char>());
do {
std::cout << s << '\n';
} while (std::next_permutation(s.begin(), s.end()));
}
/* 输出
cba*/