目录
1.问题引入
2.知识讲解
3.例题解析
【例题1】全排列。
【例题2】素数环Ⅱ。
【样例3】素数分解。
1.问题引入
上一节探讨了迷宫类问题,和平时遇到的迷宫小游戏类似,可以使用搜索程序求得迷宫的路径和最短路。本小节继续研究深搜的另一类问题——选数类问题。
选数类问题在生活中也能经常遇到,比如数独游戏、八皇后摆放问题等等。因为这些难度较高,感兴趣的同学可以主动学习研究。
拓拓最近在研究简单的素数环问题,问题是这样的:从1、2、3、4、5、6这6个整数随机填写到下图所示的圆环内,要求任意相邻的圆圈内数字之和是素数。除了下图的方案,拓拓又找到了11种方案,你能找到另外的11种填写方案的画法吗?
2.知识讲解
综合之前深搜的代码和框架,提取出新的代码框架如下:
全局状态变量
void dfs(当前状态)
{
if(当前状态是目标状态) // 判断
进行相应处理(输出当前解、更新最优解、退出返回等)
for(所有可行的新状态) { // 扩展
获取新状态;
if(新状态没有访问过 && 需要访问 && 优化) {
标记;
dfs(新状态);
取消标记;
}
}
}
int main()
{
...;
dfs(初始状态);
...;
}
3.例题解析
【例题1】全排列。
输入一个整数n(n≤9)。输出n的全排列。全排列是指从1~n这n个数中选取n个数的所有排列情况。
输入样例:
3
输出样例:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
【问题分析】首先创建一个数组(盒子)存放排列的结果,逐个往该数组(盒子)中填放可能的数字,直到填的数字个数是n个。同时注意到不能选取之前选过的数字,所以采用数组标记的方式把选过的数字做个标记。
深搜的顺序如下:
① 向第一个盒子填一个数后向下搜索、向第二个盒子填第二个数向下搜索、向第三个盒子填第三个数后结束向下搜索并输出方案1 2 3;
② 回溯到上一层填完两个数的状态(没有其他方案)、继续回溯到上一层填完一个数的状态,发现有其他可能;
③ 填第二个数后向下搜索、填第三个数后结束向下搜索并输出方案1 3 2;
④ 回溯到填完两个数的状态、回溯到填完一个数的状态、回溯到没填数的状态;
⑤ 填第一个数(也就是填2数字)后向下搜索、填第二个数向下搜索、填第三个数后结束向下搜索并输出方案2 1 3;
⑥ 回溯到填完两个数的状态、回溯到填完一个数的状态,发现有其他可能;
。。。。。。
#include<bits/stdc++.h>
using namespace std;
int n;
int a[11]; //存放排列的结果
bool f[11]; //标记选过的数字
void dfs(int k) { //向第k个盒子里填写数字
if (k > n) { //当k大于n时,说明n个盒子已经填好了
for (int i = 1; i <= n; i++)
cout << a[i] << " ";
cout << endl;
return;
}
for (int i = 1; i <= n; i++) { //把所有的数字枚举一遍
if (f[i] == 0) { // i没有选过
f[i] = 1; //标记i已选择
a[k] = i; //把i放到k下标
dfs(k + 1); //递归填写k+1位置的数字
f[i] = 0; //取消标记
}
}
}
int main() {
cin >> n;
dfs(1); //开始向第一个盒子里面填数字
return 0;
}
【例题2】素数环Ⅱ。
从1~n(2≤n≤15)这n个数中随机摆成一个环,要求相邻的两个数的和是素数,按照由小到大请输出所有可能的摆放形式。
输入样例:
4
输出样例:
1:1 2 3 4
2:1 4 3 2
3:2 1 4 3
4:2 3 4 1
5:3 2 1 4
6:3 4 1 2
7:4 1 2 3
8:4 3 2 1
total:8
【问题分析】
素数环就是本节最开始引入的问题,现在我们可以用程序求解所有摆放的答案。下面分析是以样例输入的4举例。
环的形式不好处理,转变思路,可以把环从第一个要填的盒子和第4个要填的盒子剪开,那么就可以转化成一维数组存储。
如上图所示。填写数字时和全排列问题一样,但是本题多了一个条件,需要确保相邻两项的和是素数,因此在搜索的时候加上相邻两项之和是素数的判断。
#include<bits/stdc++.h>
using namespace std;
int n, cnt; //cnt是方案个数
int a[20]; //记录方案
bool f[20]; //标记i是否选过
bool prime(int x) { //判断素数的标准代码
if (x == 1) return 0;
for (int i = 2; i <= sqrt(x); i++)
if (x % i == 0) return 0;
return 1;
}
void dfs(int k) { //要填第k个数字
if (k > n) {
if (prime(a[1] + a[n])) { //填完后,确保第1项和第n项之和也是素数
cout << cnt++ << ":";
for (int i = 1; i <= n; i++)
cout << a[i] << " ";
cout << endl;
}
return;
}
for (int i = 1; i <= n; i++) {
//选择第k项填i时,要确保i与上一项a[k-1]的和是素数
if (f[i] == 0 && ( k == 1 || prime(i + a[k - 1]))) {
f[i] = 1; //标记
a[k] = i; //填写i
dfs(k + 1); //递归填写k+1项
f[i] = 0; //取消标记
}
}
}
int main() {
cin >> n;
dfs(1);
cout << "total:" << cnt;
return 0;
}
【样例3】素数分解。
虽然素数不能分解成除1和其自身之外整数的乘积,但却可以分解成更多素数的和。你需要编程求出一个正整数n(10≤n≤200)最多能分解成多少个互不相同的素数的和。
例如,21 = 2 + 19是21的合法分解方法。21 = 2 + 3 + 5 + 11则是分解为最多素数的方法。所以21最多可以分解为4个不同素数的和。
【问题分析】当然你可以把所有分解方案都搜索出来,但实际上有更快的解法——在贪心的基础上搜索。
一个小小的贪心思路:尽可能选择更小的素数分解,这样得到的个数是最多的。证明也很简单,反证法:如果有某种方案中可以选择更大的素数分解,那么采用贪心策略用更小的素数替换掉更大的素数,分解的个数会更多(至少不会变少)。
因此我们第一次搜索得到的分解答案,就是分解的数字最多的分解方案。dfs的设计可以稍作调整,在dfs的过程中通过参数记录选到的数字、选了几个数、选的数字总和。
参考代码如下:
#include <bits/stdc++.h>
using namespace std;
int n, flag;
bool Prime(int x) {
if (x < 2) return 0;
for (int i = 2; i <= x / i; i++)
if (x % i == 0) return 0;
return 1;
}
// k:当前选到了哪个数
// cnt:已经选了几个数
// sum:已经选的数的总和
void dfs(int k, int cnt, int sum) {
if (sum > n) return ;
if (flag) return; //当有一种方案,就可以结束所有搜索
if (sum == n) {
cout << cnt;
flag = 1; //标记找到了分解方案
return ;
}
for (int i = k; i <= n; i++) { //每次从k开始选择素数,22行
if (Prime(i)) {
dfs(i + 1, cnt + 1, sum + i); //24行
}
}
}
int main() {
cin >> n;
dfs(2, 0, 0);
return 0;
}
说明:
(1) 注意理解dfs的参数,这里的k、cnt、sum都是当前的状态变量。
(2) 22行不需要从1循环到n,因为题目要求分解方案不能有相同的素数,因此之前的素数(无论选没选)不能再次选择了,这里也无需标记选择过的状态,每次从k往后找即可。
(3) 因为i满足是素数,而且因为(2)的原因也不会选择重复的素数,所以第24行就可以确定选择i。dfs(i + 1, cnt + 1, sum + i)的含义是:递归求解下一个数的选择,至少要从i+1开始选数;选择了i这个素数所以个数变成了cnt+1个,总和变成了sum+i。
(4) 一进入dfs的三个条件比较好理解,第一个if,当sum超过n时,分解方案是错误的,所以递归回到上一层选其他的数字;第二个if,当找到了一种方案,根据贪心的分析就得到了答案,此时就可以结束了所有的搜索;第三个if,第一次找到分解方案时,输出答案,标记结束所有的搜索。
谢谢观看=关注我哦~