LeetCode-Problem87#较典型的三维动态规划问题
1) 题目
2) 解析(参考自此精选解答)
- 设有两个字符串S和T,S想通过上述扰乱得到T
- 如果len(S) != len(T),或者S/T中有对方没有的字符(如S=ab/T=ac)或者比双方唯一字符集不等(如S=aab/T=aaa,前者唯一字符集[ab],后者[a])
- 如果长度一样,S/T可转换,必有某种切分使得S=S1||S2,T=T1||T2(||表示拼接),且
- 情况一:没交换,S1=>T1 AND S2=>T2
- 情况二:交换了,S1=>T2 AND S2=>T1
- 上述“某种切分”意味着一次全局长度遍历;同时,切分得到的局部片段,又蕴含着新的切分和新的局部长度遍历;所以这个问题可视为递归问题,
分析角度是从上至下
;而计算时由下至上的 递归+缓存 往往就是动态规划; - 状态state:根据上述分析,最直截了当的状态就是 s t a t e = d p [ i ] [ k ] [ j ] [ t ] = T r u e / F a l s e state=dp[i][k][j][t]=True/False state=dp[i][k][j][t]=True/False,表示S[i:k]和T[j:t]是否可以转换;考虑可转换前提是长度一样,即 k − i = t − j = l k-i=t-j=l k−i=t−j=l,则可简化为 s t a t e = d p [ i ] [ j ] [ l ] state=dp[i][j][l] state=dp[i][j][l],l为局部长度
- 由此,我们最后要求的是
d
p
[
0
]
[
0
]
[
L
]
dp[0][0][L]
dp[0][0][L],其中
L
=
l
e
n
(
S
)
=
l
e
n
(
T
)
L=len(S)=len(T)
L=len(S)=len(T),整个递归/转移方程如下,描述了上述情况一和情况二:
d p [ 0 ] [ 0 ] [ L ] = ⋃ 1 ≤ l ≤ L − 1 ( ( d p [ 0 ] [ 0 ] [ l ] ⋂ d p [ 0 + l ] [ 0 + l ] [ L − l ] ) ⋃ ( d p [ 0 ] [ 0 + L − l ] [ l ] ⋂ d p [ 0 + l ] [ 0 ] [ L − l ] ) ) dp[0][0][L]=\bigcup _{1\le l \le L-1} \Bigg(\bigg(dp[0][0][l] \bigcap dp[0+l][0+l][L-l]\bigg) \bigcup \bigg(dp[0][0+L-l][l] \bigcap dp[0+l][0][L-l]\bigg) \Bigg) dp[0][0][L]=1≤l≤L−1⋃((dp[0][0][l]⋂dp[0+l][0+l][L−l])⋃(dp[0][0+L−l][l]⋂dp[0+l][0][L−l])) - 显然上述递归式包含大量重复计算,比如计算
d
p
[
0
]
[
0
]
[
3
]
dp[0][0][3]
dp[0][0][3]和计算
d
p
[
0
]
[
0
]
[
4
]
dp[0][0][4]
dp[0][0][4],后者依赖于前者的结果;其他位置也一样,即对任意
i
,
j
∈
[
0
,
L
−
1
]
i,j\in[0,L-1]
i,j∈[0,L−1],要计算
d
p
[
i
]
[
j
]
[
l
]
dp[i][j][l]
dp[i][j][l],必须要先计算
d
p
[
i
]
[
j
]
[
1
]
dp[i][j][1]
dp[i][j][1]等长度小于
l
l
l的情况;由此可知,
计算角度是从下至上的
; - 综上,除了 l l l在L上的遍历,还需要两个字符串上各个位置 i / j i/j i/j的遍历,以及局部片段上 l l o c a l l^{local} llocal的遍历,代码如下,时间复杂度 O ( n 4 ) O(n^4) O(n4),空间复杂度 O ( n 3 ) O(n^3) O(n3)
class Solution:
def isScramble(self, s1: str, s2: str) -> bool:
# dp[i][j][k]表示s1的pos_i和s2的pos_j是否有长度为k的可转换字符串
# 返回dp[0][0][L],其中L为len(s1)=len(s2)=L
# 状态方程 dp[i][j][k] = OR(l in (1,k-1))[dp[i][j][l]&&dp[i+l][j+l][k-l]] OR
# OR(l in (1,k-1))[dp[i][j+k-l][l]]&&dp[i+l][j][k-l])
# 表示dp[i][j][k]=TRUE需要满足两大类中的某一类(调换或者不调换)
len_s1, len_s2 = len(s1), len(s2)
set_s1, set_s2 = set(s1), set(s2)
# 初步判断结果,节省时间
if len_s1 != len_s2 or len(set_s1.union(set_s2)) != len(set_s1) or len(set_s1.intersection(set_s2)) != len(set_s1):
return False
# 构造dp三维数组
dp = [[[False for _ in range(len_s1+1)] for _ in range(len_s1+1)] for _ in range(len_s1+1)]
# 初始化,形成由下至上计算的初始值
for i in range(0, len_s1):
for j in range(0, len_s1):
dp[i][j][1] = s1[i] == s2[j]
# 基于上述初始值开始回溯
for l in range(2, len_s1+1): # 长度遍历(字符串整体遍历)
for i in range(0, len_s1-l+1): # 位置遍历(s1),受限于局部子串长度l
for j in range(0, len_s1-l+1): # 位置遍历(s2),受限于局部子串长度l
for w in range(1, l): # 长度遍历(局部子串内部遍历)
dp[i][j][l] = (dp[i][j][w] and dp[i+w][j+w][l-w]) or (dp[i][j+l-w][w] and dp[i+w][j][l-w])
if dp[i][j][l]: # dp[i][j][l]出现True表示可转换,即可停止计算
break
return dp[0][0][len_s1]
- 总结:熟悉三维动态规划的应用场景和状态的意义,熟悉由上至下的递归分析以及由下至上的计算回溯