高级搜索
本文介绍双向搜索,启发式搜索等高级搜索代码,以BFS、DFS为基础的高级搜索算法。
01-BFS
01-BFS又称双端BFS搜索,典型的模型是,给你一个无向图,每个边的边权要么是 0 0 0,要么是 1 1 1。在这个图中进行BFS搜索寻找最短路问题。
我们在扩展节点的时候,如果扩展不需要花费路径,那么将新的节点插入到队列的头部,因为这个新节点应该和本次节点在同一个层次,否则就将这个新节点插入到队列的尾部,因此需要双端队列来实现。
#include <bits/stdc++.h>
using namespace std;
#define FR freopen("in.txt", "r", stdin)
#define FW freopen("out.txt", "w", stdout)
typedef long long ll;
char mp[505][505];
int dp[505][505];
int pos[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
int corn[4][2] = {{0, 0}, {-1, 0}, {-1, -1}, {0, -1}};
int porn[4][2] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}};
int H, W;
struct Res
{
int r;
int c;
};
int main()
{
for (int i = 0; i < 505; i++)
{
for (int j = 0; j < 505; j++)
{
dp[i][j] = 100000;
}
}
scanf("%d %d", &H, &W);
for (int r = 1; r <= H; r++)
{
for (int c = 1; c <= W; c++)
{
scanf(" %c", &mp[r][c]);
}
}
deque<Res> que;
que.push_back((Res){1, 1});
dp[1][1] = 0;
while (!que.empty())
{
Res curr = que.front();
que.pop_front();
int r = curr.r;
int c = curr.c;
for (int p = 0; p < 4; p++)
{
int dr = r + pos[p][0];
int dc = c + pos[p][1];
if (dr >= 1 && dr <= H && dc >= 1 && dc <= W)
{
if (mp[dr][dc] == '.')
{
if (dp[dr][dc] > dp[r][c])
{
dp[dr][dc] = dp[r][c];
que.push_front((Res){dr, dc});
}
}
else if (mp[dr][dc] == '#')
{
for (int q = 0; q < 4; q++)
{
int cr = dr + corn[q][0];
int cc = dc + corn[q][1];
for (int k = 0; k < 4; k++)
{
int kr = cr + porn[k][0];
int kc = cc + porn[k][1];
if (kr >= 1 && kr <= H && kc >= 1 && kc <= W && dp[kr][kc] > dp[r][c] + 1)
{
dp[kr][kc] = dp[r][c] + 1;
que.push_back((Res){kr, kc});
}
}
}
}
}
}
}
printf("%d", dp[H][W]);
return 0;
}
Set优化的BFS
当图中的边数太多时,使用BFS会重复访问顶点很多次,时间复杂度上是不优的。采用Set优化BFS可以实现删除点和边的操作,进而保证每一个点或者边只被访问一次。
此题就是一个边数过多的一个题目,问题中的图是隐式存在的,因此我们可以使用Set来帮助我们删除顶点,反边所连接的两个顶点在BFS搜索上是相同的,因此可以将 v v v看成 v + b [ v ] v + b[v] v+b[v],对 v v v更新其实是对 v + b [ v ] v + b[v] v+b[v]更新。
int a[300005];
int b[300005];
int dp[300005];
int passby[300005];
void solve()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
}
for (int i = 1; i <= n; i++)
{
cin >> b[i];
}
queue<int> que;
que.push(n);
dp[n] = n;
set<int> st;
for (int i = 0; i <= n - 1; i++)
st.insert(i);
while (!que.empty())
{
int curr = que.front();
que.pop();
auto l = st.lower_bound(curr - a[curr]);
auto r = st.upper_bound(curr);
for (auto it = l; it != r; it++)
{
int v = *it;
if (dp[v + b[v]] == 0)
{
dp[v + b[v]] = curr;
passby[v + b[v]] = v;
que.push(v + b[v]);
}
}
st.erase(l, r);
}
if (dp[0])
{
vector<int> ans;
for (int curr = 0; curr != n; curr = dp[curr])
{
ans.push_back(passby[curr]);
}
cout << ans.size() << endl;
for (int i = int(ans.size()) - 1; i >= 0; i--)
cout << ans[i] << " ";
cout << endl;
}
else
{
cout << -1 << endl;
}
}
优先队列BFS
给你一个无向图,每个边的边权有多种取值。在这个图中进行BFS搜索寻找最短路问题。
此时扩展一个节点不知道在那一层,因此无法进行插入,此时我们需要优先队列来帮助我们来维护节点的层次信息。
DIjkstra算法就是典型的优先队列BFS。
双向搜索
双向搜索的主要思想是设立两个端点,一般是搜索的起点和终点。对两个端点同时进行搜索,以缩小搜索空间。
双向BFS搜索
设立两个队列,分别保存从两个端点扩展的结果。如图:
可以看见,搜索空间显著比普通的BFS搜索少了。
基本模板:
d1、d2 为两个方向的队列
m1、m2 为两个方向的哈希表,记录每个节点距离起点的
// 只有两个队列都不空,才有必要继续往下搜索
// 如果其中一个队列空了,说明从某个方向搜到底都搜不到该方向的目标节点
while(!d1.isEmpty() && !d2.isEmpty()) {
if (d1.size() < d2.size()) {
update(d1, m1, m2);
} else {
update(d2, m2, m1);
}
}
// update 为从队列 d 中取出一个元素进行「一次完整扩展」的逻辑
void update(Deque d, Map cur, Map other) {}
#define p4ii pair<int[4], int>
#define MAXT 10000
int QS2I(string &str)
{
return (str[0] - '0') * 1000 + (str[1] - '0') * 100 + (str[2] - '0') * 10 + (str[3] - '0');
}
int p4ii2I(p4ii &p)
{
int(&bit)[4] = p.first;
return bit[3] * 1000 + bit[2] * 100 + bit[1] * 10 + bit[0];
}
class Solution
{
public:
unordered_set<int> deadband;
int update(queue<p4ii> &que, vector<int> &curr, vector<int> &other)
{
p4ii p = que.front();
que.pop();
if (other[p4ii2I(p)] != MAXT)
{
return curr[p4ii2I(p)] + other[p4ii2I(p)];
}
for (int i = 0; i < 4; i++)
{
p4ii nxt = p;
nxt.first[i] = (nxt.first[i] + 1) % 10;
nxt.second++;
if (!deadband.count(p4ii2I(nxt)) && curr[p4ii2I(nxt)] == MAXT)
{
que.push(nxt);
curr[p4ii2I(nxt)] = nxt.second;
}
p4ii prv = p;
prv.first[i] = (prv.first[i] + 9) % 10;
prv.second++;
if (!deadband.count(p4ii2I(prv)) && curr[p4ii2I(prv)] == MAXT)
{
que.push(prv);
curr[p4ii2I(prv)] = prv.second;
}
}
return -1;
}
int openLock(vector<string> &deadends, string target)
{
for (string str : deadends)
{
deadband.insert(QS2I(str));
}
if (deadband.count(0))
{
return -1;
}
int tag = QS2I(target);
queue<p4ii> que1, que2;
vector<int> vec1(10000, MAXT), vec2(10000, MAXT);
p4ii sp;
sp.first[0] = sp.first[1] = sp.first[2] = sp.first[3] = 0;
sp.second = 0;
que1.push(sp);
vec1[0] = 0;
p4ii ep;
ep.first[0] = tag % 10;
ep.first[1] = (tag / 10) % 10;
ep.first[2] = (tag / 100) % 10;
ep.first[3] = (tag / 1000) % 10;
ep.second = 0;
que2.push(ep);
vec2[p4ii2I(ep)] = 0;
while (!que1.empty() && !que2.empty())
{
int res = -1;
if (que1.size() > que2.size())
{
res = update(que2, vec2, vec1);
}
else
{
res = update(que1, vec1, vec2);
}
if (res != -1)
{
return res;
}
}
return -1;
}
};
Meet in the middle(折半枚举)
没有官方的中文译名(如果翻译为折半搜索就和二分重名了),大概思想是我们可以将搜索空间平分成两半,然后分别在两半中进行搜索,最后在这两个搜索结果中通过归并寻找最优解的过程。
例如:我们给定一个数组 A A A,和一个值 t a r g e t target target,询问是否存在 A A A的子集,使得子集的和是 t a r g e t target target。
运用Meet in the middle思想我们可以将 A A A平分两半,然后枚举这两个子数组的子集的和,通过迭代归并,我们可以实现该算法,并且两个子集和数组 S 1 S1 S1和 S 2 S2 S2都是有序的,这样,我们就可以在通过双指针的方法检查两个数组是否存在两个元素的和是 t a r g e t target target。
考虑将这个列表分成两份,枚举每一份中,值为负数的答案,然后根据负数的个数放进vector中即可。然后,枚举左边的所有答案,二分搜索右边对应槽内的答案即可。
class Solution {
public:
void enumerate(vector<int> & nums,vector<vector<int>> &record){
int ed = 1 << nums.size();
int sum = accumulate(nums.begin(),nums.end(),0);
for(int stat = 0;stat < ed;stat++){
int pc = __builtin_popcount(stat);
int neg = 0;
for(int i = 0;i < nums.size();i++){
if((stat >> i) & 1) neg += nums[i];
}
record[pc].push_back(sum - 2 * neg);
}
for(vector<int>& grp : record){
sort(grp.begin(),grp.end());
}
}
int minimumDifference(vector<int>& nums) {
int n = nums.size() / 2;
vector<int> lv,rv;
for(int i = 0;i < n;i++) lv.push_back(nums[i]);
for(int i = n;i < 2 * n;i++) rv.push_back(nums[i]);
vector<vector<int>> left(n + 1);
vector<vector<int>> right(n + 1);
enumerate(lv,left);
enumerate(rv,right);
int mi = INT_MAX;
for(int i = 0;i <= n;i++){
vector<int> & lgrp = left[i];
vector<int> & rgrp = right[n - i];
for(int k : lgrp){
int id = lower_bound(rgrp.begin(),rgrp.end(),-k) - rgrp.begin();
if(id <= rgrp.size()){
mi = min(mi,abs(rgrp[id] + k));
}
id--;
if(id >= 0){
mi = min(mi,abs(rgrp[id] + k));
}
}
}
return mi;
}
};
迭代加深搜索
迭代加深搜索是一种用DFS近似BFS的搜索方法。其思想是每次DFS都设置一定的深度,如果超过了搜索深度就不再搜索,如果都搜索完毕之后没有找到结果,那么增大搜索深度,从头开始搜索。
当每一层的节点比较多的时候,BFS每增加一层的时间和空间复杂度都是指数级增加的,如果我们使用迭代加深搜索用DFS模拟BFS,不仅防止了空间占用过大,也能及时的搜索答案。
上述情况中,每次从头开始重复的搜索的浪费时间在巨大的层内节点下可以忽略不计。
启发式搜索
启发式搜索相比于普通BFS和DFS搜索来说,增加了评估函数,其思想为,对每一个决策进行评估,每次都选择最优的决策,以此对搜索进行加速。
启发式搜索有两个著名的算法:A算法和IDA算法。
A*搜索算法
A*搜索算法(英文:A*search algorithm,A*读作 A-star),简称 A*算法,是一种在图形平面上,对于有多个节点的路径求出最低通过成本的算法。它属于图遍历(英文:Graph traversal)和最佳优先搜索算法(英文:Best-first search),亦是 BFS 的改进。
其必须定义三种结构:起点终点,评估函数,搜索过程。
其中起点终点和搜索过程与普通的搜索相同,此处不再赘述。
评估函数有 F ( x ) G ( x ) H ( x ) H ∗ ( x ) F(x) G(x) H(x)H^*(x) F(x)G(x)H(x)H∗(x),其含义如下:
- G ( x ) G(x) G(x)是 x x x到起点的实际距离
- H ( x ) H(x) H(x)是 x x x到终点的估计距离,称为启发函数
- H ∗ ( x ) H^*(x) H∗(x)是 x x x到终点的实际距离,一般不能求解或不好求解,通常我们用 H ( x ) H(x) H(x)来近似 H ∗ ( x ) H^*(x) H∗(x)。
- F ( x ) = H ( x ) + G ( x ) F(x)=H(x)+G(x) F(x)=H(x)+G(x)是某一条路径上的从起点到终点的估计距离,通常我们使用优先队列来进行选取最小的 F ( x ) F(x) F(x)所在的路径节点 x x x进行搜索。
当 H = 0 H=0 H=0的时候,就是算法Dijkstra。
该算法有两个重要的性质,当 H < H ∗ H < H^* H<H∗对于任意节点恒成立的时候,我们总是能找到最短路,我们称该 H H H是可接受的,但是一个节点可能入队不止一次。即当我们从队列中选取最小的 H H H,因为 H < H ∗ H < H^* H<H∗,用 H H H去更新周围节点的 H H H其不一定是最短的。
如果对于任意两个节点 x x x到节点 y y y存在一条长度为 D ( x , y ) D(x,y) D(x,y)的边,如果 ∣ H ( x ) − H ( y ) ∣ ≤ D ( x , y ) |H(x) - H(y)| \le D(x,y) ∣H(x)−H(y)∣≤D(x,y)。那么每个节点只会入队一次,可以证明,每次更新的 H H H都是该节点最短的 H H H,并且还是可接受的,我们称这样的 H H H满足三角形不等式,该函数是一致的。
我们定义 H ( x ) H(x) H(x)为不考虑不可行方案,到目标的最短距离。并且我们的 H ( x ) H(x) H(x)符合上述两条。
int getH(string &status, string &target)
{
int res = 0;
for (int i = 0; i < 4; i++)
{
res += min(abs(status[i] - target[i]), 10 - abs(status[i] - target[i]));
}
return res;
}
struct Astar
{
string status_;
int f_;
int g_;
Astar(string s, string e, int g) : status_(s), g_(g)
{
f_ = g_ + getH(s, e);
}
bool operator<(const Astar &o) const
{
return f_ > o.f_;
}
};
string getNext(string &curr, int bit)
{
string res = curr;
res[bit] = (res[bit] == '9' ? '0' : res[bit] + 1);
return res;
}
string getPrev(string &curr, int bit)
{
string res = curr;
res[bit] = (res[bit] == '0' ? '9' : res[bit] - 1);
return res;
}
class Solution
{
public:
int openLock(vector<string> &deadends, string target)
{
unordered_set<string> dead;
for (string s : deadends)
{
dead.insert(s);
}
if (dead.count("0000"))
{
return -1;
}
unordered_set<string> vis;
priority_queue<Astar> pq;
pq.emplace("0000", target, 0);
vis.insert("0000");
while (!pq.empty())
{
Astar curr = pq.top();
pq.pop();
if (curr.status_ == target)
{
return curr.g_;
}
for (int i = 0; i < 4; i++)
{
string prv = getPrev(curr.status_, i);
if (!dead.count(prv) && !vis.count(prv))
{
pq.emplace(prv, target, curr.g_ + 1);
vis.insert(prv);
}
string nxt = getNext(curr.status_, i);
if (!dead.count(nxt) && !vis.count(nxt))
{
pq.emplace(nxt, target, curr.g_ + 1);
vis.insert(nxt);
}
}
}
return -1;
}
};
#include <bits/stdc++.h>
#define FR freopen("in.txt", "r", stdin)
using namespace std;
typedef long long ll;
int pos[8][2] = {{1, 2}, {-1, 2}, {1, -2}, {-1, -2}, {2, 1}, {2, -1}, {-2, 1}, {-2, -1}};
struct Node
{
uint64_t stat;
int r, c;
int g, h;
bool operator<(const Node &o) const
{
return g + h > o.g + o.h;
}
uint64_t hash()
{
return (((stat << 5) | r) << 5) | c;
}
};
inline int getXY(uint64_t stat, int r, int c)
{
return (stat >> (r * 5 + c)) & 1;
}
inline uint64_t setXY(uint64_t stat, int r, int c, int v)
{
if (v)
return stat | (1 << (r * 5 + c));
else
return stat & (~(1 << (r * 5 + c)));
}
int FH(Node n)
{
uint64_t k = 0b0000010000110001111011111;
int ans = __builtin_popcount(k ^ n.stat);
if (getXY(n.stat, 2, 2) == 0 && (n.r != 2 || n.c != 2))
ans++;
return ans;
}
void solve()
{
priority_queue<Node> pq;
unordered_map<uint64_t, int> mp;
Node start;
start.stat = 0;
for (int r = 0; r < 5; r++)
{
char st[7];
scanf("%s", st);
// scanf("\n");
for (int c = 0; c < 5; c++)
{
char t = st[c];
// scanf("%c", &t);
if (t == '1')
start.stat = setXY(start.stat, r, c, 1);
else if (t == '0')
start.stat = setXY(start.stat, r, c, 0);
else
{
start.stat = setXY(start.stat, r, c, 0);
start.r = r;
start.c = c;
}
}
}
start.g = 0;
start.h = FH(start);
mp[start.hash()] = 0;
pq.push(start);
while (!pq.empty())
{
Node curr = pq.top();
pq.pop();
if (curr.h == 0)
{
printf("%d\n", mp[curr.hash()]);
return;
}
for (int i = 0; i < 8; i++)
{
int dr = curr.r + pos[i][0];
int dc = curr.c + pos[i][1];
if (dr >= 0 && dr < 5 && dc >= 0 && dc < 5)
{
Node nxt = curr;
int p = getXY(nxt.stat, dr, dc);
nxt.stat = setXY(nxt.stat, dr, dc, 0);
nxt.stat = setXY(nxt.stat, nxt.r, nxt.c, p);
nxt.r = dr;
nxt.c = dc;
nxt.g++;
nxt.h = FH(nxt);
if (nxt.g + nxt.h > 15)
continue;
if (mp.count(nxt.hash()))
{
if (mp[nxt.hash()] > nxt.g + nxt.h)
{
mp[nxt.hash()] = nxt.g + nxt.h;
pq.push(nxt);
}
}
else
{
mp[nxt.hash()] = nxt.g + nxt.h;
pq.push(nxt);
}
}
}
}
printf("-1\n");
return;
}
int main()
{
int T;
scanf("%d", &T);
while (T--)
{
solve();
}
return 0;
}
IDA*搜索
IDA*算法是迭代加深搜索和启发式评估的结合体,其思想为,如果 F ( x ) F(x) F(x)大于限制步数的话就不会继续搜索,因为如果继续搜索也会超过步数返回(相当于提前剪枝)。而且IDA*的搜索可能会重复,但是有步数的限制并不会导致死循环,通过这一点,IDA*算法是不需要判重的,特别适合于状态不好映射的情况。相当于是一个以时间换空间的一个算法。
一般不能判断是否存在可行解,因为最终会陷入死循环,没有判重机制。并且,IDA*算法一定是按照最短步数来的,因为该算法是按照步数迭代的。
int curr[3][3];
int limit = 0;
int ed[3][3] = {{1, 2, 3}, {8, 0, 4}, {7, 6, 5}};
int pos[4][2] = {{1, 0}, {0, -1}, {0, 1}, {-1, 0}};
int getH()
{
int res = 0;
for (int r = 0; r < 3; r++)
for (int c = 0; c < 3; c++)
{
if (curr[r][c] != ed[r][c])
{
res++;
}
}
return res;
}
int IDA(int step, int r, int c, int last)
{
if (step > limit)
{
return -1;
}
if (getH() == 0)
{
return step;
}
for (int i = 0; i < 4; i++)
{
if (i + last == 3)
{
continue;
}
int dr = pos[i][0] + r;
int dc = pos[i][1] + c;
if (dr >= 0 && dr < 3 && dc >= 0 && dc < 3)
{
swap(curr[dr][dc], curr[r][c]);
if (step + 1 + getH() <= limit) // 估值,如果
{
int res = IDA(step + 1, dr, dc, i);
if (res != -1)
{
return res;
}
}
swap(curr[dr][dc], curr[r][c]);
}
}
return -1;
}
int main()
{
int zr = 0, zc = 0;
for (int r = 0; r < 3; r++)
for (int c = 0; c < 3; c++)
{
char val;
scanf("%c", &val);
curr[r][c] = val - '0';
if (curr[r][c] == 0)
{
zr = r;
zc = c;
}
}
while (++limit)
{
int a = IDA(0, zr, zc, -5);
if (a != -1)
{
printf("%d", a);
return 0;
}
}
return 0;
}