消除字符串中特定双字词实例的最小相邻交换数及递归/DP解法入门
嘿,这个问题确实挺棘手的——当引入第三种字符后,纯a/b场景下“把所有b移到a前面”的简单逻辑直接失效了,得重新梳理子问题的拆分方式。我来一步步给你拆解怎么用动态规划(以及对应的递归记忆化)解决它:
首先明确:我们的目标是通过最少的相邻交换,把原字符串转换成不存在任何相邻"ab"子串的合法字符串。注意,合法字符串不是要求所有a都在b前面(比如"acb"是合法的,因为没有相邻ab),只是不能有a直接紧跟在b前面?不对,准确说是不能有a后面直接跟b——也就是任何位置i都不能满足s[i]='a'且s[i+1]='b'。
状态定义
我们定义dp[a][b][last]表示:已经处理了原字符串中的a个a、b个b(剩下的就是已处理的c的数量,c = 已处理总字符数 - a - b),且当前已构建的合法子串的最后一个字符是last(last可以是'a'、'b'、'c',或者初始的None表示还没有处理任何字符)时,所需的最小交换次数。
初始化
初始状态只有dp[0][0][None] = 0,表示处理0个字符时,交换次数为0,其他所有状态初始化为无穷大(表示暂时不可达)。
状态转移
我们按顺序遍历原字符串的每个字符(索引从0到n-1),对于每个当前可达的状态(a, b, last),根据当前处理的字符ch进行转移:
情况1:当前字符是'a'
a可以插入到当前子串的任何位置(因为插入a不会导致出现相邻ab),为了最小化交换次数,我们选择把它放到子串末尾(不需要额外交换):
- 新状态:
(a+1, b, 'a') - 交换次数增量:0(保持原顺序,无需交换)
- 新的交换次数:
dp[a][b][last] + 0
情况2:当前字符是'b'
b不能插入到a的后面(否则会形成相邻ab),所以分两种情况:
- 如果当前子串的最后一个字符
last != 'a'(包括空、b、c):可以直接放到子串末尾,交换次数增量0,新状态(a, b+1, 'b'),交换次数为dp[a][b][last] + 0。 - 如果当前子串的最后一个字符
last == 'a':必须把b插入到所有非a字符的后面(也就是不能跟在a后面),此时交换次数增量等于当前已处理的a的数量a(因为需要把b从当前位置移到所有a的前面,需要交换a次),新状态(a, b+1, 'b'),交换次数为dp[a][b][last] + a。
情况3:当前字符是'c'
c可以和任何字符相邻,所以直接放到子串末尾即可,交换次数增量0:
- 新状态:
(a, b, 'c') - 交换次数:
dp[a][b][last] + 0
最终结果
遍历完所有字符后,所有dp[A][B][last](其中A是原字符串中a的总数,B是b的总数,last可以是'a'、'b'、'c')中的最小值就是答案。
如果想用递归的方式,本质就是把动态规划转换成带记忆化的递归函数:
- 定义递归函数
dfs(a, b, last),返回处理了a个a、b个b,最后一个字符是last时的最小交换次数。 - 递归终止条件:当
a + b + c == n(n是字符串长度),返回0。 - 对于当前要处理的字符(根据已处理的总数
a+b+c找到原字符串中对应的字符ch),按照上面的状态转移逻辑,递归计算所有可能的下一个状态的最小交换次数,加上对应的增量,然后返回最小值。 - 用一个记忆化字典或者三维数组存储已经计算过的
(a, b, last)状态,避免重复计算。
比如原字符串是"cab"(包含1个a、1个b、1个c):
- 初始状态
dp[0][0][None] = 0。 - 处理第一个字符
'c',转移到dp[0][0]['c'] = 0。 - 处理第二个字符
'a',转移到dp[1][0]['a'] = 0。 - 处理第三个字符
'b',当前状态是(1,0,'a'),last是a,所以增量为1,转移到dp[1][1]['b'] = 0+1=1。 - 最终结果就是1,正确——因为原字符串
"cab"包含相邻的ab,需要1次交换变成"cba"或者"bac"(都是合法的)。
内容的提问来源于stack exchange,提问作者Sajad

