算法初探系列5 - 广度优先搜索之状态表示

概述

上节课我们学习了在简单迷宫问题中如何使用BFS求解,这次我们来讲解一下较复杂 ~~(花里胡哨)~~ 的迷宫问题

复习+延伸(多起点BFS)

在普通bfs问题中,我们先将起点入队,然后一直从起点延伸,直到到达目的或者队列为空(即没有达到目的)时停止bfs。整个过程就相当于在找离目标状态的最小操作次数。 在一些问题中,我们需要找n个目标状态。这样的问题我们就有两种思路。 - 第一种众所周知,就是执行n次bfs,然后不断更新表示操作次数的数组。 - 第二种就比较巧妙啦。我们可以将n个初始的状态一起塞到队列里。这些状态在队列里不断延伸,最后得到的还是一个操作次数不下降的序列,所以说在这种方法里,第一次bfs到目标状态时的结果还是结果哒~

多起点bfs例题:构造猪猪(最不相似字符串)

  • 题目概述
    n个猪猪正在做一个游戏:每个猪猪有固定的若干个特征,特征一共有m种,每个猪猪的特征均为特征集合的子集。两个猪猪的相似度为:若猪猪a和猪猪b同时有或没有某个特征,则相似度+1.
    蒟蒻君想创造出一个猪猪,使其与n个猪猪的相似度中最大的尽量小。
  • 思路+代码
#include 
using namespace std;
const int N = 1e5 + 5;
int a[1 << 15];
int n, m;
queue<int> q;
int main() {
	memset(a, -1, sizeof a);
	cin >> n >> m;
	while (n--) {
		string s;
		cin >> s;
		int sum = 0;
		for (int i = 0; i < m; ++i) {
			sum = 2 * sum + s[i] - '0';
		}
		a[sum] = m;
		q.push(sum);
	}
	int u, v;
	while (q.size()) {
		u = q.front();
		q.pop();
		for (int i = 0; i < m; ++i) {
			// 我们发现可以对队首元素u每一位取反,这样就能产生比现在相似度小1的数字 
			v = u ^ (1 << i);
			// 如果这个数字之前没出现过,就可以标记并入队 
			if (a[v] == -1) {
				a[v] = a[u] - 1;
				q.push(v);
			}
		}
	}
	// 如何找到最小数字?枚举会超时,但不难发现,BFS最后取出的那个数相似度必然最小 
	for (int i = m - 1; i >= 0; --i) {
		cout << ((u >> i) & 1);
	}
	return 0;
}

显而易见,保存和更新状态就是BFS的核心,在不同类型的问题里,需要保存和更新状态也是不一样的,这节课我们就来看看如何使用BFS解决钥匙迷宫、动态迷宫和建立模型。

钥匙迷宫——多起点BFS

定义:在迷宫里放上一些钥匙和一些门,门原本不可通行,但如果目前持有钥匙就可以同行。 我们发现,在钥匙迷宫中,不同钥匙是需要保存哒,那么我们又应该如何高效记录呢?最普遍的方式就是用一个int类的数字,其二进制就是目前钥匙持有的状态,是不是很神奇呢? 比如:如果有8把钥匙,那么最开始这个数就是0,即(0000000)2,拿到第1把则变成64,即(10000000)2。 不要忘了我们记录是否访问过的vis[n][m][1 << 钥匙个数]的数组也要与此书统一记录。

钥匙迷宫练习题:拯救公主(poj 4105)

  • 描述
    多灾多难的公主又被大魔王抓走啦!国王派遣了第一勇士阿福去拯救她。
    身为超级厉害的术士,同时也是阿福的好伙伴,你决定祝他一臂之力。你为阿福提供了一张大魔王根据地的地图,上面标记了阿福和公主所在的位置,以及一些不能够踏入的禁区。你还贴心地为阿福制造了一些传送门,通过一个传送门可以瞬间转移到任意一个传送门,当然阿福也可以选择不通过传送门瞬移。传送门的位置也被标记在了地图上。此外,你还查探到公主所在的地方被设下了结界,需要集齐K种宝石才能打开。当然,你在地图上也标记出了不同宝石所在的位置。
    你希望阿福能够带着公主早日凯旋。于是在阿福出发之前,你还需要为阿福计算出他最快救出公主的时间。
    地图用一个R×C的字符矩阵来表示。字符S表示阿福所在的位置,字符E表示公主所在的位置,字符#表示不能踏入的禁区,字符$表示传送门,字符.表示该位置安全,数字字符0至4表示了宝石的类型。阿福每次可以从当前的位置走到他上下左右四个方向上的任意一个位置,但不能走出地图边界。阿福每走一步需要花费1个单位时间,从一个传送门到达另一个传送门不需要花费时间。当阿福走到宝石所在的位置时,就视为得到了该宝石,不需要花费额外时间。
  • 输入
    第一行是一个正整数T(1 <= T <= 10),表示一共有T组数据。
    每一组数据的第一行包含了三个用空格分开的正整数R、C(2 <= R, C <= 200)和K,表示地图是一个R×C的矩阵,而阿福需要集齐K种宝石才能够打开拘禁公主的结界。
    接下来的R行描述了地图的具体内容,每一行包含了C个字符。字符含义如题目描述中所述。保证有且仅有一个S和E。$的数量不超过10个。宝石的类型在数字0至4范围内,即不会超过5种宝石。
  • 输出
    对于每一组数据,输出阿福救出公主所花费的最少单位时间。若阿福无法救出公主,则输出“oop!”(只输出引号里面的内容,不输出引号)。每组数据的输出结果占一行。
  • 样例输入
    1
    7 8 2
    ………
    …S…#0.
    .##…1…
    .0#……
    …1#…
    …##E…
    …1….
  • 样例输出
    11
  • 解题思路
    这道题虽然多了一个传送门的操作,但是只要按照钥匙迷宫的标记策略标记就能AC,代码虽长,并无难度。
  • 代码
#include 
using namespace std;
const int N = 205;
const int dir[2][4] = {1, 0, -1, 0, 0, 1, 0, -1};
struct node {
	int x, y, z;	// 其中z就是记录钥匙是否被取的整数
	node() { }
	node(int _x, int _y, int _z) {
		x = _x;
		y = _y;
		z = _z;
	} 
};
int vis[N][N][1 << 5]; 
string s[N];
queue<node> q;
// 判断x的二进制中有多少个1,也就是目前持有多少把钥匙 
inline int get(int x) {
	int res = 0;
	while (x) {
		res += x & 1;
		x >>= 1;
	}
	return res;
} 
int n, m, k;
// 结果 
int res;
// 存储钥匙位置 
vector<pair<int, int> > key;
inline bool in(node A) {
	return 0 <= A.x && A.x < n && 0 <= A.y && A.y < m; 
}
#define a first
#define b second
void bfs(node st) {
	q.push(st); 
	vis[st.x][st.y][st.z] = 0;
	while (q.size() && res == -1) {
		node u = q.front();
		q.pop();
		int c = vis[u.x][u.y][u.z];
		for (int i = 0; i < 4; ++i) {
			// 下一个点 
			node v;
			v.x = u.x + dir[0][i];
			v.y = u.y + dir[1][i];
			v.z = u.z;
			// 出格或者是墙的话 
			if (!in(v) || s[v.x][v.y] == '#') {
				continue;
			}
			// 目前字符是宝石的话 
			if ('0' <= s[v.x][v.y] && s[v.x][v.y] <= '4') {
				v.z |= 1 << (s[v.x][v.y] - '0');
			}
			// 访问过的话 
			if (vis[v.x][v.y][v.z] != -1) {
				continue;
			}
			// 到终点并且有至少k把钥匙 
			if (s[v.x][v.y] == 'E' && get(v.z) >= k) {
				res = c + 1;
				break;
			}
			// 是钥匙就捡 
			if (s[v.x][v.y] == '$') {
				for (int j = 0; j < key.size(); ++j) {
					if (vis[key[j].a][key[j].b][v.z] == -1) {
						vis[key[j].a][key[j].b][v.z] = c + 1;
						q.push(node(key[j].a, key[j].b, v.z));
					}
				}
			} else {
				// 符合条件标记并入队 
				vis[v.x][v.y][v.z] = c + 1;
				q.push(v);
			}
		}
	}
}
int main() {
	int T;
	cin >> T;
	while (T--) {
		// 重置 
		memset(vis, -1, sizeof vis);
		res = -1;
		key.clear();
		while (q.size()) {
			q.pop();
		}
		cin >> n >> m >> k;
		node st(0, 0, 0);
		for (int i = 0; i < n; ++i) {
			cin >> s[i];
			for (int j = 0; j < m; ++j) {
				// 起点 
				if (s[i][j] == 'S') {
					st.x = i;
					st.y = j;
				}
				// 钥匙 
				if (s[i][j] == '$') {
					key.push_back(make_pair(i, j));
				}
			}
		}
		bfs(st);
		if (res == -1) {
			cout << "oop!\n";
			
		} else {
			cout << res << '\n'; 
		}
	}
	return 0;
}

动态迷宫
定义:迷宫里有一些特殊的东东,这些东东的状态周期性地变化。比如,有一个梯子,最开始是横着的(—),但是过一分钟就会变成竖着的(|),人物只能沿着梯子移动。
我们发现,整个迷宫的周期是一致的,说明整个迷宫是有两种状态,也就是奇数个单位时间和偶数个单位时间的状态。
那么我们有什么策略呢?我们可以将每一个可以改变状态的位置分两个状态考虑,分别记录这个位置第一种状态和第二种状态时是否被访问过。当然,如果有k种状态,我们也可以同时记录k种状态。
对于这一道题,其实通过梯子时过不去的话最简单的方法就是等待,耗费1个单位时间。
代码

#include 
using namespace std;
const int N = 25;
const int dir[2][4] = {-1, 1, 0, 0, 0, 0, -1, 1};
string s[N];
bool vis[N][N];
struct node {
    // 位置
    int x, y;
    // 步数
    int stp;
    //是否停留
    bool pause;
    node() { }
    node(int _x, int _y, int _stp, bool _pause) {
        x = _x;
        y = _y;
        stp = _stp;
        pause = _pause;
    }
};
queue<node> q;
int n, m;
// 结果
int res = -1;
// 是否出格
inline bool in(node a) {
    return 0 <= a.x && a.x < n && 0 <= a.y && a.y < m;
}
void bfs(node st) {
    // 起点标记入队
    q.push(node(st.x, st.y, 0, false));
    vis[st.x][st.y] = true;
    while (q.size() && res == -1) {
        node u = q.front();
        q.pop();
        for (int i = 0; i < 4; ++i) {
            // 下一个点
            node v;
            v.x = u.x + dir[0][i];
            v.y = u.y + dir[1][i];
            v.stp = u.stp + 1;
            v.pause = false;
            // 当前点是否访问
            #define b vis[v.x][v.y]
            // 出格或者访问过则尝试下一个点
            if (!in(v) || b) {
                continue;
            }
            // 当前点元素
            #define c s[v.x][v.y]
            // 如果当前是竖着的
			if (c == '|') {
                if  ((i < 2 && u.stp % 2 == 0) || (i >= 2 && u.stp % 2 == 1)) {
                    v.x += dir[0][i];
                    v.y += dir[1][i];
                    if (b || c == '*') {
                        continue;
                    }
                } else {
                    continue;
                }
            }
            if (c == '-') { // 如果是横着的
                if  ((i < 2 && u.stp % 2 == 1) || (i >= 2 && u.stp % 2 == 0)) {
                    v.x += dir[0][i];
                    v.y += dir[1][i];
                    if (b || c == '*') {
                        continue;
                    }
                } else {
                    continue;
                }
            }
            // 是路的话(合法),标记入队
            if (c == '.') {
                q.push(v);
                b = true;
            }
            // 到达终点
            if (c == 'T') {
                res = v.stp;
                break;
            }
        }
        // 不停留就标记入队
		if (!u.pause) {
            u.pause = true;
            ++u.stp;
            q.push(u);
        }
    }
}
int main() {
    cin >> n >> m;
    node st;
    for (int i = 0; i < n; ++i) {
    	cin >> s[i];
        for (int j = 0; j < m; ++j) {
            if (s[i][j] == 'S') {
                st.x = i;
                st.y = j;
            }
        }
    }
    bfs(st);
    cout << res << '\n';
    return 0;
}

需要表示的状态——[NOIP2013]华容道(60~70pts解法)

在一些问题中,我们会对状态进行表示,这个时候呢,我们需要将决策(也就是可以移送的动作)结合在一块。所以,一旦某个状态下进行合法的决策,那么状态肯定会发生改变。 我们找个例子。。 [传送](https://blog.csdn.net/yueyuedog/article/details/119790152)
双向搜索

有些问题,比如:[NOIP2008提高组]传纸条,就不能只考虑一次搜索,把第一次决策做到最好,不一定是最好的总决策,这种问题就要用到双向搜索了。
什么是双向搜索呢?双向搜索就是同时从两边开始搜索,每一步都要使目前两个决策总体最优,到最后会一起搜索到结果,此时的结果就是全局最优解了。

双向搜索例题:[NOIP2008提高组]传纸条

你可能感兴趣的:(算法,C++,算法,bfs,noip)