搜索——进阶搜索算法 学习笔记
搜索——进阶搜索算法
DATE 20231031:补充一道好题 P4872 OIer们的东方梦(Dijkstra 分层图最短路)。
前情提要~
- 双向广搜、双向深搜
- 堆优化的 Dijkstra
- 一颗小小的 A-STAR
- 不大聪明的 IDDFS(IDS)
- 可爱的 IDA-STAR
广搜、深搜
这是进阶搜索算法,不说了直接上例题:
以“P1514 引水问题”为例:
点击查看代码
const int N = 510; const int dx[4] = {-1, 0, 0, 1}; const int dy[4] = {0, -1, 1, 0}; int n, m, a[N][N]; bool vis[N][N][N]; vector<pair<int, int>> nodes; int main() { scanf("%d %d", &n, &m); for (int i = 1; i <= n; ++i) for (int j = 1; j <= m; ++j) scanf("%d", a[i] + j); queue<pair<int, int>> q; for (int start = 1; start <= m; ++start) { q.push({1, start}); vis[0][1][start] = true; vis[start][1][start] = true; while (q.size()) { auto now = q.front(); int x = now.first, y = now.second; q.pop(); for (int i = 0; i < 4; ++i) { int tx = x + dx[i]; int ty = y + dy[i]; if (tx < 1 || tx > n || ty < 1 || ty > m || vis[start][tx][ty]) continue; if (a[tx][ty] < a[x][y]) q.push({tx, ty}), vis[0][tx][ty] = 1, vis[start][tx][ty] = 1; } } vector<int> g; for (int i = 1; i <= m; ++i) if (vis[start][n][i]) g.push_back(i); if (g.size()) nodes.push_back({g.front(), g.back()}); } int connected = 0; for (int i = 1; i <= m; ++i) connected += vis[0][n][i]; if (connected < m) printf("0\n%d\n", m - connected); else { sort(nodes.begin(), nodes.end()); int now = 1, res = 0; for (size_t i = 0; i < nodes.size(); ++i, ++res) { while (nodes[i].first <= now) ++i; now = nodes[i - 1].second + 1; } printf("1\n%d\n", res); } return 0; }
双向广搜、双向深搜
算法思想
- 在搜索的时候,搜索出来的树太大了
- 从起始状态和目标状态同时开始搜索一定层数
- 把搜索出来的所有状态扔到 hash 表里面,看看有没有重复的
- 能够提升一倍答案的效率,比如原来复杂度是 \(O(2^n)\),现在可以变成 \(O(2^{n / 2})\),相当于复杂度开根号
代码
以“U319719 八城堡问题”为例:
点击查看代码
using namespace std; const int N = 21; const int M = (1 << 20) + 1; int n, m; int a[N]; int n2, ans; int g[M]; int f[N][M]; void dfs1(int k, int now) { if (g[now] > m) return; if (k > n2) { int r = now ^ ((1 << n) - 1); if (g[now] == m) ++f[m][0]; for (int t = r; t; t = (t - 1) & r) ++f[g[now]][t]; return; } dfs1(k + 1, now); for (int i = 1; i <= n; ++i) { int t = 1 << (n - i); if ((a[k] & t) && (now & t) == 0) dfs1(k + 1, now | t); } } void dfs2(int k, int now) { if (g[now] > m) return; if (k > n) { ans += f[m - g[now]][now]; return; } dfs2(k + 1, now); for (int i = 1; i <= n; ++i) { int t = 1 << (n - i); if ((a[k] & t) && (now & t) == 0) dfs2(k + 1, now | t); } } int main() { for (int i = 1; i < M; ++i) g[i] = g[i & (i - 1)] + 1; scanf("%d%d", &n, &m); while (n && m) { memset(f, 0, sizeof f); ans = 0; char line[N]; for (int i = 1; i <= n; ++i) { scanf("%s", line); for (int j = 1; j <= n; ++j) a[i] = a[i] << 1 | (line[j - 1] == 'H'); } n2 = n >> 1; dfs1(1, 0); dfs2(n2 + 1, 0); printf("%d\n", ans); scanf("%d%d", &n, &m); } return 0; }
堆优化的 Dijkstra
算法思想
考虑当前走过的距离,不考虑剩下的距离,当前走的少的优先考虑
优先队列里不允许出现相同的元素,但是同一个元素可以入队和出队多次,当第二及更多次入队是,必然是遇到了更加优化的路径(到起点的距离更近)
代码
点击查看代码
typedef pair<int, int> PII; const int INF = 2147483647; const int N = 1e5 + 10; const int M = 5e5 + 10; int n, m; int h[N], e[M], w[M], ne[M], idx; void add(int u, int v, int d) { e[idx] = v; w[idx] = d; ne[idx] = h[u]; h[u] = idx++; } int dis[N]; bool st[N]; void dijkstra(int s) { memset(dis, 0x3f, sizeof dis); dis[s] = 0; priority_queue<PII, vector<PII>, greater<PII>> heap; heap.push({0, s}); while (heap.size()) { int v = heap.top().second, d = heap.top().first; heap.pop(); if (st[v]) continue; st[v] = true; for (int i = h[v] ; i != -1 ; i = ne[i]) { int j = e[i]; if (dis[j] > d + w[i]) { dis[j] = d + w[i]; heap.push({dis[j], j}); } } } } unordered_map<int, int> dist[N]; int main() { memset(h, -1, sizeof(h)); int s; scanf("%d %d %d", &n, &m, &s); int u, v, w; for (int i = 1 ; i <= m ; ++i) { scanf("%d %d %d", &u, &v, &w); if (dist[u].count(v)) dist[u][v] = min(dist[u][v], w); else dist[u][v] = w; } for (int i = 1 ; i <= n ; ++i) for (PII j : dist[i]) add(i, j.first, j.second); dijkstra(s); for (int i = 1 ; i <= n ; ++i) { if (dis[i] == 0x3f3f3f3f) dis[i] = INF; printf("%d ", dis[i]); } return 0; }
一颗小小的 A-STAR
基础知识
- 估价函数 \(f(i) = g(i) + h(i)\),每次要选取 \(f(i)\) 最小的更新
- \(g(i)\):从起始状态到当前状态 \(i\) 的代价
- \(h(i)\):从当前状态 \(i\) 到目标状态的估计代价
基础思想
好东西:https://zhuanlan.zhihu.com/p/54510444
重点在设计估价函数,估价函数 \(h(i)\) 若选取不当,则可能找不到解,或找到的解也不是最优解。
- 定义 \(h^*(i)\) 为从当前状态 \(n\) 到目标状态的实际代价
- 必须满足 \(h(i) \le h^*(i)\),否则嘿嘿嘿
- 估计总是过于乐观的
常见的估价函数
对于网格形式的图,有以下这些启发函数可以使用:
- 如果图形中只允许朝上下左右四个方向移动,则可以使用曼哈顿距离
function heuristic(node) = dx = abs(node.x - goal.x) dy = abs(node.y - goal.y) return D * (dx + dy)
- 如果图形中允许朝八个方向移动,则可以使用对角距离
function heuristic(node) = dx = abs(node.x - goal.x) dy = abs(node.y - goal.y) return D * (dx + dy) + (D2 - 2 * D) * min(dx, dy) // 这里的D2指的是两个斜着相邻节点之间的移动代价
- 如果图形中允许朝任何方向移动,则可以使用欧几里得距离
function heuristic(node) = dx = abs(node.x - goal.x) dy = abs(node.y - goal.y) return D * sqrt(dx * dx + dy * dy)
- 对于类似八数码问题的,可以使用各数码使用距离目标位置的曼哈顿距离之和
int f(string state) { int res = 0; for (int i = 0; i < 9; ++i) { if (state[i] != '0') { int t = tar[state[i] - '1']; res += abs(i / 3 - t / 3) + abs(i % 3 - t % 3); } } return res; }
- 对于数列(二维也可以)问题的,可以使用有多少个元素位置不正确,或其前驱、后继不正确
int f(string state) { int res = 0; for (int i = 0; i < 9; ++i) res += state[i] != gend[i]; return res; }
int f() { int res = 0; for (int i = 0; i + 1 < n; ++i) if (q[i + 1] != q[i] + 1) ++res; return (res + 2) / 3; }
设计要求
- 不要太过分就好,一般来说没问题的
- 脑洞要大
代码
以“P1379 八数码问题”为例:
点击查看代码
typedef pair<int, string> PIS; const int dx[4] = {-1, 0, 0, 1}; const int dy[4] = {0, -1, 1, 0}; const char op[4] = {'u', 'l', 'r', 'd'}; string gend = "123804765"; int tar[9] = {0, 1, 3, 5, 8, 7, 6, 3}; int f(string state) { int res = 0; for (int i = 0; i < 9; ++i) { if (state[i] != '0') { int t = tar[state[i] - '1']; res += abs(i / 3 - t / 3) + abs(i % 3 - t % 3); } } return res; } string a_star(string start, string end = gend) { unordered_map<string, int> dist; unordered_map<string, pair<char, string>> prev; priority_queue<PIS, vector<PIS>, greater<PIS>> heap; dist[start] = 0; heap.push({f(start), start}); while (heap.size()) { auto t = heap.top(); heap.pop(); string state = t.second; if (state == end) break; int x, y; for (int i = 0; i < 9; ++i) { if (state[i] == '0') { x = i / 3, y = i % 3; break; } } string source = state; for (int i = 0; i < 4; ++i) { int a = x + dx[i]; int b = y + dy[i]; if (a < 0 || a > 2 || b < 0 || b > 2) continue; state = source; swap(state[x * 3 + y], state[a * 3 + b]); if (!dist.count(state) || dist[state] > dist[source] + 1) { dist[state] = dist[source] + 1; prev[state] = {op[i], source}; heap.push({dist[state] + f(state), state}); } } } string res; while (end != start) { res = prev[end].first + res; end = prev[end].second; } return res; } int main() { string start; cin >> start; // 保证可以达到目标 // int cnt = 0; // for (int i = 0 ; i < 9 ; ++i) // if (start[i] != '0') // for (int j = i + 1 ; j < 9 ; ++j) // if (start[j] != '0' && start[i] > start[j]) // ++cnt; // if (cnt & 1 == 0) // cout << "-1" << endl; // else cout << a_star(start).size() << endl; return 0; }
不大聪明的 IDDFS(IDS)
算法思想
迭代加深是一种每次限制搜索深度的深度优先搜索,目的是寻找最优解。
- 给出一个限制 limit,规定:当 搜索层数 > limit 时直接剪枝
- 在最外层 循环枚举 limit,如果无解就继续
特点
缺点:重复计算
与BFS的区别:BFS 的基础是队列,空间复杂度大,当状态比较多或者单个状态比较大时,迭代加深就类似于用 DFS 方式实现的 BFS,空间复杂度相对较小。
在大多数的题目中,广度优先搜索还是比较方便的,而且容易判重。当发现广度优先搜索在空间上不够优秀,而且要找最优解的问题时,就应该考虑迭代加深。
代码
以“LOJ10021 Addition Chains(加成序列问题)”为例:
点击查看代码
const int N = 110; int read() { int num = 0, flag = 1; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') flag = -1; for (; isdigit(ch); ch = getchar()) num = (num << 3) + (num << 1) + ch - '0'; return num * flag; } int n; int path[N]; bool iddfs(int k, int limit) { if (k > limit) return false; if (path[k - 1] == n) return true; bool st[N] = {0}; for (int i = k - 1; i >= 0; --i) { for (int j = i; j >= 0; --j) { int s = path[i] + path[j]; if (s > n || s <= path[k - 1] || st[s]) continue; st[s] = true; path[k] = s; if (iddfs(k + 1, limit)) return true; } } return false; } int main() { path[0] = 1; while (n = read()) { int depth = 1; while (!iddfs(1, depth)) ++depth; for (int i = 0; i < depth; ++i) printf("%d ", path[i]); putchar('\n'); } return 0; }
可爱的 IDA-STAR
算法思想
IDA-STAR 为采用了迭代加深(IDDFS)算法的 A-STAR 算法。
所以,在一般的问题中是这样使用 IDA-star 算法的:
- (类似 IDDFS)循环枚举 limit,当 \(h(i) + g(i) >\) limit 时, 就停止继续往下搜索
- 写成代码就是:
if depth + h() > limit then return;
- 没了
估价函数
往上门口(见 A-STAR)
特点
- 省空间:各种游戏求最少步数,普通搜索会爆炸
- 省时间:不需要判重,不需要排序,利于深度剪枝
- 费时间(?):每次深度变大都要再次从头搜索,有时可能双向广搜会更快
代码
以“P2324 骑士精神”为例:
点击查看代码
const int dx[8] = {-2, -2, -1, -1, 1, 1, 2, 2}; const int dy[8] = {-1, 1, -2, 2, -2, 2, -1, 1}; const int gcount[2][5] = {{0, 1, 2, 4, 5}, {5, 4, 2, 1, 0}}; string mp[5]; int f() { int res = 0; for (int i = 0; i < 5; ++i) { for (int j = 0; j < gcount[0][i]; ++j) if (mp[i][j] != '0') ++res; for (int j = 0; j < gcount[1][i]; ++j) if (mp[i][5 - j - 1] != '1') ++res; } return res; } bool ida_star(int now, int limit) { if (now + f() > limit) return false; if (!f()) return true; int a, b; for (int i = 0; i < 5; ++i) for (int j = 0; j < 5; ++j) if (mp[i][j] == '*') a = i, b = j; for (int i = 0; i < 8; ++i) { int x = a + dx[i]; int y = b + dy[i]; if (x < 0 || x > 4 || y < 0 || y > 4) continue; swap(mp[a][b], mp[x][y]); if (ida_star(now + 1, limit)) return true; swap(mp[a][b], mp[x][y]); } return false; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int T; cin >> T; while (T--) { for (int i = 0; i < 5; ++i) cin >> mp[i]; int limit = 0; while (limit <= 15 && !ida_star(0, limit)) ++limit; if (limit > 15) limit = -1; printf("%d\n", limit); } return 0; }
练习题
见:https://www.luogu.com.cn/training/401467
Reference
[1] https://github.com/huzecong/oi-slides
[2] https://zhuanlan.zhihu.com/p/54510444
[3] https://oi-wiki.org/
[4] https://www.acwing.com/
本文来自博客园,作者:RainPPR,转载请注明原文链接:https://www.cnblogs.com/RainPPR/p/premium-search-algorithm.html
如有侵权请联系我(或 2125773894@qq.com)删除。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战