搜索

DFS(搜索)

DFS 为图论中的概念,中文名是深度优先搜索,是一种用于遍历或搜索树或图的算法。所谓深度优先,就是说每次都尝试向更深的节点走。在 搜索算法 中,该词常常指利用递归函数方便地实现暴力枚举的算法,与图论中的 DFS 算法有一定相似之处,但并不完全相同。
DFS 最显著的特征在于其 递归调用自身。同时与 BFS 类似,DFS 会对其访问过的点打上访问标记,在遍历图时跳过已打过标记的点,以确保 每个点仅访问一次。符合以上两条规则的函数,便是广义上的 DFS。

DFS(v) // v 可以是图中的一个顶点,也可以是抽象的概念,如 dp 状态等。
  在 v 上打访问标记
  for u in v 的相邻节点
    if u 没有打过访问标记 then
      DFS(u)
    end
  end
end

实现

  • 以链式前向星为例:(和上方伪代码每行一一对应)
void dfs(int u) {
  vis[u] = 1;
  for (int i = head[u]; i; i = e[i].x) {
    if (!vis[e[i].t]) {
      dfs(v);
    }
  }
}
  • DFS 序列
    DFS 序列是指 DFS 调用过程中访问的节点编号的序列。
    我们发现,每个子树都对应 DFS 序列中的连续一段(一段区间)。
  • 括号序列
    DFS 进入某个节点的时候记录一个左括号 (,退出某个节点的啥时候记录一个右括号 )。
    每个节点会出现两次。相邻两个节点的深度相差 1。
  • 一般图上 DFS
    对于非连通图,只能访问到起点所在的连通分量。
    对于连通图,DFS 序列通常不唯一。
    注:树的 DFS 序列也是不唯一的。
    在 DFS 过程中,通过记录每个节点从哪个点访问而来,可以建立一个树结构,称为 DFS 树。DFS 树是原图的一个生成树。
//全排列
#include<iostream>
#include<algorithm>
using namespace std;
int a[]={1,2,3};
int m;
void Permutation(int k) //全排列递归算法
{
	if(k==m){
		for(int i=0 ;i<=m ;i++)
		 cout<<a[i];
		 cout<<endl;
	 }
	 else{
	 	for(int i=k;i<=m;i++){
	 		swap(a[i],a[k]);
	 		Permutation(k+1);
	 		swap(a[i],a[k]);
		 }
	 }
}
int main()
{
   m=2;
   Permutation(0);
   return 0;
 }

在这里插入图片描述
1.for循环控制了固定的第k位,递归进行每两位的置换(固定后且进行的是置换操作,不会出现序列中有某一元素多次重复的现象);
2.可以理解为每次递归表示上一层都已经定好了,只对末位进行调换,置换完成后,将其复原,使得不会出现重复序列。

BFS(搜索)

BFS 是图论中的一种遍历算法,下面会说;
BFS 在搜索中也很常用,将每个状态对应为图中的一个点即可。

BFS(图论)

参考
所谓宽度优先。就是每次都尝试访问同一层的节点。 如果同一层都访问完了,再访问下一层。
这样做的结果是,BFS 算法找到的路径是从起点开始的 最短 合法路径。换言之,这条路所包含的边数最小。
在 BFS 结束时,每个节点都是通过从起点到该点的最短路径访问的。
算法过程可以看做是图上火苗传播的过程:最开始只有起点着火了,在每一时刻,有火的节点都向它相邻的所有节点传播火苗。

实现

伪代码:

bfs(s) {
  q = new queue()
  q.push(s), visited[s] = true
  while (!q.empty()) {
    u = q.pop()
    for each edge(u, v) {
      if (!visited[v]) {
        q.push(v)
        visited[v] = true
      }
    }
  }
}

C++:

void bfs(int u) {
  while (!Q.empty()) Q.pop();
  Q.push(u);
  vis[u] = 1;
  d[u] = 0;
  p[u] = -1;
  while (!Q.empty()) {
    u = Q.front();
    Q.pop();
    for (int i = head[u]; i; i = e[i].x){
      if (!vis[e[i].t]) {
        Q.push(e[i].t);
        vis[e[i].t] = 1;
        d[e[i].t] = d[u] + 1;
        p[e[i].t] = u;
      }
    }
  }
}
void restore(int x) {
  vector<int> res;
  for (int v = x; v != -1; v = p[v]) {
    res.push_back(v);
  }
  std::reverse(res.begin(), res.end());
  for (int i = 0; i < res.size(); ++i) printf("%d", res[i]);
  puts("");
}

应用:

  • 在一个无权图上求从起点到其他所有点的最短路径。举例

  • O ( n + m ) O(n+m) O(n+m) 时间内求出所有连通块。(我们只需要从每个没有被访问过的节点开始做 BFS,显然每次 BFS 会走完一个连通块)

  • 如果把一个游戏的动作看做是状态图上的一条边(一个转移),那么 BFS 可以用来找到在游戏中从一个状态到达另一个状态所需要的最小步骤。举例

  • 在一个边权为 0/1 的图上求最短路。(需要修改入队的过程,如果某条边权值为 0,且可以减小边的终点到图的起点的距离,那么把边的起点加到队列首而不是队列尾)

  • 在一个有向无权图中找最小环。(从每个点开始 BFS,在我们即将抵达一个之前访问过的点开始的时候,就知道遇到了一个环。图的最小环是每次 BFS 得到的最小环的平均值。)

  • 找到一定在(a,b) 最短路上的边。(分别从 a 和 b 进行 BFS,得到两个 d 数组。之后对每一条边 (u,v),如果 d a [ u ] + d b [ v ] + 1 = d a [ b ] d_{a}[u]+d_{b}[v]+1=d_{a}[b] da[u]+db[v]+1=da[b] ,则说明该边在最短路上)

  • 找到一定在(a,b) 最短路上的点。(分别从 a 和 b 进行 BFS,得到两个 d 数组。之后对每一个点 v,如果 d a [ u ] + d b [ v ] = d a [ b ] d_{a}[u]+d_{b}[v]=d_{a}[b] da[u]+db[v]=da[b],则说明该点在最短路上)

  • 找到一条长度为偶数的最短路。(我们需要一个构造一个新图,把每个点拆成两个新点,原图的边(u,v) 变成 ((u,0),(v,1)) 和((u,1),(v,0)) 。对新图做 BFS,(s,0) 和 (t,0) 之间的最短路即为所求)

双端队列 BFS

双端队列 BFS 又称 0-1 BFS

适用范围

边权值为可能有,也可能没有(由于 BFS 适用于权值为 1 的图,所以一般是 0 or 1),或者能够转化为这种边权值的最短路问题。
例如在走迷宫问题中,你可以花 1 个金币走 5 步,也可以不花金币走 1 步,这就可以用 0-1 BFS 解决。

实现

双端队列 BFS + Chamber of Secrets CodeForces - 173B
一般情况下,我们把没有权值的边扩展到的点放到队首,有权值的边扩展到的点放到队尾。这样即可保证在整个队列中,像普通 BFS 一样,越靠近队首,权值越小,且权值零一之间有分隔。
下面是伪代码:

while (队列不为空) {
  int u = 队首;
  弹出队首;
  for (枚举 u 的邻居) {
    更新数据
    if (...)
      添加到队首;
    else
      添加到队尾;
  }
}

双向搜索

双向同时搜索的基本思路是从状态图上的起点和终点同时开始进行 广搜 或 深搜。如果发现搜索的两端相遇了,那么可以认为是获得了可行解。
双向广搜的步骤:

将开始结点和目标结点加入队列 q
标记开始结点为 1
标记目标结点为 2
while (队列 q 不为空)
{
  从 q.front() 扩展出新的 s 个结点

  如果 新扩展出的结点已经被其他数字标记过
    那么 表示搜索的两端碰撞
    那么 循环结束

  如果 新的 s 个结点是从开始结点扩展来的
    那么 将这个 s 个结点标记为 1 并且入队 q 

  如果 新的 s 个结点是从目标结点扩展来的
    那么 将这个 s 个结点标记为 2 并且入队 q
}

A*

A ∗ A^{*} A搜索算法(英文:Asearch algorithm,A读作 A-star),简称 A*算法,是一种在图形平面上,对于有多个节点的路径求出最低通过成本的算法。它属于图遍历(英文:Graph traversal)和最佳优先搜索算法(英文:Best-first search),亦是 BFS 的改进。

定义起点 s,终点t ,从起点(初始状态)开始的距离函数g(x) ,到终点(最终状态)的距离函数 , h ( x ) , h ∗ ( x ) h(x),h^{*}(x) h(x),h(x),以及每个点的估价函数 f ( x ) = g ( x ) + h ( x ) f(x)=g(x)+h(x) f(x)=g(x)+h(x)
A*算法每次从优先队列中取出一个 f f f最小的元素,然后更新相邻的状态。
如果 h < = h ∗ h<=h^{*} h<=h ,则 A ∗ A^{*} A算法能找到最优解。
上述条件下,如果 h满足三角形不等式,则 A ∗ A^{*} A算法不会将重复结点加入队列。

h = 0 h=0 h=0 时,A*算法变为 DFS;当 h = 0 h=0 h=0 并且边权为1 时变为 BFS。

例题:

题目大意:在 3 × 3 3\times 3 3×3 的棋盘上,摆有八个棋子,每个棋子上标有1 至8 的某一数字。棋盘中留有一个空格,空格用0 来表示。空格周围的棋子可以移到空格中,这样原来的位置就会变成空格。给出一种初始布局和目标布局(为了使题目简单,设目标状态如下),找到一种从初始布局到目标布局最少步骤的移动方法。

123
804
765

解题思路

h函数可以定义为,不在应该在的位置的数字个数。
容易发现 h 满足以上两个性质,此题可以使用 A ∗ A^{*} A算法求解。
A ∗ A^{*} A是一种启发式搜索,g为已花代价,h为估计的剩余代价,而 A ∗ A^{*} A是根据f=g+h作为估价函数进行排列,也就是优先选择可能最优的节点进行扩展。

迭代加深搜索

迭代加深是一种 每次限制搜索深度的 深度优先搜索。
它的本质还是深度优先搜索,只不过在搜索的同时带上了一个深度d ,当 d达到设定的深度时就返回,一般用于找最优解。如果一次搜索没有找到合法的解,就让设定的深度加一,重新从根开始。

既然是为了找最优解,为什么不用 BFS 呢?我们知道 BFS 的基础是一个队列,队列的空间复杂度很大,当状态比较多或者单个状态比较大时,使用队列的 BFS 就显出了劣势。事实上,迭代加深就类似于用 DFS 方式实现的 BFS,它的空间复杂度相对较小。
当搜索树的分支比较多时,每增加一层的搜索复杂度会出现指数级爆炸式增长,这时前面重复进行的部分所带来的复杂度几乎可以忽略,这也就是为什么迭代加深是可以近似看成 BFS 的。

步骤

首先设定一个较小的深度作为全局变量,进行 DFS。每进入一次 DFS,将当前深度加一,当发现d 大于设定的深度 limit就返回。如果在搜索的途中发现了答案就可以回溯,同时在回溯的过程中可以记录路径。如果没有发现答案,就返回到函数入口,增加设定深度,继续搜索。

代码框架

IDDFS(u,d)
    if d>limit
        return
    else
        for each edge (u,v)
            IDDFS(v,d+1)
  return

注意事项

在大多数的题目中,广度优先搜索还是比较方便的,而且容易判重。当发现广度优先搜索在空间上不够优秀,而且要找最优解的问题时,就应该考虑迭代加深。

IDA*

回溯法
Dancing Links
优化

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值