--出自南昌理工学院acm集训队
基本概念
深度优先搜索算法(Depth First Search,简称DFS):一种用于遍历或搜索树或图的算法。 沿着树的深度遍历树的节点,尽可能深的搜索树的分支。当节点v的所在边都己被探寻过或者在搜寻时结点不满足条件,搜索将回溯到发现节点v的那条边的起始节点。整个进程反复进行直到所有节点都被访问为止。属于盲目搜索,最糟糕的情况算法时间复杂度为O(!n)。
思想(用栈的实现)
回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
基本模板加思想
int check(参数)
{
if(满足条件)
return 1;
return 0;
}
void dfs(int step)
{
判断边界
{
相应操作
}
尝试每一种可能
{
满足check条件
标记
继续下一步dfs(step+1)
恢复初始状态(回溯的时候要用到)
}
}
接触dfs的一个题目就是全排列问题
自问
dfs进行递归,表示的意义?
如何加深对dfs的理解(求法)?
为何回溯时,进行现场的恢复?
回答
此题中dfs表示的含义是:求出从第u行到最后一行的所有path。
dfs的求法:根据通项公式的含义,假设已知第u+1行到最后一行的所有path,综合1和2得出:path[u] 与 path[u+1] 合并后,即为dfs的解。
回溯的特征是:递归的最外层是一个循环。因为一次dfs得到的是所有的path。每一次都是从当前现场中去取得剩下未访问的元素。(这一块自己画个图就很容易理解)。
反证:如果不进行现场的恢复,则在第一次完成深搜后,所有元素都已经被访问过了。这样在回溯到上一层时,上层的现场中的状态都被下层更改了,数据就会乱套。
代码实现
#include<iostream>
using namespace std;
const int N=10;
int p[N];
int n;
bool st[N];
void dfs(int u)
{
if(u==n)
{
for(int i=0;i<n;i++)
printf("%d ",p[i]);
puts("");
return;
}
for(int i=1;i<=n;i++)
{
if(!st[i])
{
p[u]=i;
st[i]=true;
dfs(u+1);
st[i]=false;
}
}
}
int main()
{
cin>>n;
dfs(0);
return 0;
}
之后会遇到金典问题
#include <iostream>
#include <cstring>
using namespace std;
char a[10][10];
bool flag[10], k1[10][10], k2[10][10];
int n;
void dfs(int x){
if (x == n + 1){
for (int i = 1; i <= n; i++){
for (int j = 1; j <= n; j++){
printf("%c",a[i][j]);
}
printf("\n");
}
printf("\n");
return;
}
for (int i = 1; i <= n; i++){
if (!flag[i] && !k1[x][i] && !k2[x][i]){
a[x][i] = 'Q';
flag[i] = true;
int l = i + 1;
for (int j = x + 1; j <= n; j++){
if (l > n) break;
k1[j][l++] = true;
}
l = i - 1;
for (int j = x + 1; j <= n; j++){
if (l < 1) break;
k2[j][l--] = true;
}
dfs(x + 1);
l = i + 1;
for (int j = x + 1; j <= n; j++){
if (l > n) break;
k1[j][l++] = false;
}
l = i - 1;
for (int j = x + 1; j <= n; j++){
if (l < 1) break;
k2[j][l--] = false;
}
a[x][i] = '.';
flag[i] = false;
}
}
return;
}
int main(){
cin >> n;
memset(a,'.',sizeof(a));
dfs(1);
return 0;
}
下面分析中的(x,y)(x,y)相当于上面的(u,i)(u,i)
反对角线 y=x+by=x+b, 截距 b=y−xb=y−x,因为我们要把 bb 当做数组下标来用,显然 bb 不能是负的,所以我们加上 +n+n (实际上+n+4,+2n都行),来保证是结果是正的,即 y - x + n
而对角线 y=−x+by=−x+b, 截距是 b=y+xb=y+x,这里截距一定是正的,所以不需要加偏移量
核心目的:找一些合法的下标来表示dgdg或udgudg是否被标记过,所以如果你愿意,你取 udg[n+n−u+i]udg[n+n−u+i] 也可以,只要所有(u,i)(u,i)对可以映射过去就行
我分享一个我比较喜欢的题目
#include<iostream>
using namespace std;
const int N=10;
int n,m,ans=0,p[N][N];
bool col[N][N];
void dfs(int x,int y,int z)
{
if(x>=n)
{
ans=max(z,ans);
return;
}
int next_x=x;
int next_y=y+1;
if(next_y>=m)
{
next_y=0;
next_x=x+1;
}
if(!col[x - 1][y - 1] && !col[x - 1][y] && !col[x - 1][y + 1] && !col[x][y - 1] && !col[x][y + 1] && !col[x + 1][y - 1] && !col[x + 1][y] && !col[x + 1][y + 1])
{
col[x][y]=true;
dfs(next_x,next_y,z+p[x][y]);
col[x][y]=false;
}
dfs(next_x,next_y,z);
}
int main()
{
int t;
cin>>t;
while(t--)
{
cin >> n >> m;
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
cin>>p[i][j];
dfs(0,0,ans);
cout<<ans<<endl;
ans=0;
}
}
还有就是剪枝 本人比较菜 没有总结出来 我分享我看到的技巧
1.优化搜索顺序
在一些搜索问题中,搜索树的各个层次、各个分支之间的顺序不是固定的。不同的搜索顺序会产生不同的搜索树形态,其规模大小也想相差甚远。
2.排除等效冗余
在搜索过程中,如果我们能够判定从搜索树的当前节点上沿着几条不同分支到达的子树是等效的,那么只需要对其中的一条分支执行搜索即可。
3.可行性剪枝
在搜索过程中,及时对当前状态进行检查,如果发现分支已经无法到达递归边界,就执行回溯。这就好比我们在道路上行走时,远远看到前方是一个死胡同,就应该立即折路返回了,而不是走到路的尽头再返回。
4.最优性剪枝
在最优化问题的搜索过程中,如果当前花费的代价已经超过了当前搜到的最优解,那么无论采取多么有些的策略到达递归边界,都不可能更新答案了。此时可以停止对当前分支的搜索,执行回溯了。比如说,前面搜索过的好多分支中得到了一个最优解答案是ans,然后搜索当前分支时,当发现在某个节点时,它的答案res已经大于了我们的最优解答案ans,那么如果继续搜索这个分支的剩余部分,那么得到的答案肯定是远远大于最优解答案ans的。因此,我们需要提前退出搜索这个分支。
5.记忆化搜索
可以记录每个状态的搜索结果,在重复遍历一个状态时直接检索并返回。这就好比我们对图进行深度优先遍历时,标记一个节点是否已经被遍历过。
自己写的代码呈现
#include<iostream>
#include<algorithm>
using namespace std;
const int N=20;
int car[N],weight[N];
int n,car_max,ans;
int cmp(int a,int b)
{
return a>b;
}
void dfs(int now,int sum)
{
if(sum>=ans) return ;
if(now==n+1)
{
ans=min(ans,sum);
return ;
}
for(int i=1;i<=sum;i++)
{
if(car[i]+weight[now]<=car_max)
{
car[i]+=weight[now];
dfs(now+1,sum);
car[i]-=weight[now];
}
}
car[sum+1]=weight[now];
dfs(now+1,sum+1);
car[sum+1]=0;
}
int main()
{
cin>>n>>car_max;
for(int i=1;i<=n;i++)
cin>>weight[i];
sort(weight+1,weight+n+1,cmp);
ans=n;
dfs(1,0);
cout<<ans<<endl;
return 0;
}