深度优先搜索
今天我们来讲解的是深度优先搜索,这是我们大家学习信息是必不可少也是最总要的一个算法,那么深度优先搜索这个算法究竟是干了什么呢?这很简单。本质搜索搜索,就在于这二字,也就是一个一个查找。不过深度优先搜索,其实就是在这棵搜索树中以深度为先,也就是所谓的不撞南墙不回头,就是说我们可以把它认为是走迷宫,如果到了终点就没有关系,不然就继续走,碰到弯道一直往右,碰到死胡同再绕出来。就是怎么简单。那么接下来我们就来看一下一道比较经典的问题,也就是全排列问题。
全排列
首先,这类问题的一个特点就是跟数字有关,比如自然数的拆分这道题目,就是进行一个分解,并且数据范围的大小是我们可以接受的这种,因为搜索这种算法在现在通常是一种暴力骗分的一种手段。所以我们可以使用搜索进行一个获取部分分。所以我们先拿这道题目来举一个例子。
按照字典序输出自然数 1 1 1 到 n n n 所有不重复的排列,即 n n n 的全排列,要求所产生的任一数字序列中不允许出现重复的数字。
那么这道题的思路就是从第一个数开始看,每一个位置都从 1 1 1 到 n n n 进行一个遍历的操作。同时要注意,因为排列每个数只能使用一次,所以另外再记录一个有没有使用过的数组进行存储。那么如果所有位置都遍历完毕,我们就进行一个输出。同时最重要的回溯也必不可少。那么回溯究竟是什么呢?再说回走迷宫,每次你走到死胡同里面,那么你是不是需要进行回到上一个路口走另一个弯道。所以这就是回溯。接下来给大家贴一下代码。
#include<bits/stdc++.h>
using namespace std;
int b[1000], a[1000], n;//b就是记录有没有使用过
void dfs(int x)
{
if(x==n+1)//说明所有位置都填好了
{
cout<<" ";
for(int i=1;i<=n;i++)//输出
{
cout<<a[i]<<" ";
if(i==n)
{
cout<<endl;
}
}
}
for(int i=1;i<=n;i++)
{
if(b[i]==0)//没有填过
{
a[x]=i;
b[i]=1;
dfs(x+1);
b[i]=0;//回溯
}
}
}
int main()
{
cin>>n;
dfs(1);
return 0;
}
剪枝
众所周知,搜索的时间复杂度是非常高的,比如刚刚拿到题目,每一个位置都是放或者是不放,时间复杂度就是次方式的样子。所以剪枝就是必不可少的。剪枝分为最优性剪枝和可行性剪枝。最优性剪枝就是说如果你的答案已经比你目前算出的答案大了或者是更劣的,我们就需要直接舍弃,避免浪费时间。这个比较好理解。不过可行性剪枝就比较难了,就是通过寻找题目中给你的一些条件,来将一写1无用的减掉。比如题目需要奇数,那么偶数就可以直接剪掉,所以可行性剪枝一般比最优性剪枝优化的更多。那么接下来我们就来看一道经典但是又没有特别简单的题目——小木棍。
乔治有一些同样长的小木棍,他把这些木棍随意砍成几段,直到每段的长都不超过 50 50 50。
现在,他想把小木棍拼接成原来的样子,但是却忘记了自己开始时有多少根木棍和它们的长度。
给出每段小木棍的长度,编程帮他找出原始木棍的最小可能长度。
这道题的一个思路首先是深搜没错了,那么如何进行一个剪枝呢?这里就是一个难点。同学们需要先学会普通的最优性剪枝,比如木棍短了就不要了。那么这里我给大家总结一下:
- 预先处理出所有木棍的总长度,且保证枚举答案的值能被总长度整除,这里确保可以分开来。
- 每根木棍的长度可用桶来存储,并且预先处理出最长的和最短的木棍的长度,搜索时从最大长度到最小长度递减枚举。
- 若拼接当前木棍时已用了一根长为 x x x 的木棍,则搜索时从长度X开始搜索。
- 若某组拼接不成立,且此时 已拼接的长度为 0 0 0 或 当前已拼接的长度与刚才枚举的长度之和为最终枚举的答案 时,则可直接跳出循环,因为此时继续枚举其它更小的值时,显然可能情况更少,且同样凑不完。
那么代码的话就给大家贴一下,建议自己先尝试
#include<bits/stdc++.h>
const int N = 70 ;
int n , cnt , tot , maxn , minn , tm[ N ];
void dfs( int res , int sum , int target , int p ) {
if( res == 0 )
{
printf("%d", target );
exit( 0 );
}
if( sum == target )
{
dfs( res - 1 , 0 , target , maxn );
return;
}
for( int i = p ; i >= minn ; i -- )
{
if( tm[ i ] && i + sum <= target )
{
tm[ i ] -- ;
dfs( res , sum + i , target , i );
tm[ i ] ++ ;
if ( sum == 0 || sum + i == target )
break;
}
}
return;
}
int main()
{
scanf("%d" , &n ) ;
minn = N ;
int temp;
while( n -- )
{
scanf("%d" , &temp );
if( temp <= 50 )
{
cnt ++;
tm[ temp ] ++;
tot += temp;
maxn = maxn > temp ? maxn : temp ;
minn = minn < temp ? minn : temp ;
}
}
temp = tot >> 1;
for( int i = maxn ; i <= temp ; i ++ )
{
if( tot % i == 0 )
{
dfs( tot / i , 0 , i , maxn );
}
}
printf("%d" , tot );
return 0;
}
记忆化搜索
首先,我先问大家一个事情,如果我们每次考虑一个物品,有选或不选两种可能,所以假设有 n n n 个物品的情况下就是 2 n 2^n 2n 的时间复杂度,所以记忆化就是有所必要的,其实记忆化搜索就是进行一个存储,如果出现了跟之前一模一样的情况,你就不需要再重新计算一遍了,所以这会大大减少我们搜索的时间复杂度。然后再给大家推荐几道经典的题目:滑雪,Apple Catching G。