字符串改写求 10 步内最少变换次数:把规则看作有向图上的边,从 A、B 两端各做 5 层 BFS,用逆规则反向扩展,在两侧的交集里取距离和最小值。
OJ: roj
题目 ID: 3082
难度:提高
标签:搜索广度优先搜索BFS双向BFS
创建: 2026-10-01 15:25
更新: 2026-10-07 13:50
形式化题目
给定两个字符串
求从 NO ANSWER!。
例如 abcd,xyz,规则为 abc -> xu、ud -> y、y -> yz,则
abcd -> xud -> xy -> xyz,最少 3 步。
正解
思路
把每个长度不超过 20 的字符串看作图中的一个点,一条规则
先考虑朴素 BFS:从
双向 BFS 消除这个瓶颈:从
为什么可以在
- 若
,则 ,两侧早已在第一轮就发现了它,交点和会被某一轮统计到; - 若
,则 且 ,两侧都在前 层内发现了 。
而
以样例为例,两侧各扩 2 层就已在 xy 处接上(路径 abcd -> xud -> xy <- xyz,
flowchart LR
A["abcd<br/>A侧第0层"] --> S1["xud<br/>A侧第1层"] --> S2["xy<br/>A侧第2层"]
T["xyz<br/>B侧第0层"] --> S2
S2 --> S3["3 = 2 + 1"]
xy,靠的是逆规则 xyz 里的 yz 换回 y);
第 2 轮结束时 xy,交集出现,距离和
两个实现细节:
- 规则左串在原串中可能重叠出现(如
aaaa中a -> b命中 4 次),找到一个出现后要从下一位继续find,不能跳过; - 两侧分别用
dist字典兼当 visited 与距离表,队列中串的距离就是字典里存的值,不需要另外编码。
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 15:25
# update_at: 2026-10-01 15:25
import sys
from collections import deque
from collections.abc import Iterator
SIDE_STEPS = 5 # 双向 BFS 每侧最多扩展的层数,两侧合计覆盖题面的 10 步上限
def expand(source: str, rules: list[tuple[str, str]]) -> Iterator[str]:
"""把 source 中某个规则的左串替换一次,产出所有一步可达的串(可能重复)。"""
for left, right in rules:
at = source.find(left)
while at != -1:
yield source[:at] + right + source[at + len(left):]
at = source.find(left, at + 1) # 从下一位再找,覆盖重叠出现
def layered_bfs(queue: deque[str], dist: dict[str, int], rules: list[tuple[str, str]]) -> None:
"""把 queue 这一侧向外扩展一层:新串的距离 = 队首距离 + 1。"""
for _ in range(len(queue)):
source = queue.popleft()
if dist[source] == SIDE_STEPS: # 该侧已到层数上限,不再外扩
continue
for nxt in expand(source, rules):
if nxt not in dist:
dist[nxt] = dist[source] + 1
queue.append(nxt)
def shortest_steps(start: str, target: str, rules: list[tuple[str, str]]) -> int | None:
"""双向 BFS:返回 A 到 B 的最少变换步数,超过 10 步则返回 None。"""
if start == target:
return 0
back_rules = [(right, left) for left, right in rules] # 反向侧要沿逆规则走
side_rules = (rules, back_rules)
dist = ({start: 0}, {target: 0}) # dist[side]:该侧已发现的串 → 到所属端点的步数
queues = (deque((start,)), deque((target,)))
for k in range(1, SIDE_STEPS + 1):
for side in (0, 1):
layered_bfs(queues[side], dist[side], side_rules[side])
# 交点 s 给出一条 A→s→B 的合法路径,两侧距离之和 ≥ 答案;
# 当和 ≤ 2k 时,最短路的中间点两侧都已在 k 层内发现,该和恰为答案
best = min((dist[0][s] + dist[1][s]
for s in dist[0].keys() & dist[1].keys()), default=None)
if best is not None and best <= 2 * k:
return best
return None
def solve() -> None:
data = iter(sys.stdin.read().split())
start = next(data)
target = next(data)
rules = []
for left in data:
right = next(data)
rules.append((left, right)) # 变换规则
steps = shortest_steps(start, target, rules)
print(steps if steps is not None else "NO ANSWER!")
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 17:39
* update_at: 2026-10-06 17:39
*/
#include <iostream>
#include <string>
#include <queue>
#include <map>
#include <vector>
#include <climits>
typedef long long ll;
const int SIDE_STEPS = 5; // 双向 BFS 每侧最多扩展的层数,两侧合计覆盖题面的 10 步上限
std::vector<std::pair<std::string, std::string> > rules; // 正向规则 left -> right
std::vector<std::pair<std::string, std::string> > back_rules; // 反向规则 right -> left,B 侧沿逆规则扩展
std::map<std::string, int> dist_a; // A 侧已发现的串 -> 到 A 的最少步数
std::map<std::string, int> dist_b; // B 侧已发现的串 -> 到 B 的最少步数
std::queue<std::string> qa; // A 侧待扩展队列
std::queue<std::string> qb; // B 侧待扩展队列
// 把 q 中当前一层的所有串向 rs 中的规则扩展一层,距离写进 dist。
void extend_layer(std::queue<std::string> &q, std::map<std::string, int> &dist,
std::vector<std::pair<std::string, std::string> > &rs) {
int layer_size = q.size();
for (int idx = 0; idx < layer_size; idx++) {
std::string s = q.front();
q.pop();
int d = dist[s];
if (d == SIDE_STEPS) {
continue; // 该侧已到层数上限,不再向外扩展
}
for (int r = 0; r < (int)rs.size(); r++) {
std::string left = rs[r].first;
std::string right = rs[r].second;
// 找 left 在 s 中的每次出现;找到一次后从下一位继续,覆盖重叠出现
size_t pos = s.find(left, 0);
while (pos != std::string::npos) {
std::string nxt = s.substr(0, pos) + right + s.substr(pos + left.size());
if (dist.find(nxt) == dist.end()) {
dist[nxt] = d + 1;
q.push(nxt);
}
pos = s.find(left, pos + 1);
}
}
}
}
int main() {
std::string start;
std::string target;
if (!(std::cin >> start >> target)) {
return 0;
}
if (start == target) {
std::cout << 0 << std::endl;
return 0;
}
std::string left;
std::string right;
while (std::cin >> left >> right) {
rules.push_back(std::make_pair(left, right));
back_rules.push_back(std::make_pair(right, left));
}
dist_a[start] = 0;
qa.push(start);
dist_b[target] = 0;
qb.push(target);
for (int k = 1; k <= SIDE_STEPS; k++) {
extend_layer(qa, dist_a, rules);
extend_layer(qb, dist_b, back_rules);
// 交点 s 给出一条 A -> s -> B 的合法路径,两侧距离之和是一条真实路径长度;
// 当和 <= 2k 时,最短路的中间点两侧都已在 k 层内发现,该和恰为答案
int best = INT_MAX;
std::map<std::string, int>::iterator it;
for (it = dist_a.begin(); it != dist_a.end(); ++it) {
std::map<std::string, int>::iterator jt = dist_b.find(it->first);
if (jt != dist_b.end()) {
int sum = it->second + jt->second;
if (sum < best) {
best = sum;
}
}
}
if (best <= 2 * k) {
std::cout << best << std::endl;
return 0;
}
}
std::cout << "NO ANSWER!" << std::endl;
return 0;
}复杂度
设串长上限
图示解析
为什么反向侧要用逆规则
正向规则 ud 可以换成 y。从 xy 接上:
| 层数 |
A 侧(正向扩展,规则 |
B 侧(逆向扩展,规则 |
|---|---|---|
| 0 | abcd |
xyz |
| 1 | xud(abc 换成 xu) |
udxz(y 换回 ud)、xy(yz 换回 y) |
| 2 | xy(ud 换成 y) |
—— 扩展时发现交集 |
两侧集合的交集 xy 满足
总结
- 本题是"字符串改写"型最短路:串是点、规则的一次命中是一条边,边权为 1,BFS 求最少步数。
- 深度上限 10 保证答案存在时一定能搜到,但单侧 BFS 第 10 层会指数爆炸;双向 BFS 每侧只扩 5 层,在中间接上,复杂度从
量级降到 量级。 - 三个易错点:反向侧必须用逆规则扩展;规则左串的重叠出现要逐位
find找全;停止条件是"交集最小距离和"而不是"一出现交集就返回"(后者可能错过更短的、中间点还没被两侧同时发现的路径)。 - 若 10 步内两侧始终没有交点,输出
NO ANSWER!。