DFS
DFS复盘
0. 写在前面
先把DFS的精髓记录一下,后续就想着如何应用就好.....
DFS搜索
在搜索算法 中,该词常常指利用递归函数方便地实现暴力枚举的算法,与图论中的 DFS 算法有一定相似之处,但并不完全相同。(摘自wiki)
例如正整数n=6,将其分解为6=1+2+3。
满足条件:后面的数>前面的数
要求:输出所有分解方案。
咱刚学C语言那会儿,正常思路基本是for循环暴力枚举...当然...仅限于题目要求我们的项数很少的情况下,不然时间复杂度就是跟幂函数一样,指数级增加了...
for (int i = 1; i <= n; ++i)
for (int j = i; j <= n; ++j)
for (int k = j; k <= n; ++k)
if (i + j + k == n) printf("%d = %d + %d + %d\n", n, i, j, k);
但其实我们学过递归以后,更优的方法是可以逐项递归搜索枚举出来...
参数说明:
参数n是记录还剩下多少数。
参数i记录项数,要求是小于总项数m
参数a是记录本次搜索开始的数(即这一项从何数开始for循环)
(画的些许潦草,将就看看....)
int m, rec[103]; // rec 用于记录方案
void dfs(int n, int i, int a) {
if (n == 0) {
for (int j = 1; j <= i - 1; ++j) printf("%d ", rec[j]);
printf("\n");
}
if (i <= m) {
for (int j = a; j <= n; ++j) {
rec[i] = j;
dfs(n - j, i + 1, j); // 请仔细思考该行含义。
}
}
}
// 主函数
// n为待拆分数,m为拆分的项数
scanf("%d%d", &n, &m);
dfs(n, 1, 1);
dfs(int n,int i,int a)函数的要点:
-
题目说要求多少项,那需要一个项数i吧,我们搜索就是一项一项的往下搜索的。
-
那我们判断有没有结束,总得有一个剩下的数n吧,如果N-sum(arr)=0的话,就说明可以分解相加了。
-
题目说要后面的数比前面大,那我们for循环的起始位置得从上一层搜索中前一位的数j拿出来吧,这就是参数a。
最后用record数组保存在搜索过程中的数字。
1. n的全排列问题
输入
3
输出
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
给出一套dfs框架
// 深度优先搜索算法框架2
// int Search(int k)
// {
// if (到目的地) 输出解;
// else
// for (i=1;i<=算符种数;i++)
// if (满足条件)
// {
// 保存结果;
// Search(k+1)
// 恢复:保存结果之前的状态{回溯一步}
// }
// }
来看状态(搜索)树
dfs(1)时
dfs(2)时
源码如下
#include<stdio.h>
int n,ans[15],vis[15];
void dfs(int index)
{
if(index>n)
{
for(int i=1;i<=n;i++) printf("%5d",ans[i]);
printf("\n");
}
else
{
for(int i=1;i<=n;i++)
{
if(vis[i]==0)
{
vis[i]=1;
ans[index]=i;
dfs(index+1);
vis[i]=0;
}
}
}
}
int main()
{
scanf("%d",&n);
dfs(1);
return 0;
}
2. 女王占地盘的决策(地盘要求每行每列=1,每斜线<=1)
在6*6地图中,女王占地盘的一种情况如下:
首先,我们想想,如果我们要用深度优先搜索的话,从哪开始往下挖呢??根据前面6=1+2+3的一项一项的启发,再加上此题目的要求是输出每一行的列数,那我们是不是可以一列一列(line)地搜索哪一行(row)满足条件可以占领呢? 参数也只需要给一个列数line即可,后续递归dfs(line+1)就可以做下一列搜索了,搜索完的时候line>n表示结束, 然后根据题目要求顺带输出。
先复盘一下整个题目的思路:
女王想要占领地盘的话,就让臣民一行行(row++)往后找,如果满足条件,就占领,改变相应参数!
满足条件就往下一列继续找dfs(line+1), 即考虑占领下一个地盘,不行的话就让臣民们回到一开始的状态(回溯),让臣民们继续在下一行(dfs)看看满不满足占领的条件。
这是dfs搜索的基本思路。
dfs最外面框架先写好。
void dfs(int line)
{
if(line>n)
{
print();
return ;
}
for((int row=1;i<=n;i++))
{
//.....
}
}
那么省略的内容就是:
- 臣民判断咱占领的条件是啥?
如果可以占领:
-
怎么表示我们已经占领了?
-
怎么往下一层找?(dfs(line+1))
-
如果找不到了或者找到了,怎么回到一开始的那一层继续往后一行(row)找??(回溯)
最初判断能否占领的想法是用二维数组,不过这个条件有点类似于数独,需要好多个for循环,然后对角线又很麻烦,所以让本菜鸟放弃了挣扎。
这边比较好的方法是申请四个一维数组,分别表示列str_line[]、行str_row[],左对角线left_slope[],右对角right_slope[]
这边还有一个难点就是判断对角线的时候:
n=5时
比如(1,3)左对角线,有(2,2),(3,1) 满足如下条件
line+row=const1;
比如(1,3)右对角线,有(2,4),(3,5) 满足如下条件
line-row=const2;
这里注意const2可能为负数,而数组right_slope[]下标不能为负数。解决方法是在原有基础上+n, 后续判断也同样+n,即
right_slope[line-row+n]=1
数组的值初始化为0,表示满足条件,可以占领,表示臣民占领成功以后就分别设置为非0, 这边因为要输出具体的列,所以str_line=满足条件的行数i.
for(int row=1;row<=n;row++)
{
if(!str_row[row]&&!left_slope[line+row]&&!right_slope[line-row+n])
//因为臣民是一列列(line++)搜索的,那他找的那一列一定是都没有被占领过的,所以这里判断和回溯的时候少写了这种情形
{
str_line[line]=row; //表示第line列不可访问,且返回值为其列数
str_row[row]=1; //表示第row行不可访问
left_slope[row+line]=1;
right_slope[line-row+n]=1;
dfs(line+1);
str_row[row]=0;
left_slope[row+line]=0;
right_slope[line-row+n]=0; //回溯
}
}
最后组合在一起加个输入输出,让臣民从first line开始找,那就是源码了,女王的臣民也终于可以按照她的指示开工了嘻嘻。
#include<stdio.h>
int n;
int str_line[30],str_row[30],left_slope[30],right_slope[30];
int ans;
void print()
{
if(ans<=2)
{
for(int i=1;i<=n;i++) printf("%d ",str_line[i]);
printf("\n");
}
ans++;
}
void dfs(int line)
{
if(line>n)
{
print();
return ;
}
for(int row=1;row<=n;row++)
{
if(!str_row[row]&&!left_slope[line+row]&&!right_slope[line-row+n])
{
str_line[line]=row; //表示第line列不可访问,且令值为列数row方便输出
str_row[row]=1; //表示第row行不可访问
left_slope[row+line]=1;
right_slope[line-row+n]=1;
dfs(line+1); //往下一层找
str_row[row]=0;
left_slope[row+line]=0;
right_slope[line-row+n]=0; //回溯
}
}
}
int main()
{
scanf("%d",&n);
dfs(1);
printf("%d\n",ans);
return 0;
}
附:框架
深度优先搜索算法框架2
int Search(int k)
{
if (到目的地) 输出解;
else
for (i=1;i<=算符种数;i++)
if (满足条件)
{
保存结果;
Search(k+1)
恢复:保存结果之前的状态{回溯一步}
}
}