Flood Fill
池塘计数
#include <iostream>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 1e3 + 10;
const int M = 1e3 + 10;
int n, m;
char g[N][M];
PII q[N * M];
bool st[N][M];
void bfs(int sx, int sy)
{
st[sx][sy] = true;// 已经遍历过的,就标记一下
int hh = 0, tt = 0;
q[hh] = {sx, sy};
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 == sx && j == sy)
continue;
if (i < 0 || i >= n || j < 0 || j >= m)
continue;
if (g[i][j] == '.' || st[i][j])
continue;
st[i][j] = true;// 已经遍历过的,就标记一下
q[++tt] = {i, j};
}
}
}
int main()
{
scanf("%d %d", &n, &m);
for (int i = 0; i < n; i++)
scanf("%s", g[i]);
int cnt = 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);
cnt++;
}
// 输出结果
printf("%d\n", cnt);
return 0;
}
城堡问题
#include <iostream>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 50 + 10;
const int M = 50 + 10;
int n, m;
int g[N][M];
PII q[N * M];
bool st[N][M];
int bfs(int sx, int sy)
{
st[sx][sy] = true;// 已经遍历过的,就标记一下
int hh = 0, tt = 0;
q[0] = {sx, sy};
int area = 0;
int dx[4] = {0, -1, 0, 1}, dy[4] = {-1, 0, 1, 0};
while (hh <= tt)
{
PII 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])
continue;
if (g[t.x][t.y] >> i & 1)// 需要判断一下该方向是否有墙壁堵着
continue;
st[a][b] = true;// 已经遍历过的,就标记一下
q[++tt] = {a, b};
}
}
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;
int 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;
}
山峰和山谷
#include <iostream>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 1e3 + 10;
int n;
int h[N][N];
PII q[N * N];
bool st[N][N];
void bfs(int sx, int sy, bool &has_higher, bool &has_lower)// 这里使用了引用类型的入参
{
st[sx][sy] = true;// 已经遍历过的,就标记一下
int hh = 0, tt = 0;
q[0] = {sx, sy};
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 >= 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])
{
st[i][j] = true;// 已经遍历过的,就标记一下
q[++tt] = {i, j};
}
}
}
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
scanf("%d\n", &h[i][j]);
int peak = 0;
int 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;
}
最短路模型
迷宫问题
#include <iostream>
#include <cstring>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 1e3 + 10;
int n;
int g[N][N];
PII q[N * N];
PII pre[N][N];
void bfs(int sx, int sy)
{
int hh = 0, tt = 0;
q[0] = {sx, sy};
int dx[4] = {0, 1, 0, -1}, dy[4] = {1, 0, -1, 0};
memset(pre, -1, sizeof pre);
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 >= n)
continue;
if (g[a][b])// 判断是否为墙壁
continue;
if (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);// 从迷宫的右下角开始进行bfs操作
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;
}
武士风度的牛
#include <iostream>
#include <cstring>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 150 + 10;
const int M = 150 + 10;
int n, m;
char g[N][M];
PII q[N * M];
int dist[N][M];
int bfs()
{
// 先找到起点的坐标
int sx, sy;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
if (g[i][j] == 'K')
sx = i, sy = j;
int hh = 0, tt = 0;
q[0] = {sx, sy};
int dx[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
int dy[8] = {1, 2, 2, 1, -1, -2, -2, -1};
memset(dist, -1, sizeof dist);
dist[sx][sy] = 0;
while (hh <= tt)
{
PII 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] == '*')
continue;
if (g[a][b] == 'H')// 题目的数据保证一定有解
return dist[t.x][t.y] + 1;
if (dist[a][b] != -1)// 判断是否已经被搜到过
continue;
dist[a][b] = dist[t.x][t.y] + 1;
q[++tt] = {a, b};
}
}
}
int main()
{
scanf("%d %d", &m, &n);
for (int i = 0; i < n; i++)
scanf("%s", &g[i]);
printf("%d\n", bfs());
return 0;
}
抓住那头牛
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e5 + 10;
int n, k;
int q[N];
int dist[N];
int bfs()
{
int hh = 0, tt = 0;
q[0] = n;
memset(dist, -1, sizeof dist);
dist[n] = 0;
while (hh <= tt)
{
int t = q[hh++];
if (t == k)
return dist[t];
if (t - 1 >= 0 && dist[t - 1] == -1)
{
dist[t - 1] = dist[t] + 1;
q[++tt] = t - 1;
}
if (t + 1 <= k && dist[t + 1] == -1)
{
dist[t + 1] = dist[t] + 1;
q[++tt] = t + 1;
}
// 只要t*2大于k+1,我们就可以先减再乘
if (t * 2 <= k + 1 && dist[t * 2] == -1)
{
dist[t * 2] = dist[t] + 1;
q[++tt] = t * 2;
}
}
}
int main()
{
scanf("%d %d", &n, &k);
printf("%d\n", bfs());
return 0;
}
多源BFS
矩阵距离
#include <iostream>
#include <cstring>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 1e3 + 10;
const int M = 1e3 + 10;
int n, m;
char g[N][M];// 因为数字之间没有空格,所以用char类型读入
PII q[N * M];
int dist[N][M];
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};
}
int dx[4] = {0, 1, 0, -1};
int dy[4] = {1, 0, -1, 0};
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++)
// 因为数字之间没有空格,所以用char类型读入
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;
}
最小步数模型
魔板
#include <iostream>
#include <algorithm>
#include <unordered_map>
#include <queue>
using namespace std;
char g[2][4];
unordered_map<string, int> dist;
unordered_map<string, pair<char, string>> pre;
void set(string state)// 由当前字符串得到对应的魔板
{
for (int j = 0; j < 4; j++)
g[0][j] = state[j];
for (int j = 0, i = 7; j < 4; j++, i--)
g[1][j] = state[i];
}
string get()// 由当前魔板得到对应的字符串
{
string res = "";
for (int j = 0; j < 4; j++)
res += g[0][j];
for (int j = 3; j >= 0; j--)
res += g[1][j];
return res;
}
string move0(string state)// 执行操作A,交换上下两行
{
// 由当前字符串得到对应的魔板
set(state);
// 操作魔板
for (int j = 0; j < 4; j++)
swap(g[0][j], g[1][j]);
// 由当前魔板得到对应的字符串
return get();
}
string move1(string state)// 执行操作B,将最右边的一列插入到最左边
{
// 由当前字符串得到对应的魔板
set(state);
// 操作魔板
char t1 = g[0][3], t2 = g[1][3];
for (int i = 0; i < 2; i++)
for (int j = 3; j >= 1; j--)
g[i][j] = g[i][j - 1];
g[0][0] = t1, g[1][0] = t2;
// 由当前魔板得到对应的字符串
return get();
}
string move2(string state)// 执行操作C,魔板中央对的4个数作顺时针旋转
{
// 由当前字符串得到对应的魔板
set(state);
// 操作魔板
char t = g[0][1];
g[0][1] = g[1][1];
g[1][1] = g[1][2];
g[1][2] = g[0][2];
g[0][2] = t;
// 由当前魔板得到对应的字符串
return get();
}
int bfs(string start, string end)
{
if (start == end)
return 0;
queue<string> q;
q.push(start);
dist[start] = 0;
while (!q.empty())
{
string t = q.front();
q.pop();
string m[3];
m[0] = move0(t);// 执行操作A,交换上下两行
m[1] = move1(t);// 执行操作B,将最右边的一列插入到最左边
m[2] = move2(t);// 执行操作C,魔板中央对的4个数作顺时针旋转
for (int i = 0; i < 3; i++)
if (!dist.count(m[i]))
{
dist[m[i]] = dist[t] + 1;
pre[m[i]] = {char(i + 'A'), t};
if (m[i] == end)
return dist[m[i]];
q.push(m[i]);
}
}
}
int main()
{
string start = "12345678";
string end = "";
for (int i = 0; i < 8; i++)
{
int x;
scanf("%d", &x);
end += char(x + '0');
}
int t = bfs(start, end);
printf("%d\n", t);
if (t > 0)// 如果操作序列的长度大于0,则在第二行输出字典序最小的操作序列
{
string op = "";
while (end != start)// 从特殊状态往前追溯至基本状态
{
op += pre[end].first;
end = pre[end].second;
}
// 记得要反转一下,这样才是从基本状态到特殊状态的操作序列
reverse(op.begin(), op.end());
printf("%s\n", op.c_str());// 输出字典序最小的操作序列
}
return 0;
}
双端队列广搜
电路维修
#include <iostream>
#include <cstring>
#include <deque>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 500 + 10;
const int M = 500 + 10;
int n, m;
char g[N][M];
int dist[N][M];
bool st[N][M];
int bfs()// 这里的bfs思路,本质上就是Dijkstra思路
{
memset(st, false, sizeof st);
memset(dist, 0x3f, sizeof dist);
dist[0][0] = 0;
deque<PII> q;// 双端队列
q.push_front({0, 0});
int dx[4] = {-1, -1, 1, 1}, dy[4] = {-1, 1, 1, -1};
int ix[4] = {-1, -1, 0, 0}, iy[4] = {-1, 0, 0, -1};
// 根据四个方向偏移,所能走到的四种电路的形状,分别为:左上\,右上/,右下\,左下/
char cs[4] = {'\\', '/', '\\', '/'};
while (!q.empty())
{
PII t = q.front();
q.pop_front();
if (t.x == n && t.y == m)
return dist[t.x][t.y];
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 ia = t.x + ix[i], ib = t.y + iy[i];
int d = dist[t.x][t.y] + (g[ia][ib] != cs[i]);
if (d < dist[a][b] && dist[a][b] == 0x3f3f3f3f)
{
dist[a][b] = d;
if (g[ia][ib] != cs[i])
q.push_back({a, b});
else
q.push_front({a, b});
}
if (d < dist[a][b] && dist[a][b] != 0x3f3f3f3f)
{
dist[a][b] = d;
q.push_front({a, b});
}
}
}
}
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]);
if ((n + m) & 1)// 通过观察可以知道,当终点的横纵坐标的和为奇数时,则起点永远无法连通终点
printf("NO SOLUTION\n");
else
printf("%d\n", bfs());
}
return 0;
}
双向广搜
字串变换
#include <iostream>
#include <queue>
#include <unordered_map>
using namespace std;
const int N = 6;
string a, b;
int n;
string oa[N], ob[N];
int extend(
queue<string>& qa,
unordered_map<string, int>& da,
unordered_map<string, int>& db,
string oa[],
string ob[]
)
{
int d = da[qa.front()];
while (qa.size() && da[qa.front()] == d)// 扩展完整一层
{
string t = qa.front();
qa.pop();
for (int i = 0; i < n; i++)// 枚举规则
for (int j = 0; j < t.size(); j++)// 枚举当前字符串的每个位置
// 判断当前字符串的当前位置是否符合当前规则进行变换
if (t.substr(j, oa[i].size()) == oa[i])
{
// 变换后的字符串
string u = t.substr(0, j) + ob[i] + t.substr(j + oa[i].size());
if (da.count(u))// 判断从出发的这一边是否已经搜索过
continue;
if (db.count(u))// 如果在另一边已经搜索过,则表示从出发的这一边可以变换到另一边
return da[t] + db[u] + 1;
qa.push(u);
da[u] = da[t] + 1;
}
}
// 当扩展完整一层后,还没有找到结果的话,则继续操作下一层
return 11;
}
int bfs()// 双向bfs
{
if (a == b)
return 0;
queue<string> qa, qb;
qa.push(a), qb.push(b);
unordered_map<string, int> da, db;
da[a] = 0, db[b] = 0;
int step = 0;// 记录变换步数
while (qa.size() && qb.size())
{
int t;
// 每次扩展时,选择队列中数量较少的一边来进行扩展,每次每边扩展完整一层
if (qa.size() <= qb.size())
t = extend(qa, da, db, oa, ob);
else
t = extend(qb, db, da, ob, oa);
if (t <= 10)
return t;
step++;
// 当变换步数大于等于10步时,还没有找到结果的话,则可以结束循环了
if (step >= 10)
break;
}
return -1;
}
int main()
{
cin >> a >> b;
while (cin >> oa[n] >> ob[n])// 输入规则
n++;
int t = bfs();// 双向bfs
if (t == -1)
printf("NO ANSWER!\n");
else
printf("%d\n", t);
return 0;
}
A*
八数码
#include <iostream>
#include <unordered_map>
#include <queue>
#include <algorithm>
#define x first
#define y second
using namespace std;
typedef pair<string, char> PSC;
typedef pair<int, string> PIS;
// 预估代价函数
int f(string state)
{
int cnt = 0;
for (int i = 0; i < state.size(); i++)
if (state[i] != 'x')
{
int t = state[i] - '1';
cnt += abs(i / 3 - t / 3) + abs(i % 3 - t % 3);
}
// 返回估计距离,值为当前状态中的,每个数的位置与它对应的目标位置的曼哈顿距离,的总和
return cnt;
}
string bfs(string start)
{
string end = "12345678x";
unordered_map<string, int> dist;
unordered_map<string, PSC> prev;
priority_queue<PIS, vector<PIS>, greater<PIS>> q;// 优先队列小根堆
dist[start] = 0;
/*
优先队列小根堆中每个状态的权重是:
当前代价 + 预估代价
(其中,
当前代价为从起点到该状态的真实距离(注意并不一定是最短距离),
预估代价为从该状态到终点的估计距离,估计距离一般通过预估代价函数得到)
*/
q.push({0 + f(start), start});
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
char op[4] = {'u', 'r', 'd', 'l'};
while (!q.empty())
{
PIS t = q.top();
q.pop();
string state = t.y;
// 当终点首次弹出时,则一定是最优解,然后退出循环即可
if (state == end)
break;
int x, y;
for (int i = 0; i < state.size(); i++)
if (state[i] == 'x')
{
x = i / 3;
y = i % 3;
}
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]);
// 注意,在A*算法中,当第一次搜到某个点的时候,距离不一定是最短的,其dist[]的值是不断变小的,所以要加后一个判断
if (!dist.count(state) || dist[state] > dist[source] + 1)
{
// 更新当前状态的当前代价
dist[state] = dist[source] + 1;
// 更新优先队列小根堆
q.push({dist[state] + f(state), state});
// 记录当前状态是从哪个状态走过来的,且对应哪个操作
prev[state] = {source, op[i]};
}
// 状态恢复,继续遍历当前这个点(x,y)可以往哪个方向走
swap(state[x * 3 + y], state[a * 3 + b]);
}
}
}
// 得到操作方案
string res;
while (end != start)
{
res += prev[end].y;
end = prev[end].x;
}
reverse(res.begin(), res.end());// 记得要反转一下
return res;
}
int main()
{
string g, c, 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)
// 在八数码问题中,逆序对的数量为奇数时,则八数码问题没有解
printf("unsolvable\n");
else
printf("%s\n", bfs(g).c_str());
return 0;
}
第K短路
#include <iostream>
#include <cstring>
#include <queue>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
typedef pair<int, PII> PIPII;
const int N = 1e3 + 10;
const int M = 2 * 1e4 + 10;// 因为要反向建图,所以用到的边数需要乘2
int n, m;
int h[N], rh[N], w[M], e[M], ne[M], idx;
int s, t, k;
int dist[N];
int cnt[N];
void add(int temp[], int a, int b, int c)
{
w[idx] = c;
e[idx] = b;
ne[idx] = temp[a];
temp[a] = idx++;
}
// 预估代价函数
void dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[t] = 0;
priority_queue<PII, vector<PII>, greater<PII>> q;
q.push({0, t});
while (!q.empty())
{
PII temp = q.top();
q.pop();
int ver = temp.y;
int distance = temp.x;
for (int i = rh[ver]; ~i; i = ne[i])
{
int j = e[i];
if (dist[j] > distance + w[i])
{
dist[j] = distance + w[i];
q.push({dist[j], j});
}
}
}
}
int astar()
{
priority_queue<PIPII, vector<PIPII>, greater<PIPII>> q;// 优先队列小根堆
/*
优先队列小根堆中每个状态的权重是:
当前代价 + 预估代价
(其中,
当前代价为从起点到该状态的真实距离(注意并不一定是最短距离),
预估代价为从该状态到终点的估计距离,估计距离一般通过预估代价函数得到)
*/
q.push({0 + dist[s], {0, s}});
while (!q.empty())
{
PIPII temp = q.top();
q.pop();
int ver = temp.y.y;
cnt[ver]++;// 计算当前点第几次弹出优先队列小根堆
if (ver == t && cnt[ver] == k)
return temp.y.x;
for (int i = h[ver]; ~i; i = ne[i])
{
int j = e[i];
/*
可以对最短路径的搜索进行优化,
每个点第几次弹出优先队列小根堆就是第几短路径,
当某个点第k次弹出优先队列小根堆,准备要进行第k+1次进入优先队列小根堆时,
说明该点到终点的路径一定不会是前k短路径。
这个优化,可以把起点和终点不连通的情况也考虑到,防止TLE(Time Limit Exceeded)
*/
if (cnt[j] < k)
q.push({temp.y.x + w[i] + dist[j], {temp.y.x + 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);// 反向建图的目的是为了反向做一次dijkstra操作,充当了预估代价函数的作用
}
scanf("%d %d %d", &s, &t, &k);
/*
当起点和终点相同时,
k中隐含了一条长度为0的没有边的最短路,
但这条最短路是不对的,
因为题目要求起点和终点之间至少包含一条边,
所以k++,是为了排除掉长度为0的最短路
*/
if (s == t)
k++;
dijkstra();
printf("%d\n", astar());
return 0;
}
知识点补充
- 在Dijkstra算法中,当某个点若干次被搜索到的时候,该点的最短路还没有被最终确定,只有当该点从优先队列小根堆中第一次被弹出时,该点的距离才是被最终确定的最短路的距离
- 在BFS算法中,当某个点第一次被搜索到的时候,该点的最短路就已经被最终确定。BFS算法本质上是Dijkstra算法的特殊情况
- 在A*算法中,当某个点若干次被搜索到的时候,该点的最短路还没有被最终确定,只有当该点从优先队列小根堆中第一次被弹出时,该点的距离才是被最终确定的最短路的距离(其中,闫学灿的观点认为,只有该点为终点时才成立。与《算法竞赛进阶指南》书中的观点不一致)。A*算法的思路就是Dijkstra算法的思路,区别在于优先队列小根堆中的权重是,当前代价与预估代价,的总和
DFS之连通性模型
迷宫
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100 + 10;
int n;
char g[N][N];
int xa, ya, xb, yb;
bool st[N][N];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
bool dfs(int x, int y)
{
if (g[x][y] == '#')
return false;
// 判断是否找到终点
if (x == xb && y == yb)
return true;
st[x][y] = true;
for (int i = 0; i < 4; i++)
{
int a = x + dx[i], b = y + dy[i];
if (a < 0 || a >= n || b < 0 || b >= n)
continue;
if (st[a][b])
continue;
if (dfs(a, b))
return true;
}
return false;
}
int main()
{
int t;
scanf("%d", &t);
while (t--)
{
scanf("%d", &n);
for (int i = 0; i < n; i++)
scanf("%s", g[i]);
scanf("%d %d %d %d", &xa, &ya, &xb, &yb);
memset(st, false, sizeof st);
printf("%s\n", dfs(xa, ya) ? "YES" : "NO");
}
return 0;
}
红与黑
#include <iostream>
#include <cstring>
using namespace std;
const int N = 20 + 10;
const int M = 20 + 10;
int n, m;
char g[N][M];
bool st[N][M];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
int dfs(int x, int y)
{
int cnt = 1;
st[x][y] = true;
for (int i = 0; i < 4; i++)
{
int a = x + dx[i], b = y + dy[i];
if (a < 0 || a >= n || b < 0 || b >= m)
continue;
if (g[a][b] == '#')
continue;
if (st[a][b])
continue;
cnt += dfs(a, b);
}
return cnt;
}
int main()
{
while (cin >> m >> n, n || m)
{
for (int i = 0; i < n; i++)
scanf("%s", g[i]);
int a, b;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
if (g[i][j] == '@')
a = i, b = j;
memset(st, false, sizeof st);
printf("%d\n", dfs(a, b));
}
return 0;
}
DFS之搜索顺序
马走日
#include <iostream>
using namespace std;
const int N = 10;
const int M = 10;
int n, m;
int x, y;
int ans;
bool st[N][M];
int dx[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
int dy[8] = {1, 2, 2, 1, -1, -2, -2, -1};
void dfs(int x, int y, int cnt)
{
if (cnt == n * m)
{
ans++;
return ;
}
st[x][y] = true;
for (int i = 0; i < 8; i++)
{
int a = x + dx[i], b = y + dy[i];
if (a < 0 || a >= n || b < 0 || b >= m)
continue;
if (st[a][b])
continue;
dfs(a, b, cnt + 1);
}
st[x][y] = false;// 恢复现场
}
int main()
{
int t;
scanf("%d", &t);
while (t--)
{
scanf("%d %d %d %d", &n, &m, &x, &y);
ans = 0;
dfs(x, y, 1);
printf("%d\n", ans);
}
return 0;
}
单词接龙
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 20;
int n;
string word[N];
int g[N][N];
int ans;
int used[N];
void dfs(string dragon, int before)
{
/*
在C++中,string类型的size()函数的返回值是一个无符号类型的值
在C++中,max()函数的两个入参的类型必须要保持一致
*/
ans = max(ans, (int)dragon.size());
used[before]++;
for (int j = 0; j < n; j++)
if (g[before][j] && used[j] < 2)
dfs(dragon + word[j].substr(g[before][j]), j);
used[before]--;// 恢复现场
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i++)
cin >> word[i];
char start;
cin >> start;
// 预处理
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
{
string a = word[i];
string b = word[j];
// 我们可以任意选择重合部分的长度,但其长度必须大于等于1,且严格小于两个串的长度
for (int k = 1; k < min(a.size(), b.size()); k++)
if (a.substr(a.size() - k, k) == b.substr(0, k))
{
g[i][j] = k;
break;
}
}
for (int i = 0; i < n; i++)
if (word[i][0] == start)
dfs(word[i], i);
printf("%d\n", ans);
return 0;
}
分成互质组
#include <iostream>
using namespace std;
const int N = 10;
int n;
int a[N];
int ans = N;// 最坏情况下是,每个数单独一个组
bool st[N];
int group[N][N];
// 欧几里得算法求两个数的最大公约数
int gcd(int a, int b)
{
return b ? gcd(b, a % b) : a;
}
bool check(int group[], int gc, int b)
{
for (int i = 0; i < gc; i++)
// 如果两个数的最大公约数大于1,则两个数不互质
if (gcd(a[group[i]], b) > 1)
return false;
return true;
}
/*
四个参数的含义分别为:
当前搜索到第几组、
当前组中一共有几个数、
一共搜索过几个数、
从第几个数(具体数值为a数组的下标)开始进行搜索
*/
void dfs(int g, int gc, int tc, int start)// 这个start可以减掉很多组内的等效冗余的方案
{
if (g >= ans)
return ;
if (tc == n)
ans = g;
bool flag = true;
for (int i = start; i < n; i++)
// 判断当前a[i]这个数是否被用过,并且,当前a[i]这个数,是否和当前组g中的一共gc个数都互质
if (!st[i] && check(group[g], gc, a[i]))
{
st[i] = true;
// 把当前a[i]这个数加入到当前组g中,然后继续进行dfs操作。这个操作可以不恢复现场
group[g][gc] = i;
dfs(g, gc + 1, tc + 1, i + 1);
st[i] = false;// 恢复现场
// 如果至少有一个数可以放入到当前组中,则不需要新开一个新的组来进行dfs操作
flag = false;
/*
剪枝操作,
因为开一个新组的前提条件是每个数都不能放到旧组中了,
所以剩下的第一个数一定会放到某个新组中,那我们可以将它固定到第一个新组中,
这样可以减掉很多组与组之间的等效冗余的方案
*/
if (gc == 0)
return ;
}
// 如果至少有一个数可以放入到当前组中,则不需要新开一个新的组来进行dfs操作
if (flag)
dfs(g + 1, 0, tc, 0);
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i++)
scanf("%d", &a[i]);
// 这个dfs的搜索顺序本质上是,按照每个组的每个位置进行搜索的!!!
dfs(1, 0, 0, 0);
printf("%d\n", ans);
return 0;
}
DFS之剪枝与优化
小猫爬山
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 18;
int n, m;
int w[N];
int ans = N;// 最坏情况下是,每只猫单独一辆缆车
int sum[N];
void dfs(int u, int k)
{
if (k >= ans)// 最优性剪枝
return ;
if (u == n)
ans = k;
for (int i = 0; i < k; i++)
if (sum[i] + w[u] <= m)// 可行性剪枝
{
sum[i] += w[u];
dfs(u + 1, k);
sum[i] -= w[u];// 恢复现场
}
/*
这里并不能使用贪心,举出例子:
请尝试在maxV=16时,重量为9 5 5 5 4 3
贪心结果是9+5 5+5+4 3,结果为3
但正确结果为9+4+3 5+5+5,结果为2
*/
// 新开一辆缆车
sum[k] = w[u];
dfs(u + 1, k + 1);
sum[k] = w[u];// 恢复现场,这个现场不恢复也不影响最终答案
}
int main()
{
scanf("%d %d", &n, &m);
for (int i = 0; i < n; i++)
scanf("%d", &w[i]);
// 优化搜索顺序
sort(w, w + n);
reverse(w, w + n);
// 这个dfs的搜索顺序本质上是,按照每个小猫能放到哪个缆车上进行搜索的!!!
dfs(0, 0);
printf("%d\n", ans);
return 0;
}
数独
#include <iostream>
using namespace std;
const int N = 9;
char str[100];
int row[N], col[N], cell[3][3];
int ones[1 << N];
int map[1 << N];
// 初始化处理,先默认每一行、每一列、每一个3×3的九宫格都可以不重复地填数字1~9
void init()
{
for (int i = 0; i < N; i++)
row[i] = col[i] = (1 << N) - 1;
// 一共有9个3x3的九宫格
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
cell[i][j] = (1 << N) - 1;
}
// 处理第x行、第y列、(x,y)所属的3×3的九宫格,可行性剪枝
void draw(int x, int y, int t, int flag)
{
if (flag)
str[x * N + y] = '1' + t;
else
str[x * N + y] = '.';
int v = 1 << t;
if (!flag)
v = 0 - v;
// 可行性剪枝
row[x] -= v;
col[y] -= v;
cell[x / 3][y / 3] -= v;
}
int get(int x, int y)// 位运算优化
{
return row[x] & col[y] & cell[x / 3][y / 3];
}
int lowbit(int x)// 位运算优化
{
return x & (~x + 1);
}
bool dfs(int cnt)
{
if (!cnt)
return true;
// 优化搜索顺序,让dfs的搜索顺序从,可以选择填写的数字个数较少的位置,开始进行搜索
int minv = 10;
int x, y;
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
if (str[i * N + j] == '.')
{
int state = get(i, j);
if (ones[state] < minv)
{
minv = ones[state];
x = i, y = j;
}
}
// 优化搜索顺序,让dfs的搜索顺序从,可以选择填写的数字的个数较少的位置,开始进行搜索
int state = get(x, y);
for (int i = state; i; i -= lowbit(i))
{
int t = map[lowbit(i)];
draw(x, y, t, true);
if (dfs(cnt - 1))
return true;
draw(x, y, t, false);// 恢复现场
}
// 当state等于0时,说明已经出现了,存在有一个位置无法填写数字,的情况,即可返回false
return false;
}
int main()
{
// 位运算优化
for (int i = 0; i < (1 << N); i++)
for (int j = 0; j < N; j++)
ones[i] += (i >> j) & 1;
for (int i = 0; i < N; i++)
map[1 << i] = i;
while (cin >> str, str[0] != 'e')
{
init();// 初始化处理,先默认每一行、每一列、每一个3×3的九宫格都可以不重复地填数字1~9
int cnt = 0;
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
if (str[i * N + j] != '.')
{
int t = str[i * N + j] - '1';
// 处理第i行、第j列、(i,j)所属的3×3的九宫格,可行性剪枝
draw(i, j, t, true);
}
else
cnt++;
if (dfs(cnt))// 题目保证输入中的每个谜题都只有一个解决方案
cout << str << endl;
}
return 0;
}
木棒
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 70;
int n;
int w[N];
int sum;
int length;
bool st[N];
/*
三个参数的含义分别为:
一共搜索过几个木棒、
当前木棒的长度、
从第几个木棍(具体数值为w数组的下标)开始进行搜索
*/
bool dfs(int u, int ul, int start)// 这个start可以减掉很多组内的等效冗余的方案,这是一个优化
{
if (u * length == sum)
return true;
if (ul == length)
return dfs(u + 1, 0, 0);
for (int i = start; i < n; i++)
{
// 可行性剪枝
if (st[i] || ul + w[i] > length)
continue;
st[i] = true;
if (dfs(u, ul + w[i], i + 1))
return true;
st[i] = false;// 恢复现场
// 如果当前木棒放第一根木棍导致后续方案失败,则整个方案一定失败,这是一个优化
if (!ul)
return false;
// 如果当前木棒放最后一根木棍可以让当前这根木棒凑成length,但是导致后续方案失败,则整个方案一定失败,这是一个优化
if (ul + w[i] == length)
return false;
// 如果当前木棍放到当前木棒中失败了,则直接略过后面所有长度相等的木棍,这是一个优化
int j = i;
while (j < n && w[j] == w[i])
j++;
i = j - 1;
}
return false;
}
int main()
{
while (cin >> n, n)
{
memset(st, false, sizeof st);
sum = 0;
for (int i = 0; i < n; i++)
{
scanf("%d", &w[i]);
sum += w[i];
}
// 优化搜索顺序
sort(w, w + n);
reverse(w, w + n);
length = w[0];// 木棒的可能最小长度,可以从最长的木棍开始进行枚举,这是一个优化
while (true)
{
// 因为木棒是等长的,因此必须要满足sum % length == 0,这是一个优化
if (sum % length == 0 && dfs(0, 0, 0))
{
printf("%d\n", length);
break;
}
length++;
}
}
return 0;
}
生日蛋糕
#include <iostream>
#include <cmath>
using namespace std;
const int M = 25;
int n, m;
int r[M], h[M];
int ans;
int minv[M], mins[M];
void dfs(int u, int v, int s)
{
/*
当已经消耗了的体积v加上前u层最小的体积都大于给定的体积n时,
表示不合法,
即可结束dfs操作,
可行性剪枝
*/
if (v + minv[u] > n)
return ;
/*
当已经消耗了的表面积s加上前u层最小的侧表面积都大于等于结果ans时,
表示结果已经无法再进行优化,
即可结束dfs操作,
最优性剪枝
*/
if (s + mins[u] >= ans)
return ;
/*
设当前搜索到第u层,已经消耗了体积v,已经消耗了表面积s,
即有n - v = r[1] * r[1] * h[1] + r[2] * r[2] * h[2] + ...... + r[u] * r[u] * h[u],
然后设前u层的侧表面积为S[u],
即有S[u] = 2 * r[1] * h[1] + 2 * r[2] * h[2] + ...... + 2 * r[u] * h[u]
= 2 * (r[1] * h[1] + r[2] * h[2] + ...... + r[u] * h[u])
= 2 / r[u + 1] * (r[1] * h[1] + r[2] * h[2] + ...... + r[u] * h[u]) * r[u + 1]
= 2 / r[u + 1] * (r[1] * r[u + 1] * h[1] + r[2] * r[u + 1] * h[2] + ...... + r[u] * r[u + 1] * h[u]),
可得:
S[u] >= 2 / r[u + 1] * (r[1] * r[1] * h[1] + r[2] * r[2] * h[2] + ...... + r[u] * r[u] * h[u])
即有S[u] >= 2 / r[u + 1] * (n - v),
即我们发现了前u层的侧表面积的下界为2 / r[u + 1] * (n - v),
当已经消耗了的表面积s加上前u层的侧表面积的下界都大于等于结果ans时,
表示结果已经无法再进行优化,
即可结束dfs操作,
最优性剪枝
*/
if (s + 2 * (n - v) / r[u + 1] >= ans)// 最优性剪枝
return ;
if (!u)
{
if (v == n)
ans = s;
return ;
}
// 先搜半径r,再搜高度h,因为半径r会涉及到平方级别(圆柱体的体积为:r * r * h),从大到小搜,优化搜索顺序
/*
设当前搜索到第u层,已经消耗了体积v,
因为每一层的高度和半径至少为1且每一层的高度和半径自底向上是递减的,
所以就有:
u <= r[u] <= r[u + 1] - 1
u <= h[u] <= h[u + 1] - 1
同时还要保证体积不超过n,
即有r[u] * r[u] * h[u] <= n - v,可得:
r[u] <= sqrt((n - v) / h[u]),其中h[u]的最小值为u,即有r[u] <= sqrt((n - v) / u)
h[u] <= (n - v) / r[u] / r[u]
然后两个上界取最小,
可行性剪枝
*/
for (int i = min(r[u + 1] - 1, (int)sqrt((n - v) / u)); i >= u; i--)// 在C++中,min()函数的两个入参的类型必须要保持一致
for (int j = min(h[u + 1] - 1, (n - v) / i / i); j >= u; j--)
{
int t = 0;
if (u == m)
// 只有当搜到第m层时,才会把俯视视角所看到的蛋糕的表面积加上
t = i * i;
r[u] = i, h[u] = j;
dfs(u - 1, v + i * i * j, s + 2 * i * j + t);
}
}
// 注意!!!根据题意可知,我们在计算中不涉及到π,相当于可以把π忽略
int main()
{
scanf("%d %d", &n, &m);
// 预处理,假设第一层半径和高度为1,第二层半径和高度为2,第三层半径和高度为3,......,第m层半径和高度为m
for (int i = 1; i <= m; i++)
{
minv[i] = minv[i - 1] + i * i * i;// 前u层最小的体积
mins[i] = mins[i - 1] + 2 * i * i;// 前u层最小的侧表面积
}
r[m + 1] = h[m + 1] = 0x3f3f3f3f;
ans = 0x3f3f3f3f;
/*
我们这里设最上面一层为第1层,即半径和高度最小的一层,
最下面一层为第m层,即半径和高度最大的一层,
自底向上搜,优化搜索顺序
*/
dfs(m, 0, 0);
if (ans != 0x3f3f3f3f)
printf("%d\n", ans);
else
printf("0\n");
return 0;
}
迭代加深
加成序列
#include <iostream>
using namespace std;
const int N = 100 + 10;
int n;
int path[N];
bool dfs(int u, int depth)
{
if (u == depth)
return path[u - 1] == n;
bool st[N] = {false};// 将st数组的值全部初始化成false
// 从大到小枚举,优化搜索顺序
for (int i = u - 1; i >= 0; i--)
for (int j = i; j >= 0; j--)
{
int s = path[i] + path[j];
// 可行性剪枝
if (s > n)
continue;
// 可行性剪枝
if (s <= path[u - 1])
continue;
// 这个st数组可以减掉很多组内的等效冗余的方案
if (st[s])
continue;
st[s] = true;
path[u] = s;
if (dfs(u + 1, depth))
return true;
}
return false;
}
int main()
{
path[0] = 1;// 根据题意可知
while (cin >> n, n)
{
int k = 1;
/*
这个上限每次往上加1,
直至在当前上限进行dfs时能找到最优解,
这种dfs求最小值的方法被称为迭代加深求最小值
*/
while (!dfs(1, k))
k++;
// 输出结果
for (int i = 0; i < k; i++)
printf("%d ", path[i]);
puts("");
}
return 0;
}
双向DFS
送礼物
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 46 + 10;
int w, n;
int g[N];
int k;
int weights[1 << 23];
int cnt;
int ans;
void dfs1(int u, int s)
{
if (u == k)
{
weights[cnt++] = s;
return ;
}
dfs1(u + 1, s);
if ((LL)s + g[u] <= w)// 使用long long类型防止溢出
dfs1(u + 1, s + g[u]);
}
void dfs2(int u, int s)
{
if (u == n)
{
int l = 0, r = cnt - 1;
while (l < r)
{
int mid = (l + r + 1) / 2;
// 二分找到weights数组中,与s的和不超过w的最大的重量
if (weights[mid] <= w - s)
l = mid;
else
r = mid - 1;
}
ans = max(ans, s + weights[l]);
return ;
}
dfs2(u + 1, s);
if ((LL)s + g[u] <= w)// 使用long long类型防止溢出
dfs2(u + 1, s + g[u]);
}
int main()
{
scanf("%d %d", &w, &n);
for (int i = 0; i < n; i++)
scanf("%d", &g[i]);
// 从大到小枚举,优化搜索顺序
sort(g, g + n);
reverse(g, g + n);
k = n / 2;
// 把前k个物品的重量打一个表
dfs1(0, 0);
// 先从小到大排序,然后使用unique函数进行去重,这是在为dfs2()中的二分做准备
sort(weights, weights + cnt);
cnt = unique(weights, weights + cnt) - weights;
dfs2(k, 0);
printf("%d\n", ans);
return 0;
}
IDA*
排书
#include <iostream>
#include <cstring>
using namespace std;
const int N = 15 + 10;
int n;
int q[N];
int w[5][N];
/*
f()为预估代价函数,IDA*算法要求,预估代价函数的值<=真实的值(值的具体含义视情况而定)
本题可以将任意一段序列插到序列其他部分的任意一位置,目标是要使这个序列满足单调上升,求最小步数;
也就是说对于每一个位置上的数,它的后继一定是唯一确定的,
而每进行一次序列插入的操作,最多可以改变3个位置上的后继;
因此,不满足要求的后继的数量/3(上取整),是理论情况下最少的操作次数,也就是未来估计要递归的层数;
*/
int f()// 预估代价函数
{
int cnt = 0;
for (int i = 0; i + 1 < n; i++)
if (q[i] + 1 != q[i + 1])
cnt++;
/*
由于在c++中,除法计算默认是下取整的,
因此我们使用(cnt + 2) / 3可以实现不满足要求的后继的数量/3(上取整)的效果
*/
return (cnt + 2) / 3;
}
bool check()
{
for (int i = 0; i + 1 < n; i++)
if (q[i] + 1 != q[i + 1])
return false;
return true;
}
bool dfs(int depth, int max_depth)
{
// 可以对未来的递归层数做出判断,便于提前剪枝,优化时间复杂度;
if (depth + f() > max_depth)
return false;
if (check())
return true;
for (int len = 1; len < n; len++)// 长度为n没有必要进行枚举,因为没有意义
for (int l = 0; l + len - 1 < n; l++)// 枚举起点,左边界
{
int r = l + len - 1;// 右边界
/*
从r+1开始枚举插入操作即可,因为从r开始枚举插入操作没有意义,
枚举往后插的情况即可,避免重复枚举
*/
for (int k = r + 1; k < n; k++)
{
memcpy(w[depth], q, sizeof q);
// 实现插入操作
int x;
int y = l;
for (x = r + 1; x <= k; x++, y++)
q[y] = w[depth][x];
for (x = l; x <= r; x++, y++)
q[y] = w[depth][x];
if (dfs(depth + 1, max_depth))
return true;
memcpy(q, w[depth], sizeof q);// 恢复现场
}
}
return false;
}
/*
IDA*算法与A*算法类似,但相比之下更容易理解;
IDA*算法一般配合着迭代加深算法使用;
在dfs递归且未达到设定的最大递归层数的过程中,可以对未来的递归层数做出判断,便于提前剪枝,优化时间复杂度;
*/
int main()
{
int t;
scanf("%d", &t);
while (t--)
{
scanf("%d", &n);
for (int i = 0; i < n; i++)
scanf("%d", &q[i]);
int k = 0;
/*
这个上限每次往上加1,直至在当前上限进行dfs时能找到最优解,
并且这个上限要严格小于5,否则根据题意可知,可以停止dfs了
*/
while (k < 5 && !dfs(0, k))
k++;
if (k >= 5)
printf("5 or more\n");
else
printf("%d\n", k);
}
return 0;
}
回转游戏
/*
A B
0 1
2 3
H 4 5 6 7 8 9 10 C
11 12
G 13 14 15 16 17 18 19 D
20 21
22 23
F E
*/
#include <iostream>
using namespace std;
int q[24];
int path[100];
int center[8] = {6, 7, 8, 11, 12, 15, 16, 17};
int opposite[8] = {5, 4, 7, 6, 1, 0, 3, 2};
int op[8][7] = {
{0, 2, 6, 11, 15, 20, 22},
{1, 3, 8, 12, 17, 21, 23},
{10, 9, 8, 7, 6, 5, 4},
{19, 18, 17, 16, 15, 14, 13},
{23, 21, 17, 12, 8, 3, 1},
{22, 20, 15, 11, 6, 2, 0},
{13, 14, 15, 16, 17, 18, 19},
{4, 5, 6, 7, 8, 9, 10}
};
/*
根据题意可知,每操作一次,就会在中间8个数字里面改变1个数字,
因此,8-中间8个数字里面出现次数最多的数字的个数,是理论情况下最少的操作次数,也就是未来估计要递归的层数;
*/
int f()// 预估代价函数
{
int sum[4] = {0};
for (int i = 0; i < 8; i++)
sum[q[center[i]]]++;
int max_cnt = 0;
for (int i = 1; i <= 3; i++)
max_cnt = max(max_cnt, sum[i]);
return 8 - max_cnt;
}
void operate(int x)// 实现操作
{
int temp = q[op[x][0]];
for (int i = 0; i <= 5; i++)
q[op[x][i]] = q[op[x][i + 1]];
q[op[x][6]] = temp;
}
bool dfs(int depth, int max_depth, int before)
{
// 可以对未来的递归层数做出判断,便于提前剪枝,优化时间复杂度;
if (depth + f() > max_depth)
return false;
if (f() == 0)
return true;
for (int i = 0; i < 8; i++)
if (opposite[i] != before)// 当前操作不能是上一次操作的反向操作,可行性剪枝
{
operate(i);
path[depth] = i;// 记录移动步骤
if (dfs(depth + 1, max_depth, i))
return true;
operate(opposite[i]);// 恢复现场
}
return false;
}
int main()
{
while (cin >> q[0], q[0])
{
for (int i = 1; i < 24; i++)
scanf("%d", &q[i]);
int k = 0;
/*
这个上限每次往上加1,
直至在当前上限进行dfs时能找到结果
*/
while (!dfs(0, k, -1))
k++;
if (!k)
printf("No moves needed");
else
for (int i = 0; i < k; i++)
printf("%c", path[i] + 'A');
puts("");
printf("%d\n", q[6]);
}
return 0;
}
习题
池塘计数
#include <iostream>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 1e3 + 10;
const int M = 1e3 + 10;
int n, m;
char g[N][M];
PII q[N * M];
bool st[N][M];
void bfs(int sx, int sy)
{
st[sx][sy] = true;// 已经遍历过的,就标记一下
int hh = 0, tt = 0;
q[hh] = {sx, sy};
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 == sx && j == sy)
continue;
if (i < 0 || i >= n || j < 0 || j >= m)
continue;
if (g[i][j] == '.' || st[i][j])
continue;
st[i][j] = true;// 已经遍历过的,就标记一下
q[++tt] = {i, j};
}
}
}
int main()
{
scanf("%d %d", &n, &m);
for (int i = 0; i < n; i++)
scanf("%s", g[i]);
int cnt = 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);
cnt++;
}
// 输出结果
printf("%d\n", cnt);
return 0;
}
城堡问题
#include <iostream>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 50 + 10;
const int M = 50 + 10;
int n, m;
int g[N][M];
PII q[N * M];
bool st[N][M];
int bfs(int sx, int sy)
{
st[sx][sy] = true;// 已经遍历过的,就标记一下
int hh = 0, tt = 0;
q[0] = {sx, sy};
int area = 0;
int dx[4] = {0, -1, 0, 1}, dy[4] = {-1, 0, 1, 0};
while (hh <= tt)
{
PII 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])
continue;
if (g[t.x][t.y] >> i & 1)// 需要判断一下该方向是否有墙壁堵着
continue;
st[a][b] = true;// 已经遍历过的,就标记一下
q[++tt] = {a, b};
}
}
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;
int 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;
}
山峰和山谷
#include <iostream>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 1e3 + 10;
int n;
int h[N][N];
PII q[N * N];
bool st[N][N];
void bfs(int sx, int sy, bool &has_higher, bool &has_lower)// 这里使用了引用类型的入参
{
st[sx][sy] = true;// 已经遍历过的,就标记一下
int hh = 0, tt = 0;
q[0] = {sx, sy};
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 >= 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])
{
st[i][j] = true;// 已经遍历过的,就标记一下
q[++tt] = {i, j};
}
}
}
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
scanf("%d\n", &h[i][j]);
int peak = 0;
int 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;
}
迷宫问题
#include <iostream>
#include <cstring>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 1e3 + 10;
int n;
int g[N][N];
PII q[N * N];
PII pre[N][N];
void bfs(int sx, int sy)
{
int hh = 0, tt = 0;
q[0] = {sx, sy};
int dx[4] = {0, 1, 0, -1}, dy[4] = {1, 0, -1, 0};
memset(pre, -1, sizeof pre);
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 >= n)
continue;
if (g[a][b])// 判断是否为墙壁
continue;
if (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);// 从迷宫的右下角开始进行bfs操作
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;
}
武士风度的牛
#include <iostream>
#include <cstring>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 150 + 10;
const int M = 150 + 10;
int n, m;
char g[N][M];
PII q[N * M];
int dist[N][M];
int bfs()
{
// 先找到起点的坐标
int sx, sy;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
if (g[i][j] == 'K')
sx = i, sy = j;
int hh = 0, tt = 0;
q[0] = {sx, sy};
int dx[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
int dy[8] = {1, 2, 2, 1, -1, -2, -2, -1};
memset(dist, -1, sizeof dist);
dist[sx][sy] = 0;
while (hh <= tt)
{
PII 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] == '*')
continue;
if (g[a][b] == 'H')// 题目的数据保证一定有解
return dist[t.x][t.y] + 1;
if (dist[a][b] != -1)// 判断是否已经被搜到过
continue;
dist[a][b] = dist[t.x][t.y] + 1;
q[++tt] = {a, b};
}
}
}
int main()
{
scanf("%d %d", &m, &n);
for (int i = 0; i < n; i++)
scanf("%s", &g[i]);
printf("%d\n", bfs());
return 0;
}
抓住那头牛
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e5 + 10;
int n, k;
int q[N];
int dist[N];
int bfs()
{
int hh = 0, tt = 0;
q[0] = n;
memset(dist, -1, sizeof dist);
dist[n] = 0;
while (hh <= tt)
{
int t = q[hh++];
if (t == k)
return dist[t];
if (t - 1 >= 0 && dist[t - 1] == -1)
{
dist[t - 1] = dist[t] + 1;
q[++tt] = t - 1;
}
if (t + 1 <= k && dist[t + 1] == -1)
{
dist[t + 1] = dist[t] + 1;
q[++tt] = t + 1;
}
// 只要t*2大于k+1,我们就可以先减再乘
if (t * 2 <= k + 1 && dist[t * 2] == -1)
{
dist[t * 2] = dist[t] + 1;
q[++tt] = t * 2;
}
}
}
int main()
{
scanf("%d %d", &n, &k);
printf("%d\n", bfs());
return 0;
}
矩阵距离
#include <iostream>
#include <cstring>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 1e3 + 10;
const int M = 1e3 + 10;
int n, m;
char g[N][M];// 因为数字之间没有空格,所以用char类型读入
PII q[N * M];
int dist[N][M];
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};
}
int dx[4] = {0, 1, 0, -1};
int dy[4] = {1, 0, -1, 0};
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++)
// 因为数字之间没有空格,所以用char类型读入
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;
}
魔板
#include <iostream>
#include <algorithm>
#include <unordered_map>
#include <queue>
using namespace std;
char g[2][4];
unordered_map<string, int> dist;
unordered_map<string, pair<char, string>> pre;
void set(string state)// 由当前字符串得到对应的魔板
{
for (int j = 0; j < 4; j++)
g[0][j] = state[j];
for (int j = 0, i = 7; j < 4; j++, i--)
g[1][j] = state[i];
}
string get()// 由当前魔板得到对应的字符串
{
string res = "";
for (int j = 0; j < 4; j++)
res += g[0][j];
for (int j = 3; j >= 0; j--)
res += g[1][j];
return res;
}
string move0(string state)// 执行操作A,交换上下两行
{
// 由当前字符串得到对应的魔板
set(state);
// 操作魔板
for (int j = 0; j < 4; j++)
swap(g[0][j], g[1][j]);
// 由当前魔板得到对应的字符串
return get();
}
string move1(string state)// 执行操作B,将最右边的一列插入到最左边
{
// 由当前字符串得到对应的魔板
set(state);
// 操作魔板
char t1 = g[0][3], t2 = g[1][3];
for (int i = 0; i < 2; i++)
for (int j = 3; j >= 1; j--)
g[i][j] = g[i][j - 1];
g[0][0] = t1, g[1][0] = t2;
// 由当前魔板得到对应的字符串
return get();
}
string move2(string state)// 执行操作C,魔板中央对的4个数作顺时针旋转
{
// 由当前字符串得到对应的魔板
set(state);
// 操作魔板
char t = g[0][1];
g[0][1] = g[1][1];
g[1][1] = g[1][2];
g[1][2] = g[0][2];
g[0][2] = t;
// 由当前魔板得到对应的字符串
return get();
}
int bfs(string start, string end)
{
if (start == end)
return 0;
queue<string> q;
q.push(start);
dist[start] = 0;
while (!q.empty())
{
string t = q.front();
q.pop();
string m[3];
m[0] = move0(t);// 执行操作A,交换上下两行
m[1] = move1(t);// 执行操作B,将最右边的一列插入到最左边
m[2] = move2(t);// 执行操作C,魔板中央对的4个数作顺时针旋转
for (int i = 0; i < 3; i++)
if (!dist.count(m[i]))
{
dist[m[i]] = dist[t] + 1;
pre[m[i]] = {char(i + 'A'), t};
if (m[i] == end)
return dist[m[i]];
q.push(m[i]);
}
}
}
int main()
{
string start = "12345678";
string end = "";
for (int i = 0; i < 8; i++)
{
int x;
scanf("%d", &x);
end += char(x + '0');
}
int t = bfs(start, end);
printf("%d\n", t);
if (t > 0)// 如果操作序列的长度大于0,则在第二行输出字典序最小的操作序列
{
string op = "";
while (end != start)// 从特殊状态往前追溯至基本状态
{
op += pre[end].first;
end = pre[end].second;
}
// 记得要反转一下,这样才是从基本状态到特殊状态的操作序列
reverse(op.begin(), op.end());
printf("%s\n", op.c_str());// 输出字典序最小的操作序列
}
return 0;
}
电路维修
#include <iostream>
#include <cstring>
#include <deque>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 500 + 10;
const int M = 500 + 10;
int n, m;
char g[N][M];
int dist[N][M];
bool st[N][M];
int bfs()// 这里的bfs思路,本质上就是Dijkstra思路
{
memset(st, false, sizeof st);
memset(dist, 0x3f, sizeof dist);
dist[0][0] = 0;
deque<PII> q;// 双端队列
q.push_front({0, 0});
int dx[4] = {-1, -1, 1, 1}, dy[4] = {-1, 1, 1, -1};
int ix[4] = {-1, -1, 0, 0}, iy[4] = {-1, 0, 0, -1};
// 根据四个方向偏移,所能走到的四种电路的形状,分别为:左上\,右上/,右下\,左下/
char cs[4] = {'\\', '/', '\\', '/'};
while (!q.empty())
{
PII t = q.front();
q.pop_front();
if (t.x == n && t.y == m)
return dist[t.x][t.y];
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 ia = t.x + ix[i], ib = t.y + iy[i];
int d = dist[t.x][t.y] + (g[ia][ib] != cs[i]);
if (d < dist[a][b] && dist[a][b] == 0x3f3f3f3f)
{
dist[a][b] = d;
if (g[ia][ib] != cs[i])
q.push_back({a, b});
else
q.push_front({a, b});
}
if (d < dist[a][b] && dist[a][b] != 0x3f3f3f3f)
{
dist[a][b] = d;
q.push_front({a, b});
}
}
}
}
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]);
if ((n + m) & 1)// 通过观察可以知道,当终点的横纵坐标的和为奇数时,则起点永远无法连通终点
printf("NO SOLUTION\n");
else
printf("%d\n", bfs());
}
return 0;
}
字串变换
#include <iostream>
#include <queue>
#include <unordered_map>
using namespace std;
const int N = 6;
string a, b;
int n;
string oa[N], ob[N];
int extend(
queue<string>& qa,
unordered_map<string, int>& da,
unordered_map<string, int>& db,
string oa[],
string ob[]
)
{
int d = da[qa.front()];
while (qa.size() && da[qa.front()] == d)// 扩展完整一层
{
string t = qa.front();
qa.pop();
for (int i = 0; i < n; i++)// 枚举规则
for (int j = 0; j < t.size(); j++)// 枚举当前字符串的每个位置
// 判断当前字符串的当前位置是否符合当前规则进行变换
if (t.substr(j, oa[i].size()) == oa[i])
{
// 变换后的字符串
string u = t.substr(0, j) + ob[i] + t.substr(j + oa[i].size());
if (da.count(u))// 判断从出发的这一边是否已经搜索过
continue;
if (db.count(u))// 如果在另一边已经搜索过,则表示从出发的这一边可以变换到另一边
return da[t] + db[u] + 1;
qa.push(u);
da[u] = da[t] + 1;
}
}
// 当扩展完整一层后,还没有找到结果的话,则继续操作下一层
return 11;
}
int bfs()// 双向bfs
{
if (a == b)
return 0;
queue<string> qa, qb;
qa.push(a), qb.push(b);
unordered_map<string, int> da, db;
da[a] = 0, db[b] = 0;
int step = 0;// 记录变换步数
while (qa.size() && qb.size())
{
int t;
// 每次扩展时,选择队列中数量较少的一边来进行扩展,每次每边扩展完整一层
if (qa.size() <= qb.size())
t = extend(qa, da, db, oa, ob);
else
t = extend(qb, db, da, ob, oa);
if (t <= 10)
return t;
step++;
// 当变换步数大于等于10步时,还没有找到结果的话,则可以结束循环了
if (step >= 10)
break;
}
return -1;
}
int main()
{
cin >> a >> b;
while (cin >> oa[n] >> ob[n])// 输入规则
n++;
int t = bfs();// 双向bfs
if (t == -1)
printf("NO ANSWER!\n");
else
printf("%d\n", t);
return 0;
}
第K短路
#include <iostream>
#include <cstring>
#include <queue>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
typedef pair<int, PII> PIPII;
const int N = 1e3 + 10;
const int M = 2 * 1e4 + 10;// 因为要反向建图,所以用到的边数需要乘2
int n, m;
int h[N], rh[N], w[M], e[M], ne[M], idx;
int s, t, k;
int dist[N];
int cnt[N];
void add(int temp[], int a, int b, int c)
{
w[idx] = c;
e[idx] = b;
ne[idx] = temp[a];
temp[a] = idx++;
}
// 预估代价函数
void dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[t] = 0;
priority_queue<PII, vector<PII>, greater<PII>> q;
q.push({0, t});
while (!q.empty())
{
PII temp = q.top();
q.pop();
int ver = temp.y;
int distance = temp.x;
for (int i = rh[ver]; ~i; i = ne[i])
{
int j = e[i];
if (dist[j] > distance + w[i])
{
dist[j] = distance + w[i];
q.push({dist[j], j});
}
}
}
}
int astar()
{
priority_queue<PIPII, vector<PIPII>, greater<PIPII>> q;// 优先队列小根堆
/*
优先队列小根堆中每个状态的权重是:
当前代价 + 预估代价
(其中,
当前代价为从起点到该状态的真实距离(注意并不一定是最短距离),
预估代价为从该状态到终点的估计距离,估计距离一般通过预估代价函数得到)
*/
q.push({0 + dist[s], {0, s}});
while (!q.empty())
{
PIPII temp = q.top();
q.pop();
int ver = temp.y.y;
cnt[ver]++;// 计算当前点第几次弹出优先队列小根堆
if (ver == t && cnt[ver] == k)
return temp.y.x;
for (int i = h[ver]; ~i; i = ne[i])
{
int j = e[i];
/*
可以对最短路径的搜索进行优化,
每个点第几次弹出优先队列小根堆就是第几短路径,
当某个点第k次弹出优先队列小根堆,准备要进行第k+1次进入优先队列小根堆时,
说明该点到终点的路径一定不会是前k短路径。
这个优化,可以把起点和终点不连通的情况也考虑到,防止TLE(Time Limit Exceeded)
*/
if (cnt[j] < k)
q.push({temp.y.x + w[i] + dist[j], {temp.y.x + 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);// 反向建图的目的是为了反向做一次dijkstra操作,充当了预估代价函数的作用
}
scanf("%d %d %d", &s, &t, &k);
/*
当起点和终点相同时,
k中隐含了一条长度为0的没有边的最短路,
但这条最短路是不对的,
因为题目要求起点和终点之间至少包含一条边,
所以k++,是为了排除掉长度为0的最短路
*/
if (s == t)
k++;
dijkstra();
printf("%d\n", astar());
return 0;
}
八数码
#include <iostream>
#include <unordered_map>
#include <queue>
#include <algorithm>
#define x first
#define y second
using namespace std;
typedef pair<string, char> PSC;
typedef pair<int, string> PIS;
// 预估代价函数
int f(string state)
{
int cnt = 0;
for (int i = 0; i < state.size(); i++)
if (state[i] != 'x')
{
int t = state[i] - '1';
cnt += abs(i / 3 - t / 3) + abs(i % 3 - t % 3);
}
// 返回估计距离,值为当前状态中的,每个数的位置与它对应的目标位置的曼哈顿距离,的总和
return cnt;
}
string bfs(string start)
{
string end = "12345678x";
unordered_map<string, int> dist;
unordered_map<string, PSC> prev;
priority_queue<PIS, vector<PIS>, greater<PIS>> q;// 优先队列小根堆
dist[start] = 0;
/*
优先队列小根堆中每个状态的权重是:
当前代价 + 预估代价
(其中,
当前代价为从起点到该状态的真实距离(注意并不一定是最短距离),
预估代价为从该状态到终点的估计距离,估计距离一般通过预估代价函数得到)
*/
q.push({0 + f(start), start});
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
char op[4] = {'u', 'r', 'd', 'l'};
while (!q.empty())
{
PIS t = q.top();
q.pop();
string state = t.y;
// 当终点首次弹出时,则一定是最优解,然后退出循环即可
if (state == end)
break;
int x, y;
for (int i = 0; i < state.size(); i++)
if (state[i] == 'x')
{
x = i / 3;
y = i % 3;
}
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]);
// 注意,在A*算法中,当第一次搜到某个点的时候,距离不一定是最短的,其dist[]的值是不断变小的,所以要加后一个判断
if (!dist.count(state) || dist[state] > dist[source] + 1)
{
// 更新当前状态的当前代价
dist[state] = dist[source] + 1;
// 更新优先队列小根堆
q.push({dist[state] + f(state), state});
// 记录当前状态是从哪个状态走过来的,且对应哪个操作
prev[state] = {source, op[i]};
}
// 状态恢复,继续遍历当前这个点(x,y)可以往哪个方向走
swap(state[x * 3 + y], state[a * 3 + b]);
}
}
}
// 得到操作方案
string res;
while (end != start)
{
res += prev[end].y;
end = prev[end].x;
}
reverse(res.begin(), res.end());// 记得要反转一下
return res;
}
int main()
{
string g, c, 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)
// 在八数码问题中,逆序对的数量为奇数时,则八数码问题没有解
printf("unsolvable\n");
else
printf("%s\n", bfs(g).c_str());
return 0;
}
迷宫
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100 + 10;
int n;
char g[N][N];
int xa, ya, xb, yb;
bool st[N][N];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
bool dfs(int x, int y)
{
if (g[x][y] == '#')
return false;
// 判断是否找到终点
if (x == xb && y == yb)
return true;
st[x][y] = true;
for (int i = 0; i < 4; i++)
{
int a = x + dx[i], b = y + dy[i];
if (a < 0 || a >= n || b < 0 || b >= n)
continue;
if (st[a][b])
continue;
if (dfs(a, b))
return true;
}
return false;
}
int main()
{
int t;
scanf("%d", &t);
while (t--)
{
scanf("%d", &n);
for (int i = 0; i < n; i++)
scanf("%s", g[i]);
scanf("%d %d %d %d", &xa, &ya, &xb, &yb);
memset(st, false, sizeof st);
printf("%s\n", dfs(xa, ya) ? "YES" : "NO");
}
return 0;
}
红与黑
#include <iostream>
#include <cstring>
using namespace std;
const int N = 20 + 10;
const int M = 20 + 10;
int n, m;
char g[N][M];
bool st[N][M];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
int dfs(int x, int y)
{
int cnt = 1;
st[x][y] = true;
for (int i = 0; i < 4; i++)
{
int a = x + dx[i], b = y + dy[i];
if (a < 0 || a >= n || b < 0 || b >= m)
continue;
if (g[a][b] == '#')
continue;
if (st[a][b])
continue;
cnt += dfs(a, b);
}
return cnt;
}
int main()
{
while (cin >> m >> n, n || m)
{
for (int i = 0; i < n; i++)
scanf("%s", g[i]);
int a, b;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
if (g[i][j] == '@')
a = i, b = j;
memset(st, false, sizeof st);
printf("%d\n", dfs(a, b));
}
return 0;
}
马走日
#include <iostream>
using namespace std;
const int N = 10;
const int M = 10;
int n, m;
int x, y;
int ans;
bool st[N][M];
int dx[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
int dy[8] = {1, 2, 2, 1, -1, -2, -2, -1};
void dfs(int x, int y, int cnt)
{
if (cnt == n * m)
{
ans++;
return ;
}
st[x][y] = true;
for (int i = 0; i < 8; i++)
{
int a = x + dx[i], b = y + dy[i];
if (a < 0 || a >= n || b < 0 || b >= m)
continue;
if (st[a][b])
continue;
dfs(a, b, cnt + 1);
}
st[x][y] = false;// 恢复现场
}
int main()
{
int t;
scanf("%d", &t);
while (t--)
{
scanf("%d %d %d %d", &n, &m, &x, &y);
ans = 0;
dfs(x, y, 1);
printf("%d\n", ans);
}
return 0;
}
单词接龙
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 20;
int n;
string word[N];
int g[N][N];
int ans;
int used[N];
void dfs(string dragon, int before)
{
/*
在C++中,string类型的size()函数的返回值是一个无符号类型的值
在C++中,max()函数的两个入参的类型必须要保持一致
*/
ans = max(ans, (int)dragon.size());
used[before]++;
for (int j = 0; j < n; j++)
if (g[before][j] && used[j] < 2)
dfs(dragon + word[j].substr(g[before][j]), j);
used[before]--;// 恢复现场
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i++)
cin >> word[i];
char start;
cin >> start;
// 预处理
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
{
string a = word[i];
string b = word[j];
// 我们可以任意选择重合部分的长度,但其长度必须大于等于1,且严格小于两个串的长度
for (int k = 1; k < min(a.size(), b.size()); k++)
if (a.substr(a.size() - k, k) == b.substr(0, k))
{
g[i][j] = k;
break;
}
}
for (int i = 0; i < n; i++)
if (word[i][0] == start)
dfs(word[i], i);
printf("%d\n", ans);
return 0;
}
知识点补充
- 在C++中,string类型的size()函数的返回值是一个无符号类型的值
- 在C++中,max()函数的两个入参的类型必须要保持一致
分成互质组
#include <iostream>
using namespace std;
const int N = 10;
int n;
int a[N];
int ans = N;// 最坏情况下是,每个数单独一个组
bool st[N];
int group[N][N];
// 欧几里得算法求两个数的最大公约数
int gcd(int a, int b)
{
return b ? gcd(b, a % b) : a;
}
bool check(int group[], int gc, int b)
{
for (int i = 0; i < gc; i++)
// 如果两个数的最大公约数大于1,则两个数不互质
if (gcd(a[group[i]], b) > 1)
return false;
return true;
}
/*
四个参数的含义分别为:
当前搜索到第几组、
当前组中一共有几个数、
一共搜索过几个数、
从第几个数(具体数值为a数组的下标)开始进行搜索
*/
void dfs(int g, int gc, int tc, int start)// 这个start可以减掉很多组内的等效冗余的方案
{
if (g >= ans)
return ;
if (tc == n)
ans = g;
bool flag = true;
for (int i = start; i < n; i++)
// 判断当前a[i]这个数是否被用过,并且,当前a[i]这个数,是否和当前组g中的一共gc个数都互质
if (!st[i] && check(group[g], gc, a[i]))
{
st[i] = true;
// 把当前a[i]这个数加入到当前组g中,然后继续进行dfs操作。这个操作可以不恢复现场
group[g][gc] = i;
dfs(g, gc + 1, tc + 1, i + 1);
st[i] = false;// 恢复现场
// 如果至少有一个数可以放入到当前组中,则不需要新开一个新的组来进行dfs操作
flag = false;
/*
剪枝操作,
因为开一个新组的前提条件是每个数都不能放到旧组中了,
所以剩下的第一个数一定会放到某个新组中,那我们可以将它固定到第一个新组中,
这样可以减掉很多组与组之间的等效冗余的方案
*/
if (gc == 0)
return ;
}
// 如果至少有一个数可以放入到当前组中,则不需要新开一个新的组来进行dfs操作
if (flag)
dfs(g + 1, 0, tc, 0);
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i++)
scanf("%d", &a[i]);
// 这个dfs的搜索顺序本质上是,按照每个组的每个位置进行搜索的!!!
dfs(1, 0, 0, 0);
printf("%d\n", ans);
return 0;
}
小猫爬山
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 18;
int n, m;
int w[N];
int ans = N;// 最坏情况下是,每只猫单独一辆缆车
int sum[N];
void dfs(int u, int k)
{
if (k >= ans)// 最优性剪枝
return ;
if (u == n)
ans = k;
for (int i = 0; i < k; i++)
if (sum[i] + w[u] <= m)// 可行性剪枝
{
sum[i] += w[u];
dfs(u + 1, k);
sum[i] -= w[u];// 恢复现场
}
/*
这里并不能使用贪心,举出例子:
请尝试在maxV=16时,重量为9 5 5 5 4 3
贪心结果是9+5 5+5+4 3,结果为3
但正确结果为9+4+3 5+5+5,结果为2
*/
// 新开一辆缆车
sum[k] = w[u];
dfs(u + 1, k + 1);
sum[k] = w[u];// 恢复现场,这个现场不恢复也不影响最终答案
}
int main()
{
scanf("%d %d", &n, &m);
for (int i = 0; i < n; i++)
scanf("%d", &w[i]);
// 优化搜索顺序
sort(w, w + n);
reverse(w, w + n);
// 这个dfs的搜索顺序本质上是,按照每个小猫能放到哪个缆车上进行搜索的!!!
dfs(0, 0);
printf("%d\n", ans);
return 0;
}
数独
#include <iostream>
using namespace std;
const int N = 9;
char str[100];
int row[N], col[N], cell[3][3];
int ones[1 << N];
int map[1 << N];
// 初始化处理,先默认每一行、每一列、每一个3×3的九宫格都可以不重复地填数字1~9
void init()
{
for (int i = 0; i < N; i++)
row[i] = col[i] = (1 << N) - 1;
// 一共有9个3x3的九宫格
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
cell[i][j] = (1 << N) - 1;
}
// 处理第x行、第y列、(x,y)所属的3×3的九宫格,可行性剪枝
void draw(int x, int y, int t, int flag)
{
if (flag)
str[x * N + y] = '1' + t;
else
str[x * N + y] = '.';
int v = 1 << t;
if (!flag)
v = 0 - v;
// 可行性剪枝
row[x] -= v;
col[y] -= v;
cell[x / 3][y / 3] -= v;
}
int get(int x, int y)// 位运算优化
{
return row[x] & col[y] & cell[x / 3][y / 3];
}
int lowbit(int x)// 位运算优化
{
return x & (~x + 1);
}
bool dfs(int cnt)
{
if (!cnt)
return true;
// 优化搜索顺序,让dfs的搜索顺序从,可以选择填写的数字个数较少的位置,开始进行搜索
int minv = 10;
int x, y;
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
if (str[i * N + j] == '.')
{
int state = get(i, j);
if (ones[state] < minv)
{
minv = ones[state];
x = i, y = j;
}
}
// 优化搜索顺序,让dfs的搜索顺序从,可以选择填写的数字的个数较少的位置,开始进行搜索
int state = get(x, y);
for (int i = state; i; i -= lowbit(i))
{
int t = map[lowbit(i)];
draw(x, y, t, true);
if (dfs(cnt - 1))
return true;
draw(x, y, t, false);// 恢复现场
}
// 当state等于0时,说明已经出现了,存在有一个位置无法填写数字,的情况,即可返回false
return false;
}
int main()
{
// 位运算优化
for (int i = 0; i < (1 << N); i++)
for (int j = 0; j < N; j++)
ones[i] += (i >> j) & 1;
for (int i = 0; i < N; i++)
map[1 << i] = i;
while (cin >> str, str[0] != 'e')
{
init();// 初始化处理,先默认每一行、每一列、每一个3×3的九宫格都可以不重复地填数字1~9
int cnt = 0;
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
if (str[i * N + j] != '.')
{
int t = str[i * N + j] - '1';
// 处理第i行、第j列、(i,j)所属的3×3的九宫格,可行性剪枝
draw(i, j, t, true);
}
else
cnt++;
if (dfs(cnt))// 题目保证输入中的每个谜题都只有一个解决方案
cout << str << endl;
}
return 0;
}
木棒
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 70;
int n;
int w[N];
int sum;
int length;
bool st[N];
/*
三个参数的含义分别为:
一共搜索过几个木棒、
当前木棒的长度、
从第几个木棍(具体数值为w数组的下标)开始进行搜索
*/
bool dfs(int u, int ul, int start)// 这个start可以减掉很多组内的等效冗余的方案,这是一个优化
{
if (u * length == sum)
return true;
if (ul == length)
return dfs(u + 1, 0, 0);
for (int i = start; i < n; i++)
{
// 可行性剪枝
if (st[i] || ul + w[i] > length)
continue;
st[i] = true;
if (dfs(u, ul + w[i], i + 1))
return true;
st[i] = false;// 恢复现场
// 如果当前木棒放第一根木棍导致后续方案失败,则整个方案一定失败,这是一个优化
if (!ul)
return false;
// 如果当前木棒放最后一根木棍可以让当前这根木棒凑成length,但是导致后续方案失败,则整个方案一定失败,这是一个优化
if (ul + w[i] == length)
return false;
// 如果当前木棍放到当前木棒中失败了,则直接略过后面所有长度相等的木棍,这是一个优化
int j = i;
while (j < n && w[j] == w[i])
j++;
i = j - 1;
}
return false;
}
int main()
{
while (cin >> n, n)
{
memset(st, false, sizeof st);
sum = 0;
for (int i = 0; i < n; i++)
{
scanf("%d", &w[i]);
sum += w[i];
}
// 优化搜索顺序
sort(w, w + n);
reverse(w, w + n);
length = w[0];// 木棒的可能最小长度,可以从最长的木棍开始进行枚举,这是一个优化
while (true)
{
// 因为木棒是等长的,因此必须要满足sum % length == 0,这是一个优化
if (sum % length == 0 && dfs(0, 0, 0))
{
printf("%d\n", length);
break;
}
length++;
}
}
return 0;
}
生日蛋糕
#include <iostream>
#include <cmath>
using namespace std;
const int M = 25;
int n, m;
int r[M], h[M];
int ans;
int minv[M], mins[M];
void dfs(int u, int v, int s)
{
/*
当已经消耗了的体积v加上前u层最小的体积都大于给定的体积n时,
表示不合法,
即可结束dfs操作,
可行性剪枝
*/
if (v + minv[u] > n)
return ;
/*
当已经消耗了的表面积s加上前u层最小的侧表面积都大于等于结果ans时,
表示结果已经无法再进行优化,
即可结束dfs操作,
最优性剪枝
*/
if (s + mins[u] >= ans)
return ;
/*
设当前搜索到第u层,已经消耗了体积v,已经消耗了表面积s,
即有n - v = r[1] * r[1] * h[1] + r[2] * r[2] * h[2] + ...... + r[u] * r[u] * h[u],
然后设前u层的侧表面积为S[u],
即有S[u] = 2 * r[1] * h[1] + 2 * r[2] * h[2] + ...... + 2 * r[u] * h[u]
= 2 * (r[1] * h[1] + r[2] * h[2] + ...... + r[u] * h[u])
= 2 / r[u + 1] * (r[1] * h[1] + r[2] * h[2] + ...... + r[u] * h[u]) * r[u + 1]
= 2 / r[u + 1] * (r[1] * r[u + 1] * h[1] + r[2] * r[u + 1] * h[2] + ...... + r[u] * r[u + 1] * h[u]),
可得:
S[u] >= 2 / r[u + 1] * (r[1] * r[1] * h[1] + r[2] * r[2] * h[2] + ...... + r[u] * r[u] * h[u])
即有S[u] >= 2 / r[u + 1] * (n - v),
即我们发现了前u层的侧表面积的下界为2 / r[u + 1] * (n - v),
当已经消耗了的表面积s加上前u层的侧表面积的下界都大于等于结果ans时,
表示结果已经无法再进行优化,
即可结束dfs操作,
最优性剪枝
*/
if (s + 2 * (n - v) / r[u + 1] >= ans)// 最优性剪枝
return ;
if (!u)
{
if (v == n)
ans = s;
return ;
}
// 先搜半径r,再搜高度h,因为半径r会涉及到平方级别(圆柱体的体积为:r * r * h),从大到小搜,优化搜索顺序
/*
设当前搜索到第u层,已经消耗了体积v,
因为每一层的高度和半径至少为1且每一层的高度和半径自底向上是递减的,
所以就有:
u <= r[u] <= r[u + 1] - 1
u <= h[u] <= h[u + 1] - 1
同时还要保证体积不超过n,
即有r[u] * r[u] * h[u] <= n - v,可得:
r[u] <= sqrt((n - v) / h[u]),其中h[u]的最小值为u,即有r[u] <= sqrt((n - v) / u)
h[u] <= (n - v) / r[u] / r[u]
然后两个上界取最小,
可行性剪枝
*/
for (int i = min(r[u + 1] - 1, (int)sqrt((n - v) / u)); i >= u; i--)// 在C++中,min()函数的两个入参的类型必须要保持一致
for (int j = min(h[u + 1] - 1, (n - v) / i / i); j >= u; j--)
{
int t = 0;
if (u == m)
// 只有当搜到第m层时,才会把俯视视角所看到的蛋糕的表面积加上
t = i * i;
r[u] = i, h[u] = j;
dfs(u - 1, v + i * i * j, s + 2 * i * j + t);
}
}
// 注意!!!根据题意可知,我们在计算中不涉及到π,相当于可以把π忽略
int main()
{
scanf("%d %d", &n, &m);
// 预处理,假设第一层半径和高度为1,第二层半径和高度为2,第三层半径和高度为3,......,第m层半径和高度为m
for (int i = 1; i <= m; i++)
{
minv[i] = minv[i - 1] + i * i * i;// 前u层最小的体积
mins[i] = mins[i - 1] + 2 * i * i;// 前u层最小的侧表面积
}
r[m + 1] = h[m + 1] = 0x3f3f3f3f;
ans = 0x3f3f3f3f;
/*
我们这里设最上面一层为第1层,即半径和高度最小的一层,
最下面一层为第m层,即半径和高度最大的一层,
自底向上搜,优化搜索顺序
*/
dfs(m, 0, 0);
if (ans != 0x3f3f3f3f)
printf("%d\n", ans);
else
printf("0\n");
return 0;
}
知识点补充
- 在C++中,min()函数的两个入参的类型必须要保持一致
加成序列
#include <iostream>
using namespace std;
const int N = 100 + 10;
int n;
int path[N];
bool dfs(int u, int depth)
{
if (u == depth)
return path[u - 1] == n;
bool st[N] = {false};// 将st数组的值全部初始化成false
// 从大到小枚举,优化搜索顺序
for (int i = u - 1; i >= 0; i--)
for (int j = i; j >= 0; j--)
{
int s = path[i] + path[j];
// 可行性剪枝
if (s > n)
continue;
// 可行性剪枝
if (s <= path[u - 1])
continue;
// 这个st数组可以减掉很多组内的等效冗余的方案
if (st[s])
continue;
st[s] = true;
path[u] = s;
if (dfs(u + 1, depth))
return true;
}
return false;
}
int main()
{
path[0] = 1;// 根据题意可知
while (cin >> n, n)
{
int k = 1;
/*
这个上限每次往上加1,
直至在当前上限进行dfs时能找到最优解,
这种dfs求最小值的方法被称为迭代加深求最小值
*/
while (!dfs(1, k))
k++;
// 输出结果
for (int i = 0; i < k; i++)
printf("%d ", path[i]);
puts("");
}
return 0;
}
送礼物
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 46 + 10;
int w, n;
int g[N];
int k;
int weights[1 << 23];
int cnt;
int ans;
void dfs1(int u, int s)
{
if (u == k)
{
weights[cnt++] = s;
return ;
}
dfs1(u + 1, s);
if ((LL)s + g[u] <= w)// 使用long long类型防止溢出
dfs1(u + 1, s + g[u]);
}
void dfs2(int u, int s)
{
if (u == n)
{
int l = 0, r = cnt - 1;
while (l < r)
{
int mid = (l + r + 1) / 2;
// 二分找到weights数组中,与s的和不超过w的最大的重量
if (weights[mid] <= w - s)
l = mid;
else
r = mid - 1;
}
ans = max(ans, s + weights[l]);
return ;
}
dfs2(u + 1, s);
if ((LL)s + g[u] <= w)// 使用long long类型防止溢出
dfs2(u + 1, s + g[u]);
}
int main()
{
scanf("%d %d", &w, &n);
for (int i = 0; i < n; i++)
scanf("%d", &g[i]);
// 从大到小枚举,优化搜索顺序
sort(g, g + n);
reverse(g, g + n);
k = n / 2;
// 把前k个物品的重量打一个表
dfs1(0, 0);
// 先从小到大排序,然后使用unique函数进行去重,这是在为dfs2()中的二分做准备
sort(weights, weights + cnt);
cnt = unique(weights, weights + cnt) - weights;
dfs2(k, 0);
printf("%d\n", ans);
return 0;
}
排书
#include <iostream>
#include <cstring>
using namespace std;
const int N = 15 + 10;
int n;
int q[N];
int w[5][N];
/*
f()为预估代价函数,IDA*算法要求,预估代价函数的值<=真实的值(值的具体含义视情况而定)
本题可以将任意一段序列插到序列其他部分的任意一位置,目标是要使这个序列满足单调上升,求最小步数;
也就是说对于每一个位置上的数,它的后继一定是唯一确定的,
而每进行一次序列插入的操作,最多可以改变3个位置上的后继;
因此,不满足要求的后继的数量/3(上取整),是理论情况下最少的操作次数,也就是未来估计要递归的层数;
*/
int f()// 预估代价函数
{
int cnt = 0;
for (int i = 0; i + 1 < n; i++)
if (q[i] + 1 != q[i + 1])
cnt++;
/*
由于在c++中,除法计算默认是下取整的,
因此我们使用(cnt + 2) / 3可以实现不满足要求的后继的数量/3(上取整)的效果
*/
return (cnt + 2) / 3;
}
bool check()
{
for (int i = 0; i + 1 < n; i++)
if (q[i] + 1 != q[i + 1])
return false;
return true;
}
bool dfs(int depth, int max_depth)
{
// 可以对未来的递归层数做出判断,便于提前剪枝,优化时间复杂度;
if (depth + f() > max_depth)
return false;
if (check())
return true;
for (int len = 1; len < n; len++)// 长度为n没有必要进行枚举,因为没有意义
for (int l = 0; l + len - 1 < n; l++)// 枚举起点,左边界
{
int r = l + len - 1;// 右边界
/*
从r+1开始枚举插入操作即可,因为从r开始枚举插入操作没有意义,
枚举往后插的情况即可,避免重复枚举
*/
for (int k = r + 1; k < n; k++)
{
memcpy(w[depth], q, sizeof q);
// 实现插入操作
int x;
int y = l;
for (x = r + 1; x <= k; x++, y++)
q[y] = w[depth][x];
for (x = l; x <= r; x++, y++)
q[y] = w[depth][x];
if (dfs(depth + 1, max_depth))
return true;
memcpy(q, w[depth], sizeof q);// 恢复现场
}
}
return false;
}
/*
IDA*算法与A*算法类似,但相比之下更容易理解;
IDA*算法一般配合着迭代加深算法使用;
在dfs递归且未达到设定的最大递归层数的过程中,可以对未来的递归层数做出判断,便于提前剪枝,优化时间复杂度;
*/
int main()
{
int t;
scanf("%d", &t);
while (t--)
{
scanf("%d", &n);
for (int i = 0; i < n; i++)
scanf("%d", &q[i]);
int k = 0;
/*
这个上限每次往上加1,直至在当前上限进行dfs时能找到最优解,
并且这个上限要严格小于5,否则根据题意可知,可以停止dfs了
*/
while (k < 5 && !dfs(0, k))
k++;
if (k >= 5)
printf("5 or more\n");
else
printf("%d\n", k);
}
return 0;
}
回转游戏
/*
A B
0 1
2 3
H 4 5 6 7 8 9 10 C
11 12
G 13 14 15 16 17 18 19 D
20 21
22 23
F E
*/
#include <iostream>
using namespace std;
int q[24];
int path[100];
int center[8] = {6, 7, 8, 11, 12, 15, 16, 17};
int opposite[8] = {5, 4, 7, 6, 1, 0, 3, 2};
int op[8][7] = {
{0, 2, 6, 11, 15, 20, 22},
{1, 3, 8, 12, 17, 21, 23},
{10, 9, 8, 7, 6, 5, 4},
{19, 18, 17, 16, 15, 14, 13},
{23, 21, 17, 12, 8, 3, 1},
{22, 20, 15, 11, 6, 2, 0},
{13, 14, 15, 16, 17, 18, 19},
{4, 5, 6, 7, 8, 9, 10}
};
/*
根据题意可知,每操作一次,就会在中间8个数字里面改变1个数字,
因此,8-中间8个数字里面出现次数最多的数字的个数,是理论情况下最少的操作次数,也就是未来估计要递归的层数;
*/
int f()// 预估代价函数
{
int sum[4] = {0};
for (int i = 0; i < 8; i++)
sum[q[center[i]]]++;
int max_cnt = 0;
for (int i = 1; i <= 3; i++)
max_cnt = max(max_cnt, sum[i]);
return 8 - max_cnt;
}
void operate(int x)// 实现操作
{
int temp = q[op[x][0]];
for (int i = 0; i <= 5; i++)
q[op[x][i]] = q[op[x][i + 1]];
q[op[x][6]] = temp;
}
bool dfs(int depth, int max_depth, int before)
{
// 可以对未来的递归层数做出判断,便于提前剪枝,优化时间复杂度;
if (depth + f() > max_depth)
return false;
if (f() == 0)
return true;
for (int i = 0; i < 8; i++)
if (opposite[i] != before)// 当前操作不能是上一次操作的反向操作,可行性剪枝
{
operate(i);
path[depth] = i;// 记录移动步骤
if (dfs(depth + 1, max_depth, i))
return true;
operate(opposite[i]);// 恢复现场
}
return false;
}
int main()
{
while (cin >> q[0], q[0])
{
for (int i = 1; i < 24; i++)
scanf("%d", &q[i]);
int k = 0;
/*
这个上限每次往上加1,
直至在当前上限进行dfs时能找到结果
*/
while (!dfs(0, k, -1))
k++;
if (!k)
printf("No moves needed");
else
for (int i = 0; i < k; i++)
printf("%c", path[i] + 'A');
puts("");
printf("%d\n", q[6]);
}
return 0;
}