专题:DFS(基础)
一、基础知识
1. 引入
搜索与回溯是计算机解题中常用的算法,很多问题无法根据某种确定的计算法则来求解,可以利用搜索与回溯的技术求解。回溯是搜索算法中的一种控制策略。
2. 基本思想
(1)为了求得问题的解,先选择某一种可能情况向前探索;
(2)在探索过程中,一旦发现原来的选择是错误的,就退回一步重新选择,继续向前探索;
(3)如此反复进行,直至得到解或证明无解。
举例:迷宫问题:进入迷宫后,先随意选择一个前进方向,一步步向前试探前进,如果碰到死胡同,说明前进方向已无路可走,这时,首先看其它方向是否还有路可走,如果有路可走,则沿该方向再向前试探;如果已无路可走,则返回一步,再看其它方向是否还有路可走;如果有路可走,则沿该方向再向前试探。按此原则不断搜索回溯再搜索,直到找到新的出路或从原路返回入口处无解为止。
二、典例
1. 素数环
给定一个N,求1——N组成的环,使得环上相邻的元素和为素数。
输入描述:
一个整数N(1<=N<=10)
输出描述:
把1放在第一位置,按照字典顺序不重复地输出所有解(顺时针,逆时针算不同的两种),相邻两数之间严格用一个空格隔开,每一行的末尾不能有多余的空格。如果无解,则输出“no”。
样例输入:
8
样例输出:
1 2 3 8 5 6 7 4
1 2 5 8 3 4 7 6
1 4 7 6 5 8 3 2
1 6 7 4 3 8 5 2
【分析】环中只确定起点为1,而其它位置上的数不确定,也就是说有多种可能的组合。遇到此种情境时要考虑搜索+回溯求解。
在这里,除去环的起点,每个位置上的数有N种可能。只要满足以下3点:
(1)填进去的数合法(2~N)
(2)与前面的数不相同;
(3)与左边相邻的数的和是一个素数,特别注意环的终点与环的起点上的数之和也是一个素数。
就可确定所求的环是素数环。
过程如下:
(1)使用两个数组ring和vis。其中ring保存所求素数环中的各个数,vis记录1~N 这N个数是否已使用(因为要保证不重复)
(2)数据初始化;
(3)递归填数:判断第i个数填入是否合法;
①合法:填数,并判断是否已经到达环的终点。如果到达终点,打印结果;否则,继续填下一个数;
②不合法:选择下一种可能。
#include <stdio.h>
#include <math.h>
#include <string.h>
#define maxn 15
int N;
int ans=0; //ans记录解的数目,ans=0时表示无解,输出"no"
int ring[maxn]; //ring数组保存素数环中的数
int vis[maxn]; //vis记录1~N 这N个数是否已填入素数环
int is_Prime(int n) //判素数
{
int i;
if(n<=1) //负数.0和1都不是素数
return 0;
for(i=2;i<=sqrt(n);i++)
{
if(n%i==0)
return 0;
}
return 1;
}
void dfs(int cur) //从第2个数开始,向后搜索符合条件的素数环
{
int i;
if(cur==N) //搜索结束
{
if(is_Prime(ring[0]+ring[cur-1])) //判断素数环首尾数之和是否为素数,如果是,则找到解
{
for(i=0;i<cur-1;i++)
printf("%d ",ring[i]);
printf("%d\n",ring[cur-1]);
ans++;
}
return;
}
for(i=2;i<=N;i++) //试探2~N中的每个数
{
//当i未被使用且i与环中最后一个数之和为素数时,将其填入素数环
if(!vis[i] && is_Prime(ring[cur-1]+i))
{
vis[i]=1; //置已使用标记
ring[cur]=i;//填充
dfs(cur+1); //继续搜索
vis[i]=0; //回溯-清除已使用标记
}
}
}
int main()
{
scanf("%d",&N);
memset(vis,0,sizeof(vis));
if(N==1) //N为1时,因为1不是素数,所以可直接判断无解
printf("no\n");
else //其它情况:搜索
{
ring[0]=1; //先将1填入,作为素数环的起点
dfs(1); //从环的第2个数开始搜素
if(ans==0) //搜索结束后,如果无解,输出"no"
printf("no\n");
}
return 0;
}
2. 排列组合问题
(Ⅰ)设有n个整数的集合{1,2,…,n},从中取出任意r个数进行排列(1<=r<n<=10),试列出所有的排列。
【分析】元素的排列问题
总体思路同上,这里某个元素按不同次序出现的组合应视为不同的排列。例如:1 2 3和2 1 3,元素均为1.2.3,只是排列顺序不同,因此应视为元素1.2.3的不同排列。
特别地,当n=r时,称为n的全排列。实现时只需把下面程序的终点改为cur==n即可。
#include <stdio.h>
#define maxn 12
int n,r;
int ans=0; //符合要求的排列总数
int ret[maxn]; //保存产生的排列
int vis[maxn]; //记录1~n是否已使用 1-是 0-否
void dfs(int cur) //从{1,2,...,n}中取r个数构成的排列
{
int i;
if(cur==r) //r个数已经选完,输出选择方案
{
for(i=0;i<cur-1;i++)
printf("%d ",ret[i]);
printf("%d\n",ret[cur-1]);
ans++;
return;
}
//试探1~n n个数
for(i=1;i<=n;i++)
{
if(!vis[i]) //i未被使用
{
vis[i]=1; //置已使用标记
ret[cur]=i; //将i填入ret[cur]
dfs(cur+1); //继续搜索
vis[i]=0; //回溯:清除标记
}
}
}
int main()
{
scanf("%d %d",&n,&r);
dfs(0);
printf("count=%d\n",ans);
return 0;
}
(Ⅱ)组合的输出
排列与组合是常用的数学方法,其中组合就是从n个元素中抽出r个元素(不分顺序且r<=n),我们可以简单地将n个元素理解为自然数1,2,…,n,从中任取r个数。
现要求你用递归的方法输出所有组合。
例如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
【输入】
一行两个自然数n、r(1<n<21,1<=r<=n)。
【输出】
所有的组合,每一个组合占一行且其中的元素按由小到大的顺序排列,每个元素占三个字符的位置,所有的组合也按字典顺序。
【样例输入】
5 3
【样例输出】
1 2 3