分享目标
- DFS 与排列组合问题
互联网的诞生和发展(四)
互联网的商业化应用1994年:雅虎(Yahoo!)成立,成为早期互联网上重要的搜索引擎和门户网站。同年,亚马逊(Amazon)也成立,标志着电子商务的兴起。互联网开始从一个信息交流平台转变为一个商业活动的重要场所。1998年:谷歌(Google)成立,它以其强大的搜索引擎技术和简洁的用户界面迅速成为互联网上最受欢迎的搜索引擎。谷歌的出现进一步推动了互联网信息检索的发展,使得用户能够更快速、更准确地找到自己需要的信息。
DFS的应用场景
已经学过,DFS应用于迷宫问题
1.路径连通性问题:检查从A点到达B点是否有可行的线路。
- 统计路径问题:搜索地图中从A点到达B点的路径数量。
3.格子数量:搜索地图中某个连通块内的格子数量。
- 连通块的数量:搜索地图中连通块的数量。
即将学习,DFS应用于排列组合问题
(排列与组合是组合数学中的两个基本概念)
什么叫排列
● 排列是指从n个不同元素中取出r个元素,r个元素按照一定的顺序排列起来。
●当n=r时,所有的排列情况叫全排列。
假设有1、2、3三个元素。
从3个中取2个的排列有:
1, 2
1,3
2,3
2,1
3,1
3,2
共6种排列
元素顺序不同视为不同的排列
(3个中取3个)全排列有:
1, 2, 3
1,3, 2
2, 1, 3
2, 3, 1
3, 1, 2
共6种排列
全排列的生成过程
还是1、2、3三个元素,如何生成所有可能的全排列?
● 想象有三个盒子,每个盒子放一个数就得到一个排列。
● 不同的选择得到不同的排列。

为了保证不重复不遗漏,一般都要有序地选数,而且用过的数不能用。

全排列的生成过程(总结)
● 从第1个盒子开始,依次为每个盒子选数,每个盒子的选数逻辑都是相同的。
关键点:
1.有序地选数(例如从小到大)
- 标记已使用的元素,避免重复选择。
● 可以用深度优先搜索找到全部的全排列方案。
应用深度优先搜索找到全部的全排列方案:
-
有序地选数
-
通过标记数组标记已使用的元素,避免重复选择。
-
回溯上一层之前要清空标记,以尝试其他分支。

全排列的实现
实现递归函数dfs(x)

cpp
#include<iostream>
using namespace std;
bool visited[4];// 标记数组
int box [4];// 3个盒子
void dfs(int x) {
if(x == 4){//x=4,说明前面3个盒子都放好了
for(int i=1;i <= n;i++)//按照盒子顺序输出盒子里的数
cout << box[i] << " ";
cout << endl;// 每组排列占一行
return;// 结束当前递归,返回上一层
}
for(int i=1;i <= 3;i++){//(从小到大)找一个可以放入盒子的数
if(!visited[i]) {
box [x] = i;
visited[i] = true;
dfs(x+1);// 开始为第x+1个盒子选数
visited[i]= false;//【重要】清除标记
}
}
}
int main() {
dfs(1);//从第1个盒子开始
return 0;
}
1~3的全排列:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
如果要得到1~n这n个数的全排列,dfs()函数怎么改?
MAXN:n的最大值

全排列DFS的算法复杂度
时间复杂度主要取决于生成的排列数量:
●排列数量:对于n个不同元素,排列的数量为n!(即n的阶乘)。
● 每个排列的生成时间:生成每个排列需要O(n)的时间,因为需要将n个元素依次放入盒子中。
因此,DFS递归算法的总时间复杂度为:O(nxn!)
空间复杂度主要由递归栈的深度、辅助数组决定:
● 递归栈的深度:递归调用的最大深度为n,因此递归栈的空间复杂度为O(n)。
● 辅助数组:标记数组的大小为n+1,因此空间复杂度为O(n)。
因此,DFS递归算法的总空间复杂度为:O(n)
n个数的全排列
给定n个互不相同的正整数,这些数已经按照升序排好。现要将这n个数的所有可能的全排列输出,且每个数只能使用一次。
【输入格式】两行,第一行一个正整数n(1≤n≤9),第二行n个空格隔开的正整数。
【输出格式】输出若干行,每行包括n个空格分隔的数字,表示一组序列。所有的排列按字典序从小到大排序输出。

本题中,除了盒子数组和标记数组之外,还需要一个数组保存输入的n个数。
cpp
MAXN:指代n的最大值
int in [MAXN+1];// n个数
bool visited [MAXN+1];// 标记数组
int box [MAXN+1];// 盒子数组---所有数组从下标1开始用

n个数的全排列的模版代码
cpp
//全局变量
int in [MAXN+1];// n个数
bool visited [MAXN+1];// 标记数组
int box [MAXN+1];// 盒子数组
void dfs(int x) {
if( x == n+1 ) {
// 按格式输出这个全排列
return;
}
for(int i=1;i <= n;i++) {
if(!visited[i]) {
box [x] = in[i];//改成从输入数组中选数
visited[i] = true;
dfs(x+1);// 开始为第x+1个盒子选数
visited[i] = false; //【重要】清除标记
}
}
}
main函数调用dfs():
cpp
int main() {
cin >> n;
for(int i=1;i <= n;i++) //输入--注意数组从下标1开始用
cin >> in[i];
dfs(1);//第1个盒子
return 0;
}
cpp
#include<iostream>
using namespace std;
//全局变量
int in [MAXN+1];// n个数
bool visited [MAXN+1];// 标记数组
int box [MAXN+1];// 盒子数组
void dfs(int x) {
if( x == n+1 ) {
// 按格式输出这个全排列
return;
}
for(int i=1;i <= n;i++) {
if(!visited[i]) {
box [x] = in[i];//改成从输入数组中选数
visited[i] = true;
dfs(x+1);// 开始为第x+1个盒子选数
visited[i] = false; //【重要】清除标记
}
}
}
int main() {
cin >> n;
for(int i=1;i <= n;i++) //输入--注意数组从下标1开始用
cin >> in[i];
dfs(1);//第1个盒子
return 0;
}
n个数的全排列-思考1
如果题目改动:
给定n个互不相同的正整数,这些数已经按照升序排好(这句话删除--也就是说输入的n个数不再保证有序)。现要将这n个数的所有可能的全排列输出,所有的排列按字典序从小到大排序输出。
代码怎么改?在DFS前先对数组排序。可以用算法库的sort函数。
cpp
1、包含算法头文件
#include<algorithm>
cpp
2、main函数中先进行排序
int main () {
cin>>n;
for(int i=0;i<n;i++) //输入
cin >> in[i+1];
//先排序
sort (in+1,in+n+1);
dfs(1);//从第一个盒子开始
return 0;
}
输入:
3
2 5 3
输出:
2 3 5
2 5 3
3 2 5
3 5 2
5 2 3
5 3 2
n个数的全排列-思考2
如果题目改动:
给定n个互不相同的正整数,这些数已经按照升序排好。现要将这n个数的所
有可能的全排列输出,所有的排列按字典序从小到大(改成:从大到小 )排序输出。
【输入样例】
3
2 3 5
【输出样例】
5 3 2
5 2 3
3 5 2
3 2 5
2 5 3
2 3 5
【分析】已知,对升序排好的n个数,在应用DFS生成全排列时,遵循从小到大的顺序选数,可以得到
字典序从小到大的所有全排列。
所以,如果要得到字典序从大到小的所有全排列,只要在选数时遵循从大到小的顺序即可。

n个数取r个数的排列
按升序输入n个不相同的数。现在从这n个数中挑出r个数进行排列(r <= n),请从小到大输出
所有可能的排列结果。
【输入描述】两个整数n和r(n和r都是3~10之间的整数)
【输出描述】从n个数中取r个数的排列结果!一行一个排列(数字之间空格隔开),行之间按字典序升序排序。
【输入样例】
3 2
2 3 5
【输出样例】
2 3
2 5
3 2
3 5
5 2
5 3
根据题意修改全排列的模版代码
cpp
int in[11];//n个数
bool visited [11];//标记数组
int box [11];//盒子数组
void dfs(int x) {
if( x == r+1 ) {
// ..< 按格式输出这个排列
for(int i=1;i <= r;i++)
cout << box[i]
cout << endl;
return;
}
for(int i=1;i <= n; i++) {
if(!visited[i]) {
box [x] = in[i];
visited[i] = true;
dfs (x+1);// 开始为第x+1个盒子选数
visited [i] = false; //【重要】清除标记
}
}
}
main函数调用dfs()
cpp
int main() {
cin >> n >> r;
for(int i=1;i <= n;i++) //输入
cin >> in[i];
dfs(1);//第1个盒子
return 0;
}
cpp
#include<iostream>
using namespace std;
int in[11];//n个数
bool visited [11];//标记数组
int box [11];//盒子数组
void dfs(int x) {
if( x == r+1 ) {
// ..< 按格式输出这个排列
for(int i=1;i <= r;i++)
cout << box[i]
cout << endl;
return;
}
for(int i=1;i <= n; i++) {
if(!visited[i]) {
box [x] = in[i];
visited[i] = true;
dfs (x+1);// 开始为第x+1个盒子选数
visited [i] = false; //【重要】清除标记
}
}
}
int main() {
cin >> n >> r;
for(int i=1;i <= n;i++) //输入
cin >> in[i];
dfs(1);//第1个盒子
return 0;
}
n取r的排列的模版代码
cpp
int in[MAXN+1];//n个数
bool visited [MAXN+1];//标记数组
int box [MAXN+1];//盒子数组
void dfs(int x) {
if( x == r+1 ) {
// ..< 按格式输出这个排列
for(int i=1;i <= r;i++)
cout << box[i]
cout << endl;
return;
}
for(int i=1;i <= n; i++) {
if(!visited[i]) {
box [x] = in[i];
visited[i] = true;
dfs (x+1);// 开始为第x+1个盒子选数
visited [i] = false; //【重要】清除标记
}
}
}
什么叫组合
假设有1、2、3三个元素。
● 排列是指从n个不同元素中取出r个元素,r个元素按照一定的顺序排列起来。
元素顺序不同视为不同的排列
从3个中取2个的排列有:
1,2
1,3
2,1
2,3
3,1
3, 2
共6种排列
● 组合是指从n个不同元素中取出r个元素,不考虑元素的顺序。
元素顺序不同视为相同的组合
从3个中取2个的组合有:
1, 2
1, 3
2,3
共3种组合
排列考虑元素的顺序,而组合不考虑元素的顺序
排列考虑元素的顺序,而组合不考虑元素的顺序
排列考虑元素的顺序,而组合不考虑元素的顺序
练一练
以下问题是排列问题还是组合问题?关键看结果是否与顺序有关。
1、从6个不同的球中取出2个球,放入一个盒子中,有多少种不同的放法?
【答】这是一个组合问题。例如被选中的是1号球和2号球,不管是1先放入还是2先放入,最后盒子里的球都是1、2号球的组合。结果与顺序无关。
2、用数字0、1、2、3、4组成一个五位数,每个数字只能使用一次,有多少种不同的五位数?
【答】这是一个排列问题。例如10234和12034明显是不同的五位数。结果与元素顺序有关。
3、一个班级有10名学生,从中选出3名学生组成一个小组,有多少种不同的选法?
【答】这是一个组合问题。同样的3名学生组成的就是同一个小组,与他们被选中的顺序无关。
组合结果的输出
首先,数学概念里,组合的结果与顺序无关。例如,{1,2}和{2,1}是同一个组合。
但是在编程题目里,为了方便核对输出结果,一般会对输出的数据进行排序的要求。
例如1、2、3三个数,要求输出从中取2个数的所有组合。
1、结果按字典序升序输出:

2、结果按字典序降序输出:
3,2
3,1
2,1
考考你
请问从n个数取n个数的组合有多少个?即全部元素都参与的组合。
例如,有1、2、3三个元素,从中取3个的组合。
因为组合与元素的顺序无关,所以{1,2,3},{1,3,2}, ... {3,2,1}都是同样的组合。按升序展示组合内元素的话,这个唯一的组合就是{1,2,3}。
所以,n个数取n的组合总是只有一个。
组合的生成过程
给定3个不同的数中取2个的组合有哪些?(结果按升序输出)

解决方法:给盒子选数字时,从前一个已选数字之后的数字中依序选择。

n取r的组合的模版代码
cpp
从n取r的排列的模版代码修改而来:添加逻辑:从前一个已选数字之后的数字中依序选择
int in[MAXN+1];// 输入数组(n个数)
bool visited [MAXN+1];// 标记数组
int box [MAXN+1];// 盒子数组
void dfs(int x ,int id) {// 第x个盒子,从第id个数开始选
if( x == r+1 ) {
// ... 按格式输出这个排列
return;
}
for (int i=id;i <= n;i++){//从第id个数开始选
if(!visited[i]) {
box [x] = in[i];//、第i个被选中
visited[i] = true;
dfs(x+1,i+1);//开始为第x+1个盒子选数,从i+1开始选
visited[i] = false; //【重要】清除标记
}
}
}
n个数取r个数的组合
按升序输入n个不相同的数。现在从这n个数中挑出r个数进行组合(r <= n),请从小到大输出所有可能的组合结果。
【输入描述】两个整数n和r(n和r都是3~10之间的整数)
【输出描述】从n个数中取r个数的组合结果!结果按字典序升序排序。
【输入样例】4 3
3 7 12 19
【输出样例】3 7 12
3 71 9
3 12 19
7 12 19
n个数取r个数的组合
根据题意修改n取r组合的模版代码n和r <= 10
cpp
int in[11];// 输入数组(n个数)
bool visited [11];// 标记数组
int box [11];// 盒子数组
void dfs(int x ,int id) {// 第x个盒子,从第id个数开始选
if( x == r+1 ) {
// ... 按格式输出这个排列
for(int i=1;i<=r;i++)
cout<<box[i];<<" ";
cout<<endl;
return;
}
for (int i=id;i <= n;i++){//从第id个数开始选
if(!visited[i]) {
box [x] = in[i];//、第i个被选中
visited[i] = true;
dfs(x+1,i+1);//开始为第x+1个盒子选数,从i+1开始选
visited[i] = false; //【重要】清除标记
}
}
}
main函数调用dfs()
cpp
int main() {
cin >> n >> r;
for(int i=1;i <= n;i++) //输入--约定数组从下标1开始用
cin>>in[i];
dfs(1,1);//第1个盒子,从第1个数开始选
return 0;
}
完整代码示例
cpp
#include<iostream>
using namespace std;
int in[11];// 输入数组(n个数)
bool visited [11];// 标记数组
int box [11];// 盒子数组
void dfs(int x ,int id) {// 第x个盒子,从第id个数开始选
if( x == r+1 ) {
// ... 按格式输出这个排列
for(int i=1;i<=r;i++)
cout<<box[i];<<" ";
cout<<endl;
return;
}
for (int i=id;i <= n;i++){//从第id个数开始选
if(!visited[i]) {
box [x] = in[i];//、第i个被选中
visited[i] = true;
dfs(x+1,i+1);//开始为第x+1个盒子选数,从i+1开始选
visited[i] = false; //【重要】清除标记
}
}
}
int main() {
cin >> n >> r;
for(int i=1;i <= n;i++) //输入--约定数组从下标1开始用
cin>>in[i];
dfs(1,1);//第1个盒子,从第1个数开始选
return 0;
}
n个数取r个数的组合-思考
如果题目改动:
结果按字典序降序排序(组合内数值降序,且不同组合也按降序输出)代码怎么改?
把输入的n个数从升序变成降序,其他代码不变。
【输入样例】
4 3
3 7 12 19
【输出样例】
19 12 7
19 12 3
19 7 3
12 7 3
cpp
#include<algorithm>
bool cmp (int a, int b) {
if (a>b) return true;
return false;
}
int main() {
cin >> n >> r;
for(int i=1;i <= n;i++) //输入
cin >> in[i];
sort(in+1,in+n+1,cmp);//先降序排序
dfs(1,1);//第1个盒子,从第1个数开始选
return 0;
}
本次分享的知识点
-
排列与组合的概念
-
用DFS解决排列问题(全排列、n取r的排列)
-
用DFS解决组合问题(n取r的组合)
1、给定一组字符串{"a1","a2","a10","a20"},按照字典序升序排列后,正确
的顺序是?A
A、{"a1","a10", "a2", "a20"}
B、{"a1","a2","a10","a20"}
C、{"a1","a2","a20","a10"}
D、{"a1","a10", "a20", "a2"}
【提示】
在字典序中,字符的顺序是基于它们的ASCII值:0<1<2 <...< 9<A<B <...< Z<a<b <...< z.
比较两个字符串时,从左到右逐个字符进行比较,直到分出先后为止。
2、关于下述代码,说法错误的是?A
A、函数multiply的定义应该放到函数main之前。
B、函数声明int multiply(int x,int y);中明确指定了函数multiply()的返回值为整数类型。
C、在main函数中,函数multiply通过multiply(a,b)被调用,其中a和b是定义在main函数中的变量,它们作为实参传递给了multiply函数的形参x和y。
D、运行上述代码,将输出:20。
cpp
int multiply (int x, int y) ;
int main() {
int a = 4;
int b = 5;
int result = multiply (a,b);
cout << result << endl;
return 0;
}
int multiply (int x, int y) {
return x*y;
}
降序组合
按升序输入n个不相同的数。现在从这n个数中挑出r个数进行组合(r <= n),请按字典序降序排序输出所有可能的组合结果。
【输入描述】两个整数n和r(n和r都是3~10之间的整数)
【输出描述】从n个数中取r个数的组合结果!结果按字典序降序排序。
【输入样例】43
3 7 12 19
【输出样例】
19 12 7
19 12 3
19 7 3
12 7 3
cpp
#include<iostream>
#include<algorithm>
using namespace std;
int n,r;
int in[11];
int visited[11];//标记数组对应n个数,从下标1开始放
int box [11] ;// r个盒子
bool cmp(int a,int b) {
if (a>b) return true;
return false;
}
void dfs(int x,int id){// 第x个盒子,从第id个数开始选
if(x == r+1){// 1~r个盒子都放好了
for(int i=1;i <= r;i++)
cout << box[i] << "";//按照盒子顺序输出盒子里的数
cout << endl;// 每组全组合占一行
return;// 结束当前递归,返回上一层
}
for(int i=id;i <= n;i++){//从n个数中依序找一个可以放入盒子的数
if(!visited[i]) {
box[x]=in[i];//选中第i个数
visited[i] = true;
dfs(x+1,i+1);//开始往第x+1个盒子放数字,从最后被选中的数的下一个开始选
visited[i]= false;//清除标记,这个数字可使用
}
}
}
int main(){
cin >> n >> r;
for(int i=1;i <= n;i++)//输入
cin>>in[i];
sort(in+1,in+n+1,cmp);//先降序排序
dfs(1,1);//第1个盒子,从第1个数开始选
return 0;
}