启发式搜索

下面将简要介绍启发式搜索及其用法。

定义

启发式搜索(英文:\(\text{heuristic search}\))是一种在普通搜索算法的基础上引入了启发式函数的搜索算法。

启发式函数的作用是基于已有的信息对搜索的每一个分支选择都做估价,进而选择分支。简单来说,启发式搜索就是对取和不取都做分析,从中选取更优解或删去无效解。

例题

由于概念过于抽象,这里使用例题讲解。

「NOIP2005 普及组」采药

题目大意:有 \(N\) 种物品和一个容量为 \(W\) 的背包,每种物品有重量 \(w_i\) 和价值 \(v_i\) 两种属性,要求选若干个物品(每种物品只能选一次)放入背包,使背包中物品的总价值最大,且背包中物品的总重量不超过背包的容量。

解题思路:

我们写一个估价函数 \(f\),可以剪掉所有无效的 \(0\) 枝条(就是剪去大量无用不选枝条)。

估价函数 \(f\) 的运行过程如下:

我们在取的时候判断一下是不是超过了规定体积(可行性剪枝);在不取的时候判断一下不取这个时,剩下的药所有的价值 + 现有的价值是否大于目前找到的最优解(最优性剪枝)。

示例代码:

#include <algorithm>
#include <cstdio>
using namespace std;
const int N = 105;
int n, m, ans; struct Node {
int a, b; // a 代表时间,b 代表价值
double f;
} node[N]; bool operator<(Node p, Node q) { return p.f > q.f; } int f(int t, int v) { // 计算在当前时间下,剩余物品的最大价值
int tot = 0;
for (int i = 1; t + i <= n; i++)
if (v >= node[t + i].a) {
v -= node[t + i].a;
tot += node[t + i].b;
} else
return (int)(tot + v * node[t + i].f);
return tot;
} void work(int t, int p, int v) {
ans = max(ans, v);
if (t > n) return; // 边界条件:只有n种物品
if (f(t, p) + v > ans) work(t + 1, p, v); // 最优性剪枝
if (node[t].a <= p) work(t + 1, p - node[t].a, v + node[t].b); // 可行性剪枝
} int main() {
scanf("%d %d", &m, &n);
for (int i = 1; i <= n; i++) {
scanf("%d %d", &node[i].a, &node[i].b);
node[i].f = 1.0 * node[i].b / node[i].a; // f为性价比
}
sort(node + 1, node + n + 1); // 根据性价比排序
work(1, m, 0);
printf("%d\n", ans);
return 0;
}

A* 算法

本页面将简要介绍 A * 算法。

定义

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

过程

定义起点 \(s\),终点 \(t\),从起点(初始状态)开始的距离函数 \(g(x)\),到终点(最终状态)的距离函数 \(h(x)\),\(h^{\ast}(x)^1\),以及每个点的估价函数 \(f(x)=g(x)+h(x)\)。

A * 算法每次从优先队列中取出一个 \(f\) 最小的元素,然后更新相邻的状态。

如果 \(h\leq h*\),则 A * 算法能找到最优解。

上述条件下,如果 \(h\) 满足三角形不等式,则 A * 算法不会将重复结点加入队列。

当 \(h=0\) 时,A * 算法变为 \(\text{Dijkstra}\);当 \(h=0\) 并且边权为 \(1\) 时变为 \(\text{BFS}\)。

例题

八数码

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

    123
804
765

解题思路:

\(h\) 函数可以定义为,不在应该在的位置的数字个数。

容易发现 \(h\) 满足以上两个性质,此题可以使用 A * 算法求解。

参考代码:

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <queue>
#include <set>
using namespace std;
const int dx[4] = {1, -1, 0, 0}, dy[4] = {0, 0, 1, -1};
int fx, fy;
char ch; struct matrix {
int a[5][5]; bool operator<(matrix x) const {
for (int i = 1; i <= 3; i++)
for (int j = 1; j <= 3; j++)
if (a[i][j] != x.a[i][j]) return a[i][j] < x.a[i][j];
return false;
}
} f, st; int h(matrix a) {
int ret = 0;
for (int i = 1; i <= 3; i++)
for (int j = 1; j <= 3; j++)
if (a.a[i][j] != st.a[i][j]) ret++;
return ret;
} struct node {
matrix a;
int t; bool operator<(node x) const { return t + h(a) > x.t + h(x.a); }
} x; priority_queue<node> q; // 搜索队列
set<matrix> s; // 防止搜索队列重复 int main() {
st.a[1][1] = 1; // 定义标准表
st.a[1][2] = 2;
st.a[1][3] = 3;
st.a[2][1] = 8;
st.a[2][2] = 0;
st.a[2][3] = 4;
st.a[3][1] = 7;
st.a[3][2] = 6;
st.a[3][3] = 5;
for (int i = 1; i <= 3; i++) // 输入
for (int j = 1; j <= 3; j++) {
scanf(" %c", &ch);
f.a[i][j] = ch - '0';
}
q.push({f, 0});
while (!q.empty()) {
x = q.top();
q.pop();
if (!h(x.a)) { // 判断是否与标准矩阵一致
printf("%d\n", x.t);
return 0;
}
for (int i = 1; i <= 3; i++)
for (int j = 1; j <= 3; j++)
if (!x.a.a[i][j]) fx = i, fy = j; // 查找空格子(0号点)的位置
for (int i = 0; i < 4; i++) { // 对四种移动方式分别进行搜索
int xx = fx + dx[i], yy = fy + dy[i];
if (1 <= xx && xx <= 3 && 1 <= yy && yy <= 3) {
swap(x.a.a[fx][fy], x.a.a[xx][yy]);
if (!s.count(x.a))
s.insert(x.a),
q.push({x.a, x.t + 1}); // 这样移动后,将新的情况放入搜索队列中
swap(x.a.a[fx][fy], x.a.a[xx][yy]); // 如果不这样移动的情况
}
}
}
return 0;
}

注:对于 k 短路问题,原题已经可以构造出数据使得 A* 算法无法通过,故本题思路仅供参考,A* 算法非正解,正解为可持久化可并堆做法。

k 短路

按顺序求一个有向图上从结点 \(s\) 到结点 \(t\) 的所有路径最小的前任意多(不妨设为 \(k\))个。

解题思路:

很容易发现,这个问题很容易转化成用 A * 算法解决问题的标准程式。

初始状态为处于结点 \(s\),最终状态为处于结点 \(t\),距离函数为从 \(s\) 到当前结点已经走过的距离,估价函数为从当前结点到结点 \(t\) 至少要走过的距离,也就是当前结点到结点 \(t\) 的最短路。

就这样,我们在预处理的时候反向建图,计算出结点 \(t\) 到所有点的最短路,然后将初始状态塞入优先队列,每次取出 \(f(x)=g(x)+h(x)\) 最小的一项,计算出其所连结点的信息并将其也塞入队列。当你第 \(k\) 次走到结点 \(t\) 时,也就算出了结点 \(s\) 到结点 \(t\) 的 \(k\) 短路。

由于设计的距离函数和估价函数,每个状态需要存储两个参数,当前结点 \(x\) 和已经走过的距离 \(v\)。

我们可以在此基础上加一点小优化:由于只需要求出第 \(k\) 短路,所以当我们第 \(k+1\) 次或以上走到该结点时,直接跳过该状态。因为前面的 \(k\) 次走到这个点的时候肯定能因此构造出 \(k\) 条路径,所以之后再加边更无必要。

参考代码:

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int maxn = 5010;
const int maxm = 400010;
const double inf = 2e9;
int n, m, k, u, v, cur, h[maxn], nxt[maxm], p[maxm], cnt[maxn], ans;
int cur1, h1[maxn], nxt1[maxm], p1[maxm];
double e, ww, w[maxm], f[maxn];
double w1[maxm];
bool tf[maxn]; void add_edge(int x, int y, double z) { // 正向建图函数
cur++;
nxt[cur] = h[x];
h[x] = cur;
p[cur] = y;
w[cur] = z;
} void add_edge1(int x, int y, double z) { // 反向建图函数
cur1++;
nxt1[cur1] = h1[x];
h1[x] = cur1;
p1[cur1] = y;
w1[cur1] = z;
} struct node { // 使用A*时所需的结构体
int x;
double v; bool operator<(node a) const { return v + f[x] > a.v + f[a.x]; }
}; priority_queue<node> q; struct node2 { // 计算t到所有结点最短路时所需的结构体
int x;
double v; bool operator<(node2 a) const { return v > a.v; }
} x; priority_queue<node2> Q; int main() {
scanf("%d%d%lf", &n, &m, &e);
while (m--) {
scanf("%d%d%lf", &u, &v, &ww);
add_edge(u, v, ww); // 正向建图
add_edge1(v, u, ww); // 反向建图
}
for (int i = 1; i < n; i++) f[i] = inf;
Q.push({n, 0});
while (!Q.empty()) { // 计算t到所有结点的最短路
x = Q.top();
Q.pop();
if (tf[x.x]) continue;
tf[x.x] = true;
f[x.x] = x.v;
for (int j = h1[x.x]; j; j = nxt1[j]) Q.push({p1[j], x.v + w1[j]});
}
k = (int)e / f[1];
q.push({1, 0});
while (!q.empty()) { // 使用A*算法
node x = q.top();
q.pop();
cnt[x.x]++;
if (x.x == n) {
e -= x.v;
if (e < 0) {
printf("%d\n", ans);
return 0;
}
ans++;
}
for (int j = h[x.x]; j; j = nxt[j])
if (cnt[p[j]] <= k && x.v + w[j] <= e) q.push({p[j], x.v + w[j]});
}
printf("%d\n", ans);
return 0;
}

参考资料与注释

\(^1\): 此处的 \(h\) 意为 \(\text{heuristic}\)。详见 启发式搜索 - 维基百科A*\(\text{search algorithm - Wikipedia}\) 的 \(\text{Bounded relaxation}\) 一节。

迭代加深搜索

定义

迭代加深是一种 每次限制搜索深度的 深度优先搜索。

解释

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

既然是为了找最优解,为什么不用 \(\text{BFS}\) 呢?我们知道 \(\text{BFS}\) 的基础是一个队列,队列的空间复杂度很大,当状态比较多或者单个状态比较大时,使用队列的 \(\text{BFS}\) 就显出了劣势。事实上,迭代加深就类似于用 \(\text{DFS}\) 方式实现的 \(\text{BFS}\),它的空间复杂度相对较小。

当搜索树的分支比较多时,每增加一层的搜索复杂度会出现指数级爆炸式增长,这时前面重复进行的部分所带来的复杂度几乎可以忽略,这也就是为什么迭代加深是可以近似看成 \(\text{BFS}\) 的。

过程

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

实现(伪代码):

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

注意事项

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

模拟

下面将简要介绍模拟算法。

简介

模拟就是用计算机来模拟题目中要求的操作。

模拟题目通常具有码量大、操作多、思路繁复的特点。由于它码量大,经常会出现难以查错的情况,如果在考试中写错是相当浪费时间的。

技巧

写模拟题时,遵循以下的建议有可能会提升做题速度:

  • 在动手写代码之前,在草纸上尽可能地写好要实现的流程。
  • 在代码中,尽量把每个部分模块化,写成函数、结构体或类。
  • 对于一些可能重复用到的概念,可以统一转化,方便处理:如,某题给你 "\(\text{YY-MM-DD}\) 时:分" 把它抽取到一个函数,处理成秒,会减少概念混淆。
  • 调试时分块调试。模块化的好处就是可以方便的单独调某一部分。
  • 写代码的时候一定要思路清晰,不要想到什么写什么,要按照落在纸上的步骤写。

实际上,上述步骤在解决其它类型的题目时也是很有帮助的。

例题详解

\(\text{Climbing Worm}\)

一只长度不计的蠕虫位于 \(n\) 英寸深的井的底部。它每次向上爬 \(u\) 英寸,但是必须休息一次才能再次向上爬。在休息的时候,它滑落了 \(d\) 英寸。之后它将重复向上爬和休息的过程。蠕虫爬出井口需要至少爬多少次?如果蠕虫爬完后刚好到达井的顶部,我们也设作蠕虫已经爬出井口。

解题思路:

直接使用程序模拟蠕虫爬井的过程就可以了。用一个循环重复蠕虫的爬井过程,当攀爬的长度超过或者等于井的深度时跳出。

参考代码:

#include <cstdio>

int main(void) {
int n = 0, u = 0, d = 0;
std::scanf("%d%d%d", &u, &d, &n);
int time = 0, dist = 0;
while (true) { // 用死循环来枚举
dist += u;
time++;
if (dist >= n) break; // 满足条件则退出死循环
dist -= d;
}
printf("%d\n", time); // 输出得到的结果
return 0;
}

习题

Day 4 - 搜索进阶与模拟的更多相关文章

  1. 【算法系列学习三】[kuangbin带你飞]专题二 搜索进阶 之 A-Eight 反向bfs打表和康拓展开

    [kuangbin带你飞]专题二 搜索进阶 之 A-Eight 这是一道经典的八数码问题.首先,简单介绍一下八数码问题: 八数码问题也称为九宫问题.在3×3的棋盘,摆有八个棋子,每个棋子上标有1至8的 ...

  2. C/C++深度优先搜索(递归树模拟)

    //C++深度优先搜索(递归树模拟) #define _CRT_SECURE_NO_WARNINGS #include <iostream> #define MAX_N 1000 usin ...

  3. c# JD快速搜索工具,2015分析JD搜索报文,模拟请求搜索数据,快速定位宝贝排行位置。

    分析JD搜索报文 搜索关键字 女装 第二页,分2次加载. rt=1&stop=1&click=&psort=&page=3http://search.jd.com/Se ...

  4. USACO 1.3... 虫洞 解题报告(搜索+强大剪枝+模拟)

    这题可真是又让我找到了八数码的感觉...哈哈. 首先,第一次见题,没有思路,第二次看题,感觉是搜索,就这样写下来了. 这题我几乎是一个点一个点改对的(至于为什么是这样,后面给你看一个神奇的东西),让我 ...

  5. JavaScript表格搜索高亮功能模拟

    在网页表格中模拟excle的搜索高亮显示功能.当在搜索框中输入需要的姓名时,若表格中存在对应的数据,则该表格背景色变为黄色. 下面为表的HTML源码: <!doctype html> &l ...

  6. BFS 搜索 蓝桥杯模拟赛

    题目链接:https://nanti.jisuanke.com/t/36117 这个题目想不到用广搜来做,一直在想深搜. 广搜的思路呢,是把最外圈不是黑色(不是0)的数 的位置 i 和 j 进队,赋值 ...

  7. NEUQOJ 1999: 三角形or四边形?【搜索联通块/模拟】

    http://newoj.acmclub.cn/problems/1999 1999: 三角形or四边形? 描述 题目描述: JiangYu很无聊,所以他拿钉子在板子上戳出了一个由.#组成的10*10 ...

  8. poj 1426 Find The Multiple 搜索进阶-暑假集训

    E - Find The Multiple Time Limit:1000MS     Memory Limit:10000KB     64bit IO Format:%I64d & %I6 ...

  9. poj3984《迷宫问题》暑假集训-搜索进阶

    K - 迷宫问题 Crawling in process... Crawling failed Time Limit:1000MS     Memory Limit:65536KB     64bit ...

  10. kuangbin专题 专题二 搜索进阶 Nightmare Ⅱ HDU - 3085

    题目链接:https://vjudge.net/problem/HDU-3085 题意:有两个鬼和两个人和墙,鬼先走,人再走,鬼每走过的地方都会复制一个新鬼, 但新鬼只能等待旧鬼走完一次行程之后,下一 ...

随机推荐

  1. flask blinker信号

    Flask框架中的信号基于blinker,其主要就是让开发者可是在flask请求过程中定制一些用户行为. pip3 install blinker 1.内置信号 request_started = _ ...

  2. Python 多线程、线程池、协程 爬虫

    多线程生产者消费者模型爬虫 import queue import requests from bs4 import BeautifulSoup import threading import tim ...

  3. 鸿蒙极速入门(二)-开发准备和HelloWorld

    一.开发准备 本篇博客基于的系统版本:华为官方HarmonyOS版本3.1.OpenHarmony版本4.0Beta 开发语言 ArkTS语言(推荐) JS语言(支持) Java语言(已放弃支持) 从 ...

  4. Vue 页面传参方式 Query 和 Params

    1. query 与 params 传参 query 需要和配合 path 属性使用,携带参数会拼接在请求路径后,效果同 Get 请求方式 http://localhost:8033/Permissi ...

  5. opencv-python 实现鱼眼矫正 棋盘矫正法

    .htmledit_views address, .htmledit_views cite, .htmledit_views dfn, .htmledit_views em, .htmledit_vi ...

  6. NOIP模拟82

    T1 魔法 解题思路 发现选择情况无非就是两种,连续的一段或者间隔为 \(R+B\) 的倍数的一段. 直接对于原序列贪心,每次选择可以消除的部分并将其删掉. 对于合法的情况将操作倒序输出即可. cod ...

  7. 关于 ajax在前端提示SyntaxError: Unexpected end of JSON input

    前几日,在开发微信公众号上的网页时候,前端采用h5+jquery开发,后端采用ASP.net的ashx接收前端的参数,restful采用的是java开发,由于在ASP.ENT的 webconfig中增 ...

  8. 震惊!docker镜像还有这些知识,你都知道吗?----镜像(一)

    镜像操作命令表格 docker image 子命令 docker子命令 功能 docker image build docker build 从Dockerfile开始构建镜像 docker imag ...

  9. Linux驱动--IOCTL实现

    参考:[Linux]实现设备驱动的ioctl函数_哔哩哔哩_bilibili.<Linux设备驱动程序(中文第三版).pdf> 1 用户空间ioctl 用户空间的ioctl函数原型,参数是 ...

  10. C# .NET MVC 表单提交前校验数据等

    页面上写2个button,一个普通button,另一个是submit,submit的这个隐藏.校验函数写在普通button里,普通button click函数中去提交表单. 页面: <input ...