用最少的动作打开一个锁

时间:2013-06-11 20:55:24

标签: algorithm dynamic-programming

考虑一个由车轮系统组成的锁。每个轮子按顺序有26个字母,每个轮子用'a'初始化。如果向上移动一个轮子,该轮子的显示将移动到字母表的下一个字母;另一方面,向下移动一个轮,将显示切换到字母表的前一个字母。例如:

['a'] ->  UP  -> ['b']
['b'] -> DOWN -> ['a']
...
['z'] ->  UP  -> ['a']
['a'] -> DOWN -> ['z']

只需轻弹就可以将任何轮子的连续子序列移动到同一方向。这与以单一运动方式移动子序列的所有轮子具有相同的效果。例如,如果目标字符串为'zzzzzzzzz',则将'a'更改为'z'的单个移动会将整个轮子序列从'a'更改为'z' ,从而到达目标字符串 - 打开锁。

如何确定打开锁定的移动次数最少?这个问题有动态解决方案吗?该算法必须产生以下结果:

           Target string         | # moves
   ______________________________ __________
1 | abcxyz                       |    5
2 | abcdefghijklmnopqrstuvwxyz   |   25
3 | aaaaaaaaa                    |    0
4 | zzzzzzzzz                    |    1
5 | zzzzbzzzz                    |    3

案例1,目标abcxyz

aaa[aaa] -> DOWN -> aaazzz
aaa[zz]z -> DOWN -> aaayyz
aaa[y]yz -> DOWN -> aaaxyz
a[aa]xyz ->  UP  -> abbxyz
ab[b]xyz ->  UP  -> abcxyz

案例5,目标zzzzbzzzz

[aaaaaaaaa] -> DOWN -> zzzzzzzzz
zzzz[z]zzzz ->  UP  -> zzzzazzzz
zzzz[a]zzzz ->  UP  -> zzzzbzzzz

2 个答案:

答案 0 :(得分:4)

此问题可能会重申为:

  

将字符串S转换为仅包含'a'的字符串的最小移动次数是多少?

<强>定义

将连续的子序列视为字符串中相等字符的序列。最小的连续子序列自然是单个字符。如果你规范化小的子序列,你自然会得到更大的子序列,最终达到一个子序列 - 整个字符串。

规范化的内容

只能向上或向下移动一个字符,因此,一个字符本身就是UP和DOWN移动的序列。表示字符的最坏情况是字母表中间的字母,这需要至少len(alphabet) / 2个移动进行描述。在字母{a..z}中,最糟糕的情况是'm''n'

由于我们希望最大限度地减少移动次数,因此我们需要拉下来字母C <= m拉起那些C >= n。因此,为了最小化归一化过程,我们必须找到需要相等归一化移动的最大子序列。例如,如果我们有一个目标zzzzbzzzz,我们知道最小方向为UUUUDUUUU - U代表UP,D代表。

<强>正火

对于每次移动,计数器都会递增,从而产生转换字符串所需的最少移动次数。考虑到上面的例子,我们可以采取以下步骤:

# = 0 | zzzzbzzzz | UUUUDUUUU  (choose the smallest subsequence to normalize)
# = 1 | zzzzazzzz | UUUUDUUUU  (since 'a' is the base character, we choose
                              the move that increases the largest subsequence;
                              if 'a' was the first or last character,
                              moving it would simply be overhead)
# = 2 | zzzzzzzzz | UUUUUUUUU  (choose the subsequence to normalize)
# = 3 | aaaaaaaaa | _________  (choose the subsequence to normalize)

另一个例子,目标字符串为abcxyz

# = 0 | abcxyz | _DDUUU  (choose the smallest subsequence to normalize)
# = 1 | aabxyz | __DUUU  (choose the smallest subsequence to normalize)
# = 2 | aaaxyz | ___UUU  (choose the smallest subsequence to normalize)
# = 3 | aaayza | ___UU_  (choose the smallest subsequence to normalize)
# = 4 | aaazaa | ___U__  (choose the smallest subsequence to normalize)
# = 5 | aaaaaa | ______  (choose the smallest subsequence to normalize)

修改

正如@ user1884905指出的那样,此解决方案,如建议的那样,不是最佳。对于目标字符串mn,算法不会产生最佳解决方案:

# = 0  | mn | DU  (choose the smallest subsequence to normalize)
# = 1  | ln | DU  (choose the smallest subsequence to normalize)
# = 2  | kn | DU  (choose the smallest subsequence to normalize)
...
# = 12 | an | _U  (choose the smallest subsequence to normalize)
# = 13 | ao | _U  (choose the smallest subsequence to normalize)
# = 14 | ap | _U  (choose the smallest subsequence to normalize)
...
# = 24 | aa | __  (choose the smallest subsequence to normalize)

这不是最佳选择,因为以下步骤需要较少的动作:

#0    #1    #2    ...    #12
mn -> mm -> ll -> ... -> aa

贪婪算法的最佳子结构可能在于减少字符串之间的全局距离,而不是关注这些字符与基本情况之间的差异('a' )。

答案 1 :(得分:2)

由于这只是一些额外的信息,也许是一些优化,这应该是对鲁本斯答案的评论,但在答案中我可以更好地解释它,它也可以对提问者有用。

我也使用鲁本斯的反面思想。

所以,我认为没有必要将a转换为其他内容。如果这是正确的(我没有反例),我们不应该将某些东西转向错误的方向(我没有数学证明,但可能这是正确的)。

因此,UD s的每个子序列每次都会以一个动作旋转。该算法不会花费O(n ^ 2)时间。这是算法:

我们打电话给鲁本斯的字符串direction string

  1. 将计数器设为0
  2. 计算方向字符串
  3. 扫描方向字符串。
  4. 如果找到U或D字母的连续子序列,请将目标字符串旋转到a的位置并递增计数器(每个子序列一次)。
  5. 如果有任何操作,请返回步骤2
  6. 此算法会将每个轮子旋转到a,并且最多会在k/2次扫描后完成,其中k是字母表元素的数量,所以这可能是一个在线性时间内运行的解决方案。


    也许有一个解决方案甚至更少的操作。这只是一个想法,找到增加,减少或“山形”子子序列并提取最大值。例如:我们可以说没有计算,解决的成本

    • abcde
    • ecb
    • abceeddcb

    等于解决单个e

    的成本

    编辑:我见过user1884905的反例。所以我的算法找不到最优解,但它可以用来找到正确的算法,所以我还没有删除它。

    编辑2:使用示例目标字符串的另一个想法:可以计算平均字母。它是与目标字符串的字母之间的距离之和最小的那个。每个字母都应该使用上面的算法旋转,然后整个字符串可以旋转到aaaaaaaaaa。由于字母表是循环的,因此可能存在多个平均字母(如问题中的第二个示例),在这种情况下,我们应该选择与a的距离最小的字母。