1. 问题描述:
已知有两个字串 A,B 及一组字串变换的规则(至多 6 个规则):
A1→B1
A2→B2
…
规则的含义为:在 A 中的子串 A1 可以变换为 B1、A2 可以变换为 B2…。
例如:A=abcd B=xyz
变换规则为:
abc → xu ud → y y → yz
则此时,A 可以经过一系列的变换变为 B,其变换的过程为:
abcd → xud → xy → xyz
共进行了三次变换,使得 A 变换为 B。
输入格式
输入格式如下:
A B
A1 B1
A2 B2
… …
第一行是两个给定的字符串 A 和 B。接下来若干行,每行描述一组字串变换的规则。
所有字符串长度的上限为 20。
输出格式
若在 10 步(包含 10 步)以内能将 A 变换为 B ,则输出最少的变换步数;否则输出 NO ANSWER!。
输入样例:
abcd xyz
abc xu
ud y
y yz
输出样例:
3
来源:https://www.acwing.com/problem/content/description/192/
2. 思路分析:
这道题目类似于1107题魔板,1107题中将每一个棋盘看成是一个状态,通过对棋盘进行相应的操作转换到其他状态,求解从起始状态到目标状态的最短距离,这道题目与1107题本质上是一样的,将每一个字符串看成是一个状态,通过对字符串中的某一个部分进行替换得到新的字符串,而新的字符串表示另外一个新的状态,我们也是需要求解从起始状态转移到目标状态的最短距离,这两题都属于最小步数模型的题目,可以使用bfs来解决,一般来说对于最小步数模型的题目,在搜索的过程中可以搜索到的状态数量是非常庞大的,有的棋盘类的题目可能会达到10 ^ 15或者10 ^ 20的状态数量,所以如果还是从起点开始一个方向搜索直到搜索到目标状态那么中间过程中遍历的状态数量是非常庞大的,非常容易MLE或者TLE,对于最小步数模型的题目一般可以使用双向广搜进行优化,双向广搜在搜索的时候是从两个方向同时搜索而不是从某一个方向一直搜到另外一个方向,两个方向搜索可以减少搜索到的状态数量。对于这道题目来说我们可以将每一个字符串看成是单独的状态,通过替换某些字符串将其转换到其他状态,最坏情况下每一步需要转换20 * 6的状态数目(每一次替换一个字符),而题目中要求最多可以走10步,所以总共的状态数目为:120 ^ 10,如果规定只有6个位置可以替换那么状态数目为:6 ^ 10,所以状态数目是非常庞大的,如果从起始状态开始搜一直到目标状态,肯定会MLE或者TLE的,所以需要双向广搜进行优化才可以通过,双向广搜的状态数目为2 * 6 ^ 5,所以优化之后的状态数目相对于单向bfs状态数目少了非常多;双向广搜需要使用两个队列,实现方式有很多种,一般每一次会选择队列中元素较少的一个方向扩展状态,这样可以使得两个方向在扩展的时候状态数目相对平衡;这道题目属于最小步数模型,所以需要使用哈希表来记录状态,因为使用的是python语言所以可以使用字典来记录所有已经得到的状态,并且使用哈希表的一个好处是能够记录到达某个状态的最短距离,所以需要声明从起始状态和终点状态使用到的队列qa,qb,字典da,db,只有当两个队列非空的时候才执行循环,因为当一个队列为空但是另外一个队列还有元素的时候说明起点与终点是不连通的,在搜索的时候判断起点方向还是终点方向对应的队列元素的数量多,选择元素数量较少的方向进行搜索,当搜索到另外一个方向出现过的状态的时候说明从初始状态搜索到了目标状态,这个时候得到的最短距离是最短的,直接返回这个最短距离即可,这里需要注意一个问题是双向广搜每一次在扩展的时候扩展的是同一深度的所有元素,也即同一层的元素,避免只扩展一个元素得到的不是最优解的情况。
3. 代码如下:
import sys
from typing import List
class Solution:
def extend(self, n: int, a: List[str], b: List[str], q: List[str], da: dict, db: dict):
# 队头元素的深度
d = da[q[0]]
# 使用最外面的while循环扩展一圈, 只要是深度相等说明在同一层, 每一次扩展同一层的节点这样可以找到最优解
while q and da[q[0]] == d:
# 注意是pop(0)而不是pop()一开始写错了导致MLE
t = q.pop(0)
# 最外层循环表示n个字符串的变换规则
for i in range(n):
for j in range(len(t)):
# 从t的第一个位置开始判断是否可以将t中某一部分字符串替换为b[i]
if j + len(a[i]) <= len(t) and t[j: j + len(a[i])] == a[i]:
s = t[0: j] + b[i] + t[j + len(a[i]):]
# 判断是否在a中出现过, 如果在a中出现需要跳过
if s in da: continue
# 判断是否在b中是否出现过, 如果在b中出现过说明从起点是可以到达终点的, 并且当前可以计算出从起点到终点的最短距离
if s in db: return da[t] + 1 + db[s]
# 更新到达状态s的最短距离
da[s] = da[t] + 1
# 将状态s加入到队列中
q.append(s)
# 返回大于10的数即可
return 11
def bfs(self, n: int, A: str, B: str, a: List[str], b: List[str]):
if A == B: return 0
# python中的双端队列好像不可以直接访问队头元素, 只能够弹出来所以这里使用列表来充当队列
qa, qb = [A], [B]
t = 0
# 哈希表记录最短距离
da, db = dict(), dict()
# 起点的距离为0
da[A] = db[B] = 0
count = 0
while qa and qb:
# 注意传递参数的顺序, 从终点方向往前搜参数需要反过来
if len(qa) < len(qb):
t = self.extend(n, a, b, qa, da, db)
else:
t = self.extend(n, b, a, qb, db, da)
if t <= 10: return t
count += 1
if count >= 10: return -1
return -1
def process(self):
# 初始状态为A, 目标状态为B
A, B = input().split()
# n记录字符串规则的数量
n = 0
# a, b存储字符串变换规则
a, b = list(), list()
while True:
# 判断是否还有输入
s = sys.stdin.readline().strip()
if not s: break
s1, s2 = s.split()
a.append(s1)
b.append(s2)
n += 1
t = self.bfs(n, A, B, a, b)
# t = -1说明无解
if t == -1:
print("NO ANSWER!")
else:
print(t)
if __name__ == '__main__':
Solution().process()