深搜-选数类问题

目录

1.问题引入

2.知识讲解 

3.例题解析 

【例题1】全排列。

【例题2】素数环Ⅱ。 

【样例3】素数分解。 


1.问题引入

上一节探讨了迷宫类问题,和平时遇到的迷宫小游戏类似,可以使用搜索程序求得迷宫的路径和最短路。本小节继续研究深搜的另一类问题——选数类问题。

选数类问题在生活中也能经常遇到,比如数独游戏、八皇后摆放问题等等。因为这些难度较高,感兴趣的同学可以主动学习研究。

拓拓最近在研究简单的素数环问题,问题是这样的:从1、2、3、4、5、6这6个整数随机填写到下图所示的圆环内,要求任意相邻的圆圈内数字之和是素数。除了下图的方案,拓拓又找到了11种方案,你能找到另外的11种填写方案的画法吗?

深搜-选数类问题_第1张图片

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个。同时注意到不能选取之前选过的数字,所以采用数组标记的方式把选过的数字做个标记。

深搜-选数类问题_第2张图片

深搜的顺序如下:

深搜-选数类问题_第3张图片

① 向第一个盒子填一个数后向下搜索、向第二个盒子填第二个数向下搜索、向第三个盒子填第三个数后结束向下搜索并输出方案1 2 3;

② 回溯到上一层填完两个数的状态(没有其他方案)、继续回溯到上一层填完一个数的状态,发现有其他可能;

③ 填第二个数后向下搜索、填第三个数后结束向下搜索并输出方案1 3 2;

④ 回溯到填完两个数的状态、回溯到填完一个数的状态、回溯到没填数的状态;

⑤ 填第一个数(也就是填2数字)后向下搜索、填第二个数向下搜索、填第三个数后结束向下搜索并输出方案2 1 3;

⑥ 回溯到填完两个数的状态、回溯到填完一个数的状态,发现有其他可能;

。。。。。。

#include
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举例。

环的形式不好处理,转变思路,可以把环从第一个要填的盒子和第4个要填的盒子剪开,那么就可以转化成一维数组存储。

如上图所示。填写数字时和全排列问题一样,但是本题多了一个条件,需要确保相邻两项的和是素数,因此在搜索的时候加上相邻两项之和是素数的判断。 

#include
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 
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,第一次找到分解方案时,输出答案,标记结束所有的搜索。 

谢谢观看=关注我哦~

你可能感兴趣的:(深搜-选数类问题,C++,知识点,算法,Mark1277,c++,深搜-选数类问题)