题目描述
你为自己的获奖玫瑰园感到自豪,但嫉妒的园丁绑架了你,把你蒙住眼睛、戴上手铐扔在了玫瑰园的正中央!你站在一个方形大理石基座上,这个基座位于 N×NN \times NN×N 网格(NNN 为奇数)的正中心。你不知道自己面朝哪个方向,但可以确定自己面向北、东、南、西四个基本方向之一。
你必须设计一条逃生路径,使得无论初始面朝哪个方向,沿着这条路径走(根据实际方向调整移动方向)所踩到的玫瑰数量都尽可能少。路径必须从中心开始,只包含水平和垂直移动,最终要走出网格边界。
输入格式
- 每个测试用例以整数 NNN(1≤N≤211 \le N \le 211≤N≤21,NNN 为奇数)开始
- 接下来 NNN 行,每行 NNN 个字符,描述花园布局:
.表示空地R表示玫瑰P表示中心基座
- 输入以 N=0N = 0N=0 结束
输出格式
对于每个测试用例,输出一行:At most X rose(s) trampled.,其中 XXX 是保证能踩到的最少玫瑰数。
题目分析与解题思路
问题本质
这个问题需要我们在不知道初始方向的情况下设计一条路径。关键观察是:虽然我们不知道初始方向,但四个可能的初始方向(北、东、南、西)是对称的。如果我们设计一条指令序列(如前、左、右等),那么在四个不同初始方向下执行这个序列,会得到四条不同的路径,但这些路径之间存在旋转对称关系。
对称性利用
设我们在第一象限(以中心为原点)走的路径为一系列移动。那么对于初始方向:
- 北:直接按路径走
- 东:路径顺时针旋转 90∘90^\circ90∘
- 南:路径旋转 180∘180^\circ180∘
- 西:路径逆时针旋转 90∘90^\circ90∘
因此,我们只需要搜索第一象限的路径,其他三个方向的路径可以通过旋转得到。这大大减少了搜索空间。
基础思路:朴素 DFS\texttt{DFS}DFS
解题思路
最直接的想法是使用深度优先搜索(DFS\texttt{DFS}DFS)探索所有可能的路径。我们只在一个方向上(比如第一象限)搜索路径,然后通过对称性计算其他三个方向的对应路径。在搜索过程中,我们记录四个方向上各自踩到的玫瑰数,并跟踪当前最优解。
算法框架
- 状态表示:当前位置 (x,y)(x, y)(x,y),四个方向已踩玫瑰数数组 roses[4]\texttt{roses}[4]roses[4]
- 搜索过程:从中心点开始,每次尝试向四个方向移动一步
- 对称性处理:对于每个移动,计算四个旋转方向对应的实际位置
- 剪枝策略:如果当前四个方向的最大玫瑰数已经不小于当前最优解,则放弃该分支
- 终止条件:到达网格边界时更新最优解
代码实现
// Be Wary of Roses
// UVa ID: 10798
// Verdict: Accepted
// Submission Date: 2025-12-23
// UVa Run Time: 1.520s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net
#include <bits/stdc++.h>
using namespace std;
int n;
string grid[32];
int bestAnswer;
int dx[4] = {0, 1, -1, 0}, dy[4] = {1, 0, 0, -1};
int visited[32][32], roses[4];
void dfs(int x, int y) {
// 检查路径是否在边界上,已在边界上则更新答案
if (x == 0 || y == 0 || x == n - 1 || y == n - 1) {
bestAnswer = min(bestAnswer, *max_element(roses, roses + 4));
return;
}
// 尝试四个方向移动
for (int d = 0; d < 4; d++) {
int newX = x + dx[d], newY = y + dy[d];
if (newX < 0 || newX >= n || newY < 0 || newY >= n) continue;
if (visited[newX][newY]) continue;
for (int i = 0; i < 4; i++) {
int xx = newX, yy = newY;
if (i == 1) { xx = newY; yy = n - 1 - newX; }
if (i == 2) { xx = n - 1 - newX; yy = n - 1 - newY; }
if (i == 3) { xx = n - 1 - newY; yy = newX; }
roses[i] += grid[xx][yy] == 'R';
}
// 剪枝
if (*max_element(roses, roses + 4) < bestAnswer) {
visited[newX][newY] = true;
dfs(newX, newY);
visited[newX][newY] = false;
}
for (int i = 0; i < 4; i++) {
int xx = newX, yy = newY;
if (i == 1) { xx = newY; yy = n - 1 - newX; }
if (i == 2) { xx = n - 1 - newX; yy = n - 1 - newY; }
if (i == 3) { xx = n - 1 - newY; yy = newX; }
roses[i] -= grid[xx][yy] == 'R';
}
}
}
int main() {
cin.tie(0); cout.tie(0); ios::sync_with_stdio(false);
while (cin >> n && n) {
for (int i = 0; i < n; i++) cin >> grid[i];
// 先直走,确定一个初始最优值
bestAnswer = 0;
for (int i = 0; i < 4; i++) {
int cnt = 0, x = n / 2, y = n / 2;
while (true) {
x += dx[i], y += dy[i];
if (x < 0 || x >= n || y < 0 || y >= n) break;
cnt += grid[x][y] == 'R';
}
bestAnswer = max(bestAnswer, cnt);
}
memset(roses, 0, sizeof roses);
memset(visited, 0, sizeof visited);
visited[n / 2][n / 2] = 1;
dfs(n / 2, n / 2);
cout << "At most " << bestAnswer << " rose(s) trampled.\n";
}
return 0;
}
算法分析
- 时间复杂度:指数级,最坏情况下需要探索所有可能的路径
- 空间复杂度:O(N2)O(N^2)O(N2),主要用于访问标记数组
- 运行时间:1520ms1520 \texttt{ms}1520ms(在评测系统中)
- 优点:实现简单直观,易于理解
- 缺点:效率较低,搜索空间大,仅适用于小规模问题
存在的问题
朴素 DFS\texttt{DFS}DFS 虽然能够解决问题,但由于搜索空间呈指数增长,当网格尺寸较大时效率很低。我们需要更高效的剪枝策略来减少不必要的搜索。
优化思路:启发式剪枝 DFS\texttt{DFS}DFS
解题思路
为了加速搜索过程,我们引入启发式剪枝。核心思想是:预处理每个位置到边界的最少玫瑰数,作为从该位置到边界的下界估计。在搜索过程中,如果当前某个方向已踩玫瑰数加上从该位置到边界的最少玫瑰数已经不小于当前最优解,那么这个分支不可能产生更好的解,可以剪枝。
算法改进
- 预处理阶段:使用 BFS\texttt{BFS}BFS 从边界向内传播,计算 minRoses[x][y]\texttt{minRoses}[x][y]minRoses[x][y],表示从位置 (x,y)(x, y)(x,y) 到任意边界的最少玫瑰数
- 启发式剪枝:对于每个方向 iii,如果 roses[i]+minRoses[xx][yy]≥bestAnswer\texttt{roses}[i] + \texttt{minRoses}[xx][yy] \ge \texttt{bestAnswer}roses[i]+minRoses[xx][yy]≥bestAnswer,则剪枝
- 更精细的状态管理:将四个方向的玫瑰计数更新合并到一次循环中,减少重复计算
代码实现
// Be Wary of Roses
// UVa ID: 10798
// Verdict: Accepted
// Submission Date: 2025-12-23
// UVa Run Time: 0.240s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net
#include <bits/stdc++.h>
using namespace std;
int n;
string grid[32];
int bestAnswer;
int dx[4] = {0, 1, -1, 0}, dy[4] = {1, 0, 0, -1};
int minRoses[32][32];
int visited[32][32], roses[4];
// 预计算从边界到某个位置踩下的最少玫瑰数量作为启发式剪枝
void bfs() {
memset(minRoses, 0x3f, sizeof minRoses);
queue<pair<int, int>> q;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
if (i == 0 || i == n - 1 || j == 0 || j == n - 1) {
minRoses[i][j] = (grid[i][j] == 'R') ? 1 : 0;
q.push({i, j});
}
while (!q.empty()) {
pair<int, int> xy = q.front(); q.pop();
for (int d = 0; d < 4; d++) {
int nx = xy.first + dx[d], ny = xy.second + dy[d];
if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
if (grid[nx][ny] == 'P') continue;
int v = minRoses[xy.first][xy.second] + ((grid[nx][ny] == 'R') ? 1 : 0);
if (v < minRoses[nx][ny]) {
minRoses[nx][ny] = v;
q.push({nx, ny});
}
}
}
}
void dfs(int x, int y) {
// 检查路径是否在边界上,已在边界上则更新答案
if (x == 0 || y == 0 || x == n - 1 || y == n - 1) {
bestAnswer = min(bestAnswer, *max_element(roses, roses + 4));
return;
}
// 尝试四个方向移动
int tmp[4];
for (int d = 0; d < 4; d++) {
int newX = x + dx[d], newY = y + dy[d];
if (newX < 0 || newX >= n || newY < 0 || newY >= n) continue;
if (visited[newX][newY]) continue;
bool valid = true;
for (int i = 0; i < 4; i++) {
// 利用对称性获取四条路径要走的下一个位置
int xx = newX, yy = newY;
if (i == 1) { xx = newY; yy = n - 1 - newX; }
if (i == 2) { xx = n - 1 - newX; yy = n - 1 - newY; }
if (i == 3) { xx = n - 1 - newY; yy = newX; }
// 检查要走的位置是否有玫瑰
tmp[i] = grid[xx][yy] == 'R';
// 剪枝
// 从当前位置到边界至少还需要踩下的玫瑰数量加上到达当前位置已经踩下的玫瑰数量,
// 如果该数量大于等于最优值,则该条路径可以放弃继续搜索
if (roses[i] + minRoses[xx][yy] >= bestAnswer) {
valid = false;
break;
}
}
if (!valid) continue;
for (int i = 0; i < 4; i++) roses[i] += tmp[i];
visited[newX][newY] = true;
dfs(newX, newY);
visited[newX][newY] = false;
for (int i = 0; i < 4; i++) roses[i] -= tmp[i];
}
}
int main() {
cin.tie(0); cout.tie(0); ios::sync_with_stdio(false);
while (cin >> n && n) {
for (int i = 0; i < n; i++) cin >> grid[i];
// 确定从边界到任意一个位置至少需要踩下多少玫瑰
bfs();
// 先直走,确定一个初始最优值
bestAnswer = 0;
for (int i = 0; i < 4; i++) {
int cnt = 0, x = n / 2, y = n / 2;
while (true) {
x += dx[i], y += dy[i];
if (x < 0 || x >= n || y < 0 || y >= n) break;
cnt += grid[x][y] == 'R';
}
bestAnswer = max(bestAnswer, cnt);
}
memset(roses, 0, sizeof roses);
memset(visited, 0, sizeof visited);
visited[n / 2][n / 2] = 1;
dfs(n / 2, n / 2);
cout << "At most " << bestAnswer << " rose(s) trampled.\n";
}
return 0;
}
算法分析
- 时间复杂度:预处理 O(N2)O(N^2)O(N2),搜索过程因剪枝大幅减少
- 空间复杂度:O(N2)O(N^2)O(N2),增加了 minRoses\texttt{minRoses}minRoses 数组
- 运行时间:240ms240 \texttt{ms}240ms,效率提升约 666 倍
- 优点:剪枝效果显著,效率高,实现相对简单
- 缺点:启发式函数可能不够紧,仍可能搜索不必要分支
进一步优化空间
虽然启发式剪枝显著提升了效率,但我们仍然可能搜索许多不会产生更优解的状态。一个自然的想法是:能否系统地、按最优性顺序探索状态,而不是依赖深度优先的顺序?
高级思路:优先队列 BFS\texttt{BFS}BFS 与状态记忆化
解题思路
我们采用优先队列 BFS\texttt{BFS}BFS(类似 Dijkstra\texttt{Dijkstra}Dijkstra 算法)的策略,按当前最坏情况玫瑰数从小到大扩展状态。这样可以保证首次找到的解就是最优解。同时,我们使用六维状态记忆化避免重复搜索相同状态。
算法设计
- 状态定义:(x,y,l,u,r,d)(x, y, l, u, r, d)(x,y,l,u,r,d),其中:
- (x,y)(x, y)(x,y):第一象限路径的当前位置(绝对坐标)
- l,u,r,dl, u, r, dl,u,r,d:四个方向(北、东、南、西)已踩的玫瑰数
- 优先队列:按 max(l,u,r,d)\max(l, u, r, d)max(l,u,r,d) 从小到大排序
- 状态记忆化:visited[x][y][l][u][r][d]\texttt{visited}[x][y][l][u][r][d]visited[x][y][l][u][r][d] 记录已访问状态
- 扩展策略:每次从队列取出最坏情况最小的状态,尝试四个方向移动
- 终止条件:当尝试移动到网格外时,返回当前状态的最大玫瑰数
代码实现
// Be Wary of Roses
// UVa ID: 10798
// Verdict: Accepted
// Submission Date: 2025-12-23
// UVa Run Time: 0.270s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 24, MAXM = 12;
int offset[4][2] = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}};
struct state {
int x, y, l, u, r, d, w;
state (int x = 0, int y = 0, int l = 0, int u = 0, int r = 0, int d = 0): x(x), y(y), l(l), u(u), r(r), d(d) {
w = max(max(l, r), max(u, d));
}
bool operator<(const state& s) const { return w > s.w; }
};
int n, visited[MAXN][MAXN][MAXM][MAXM][MAXM][MAXM];
string g[MAXN];
int bfs (int x, int y) {
memset(visited, 0, sizeof(visited));
priority_queue<state> q;
visited[x][y][0][0][0][0] = 1;
q.push(state(x, y, 0, 0, 0, 0));
while (!q.empty()) {
state current = q.top(); q.pop();
for (int i = 0; i < 4; i++) {
state nxt = current;
int x = current.x + offset[i][0], y = current.y + offset[i][1];
if (x < 0 || x >= n || y < 0 || y >= n) return current.w;
nxt.x = x, nxt.y = y;
if (g[x][y] == 'R') nxt.l++;
if (g[y][n - 1 - x] == 'R') nxt.u++;
if (g[n - 1 - x][n - 1 - y] == 'R') nxt.r++;
if (g[n - 1 - y][x] == 'R') nxt.d++;
if (visited[x][y][nxt.l][nxt.u][nxt.r][nxt.d]) continue;
visited[x][y][nxt.l][nxt.u][nxt.r][nxt.d] = 1;
q.push(state(x, y, nxt.l, nxt.u, nxt.r, nxt.d));
}
}
return -1;
}
int main () {
cin.tie(0); cout.tie(0); ios::sync_with_stdio(false);
while (cin >> n && n) {
for (int i = 0; i < n; i++) cin >> g[i];
cout << "At most " << bfs(n / 2, n / 2) << " rose(s) trampled.\n";
}
return 0;
}
旋转公式的数学推导
代码中的旋转计算基于以下坐标变换(假设网格索引从 000 开始):
- 北(0∘0^\circ0∘):(x,y)(x, y)(x,y)
- 东(90∘90^\circ90∘):(y,n−1−x)(y, n-1-x)(y,n−1−x)
- 南(180∘180^\circ180∘):(n−1−x,n−1−y)(n-1-x, n-1-y)(n−1−x,n−1−y)
- 西(270∘270^\circ270∘):(n−1−y,x)(n-1-y, x)(n−1−y,x)
这些变换确保了四个路径关于中心点的对称性。
算法分析
- 时间复杂度:O(N2×R4)O(N^2 \times R^4)O(N2×R4),其中 RRR 是最大玫瑰数(约 101010)
- 空间复杂度:O(N2×R4)O(N^2 \times R^4)O(N2×R4),六维状态数组
- 运行时间:240ms240 \texttt{ms}240ms,与启发式剪枝 DFS\texttt{DFS}DFS 相当
- 优点:保证找到最优解,逻辑清晰,无过度剪枝风险
- 缺点:状态空间大,内存消耗较高,实现稍复杂
状态空间分析
对于 N=21N=21N=21,每个坐标维度有 212121 种可能,每个玫瑰数维度最多 111111 种可能(0−100-100−10),总状态数约为:
21×21×11×11×11×11≈6.5×10621 \times 21 \times 11 \times 11 \times 11 \times 11 \approx 6.5 \times 10^621×21×11×11×11×11≈6.5×106
这在现代计算机上是可接受的。
算法对比与总结
三种算法对比表
| 算法 | 核心思想 | 时间复杂度 | 空间复杂度 | 运行时间 | 优点 | 缺点 |
|---|---|---|---|---|---|---|
| 朴素 DFS\texttt{DFS}DFS | DFS\texttt{DFS}DFS + 基本剪枝 | 指数级 | O(N2)O(N^2)O(N2) | 1520ms1520 \texttt{ms}1520ms | 实现简单,直观易懂 | 效率低,搜索空间大 |
| 启发式 DFS\texttt{DFS}DFS | DFS\texttt{DFS}DFS + 下界估计剪枝 | 指数级(大幅剪枝) | O(N2)O(N^2)O(N2) | 240ms240 \texttt{ms}240ms | 剪枝高效,实现相对简单 | 可能剪枝不足或过度 |
| 优先队列 BFS\texttt{BFS}BFS | BFS\texttt{BFS}BFS + 状态记忆化 | O(N2R4)O(N^2 R^4)O(N2R4) | O(N2R4)O(N^2 R^4)O(N2R4) | 240ms240 \texttt{ms}240ms | 保证最优性,逻辑清晰 | 状态空间大,内存消耗高 |
关键解题技巧总结
- 对称性利用:将四方向问题简化为单方向搜索,是本题的核心优化
- 搜索策略选择:
- DFS\texttt{DFS}DFS 适合结合强力剪枝
- 优先队列 BFS\texttt{BFS}BFS 适合保证最优性
- 剪枝策略:
- 简单剪枝:比较当前最优解
- 启发式剪枝:使用下界估计
- 状态设计:合理的状态表示可以避免重复搜索
推荐算法
对于本题,推荐使用第二种算法(启发式剪枝 DFS\texttt{DFS}DFS),原因如下:
- 效率高:240ms240 \texttt{ms}240ms的运行时间完全满足要求
- 实现简单:相对于优先队列 BFS\texttt{BFS}BFS 的六维状态,DFS\texttt{DFS}DFS 实现更直观
- 内存友好:不需要存储大量状态,内存消耗小
- 稳定可靠:在实际评测中表现稳定
扩展思考
- 能否进一步优化?可以考虑结合 A*\texttt{A*}A* 搜索,使用更精确的启发函数
- 更大规模的问题:如果 NNN 更大,可能需要更高效的剪枝或近似算法
- 实际问题应用:这种对称性搜索技巧可以应用于许多机器人路径规划问题
总结
本题是一道经典的搜索优化问题,通过分析三种不同的解题思路,我们看到了算法设计的演进过程:从朴素搜索到启发式剪枝,再到系统性的优先队列搜索。每种方法都有其适用场景和优缺点,理解这些差异有助于我们在解决类似问题时选择最合适的策略。
这道题不仅考察了搜索算法的实现能力,更重要的是考察了对问题对称性的洞察力和剪枝策略的设计能力。掌握这些技巧对于解决竞赛中的搜索优化问题至关重要。
640

被折叠的 条评论
为什么被折叠?



