树和图的存储
树是一种特殊的图,即无环连通图,其存储方式与图的存储方式相同;
对于无向图而言,可以按照有向图来存储,即对无向图的边a - b
,存储两条有向图的边即可a -> b; b -> a
;
因此仅考虑有向图的存储即可。
有两种存储方式:
邻接矩阵:适用于存储边稠密的图。
邻接表:适用于存储边稀疏的图。
// 邻接表存储
const int N = ___, M = 2 * M; // 因为邻接表的结点可能多次出现
// h存储链表头
// e存储每个结点的值
// ne存储每个结点的next指针
int h[N], e[M], ne[M], idx;
// 添加一条有向边 a -> b
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ; // 头插法
}
// 初始化
idx = 0;
memset(h, -1, sizeof h);
BFS分类
BFS特点
代码模板
queue<int> q;
st[1] = true; // 表示1号点已经被遍历过
q.push(1);
while (q.size())
{
int t = q.front();
q.pop();
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j])
{
st[j] = true; // 表示点j已经被遍历过
q.push(j);
}
}
}
可以在线性时间复杂度内,找到某个点所在的连通块。
ACwing 1097
连通
本题是八连通。
#include
#include
using namespace std;
#define x first
#define y second
typedef pair<int, int> PII;
const int N = 1010, M = N * N;
int n, m;
char g[N][N];
PII q[M];
bool st[N][N];
void bfs(int sx, int sy) {
int hh = 0, tt = 0; q[0] = {sx, sy};
st[sx][sy] = true;
while (hh <= tt) {
PII t = q[hh++];
for (int i = t.x - 1; i <= t.x + 1; i++)
for (int j = t.y - 1; j <= t.y + 1; j++) {
if (i == t.x && j == t.y) continue;
if (i < 0 || i >= n || j < 0 || j >= m) continue;
if (g[i][j] == '.' || st[i][j]) continue;
q[++tt] = {i, j};
st[i][j] = true;
}
}
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i++) scanf("%s", g[i]);
int res = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
if (g[i][j] == 'W' && !st[i][j]) {
bfs(i, j); res++;
}
printf("%d\n", res);
return 0;
}
DFS版本:
#include
#include
using namespace std;
const int N = 1010;
int n, m;
char g[N][N];
bool st[N][N];
void dfs(int x, int y) {
st[x][y] = true;
for (int i = -1; i <= 1; i ++ )
for (int j = -1; j <= 1; j ++ ) {
int a = x + i, b = y + j;
if (a < 0 || a >= n || b < 0 || b >= m) continue;
if (g[a][b] == '.' || st[a][b]) continue;
dfs(a, b);
}
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i ++ ) scanf("%s", g[i]);
int res = 0;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < m; j ++ )
if (g[i][j] == 'W' && !st[i][j]) {
dfs(i, j);
res ++ ;
}
printf("%d\n", res);
return 0;
}
ACwing 1098
注意这个题的数据输入,相当于用一个 4 4 4 位二进制数来表示一个小方格四周 w a l l wall wall 的数量。比如: 11 = 8 + 2 + 1 11 = 8 + 2 + 1 11=8+2+1,所以值为 11 11 11 的小方格周围有西墙、北墙和南墙。
代码中,实现判断该处是否是墙的代码:g[t.x][t.y] >> i & 1
,如果该值为 1 1 1,则该处为墙。
可见,这个题是四连通。
#include
#include
using namespace std;
#define x first
#define y second
typedef pair<int, int> PII;
const int N = 55, M = N * N;
int n, m;
int g[N][N];
PII q[M];
bool st[N][N];
int dx[] = {0, -1, 0, 1}, dy[] = {-1, 0, 1, 0}; // 方向按照 西北东南 顺序设置
int bfs(int sx, int sy) {
int area = 0;
int hh = 0, tt = 0; q[0] = {sx, sy};
st[sx][sy] = true;
while (hh <= tt) {
auto t = q[hh++];
area++;
for (int i = 0; i < 4; i++) {
int a = t.x + dx[i], b = t.y + dy[i];
if (a < 0 || a >= n || b < 0 || b >= m) continue;
if (st[a][b] || g[t.x][t.y] >> i & 1) continue;
q[++tt] = {a, b};
st[a][b] = true;
}
}
return area;
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
scanf("%d", &g[i][j]);
int cnt = 0, area = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
if (!st[i][j]) area = max(area, bfs(i, j)), cnt++;
printf("%d\n%d\n", cnt, area);
return 0;
}
ACwing 1106
BFS
找连通块:它与周围格子一样大,有可能是山峰,有肯能是山谷,也可能啥也不是,需要判断它与相邻的格子的大小关系这个题是八连通。
对一段代码的解释:
// if (st[i][j]) continue;
if (h[i][j] != h[t.x][t.y]) {
if (h[i][j] > h[t.x][t.y]) has_higher = true;
else has_lower = true;
} else if (!st[i][j])
q[++tt] = {i, j}, st[i][j] = true;
每一次都是遍历一个数值相等的连通块,如果数值相等就不需要遍历。如果将条件判断 if (st[i][j]) continue
放在前面,这时即便 h[i][j] != h[t.x][t.y]
,也不需要遍历,但是会错失依次判断山峰和山谷的机会。
#include
#include
using namespace std;
#define x first
#define y second
typedef pair<int, int> PII;
const int N = 1010, M = N * N;
int n;
int h[N][N];
PII q[M];
bool st[N][N];
void bfs(int sx, int sy, bool &has_higher, bool &has_lower) {
int hh = 0, tt = 0; q[0] = {sx, sy};
st[sx][sy] = true;
while (hh <= tt) {
auto t = q[hh++];
for (int i = t.x - 1; i <= t.x + 1; i++)
for (int j = t.y - 1; j <= t.y + 1; j++) {
if (i == t.x && j == t.y) continue;
if (i < 0 || i >= n || j < 0 || j >= n) continue;
if (h[i][j] != h[t.x][t.y]) { // 山脉的边界
if (h[i][j] > h[t.x][t.y]) has_higher = true;
else has_lower = true;
} else if (!st[i][j]) {
q[++tt] = {i, j};
st[i][j] = true;
}
}
}
}
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
scanf("%d", &h[i][j]);
int peak = 0, valley = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
if (!st[i][j]) {
bool has_higher = false, has_lower = false;
bfs(i, j, has_higher, has_lower);
if (!has_higher) peak++;
if (!has_lower) valley++;
}
printf("%d %d\n", peak, valley);
return 0;
}
下面有两种 B F S BFS BFS 代码,一种是对数组 d i s t dist dist 初始化为 0 x 3 f 0x3f 0x3f,另一种是初始化为 − 1 -1 −1,这两种代码在后续的判断中略有不同。第二种代码更像是一个 b o o l bool bool 数组,记录其点是否走过并且同时记录其到起点的最短距离。这两种代码都是可以的,因为 B F S BFS BFS 是在第一次遍历到某点的时候,这个时候的距离就是最短距离,即整个遍历过程仅入队一次,后续就不需要对其进行更新。
ACWing 844
#include
#include
#include
using namespace std;
#define x first
#define y second
typedef pair<int, int> PII;
const int N = 110, M = N * N;
int n, m;
int g[N][N];
PII q[M];
int dist[N][N];
int dx[] = {0, -1, 0, 1}, dy[] = {-1, 0, 1, 0};
int bfs() {
memset(dist, 0x3f, sizeof dist); dist[0][0] = 0;
int hh = 0, tt = 0; q[0] = {0, 0};
while (hh <= tt) {
auto t = q[hh++];
for (int i = 0; i < 4; i++) {
int a = t.x + dx[i], b = t.y + dy[i];
if (a < 0 || a >= n || b < 0 || b >= m) continue;
if (g[a][b]) continue;
if (dist[a][b] > dist[t.x][t.y] + 1) {
dist[a][b] = dist[t.x][t.y] + 1;
q[++tt] = {a, b};
}
}
}
return dist[n - 1][m - 1];
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
scanf("%d", &g[i][j]);
printf("%d\n", bfs());
return 0;
}
ACwing 3230
这个题目和 ACwing 3200 无线网络 有些类似的地方。
代码中 M = 310 M = 310 M=310 的由来:由题目中可知 0 ≤ a , b ≤ 100 0 \le a, b \le 100 0≤a,b≤100,因此当时间 t ≥ 100 t \ge 100 t≥100 的时候,所有格子都可以走,从左上角到右下角最多走 200 200 200 步,因此 M M M 最多为 300 300 300。
#include
#include
#include
using namespace std;
const int N = 110, M = 310;
int n, m, T;
bool g[N][N][M]; // 表示格子(x,y)在t时刻能否走
int dist[N][N][M]; // 从起点到(x,y)的最短时间为m
struct Node {
int x, y, t;
} q[N * N * M];
int dx[] = {0, -1, 0, 1}, dy[] = {-1, 0, 1, 0};
inline int bfs() {
memset(dist, 0x3f, sizeof dist);
int tt = 0, hh = 0; q[0] = {1, 1, 0};
while (hh <= tt) {
Node t = q[hh++];
if (t.x == n && t.y == m) return dist[t.x][t.y][t.t];
for (int i = 0; i < 4; i++) {
int a = t.x + dx[i], b = t.y + dy[i];
if (a <= 0 || a > n || b <= 0 || b > m) continue;
if (g[a][b][t.t + 1]) continue;
if (dist[a][b][t.t + 1] > t.t + 1) {
dist[a][b][t.t + 1] = t.t + 1;
q[++tt] = {a, b, t.t + 1};
}
}
}
return 0;
}
int main() {
scanf("%d%d%d", &n, &m, &T);
while (T--) {
int r, c, a, b; scanf("%d%d%d%d", &r, &c, &a, &b);
for (int i = a; i <= b; i++) g[r][c][i] = true;
}
printf("%d\n", bfs());
return 0;
}
ACwing 1076
注意代码中 pre[sx][sy] = {0, 0}
,这里起点 (sx, sy)
即 (n-1, n-1)
的上一个状态是可以任意填写,只要不为 -1
即可,因为在最后输出的时候并不会遍历到。
#include
#include
#include
using namespace std;
#define x first
#define y second
typedef pair<int, int> PII;
const int N = 1010, M = N * N;
int n;
int g[N][N];
PII q[M];
PII pre[N][N]; // 以前的st变成了pre,记录方案
int dx[] = {0, -1, 0, 1}, dy[] = {-1, 0, 1, 0};
void bfs(int sx, int sy) {
memset(pre, -1, sizeof pre); pre[sx][sy] = {0, 0};
int hh = 0, tt = 0; q[0] = {sx, sy};
while (hh <= tt) {
auto t = q[hh++];
for (int i = 0; i < 4; i++) {
int a = t.x + dx[i], b = t.y + dy[i];
if (a < 0 || a >= n || b < 0 || b >= n) continue;
if (g[a][b] || pre[a][b].x != -1) continue;
pre[a][b] = t;
q[++tt] = {a, b};
}
}
}
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
scanf("%d", &g[i][j]);
bfs(n - 1, n - 1);
PII end(0, 0);
while (true) {
printf("%d %d\n", end.x, end.y);
if (end.x == n - 1 && end.y == n - 1) break;
end = pre[end.x][end.y];
}
return 0;
}
ACWing 847
#include
#include
#include
using namespace std;
const int N = 1e5 + 10;
int n, m;
int h[N], e[N], ne[N], idx;
int dist[N], q[N];
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
int bfs() {
memset(dist, -1, sizeof dist);
dist[1] = 0;
int hh = 0, tt = 0;
q[0] = 1;
while (hh <= tt) {
int t = q[hh ++ ];
for (int i = h[t]; i != -1; i = ne[i]) {
int j = e[i];
if (dist[j] != -1) continue;
dist[j] = dist[t] + 1;
q[ ++ tt] = j;
}
}
return dist[n];
}
int main() {
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
for (int i = 0; i < m; i ++ ) {
int a, b;
scanf("%d%d", &a, &b);
add(a, b);
}
printf("%d\n", bfs());
return 0;
}
ACwing 188
注意本题先输入的是列,后输入的是行!
#include
#include
#include
using namespace std;
#define x first
#define y second
typedef pair<int, int> PII;
const int N = 155, M = N * N;
int n, m;
char g[N][N];
PII q[M];
int dist[N][N];
int dx[] = {-2, -2, -1, 1, 2, 2, 1, -1};
int dy[] = {-1, 1, 2, 2, 1, -1, -2, -2};
int bfs(int sx, int sy) {
memset(dist, -1, sizeof dist); dist[sx][sy] = 0;
int hh = 0, tt = 0; q[0] = {sx, sy};
while (hh <= tt) {
auto t = q[hh++];
for (int i = 0; i < 8; i++) {
int a = t.x + dx[i], b = t.y + dy[i];
if (a < 0 || a >= n || b < 0 || b >= m) continue;
if (g[a][b] == '*' || dist[a][b] != -1) continue;
if (g[a][b] == 'H') return dist[t.x][t.y] + 1;
dist[a][b] = dist[t.x][t.y] + 1;
q[++tt] = {a, b};
}
}
return -1;
}
int main() {
scanf("%d%d", &m, &n);
for (int i = 0; i < n; i++) scanf("%s", g[i]);
int sx = 0, sy = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
if (g[i][j] == 'K') sx = i, sy = j;
printf("%d\n", bfs(sx, sy));
return 0;
}
ACwing 1100
首先,农夫不可能走到 ≤ 0 \le 0 ≤0 的点。如果农夫走到了 ≤ 0 \le 0 ≤0 的点,而题目中 k ≥ 0 k\ge0 k≥0,那么农夫要想抓到牛,就必须走回 ≥ 0 \ge0 ≥0 的点上。从 ≥ 0 \ge0 ≥0 的点走到 ≤ 0 \le0 ≤0 的点只有通过 x − 1 x-1 x−1,而从 ≤ 0 \le0 ≤0 的点走到 ≥ 0 \ge0 ≥0 的点只有通过 x + 1 x+1 x+1,这样一去一来又回到了原点,本题求的是最短路径,所以这样走等于农夫白走了一段路,故农夫走到的点都是 ≥ 0 \ge0 ≥0 的。
这里有个细节,就是要确定 N N N 的取值范围。考虑当 n > k n > k n>k 的时候,这个时候就只能通过 x − 1 x-1 x−1 来抓到牛;可是当 n < k n < k n<k 的时候,一个极端的情况是 n = k − 1 n = k-1 n=k−1,这个时候如果使用 2 × x 2 \times x 2×x,那么 N N N 的取值范围最大就要开到 2 e 5 2e5 2e5。但是代码中使用的是 1 e 5 1e5 1e5,是因为上面这种情况考虑的是先乘后减,其实也可以考虑先减后乘,同样可以抓到牛,这个时候 N N N 的范围就不用开到 2 e 5 2e5 2e5了。
#include
#include
#include
using namespace std;
const int N = 1e5 + 10;
int n, k;
int q[N], dist[N];
int bfs() {
memset(dist, -1, sizeof dist); dist[n] = 0;
int hh = 0, tt = 0; q[0] = n;
while (hh <= tt) {
auto t = q[hh++];
if (t == k) return dist[k];
if (t + 1 < N && dist[t + 1] == -1) {
dist[t + 1] = dist[t] + 1;
q[++tt] = t + 1;
}
if (t - 1 >= 0 && dist[t - 1] == -1) {
dist[t - 1] = dist[t] + 1;
q[++tt] = t - 1;
}
if (t * 2 < N && dist[t * 2] == -1) {
dist[t * 2] = dist[t] + 1;
q[++tt] = t * 2;
}
}
return -1;
}
int main() {
scanf("%d%d", &n, &k);
printf("%d\n", bfs());
return 0;
}
ACwing 3245
从点 1 1 1 到点 n n n 有 m m m 条边可以选择,现在要求做多选择 n n n 条边,使得从 1 1 1 到 n n n 连通且边上的最大权值最小。
注意审题,因为每家公司的施工时间一样,所以如果选择的 k k k 条线路同时施工,那么整条线路的施工时间就取决于施工时间最长的一条线路。
如果每一次都只选择边权小于等于 m i d mid mid 的边,那么这个图中所有边权小于等于 m i d mid mid 的边的边权可以看做 1 1 1,大于 m i d mid mid 的边的边权看做 0 0 0,因此可以使用 b f s bfs bfs 即可。
#include
#include
#include
using namespace std;
const int N = 1e5 + 10, M = 4e5 + 10;
int n, m;
int h[N], e[M], w[M], ne[M], idx;
int q[N], dist[N];
inline void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
inline int bfs(int mid) {
memset(dist, 0x3f, sizeof dist); dist[1] = 0;
int hh = 0, tt = 0; q[0] = 1;
while (hh <= tt) {
int t = q[hh++];
for (int i = h[t]; ~i; i = ne[i]) {
if (w[i] > mid) continue;
int j = e[i];
if (dist[j] > dist[t] + 1)
dist[j] = dist[t] + 1, q[++tt] = j;
}
}
return dist[n];
}
int main() {
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m--) {
int a, b, c; scanf("%d%d%d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
int l = 0, r = 1e6;
while (l < r) {
int mid = (l + r) >> 1;
if (bfs(mid) <= n) r = mid;
else l = mid + 1;
}
printf("%d\n", r);
return 0;
}
ACwing 173
题意:求解题目中每个值为 1 1 1 的格子到其余所有格子为的最短距离。
将其转换成单源最短路径问题。可以建立一个虚拟起点,其到所有真实起点的距离是 0 0 0。
#include
#include
#include
using namespace std;
#define x first
#define y second
typedef pair<int, int> PII;
const int N = 1010, M = N * N;
int n, m;
char g[N][N]; // 输入没有空格
PII q[M];
int dist[N][N];
int dx[] = {0, -1, 0, 1}, dy[] = {-1, 0, 1, 0};
void bfs() {
memset(dist, -1, sizeof dist);
int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < m; j ++ )
if (g[i][j] == '1')
dist[i][j] = 0, q[ ++ tt] = {i, j};
while (hh <= tt) {
PII t = q[hh ++ ];
for (int i = 0; i < 4; i ++ ) {
int a = t.x + dx[i], b = t.y + dy[i];
if (a < 0 || a >= n || b < 0 || b >= m) continue;
if (dist[a][b] != -1) continue;
dist[a][b] = dist[t.x][t.y] + 1;
q[ ++ tt] = {a, b};
}
}
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i ++ ) scanf("%s", g[i]);
bfs();
for (int i = 0; i < n; i ++ ) {
for (int j = 0; j < m; j ++ )
printf("%d ", dist[i][j]);
puts("");
}
return 0;
}
ACWing 845
算法思想:
将网格的每一个状态看成一个结点,求出从最初状态到规定状态的最小步数即可。
难点:
queue
来表示,距离使用unordered map dist
来表示。3x3
矩阵,然后利用bfs
进行变换,最后将3x3
矩阵转化成字符串。注:
int x = k / 3, y = k % 3;
k = a * 3 + b
#include
#include
#include
#include
#include
using namespace std;
string start;
queue<string> q;
unordered_map<string, int> dist;
int dx[] = {0, -1, 0, 1}, dy[] = {-1, 0, 1, 0};
int bfs() {
string end = "12345678x";
if (start == end) return 0;
dist[start] = 0;
q.push(start);
while (q.size()) {
auto t = q.front(); q.pop();
if (t == end) return dist[t];
int k = t.find('x');
int x = k / 3, y = k % 3;
for (int i = 0; i < 4; i++) {
int a = x + dx[i], b = y + dy[i];
if (a < 0 || a >= 3 || b < 0 || b >= 3) continue;
string r = t;
swap(r[k], r[a * 3 + b]);
if (dist.count(r)) continue;
dist[r] = dist[t] + 1;
q.push(r);
}
}
return -1;
}
int main() {
for (int i = 0; i < 9; i++) {
char c[2]; scanf("%s", c);
start += *c;
}
printf("%d\n", bfs());
return 0;
}
ACwing 3385
#include
#include
#include
#include
#include
using namespace std;
int n;
string start;
int bfs() {
unordered_map<string, int> dist; dist[start] = 0;
queue<string> q; q.push(start);
while (q.size()) {
auto t = q.front(); q.pop();
for (int i = 0; i < n; i++)
if (t.substr(i, 4) == "2012")
return dist[t];
for (int i = 1; i < n; i++) {
string r = t;
swap(r[i], r[i - 1]);
if (!dist.count(r))
dist[r] = dist[t] + 1, q.push(r);
}
}
return -1;
}
int main() {
cin >> n >> start;
cout << bfs() << endl;
return 0;
}
ACwing 1107
代码中一点解释:
#include
#include
#include
#include
#include
using namespace std;
#define x first
#define y second
char g[2][4];
unordered_map<string, pair<char, string>> pre;
unordered_map<string, int> dist;
inline void set(string state) {
for (int i = 0; i < 4; i++) g[0][i] = state[i];
for (int i = 7, j = 0; j < 4; i--, j++) g[1][j] = state[i];
}
inline string get() {
string res;
for (int i = 0; i < 4; i++) res += g[0][i];
for (int i = 3; i >= 0; i--) res += g[1][i];
return res;
}
inline string move0(string state) {
set(state);
for (int i = 0; i < 4; i++) swap(g[0][i], g[1][i]);
return get();
}
inline string move1(string state) {
set(state);
int v0 = g[0][3], v1 = g[1][3];
for (int i = 3; i; i--)
g[0][i] = g[0][i - 1], g[1][i] = g[1][i - 1];
g[0][0] = v0, g[1][0] = v1;
return get();
}
inline string move2(string state) {
set(state);
int v = g[0][1];
g[0][1] = g[1][1], g[1][1] = g[1][2];
g[1][2] = g[0][2], g[0][2] = v;
return get();
}
inline int bfs(string start, string end) {
if (start == end) return 0;
dist[start] = 0;
queue<string> q; q.push(start);
while (q.size()) {
string t = q.front(); q.pop();
if (t == end) return dist[t];
string m[3];
m[0] = move0(t), m[1] = move1(t), m[2] = move2(t);
for (int i = 0; i < 3; i++)
if (!dist.count(m[i])) {
dist[m[i]] = dist[t] + 1;
pre[m[i]] = {'A' + i, t};
q.push(m[i]);
}
}
return -1;
}
int main() {
int x; string start, end;
for (int i = 0; i < 8; i++) {
scanf("%d", &x); end += char('0' + x);
}
for (int i = 1; i <= 8; i++) start += char('0' + i);
int step = bfs(start, end);
printf("%d\n", step);
string res;
while (end != start) res += pre[end].x, end = pre[end].y;
reverse(res.begin(), res.end());
if (step > 0) printf("%s\n", res.c_str());
return 0;
}
ACwing 175
题解参考
这种图中存在一些性质。
因为输入的的格子中的值,所以对应的是格子的坐标,而我们遍历的是定点,所以要找到定点与格子坐标的关系。代码中的 d x dx dx 与 i x ix ix 数组表示:
上图中表示出了定点坐标与格子坐标的对应关系,比如要从定点 ( 1 , 1 ) (1,1) (1,1) 走到 ( 0 , 2 ) (0,2) (0,2),就必须要经过格子 ( 0 , 1 ) (0,1) (0,1),所以定点 ( 1 , 1 ) (1,1) (1,1) 与路过的格子 ( 0 , 1 ) (0,1) (0,1) 的对应关系即为 ( − 1 , 0 ) (-1, 0) (−1,0),表示横坐标 − 1 -1 −1,纵坐标不变。
代码中的几点解释:
#include
#include
#include
#include
using namespace std;
#define x first
#define y second
typedef pair<int, int> PII;
const int N = 510;
int n, m;
char g[N][N];
int dist[N][N];
bool st[N][N];
char cs[] = "\\/\\/"; // 每个方向上的通路(注意转义字符):左上、右上、右下、左下
int dx[] = {-1, -1, 1, 1}, dy[] = {-1, 1, 1, -1};
int ix[] = {-1, -1, 0, 0}, iy[] = {-1, 0, 0, -1};
int bfs() {
memset(dist, 0x3f, sizeof dist); dist[0][0] = 0;
deque<PII> q; q.push_back({0, 0});
memset(st, 0, sizeof st);
while (q.size()) {
PII t = q.front(); q.pop_front();
if (st[t.x][t.y]) continue; st[t.x][t.y] = true;
for (int i = 0; i < 4; i++) {
int a = t.x + dx[i], b = t.y + dy[i];
if (a < 0 || a > n || b < 0 || b > m) continue;
int ca = t.x + ix[i], cb = t.y + iy[i];
int d = dist[t.x][t.y] + (g[ca][cb] != cs[i]);
if (d < dist[a][b]) {
dist[a][b] = d;
if (g[ca][cb] != cs[i]) q.push_back({a, b});
else q.push_front({a, b});
}
}
}
return dist[n][m];
}
int main() {
int T; scanf("%d", &T);
while (T--) {
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i++) scanf("%s", g[i]);
int t = bfs();
if (t == 0x3f3f3f3f) puts("NO SOLUTION");
else printf("%d\n", t);
}
return 0;
}
这个题目没有告诉输入中具体有多少个规则。
ACwing 190
对代码中的一点解释:
#include
#include
#include
#include
using namespace std;
const int N = 6; // 最多6个变换规则
int n;
string A, B;
string a[N], b[N]; // 变换规则
int extend(queue<string> &q, unordered_map<string, int> &da, unordered_map<string, int> &db,
string a[], string b[]) {
int d = da[q.front()];
while (q.size() && da[q.front()] == d) { // 每一次都扩展一层:即到起始节点距离都等于d的点
auto t = q.front(); q.pop();
for (int i = 0; i < n; i++) // 枚举扩展的规则
for (int j = 0; j < t.size(); j++) // 枚举t从哪个字母开始扩展
if (t.substr(j, a[i].size()) == a[i]) { // 是否匹配规则
string r = t.substr(0, j) + b[i] + t.substr(j + a[i].size()); // 使用规则变换
if (db.count(r)) return da[t] + 1 + db[r]; // 如果这个状态在另一个方向存在,则两个方向连通了
if (da.count(r)) continue; // 如果这个状态被扩展过了
da[r] = da[t] + 1;
q.push(r);
}
}
return 11; // 返回一个比10的的数即可
}
int bfs() {
if (A == B) return 0;
queue<string> qa, qb; qa.push(A), qb.push(B);
unordered_map<string, int> da, db; da[A] = db[B] = 0;
int step = 0;
while (qa.size() && qb.size()) { // 必须都有值,否则就两个状态会不连通
int t;
if (qa.size() < qb.size()) t = extend(qa, da, db, a, b); // 每次都往扩展出来规模更小的方向扩展
else t = extend(qb, db, da, b, a);
if (t <= 10) return t;
if (++step == 10) return -1;
}
return -1;
}
int main() {
cin >> A >> B;
while (cin >> a[n] >> b[n]) n++;
int t = bfs();
if (t == -1) puts("NO ANSWER!"); else cout << t << endl;
return 0;
}
无注释代码:
#include
#include
#include
#include
using namespace std;
const int N = 10;
int n;
string A, B;
string a[N], b[N];
inline int extend(queue<string> &q, unordered_map<string, int> &da, unordered_map<string, int> &db,
string a[], string b[]) {
int d = da[q.front()];
while (q.size() && da[q.front()] == d) {
string t = q.front(); q.pop();
for (int i = 0; i < n; i++)
for (int j = 0; j < t.size(); j++)
if (t.substr(j, a[i].size()) == a[i]) {
string r = t.substr(0, j) + b[i] + t.substr(j + a[i].size());
if (db.count(r)) return da[t] + db[r] + 1;
if (da.count(r)) continue;
da[r] = da[t] + 1;
q.push(r);
}
}
return 11;
}
inline int bfs() {
if (A == B) return 0;
queue<string> qa, qb; qa.push(A), qb.push(B);
unordered_map<string, int> da, db; da[A] = db[B] = 0;
int step = 0;
while (qa.size() && qb.size()) {
int t;
if (qa.size() < qb.size()) t = extend(qa, da, db, a, b);
else t = extend(qb, db, da, b, a);
if (t <= 10) return t;
if (++step == 10) return -1;
}
return -1;
}
int main() {
cin >> A >> B;
while (cin >> a[n] >> b[n]) n++;
int t = bfs();
if (t == -1) puts("NO ANSWER!"); else cout << t << endl;
return 0;
}
应用场景
过程
首先将 B F S BFS BFS 中的队列换成优先队列。队列中的每个状态需要存储:从起到走到当前点的真实距离 a a a 和 从当前点到终点的估价距离 b b b。优先队列中的排序是根据 a + b a + b a+b 的和来排序的。
while (!q.empty())
{
取优先队列队头t(小根堆)
当终点第一次出队的时候 break;
for t的所有邻边
将邻边入队
}
A*算法正确性的前提
假设当期状态为 s t a t e state state,从起点到当前状态的距离为 d ( s t a t e ) d(state) d(state),从当前状态到终点的距离为 g ( s t a t e ) g(state) g(state),从当前状态到终点的估价距离是 f ( s t a t e ) f(state) f(state)。要使 A ∗ A^* A∗ 算法正确就必须保证以下式子成立: d ( s t a t e ) + g ( s t a t e ) ≥ d ( s t a t e ) + f ( s t a t e ) d(state)+g(state) \ge d(state)+f(state) d(state)+g(state)≥d(state)+f(state)即 g ( s t a t e ) ≥ f ( s t a t e ) g(state) \ge f(state) g(state)≥f(state)
正确性证明:假设终点出队的时候不是最小值,设其到起点的距离距离为 d i s t dist dist,最优解的距离为 d i s t u dist_u distu,那么就一定有 d i s t > d i s t u dist > dist_u dist>distu。因为当前优先队列中一定会存在最优解中的某个点记为 u u u(比如起点一定是最优解中的点),那么从起点到 u u u 的距离加上从 u u u 到终点的估计距离 d ( u ) + f ( u ) d(u)+f(u) d(u)+f(u) ,就一定有 d ( u ) + f ( u ) ≤ d ( u ) + g ( u ) = d(u) + f(u) \le d(u) + g(u) = d(u)+f(u)≤d(u)+g(u)= 最优解 d i s t u dist_u distu。于是有 d i s t > d i s t u ≥ d ( u ) + f ( u ) dist > dist_u \ge d(u)+f(u) dist>distu≥d(u)+f(u),即优先队列中存在一个点 u u u 到起点的距离 d i s t u dist_u distu 比一个出队点到起点的距离 d i s t dist dist 更小,与优先队列的性质矛盾,因此在优先队列中,终点第一次出队时的距离一定是最小的(注意,不能保证除了终点以外其它点第一次出队时是最优解上的值)。
宽搜对比
ACwing 179
八数码有解的充要条件
将 3 × 3 3\times3 3×3 矩阵中的数据读入一个数组中,这组数据逆序对的数量是偶数,则一定有解;为奇数,则无解。
估价函数
当前状态中每个数与它的目标位置的曼哈顿距离之和:当前位置和目标位置横坐标和纵坐标之差的和。
#include
#include
#include
#include
using namespace std;
// 求每个位置与其目标位置的曼哈顿距离
int f(string state) {
int res = 0;
for (int i = 0; i < state.size(); i++)
if (state[i] != 'x') {
int t = state[i] - '1';
res += abs(i / 3 - t / 3) + abs(i % 3 - t % 3);
}
return res;
}
string bfs(string start) {
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
char op[4] = {'u', 'r', 'd', 'l'};
string end = "12345678x";
unordered_map<string, int> dist; // 记录距离
unordered_map<string, pair<string, char>> prev; // 前驱状态以及转移到当前状态的操作
priority_queue<pair<int, string>, vector<pair<int, string>>, greater<pair<int, string>>> heap; // 存储估计距离,状态
heap.push({f(start), start});
dist[start] = 0;
while (heap.size()) {
auto t = heap.top();
heap.pop();
string state = t.second;
if (state == end) break;
int step = dist[state];
int x, y;
for (int i = 0; i < state.size(); i++) // 找到t中x的坐标
if (state[i] == 'x') {
x = i / 3, y = i % 3;
break;
}
string source = state; // 方便恢复现场
for (int i = 0; i < 4; i++) {
int a = x + dx[i], b = y + dy[i];
if (a >= 0 && a < 3 && b >= 0 && b < 3) {
swap(state[x * 3 + y], state[a * 3 + b]);
if (!dist.count(state) || dist[state] > step + 1) { // 未被扩展过或者当前距离比较大
dist[state] = step + 1;
prev[state] = {source, op[i]};
heap.push({dist[state] + f(state), state});
}
swap(state[x * 3 + y], state[a * 3 + b]);
}
}
}
string res; // 存储方案
while (end != start) {
res += prev[end].second;
end = prev[end].first;
}
reverse(res.begin(), res.end());
return res;
}
int main() {
string g, c, seq; // seq存储输入的数字,方便求逆序对
while (cin >> c) {
g += c;
if (c != "x") seq += c;
}
int t = 0;
for (int i = 0; i < seq.size(); i++) // 求逆序对数量
for (int j = i + 1; j < seq.size(); j++)
if (seq[i] > seq[j])
t++;
if (t % 2) puts("unsolvable");
else cout << bfs(g) << endl;
return 0;
}
ACwing 178
估价函数
每个点到终点的最小距离
第k短路求法
由 A ∗ A^* A∗算法的性质从优先队列中,终点第一次出队时的距离一定是最小的,那么终点从优先队列第二次出队的时候,就是第二小的,…,从终点优先队列第 k k k 次出队的时候,就是第 k k k 小的。
正确性证明:由之前的证明可知 d i s t > d i s t u ≥ d ( u ) + f ( u ) dist > dist_u \ge d(u)+f(u) dist>distu≥d(u)+f(u),同理,假设第二次从优先队列中出队的值不是第二小的值, d i s t 2 dist_2 dist2 表示第二小的值, v v v 表示第二最短路上的某一个点,那么就有 d i s t 2 > d i s t 2 ≥ d ( v ) + f ( v ) dist_2 > dist_2 \ge d(v)+f(v) dist2>dist2≥d(v)+f(v),所以优先队列中的值 d ( v ) + f ( v ) d(v)+f(v) d(v)+f(v) 小于了出队的值 d i s t 2 dist_2 dist2,矛盾。因此第二次从优先队列中出队的值是第二小的值,对第 k k k 次出队的值同理可证:第 k k k 次优先队列中出队的值是第 k k k 小的值。
本题的估价距离使用Dijkstra逆向求解每个点到终点的距离,所以有邻接表表头rh[N]
。
#include
#include
#include
#include
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
typedef pair<int, PII> PIII;
const int N = 1010, M = 200010;
int n, m, S, T, K;
int h[N], rh[N], e[M], w[M], ne[M], idx; // rh表示方向遍历的表头
int dist[N], cnt[N];
bool st[N];
void add(int h[], int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
// 处理估价函数:Dijkstra倒序扩展
void dijkstra() {
priority_queue<PII, vector<PII>, greater<PII>> heap;
heap.push({0, T});
memset(dist, 0x3f, sizeof dist);
dist[T] = 0;
while (heap.size()) {
auto t = heap.top();
heap.pop();
int ver = t.y;
if (st[ver]) continue; // 之前被遍历过
st[ver] = true;
for (int i = rh[ver]; ~i; i = ne[i]) {
int j = e[i];
if (dist[j] > dist[ver] + w[i]) {
dist[j] = dist[ver] + w[i];
heap.push({dist[j], j});
}
}
}
}
int astar() {
priority_queue<PIII, vector<PIII>, greater<PIII>> heap; // 起点估价值,真实值,编号
heap.push({dist[S], {0, S}});
while (heap.size()) {
auto t = heap.top();
heap.pop();
int ver = t.y.y, distance = t.y.x;
cnt[ver]++; //如果终点已经被访问过k次了 则此时的ver就是终点T 返回答案
if (cnt[T] == K) return distance;
for (int i = h[ver]; ~i; i = ne[i]) {
int j = e[i];
/*
如果走到一个中间点都cnt[j]>=K,则说明j已经出队k次了,且astar()并没有return distance,
说明从j出发找不到第k短路(让终点出队k次),
即继续让j入队的话依然无解,
那么就没必要让j继续入队了
*/
if (cnt[j] < K) heap.push({distance + w[i] + dist[j], {distance + w[i], j}});
}
}
return -1;
}
int main() {
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
memset(rh, -1, sizeof rh);
for (int i = 0; i < m; i++) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(h, a, b, c); // 建立正边
add(rh, b, a, c); // 建立反边
}
scanf("%d%d%d", &S, &T, &K);
// 起点==终点时, 则 d[S→S]=0 这种情况就要舍去, 总共第 K 大变为总共第 K+1 大
if (S == T) K++;
dijkstra(); // 先做一遍dijkstra
printf("%d\n", astar());
return 0;
}
ACwing 3402
清华研究生复试上机题
// 一个等差数列只需要知道两项就可以补全所有位置
#include
#include
using namespace std;
#define x first
#define y second
typedef pair<int, int> PII;
const int N = 1010, M = N * 2;
int n, m; // 行、列
int row[N], col[N]; // 行、列中元素个数
int q[M], hh, tt = -1; // 队列中0~n表示行,n~n+m-1表示列
bool st[M]; // 判重,每行、列更新一次后就全部填满,以后就不需要更新
PII ans[N * N]; // 结果
int top; // 结果数量
int g[N][N]; // 矩阵
int main() {
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++) {
scanf("%d", &g[i][j]);
if (g[i][j]) row[i]++, col[j]++;
}
for (int i = 0; i < n; i++) // 枚举所有行
if (row[i] >= 2 && row[i] < m) { // 如果该行元素个数为m,就不用更新
q[++tt] = i;
st[i] = true;
}
for (int i = 0; i < m; i++) // 枚举所有列
if (col[i] >= 2 && col[i] < n) {
q[++tt] = i + n;
st[i + n] = true;
}
while (hh <= tt) {
auto t = q[hh++];
if (t < n) { // 行
PII p[2];
int cnt = 0;
for (int i = 0; i < m; i++)
if (g[t][i]) {
p[cnt++] = {i, g[t][i]};
if (cnt == 2) break;
}
int d = (p[1].y - p[0].y) / (p[1].x - p[0].x); // 公差
int a = p[1].y - d * p[1].x; // 首项
for (int i = 0; i < m; i++) // 将该行所有项填满
if (!g[t][i]) {
g[t][i] = a + d * i;
ans[top++] = {t, i};
col[i]++;
if (col[i] >= 2 && col[i] < m && !st[i + n]) // 判断该元素所在的列是否需要更新
q[++tt] = i + n, st[i + n] = true;
}
} else { // 列
t -= n;
PII p[2];
int cnt = 0;
for (int i = 0; i < n; i++)
if (g[i][t]) {
p[cnt++] = {i, g[i][t]};
if (cnt == 2) break;
}
int d = (p[1].y - p[0].y) / (p[1].x - p[0].x);
int a = p[1].y - d * p[1].x;
for (int i = 0; i < n; i++)
if (!g[i][t]) {
g[i][t] = a + d * i;
ans[top++] = {i, t};
row[i]++;
if (row[i] >= 2 && row[i] < n && !st[i])
q[++tt] = i, st[i] = true;
}
}
}
sort(ans, ans + top);
for (int i = 0; i < top; i++) {
auto &p = ans[i];
printf("%d %d %d\n", p.x + 1, p.y + 1, g[p.x][p.y]);
}
return 0;
}