我是靠谱客的博主 笑点低冰棍,这篇文章主要介绍回溯法--排列组合类,现在分享给大家,希望可以做个参考。

视频讲解地址

一、DFS的框架,必须熟记

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void dfs(int k) { if(到达终点或者目的地) { 输出问题解或者解得方案数+1} for(int i = 0;i<="可扩展状态总数";i++) { 按照规则,产生新的状态; if(判断新状态是否合法) { 保存合法结果; dfs(k+1); 恢复现场,回溯;//可以没有,根据情况来定 } } } int main() { ..... dfs(k); return 0; }

回溯法是DFS的一种搜索策略;那么它和DFS有什么区别呢?我们在应用的时候应该怎么辨别是否需要回溯呢?

听视频讲解吧,文字太难表述了!

回溯法是DFS的一种搜索策略
回溯法可以解决如下几种问题:
排列问题:N个数按照一定规则全排列,有几种排列方式
组合问题:如何按照一定规则在N个数中找出K个数的集合
切割问题:一个字符串按照一定规则切割,有几种切割方式
子集问题:一个N个数的集合中有多少符合条件的子集
棋盘问题:N皇后、数独等问题

二、排列组合类DFS

例1 全排列:

输出1…n(n<100)的全排列。 输入:n 输出:输出全排列,每个排列换行
样例输入:
3
样例输出:
1 2 3
1 3 2
21 3
2 3 1
3 1 2
3 2 1

【分析】
首先,需要一个数组来保存已经产生的排列;我们用int a[102];
接下来根据上面的搜索框架来构造该题的代码框架;

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int n; void dfs (int k) { if(k==n+1) { 输出数组a[]; } for(int i=1;i<=n;i++) { if(i是可用的) { a[k]=i;//保存当前的结果 dfs(k+1);//继续搜索第k+1个数 恢复现场,回溯; } } } int main() { cin >> n; dfs(1); return 0; }

我们来解决这些问题:
1、输出数组a[],for(int i= 1;i <=n;i++) cout <<a[i]<<" ";
2、如何判定产生的i是否可用,也就是i是否已经出现过,两种方式:
(1)我们可以构造一个judge(int x,int y);来判断1-(x-1)的位置是否出现过y;

复制代码
1
2
3
4
5
6
7
8
9
bool judge(int x,int y) { bool flag = true; for(int i = 1;flag && i < x;i++) { if(a[i]==y) flag = false; } return flag;

则“i是可用的”就可以使用上述函数来代替了if(judge(k,i);这时候我们不需要恢复现场的,也就是没有回溯过程。
(2)使用标记数组vist[102];来记录1…n是否被使用过了,具体完整代码如下:
强烈推荐这种方式,比较通用!
在这里插入图片描述

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include<iostream> using namespace std; int a[102]; int vist[102]={0}; int n;//两个函数公用n,要使用全局变量。 void dfs(int k) { if(k==n+1)//已经放到n+1位置,说明已经完成任务 { for(int i = 1;i <= n;i++) cout<<a[i]<<" "; cout<<endl; } for(int i = 1;i <=n;i ++) { if(vist[i]==0)//i可以使用 { a[k]=i; vist[i]=1;//标记i已经被使用过了 dfs(k+1);//继续搜索第k+1个数 vist[i]=0;//回溯,把已经使用的i退回去 } } } int main() { cin >> n; dfs(1); return 0; }

扩展思维:
1、如果给定的数组a[]中进行全排列,代码应该如何改呢?自己试着实现以下。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include<iostream> using namespace std; const int N = 10; int path[N]; int vis[N]={ 0 }; int n; int a[N]; void dfs(int k) { if(k==n+1) { for(int i= 1;i <=n;i++) { cout<< path[i]<<" "; } cout<<endl; return ; } for(int i = 1;i <=n;i++) { if(!vis[i]) { path[k]=a[i];//注意此处的变化 vis[i]=1; dfs(k+1); vis[i]=0; } } } int main() { cin >> n; for(int i = 1;i <=n ;i++) cin >> a[i];//注意该代码是什么意思 dfs(1); return 0; }

排序和组合的本质区别在于是否有序。
体现在代码阶段:
那么既然是⽆序,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始,⽽不是从0开始!

例2、组合输出

从n个元素中抽出r个元素(不分顺序且r≤n),我们可以简单地将n个元素理解为自然数1,2,…,n,从中任取r个数。

现要求你用DFS的方法输出所有组合。

例如
n=5,r=3,
所有组合为:
1 2 3
1 2 4
1 2 5
1 3 4
1 3 5
1 4 5
2 3 4
2 3 5
2 4 5
3 4 5

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include<iostream> #include<cstdio> #include<cstdlib> #include<cstring> #define N 30 using namespace std; int n,r; int a[N]; int vis[N]; void dfs(int step) { int i; if(step==r+1) { for(i=1;i<=r;i++) cout<<" "<<a[i]; cout<<endl; return; } for(i=a[step-1];i<=n;i++)//从当前已经选择的位置向后选 { if(vis[i]==0) { a[step]=i; vis[i]=1; dfs(step+1); vis[i]=0; } } } int main() { cin>>n>>r; a[0]=1; dfs(1); return 0; }

视频讲解

例题3组合总和

给定一个数组c和一个目标数t,找出c中所有可以使数字和为t的组合。 c中的数字每个组合中只能使用一次。
说明:
所有数字(包括目标数)都是正整数。
解集中不能包含重复的组合。
输入
第一行为n和t,表示数组c中包含n个正整数和目标数t;
第二行为n个正整数,中间用一个空格隔开
输出
若干行,一行表示一个符合题意的划分
样例:
输入
7 8
10 1 2 7 6 1 5
输出
1 1 6
1 2 5
1 7
2 6

【解析】
1、本题和例3有如下区别:
本题中c中的数字在每个组合中只能使用一次
本题中c中有重复的元素,而例3中没有重复元素
2、本题的难点在于:**集合c中有重复元素,但不能有重复组合 **
【知识点】
1、借助本题,请理解好搜索中的两种去重“树层去重”和“树枝去重”
2、所谓去重,其实就是使用过的元素不能重复选取。
”使用过“在树形结构上是有两个维度的,一个维度是同一树枝上使用过,另一个维度是同一树层上使用过。
在这里插入图片描述

回看一下题目,元素在同一个组合内是可以重复的,但是两个组合不能相同。所以我们要去重的是同一树层上”使用过“的元素,同一树枝上的元素都是一个组合里的,不用去重。
强调一下,树层去重是需要对数组排序的
本题中增加一个bool类型数组used[],用来记录同一树枝上的元素是否使用过。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include<bits/stdc++.h> using namespace std; int a[105]; bool used[105]; int ans[105]; int n,t; int sum = 0; int x = 0; void dfs(int index) { if(sum==t) { for(int i = 1;i<=x;i++) cout<<ans[i]<<" "; cout<<endl; return ; } for(int i = index;i<=n;i++) { //used[i-1]==ture,说明 同一树枝使用过a[i-1]元素;used[i-1]==false,说明同一树层使用过a[i-1]元素 if(i>0&&a[i]==a[i-1]&&used[i-1]==false) continue; sum+=a[i]; ans[++x]=a[i]; used[i]=true; dfs(i+1); //开始回溯 sum-=a[i]; used[i]=false; x--; } } int main() { cin>>n>>t; for(int i = 1;i<=n;i++) cin>>a[i]; sort(a+1,a+n+1); dfs(1); return 0; }

视频讲解

例四选数【NOIP普及组2002】

提示性图解
在这里插入图片描述
可以适当减纸
视频讲解

练习题目:(实践出真知,实践中体会编程成就感)

1、挑选数字
–视频讲解
2、小明爱正方形
–视频讲解
3、分书问题
–视频讲解

搜索相关内容

1、DFS第一课
3、回溯法–分割问题与子集问题

最后

以上就是笑点低冰棍最近收集整理的关于回溯法--排列组合类的全部内容,更多相关回溯法--排列组合类内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(66)

评论列表共有 0 条评论

立即
投稿
返回
顶部