Leetcode:No.651 4 Keys Keyboard

Imagine you have a special keyboard with the following keys:
Key 1: (A): Prints one 'A' on screen.
Key 2: (Ctrl-A): Select the whole screen.
Key 3: (Ctrl-C): Copy selection to buffer.
Key 4: (Ctrl-V): Print buffer on screen appending it after what has already been printed.
Now, you can only press the keyboard for N times (with the above four keys), find out the maximum numbers of 'A' you can print on screen.
Example 1:
Input: N = 3
Output: 3
Explanation:
We can at most get 3 A's on screen by pressing following key sequence:
A, A, A
Example 2:
Input: N = 7
Output: 9
Explanation:
We can at most get 9 A's on screen by pressing following key sequence:
A, A, A, Ctrl A, Ctrl C, Ctrl V, Ctrl V
Note:

  1. 1 <= N <= 50
  2. Answers will be in the range of 32-bit signed integer.

看起来和2 keys keyboard有点像,但是并不一样。这里全选(以下简称a),复制(以下简称c),粘贴(以下简称v)比之前多了一步,而且问题是给步数n求能得到最大A的数量。(平A=直接按A)

注意这个一开始没有A,一个也没有。
可以先自己手算前几个(r代表结果result):
n=1,r=1
n=2,r=2
n=3,r=3
这几个非常明显。这时候应该稍微有点意识了,acv三步走实现加倍,如果本身比3小,那是不如平A的。
n=4,r=4
这时候n比3大,但还是不能用acv,因为acv必须要求至少3步,少了什么也没有,如果这里硬要用的话只能对1个A使用,太不划算。
这时候应该能想到,要想让acv三步至少不亏,那么之前至少要留有3个A,也就是说n=6的时候不亏。即:
n=6,r=6
前面的5自然还是一个个A拼出来:n=5,r=5

n=7的时候,又是一个需要思考的地方。按前面说的,6个A可以通过平A得到,也可以3个A再acv,acv的方法肯定更优,因为给后面留下了继续v的可能。
所以如果从n=6加v,结果是9.
当然还需要考虑使用acv的不同情况。可能性已经有不少了,比如只使用一轮acv+(+指1个或多个),或者极限的话可以在A之后使用连续两轮acv。这些结果都不如9大,但是可以提供一些思路。

到这里其实没太多必要继续推下去了。从最优角度来看,除了前面平A,后面应该都要用acv+及其组合。因为这里只给了步数,并不存在要套数字或者溢出的问题,所以尽量争取A的数量最多。
也就是说最优解应该是这种形状:
初始若干平A + (acv+)+(acv+)...
关于acv的效率,3步相当于x2,4步(acvv)相当于x3,5步相当于x4,或者设acv+步数为k,那么效果是x(k-1)。
说了这么多,有什么用呢?
其实这些就是问题的本质。相当于动态规划。因为总步数一定,目的就是让这些数乘起来尽可能大。
当然这里面还掺杂了初始平A这种东西。至少得3次平A之后再使用acv。

其实有鉴于上一道题,我本来一开始想尝试数学解法,结果还是不行。看来面试的时候还是直接奔DP比较靠谱。当然有大神掏出了O(1)的数学解法,我表示牛到不行,只能佩服。后面有讲,总之数学解法难度是ACM级别的(臆测)。

DP就不同了,只要知道关系式就行。
这里的话,很明显有子问题。拿上面的形状来说:
初始若干平A + (acv+)+(acv+)...
我们可以单独把最后一个acv+拿出来,拿它的长度做文章。
设dp[i]是i步能获得的最大A数量,因为它总是以acv+结尾(假设i>=6),其长度在3~(i-3)之间。我们可以遍历这个长度,结合之前剩下的dp,来取一个最大值。也就是说

for j in xrange(3, i - 2):
    dp[i] = max(dp[i], dp[i-j]*(j-1))

更详细的解释:i指目前我们考虑的下标,j是最后acv+的长度,按照之前说的acv+长度为j,那么效果就是乘以(j-1),剩下的部分长度为i-j,之前已经算过了直接拿出来就好。要做的就是遍历长度j可能的值计算dp[i],然后取一个最大值。

完整代码:

# Time:  O(n^2)
# Space: O(n)
class Solution(object):
    def maxA(self, n):
        dp = [_ for _ in xrange(n + 1)]
        for i in xrange(7, n + 1):
            for j in xrange(3, i - 2):
                dp[i] = max(dp[i], dp[i - j] * (j - 1))
        return dp[-1]

时间复杂度不能说很好,但是怎么说也算是解答了问题。

然后是数学解法,参加一亩三分地的帖子
还是回到上面的动态规划那里,转化为如下动态规划问题:

已知N,求x0 * x1 * ... * xk最大值,有:
x0 + x1 + ... + xk = N - k
k >= 0
x0 >= 1
x1, ..., xk >= 2

这里的x0就是初始平A,x1~xk就是acv+,其长度>=3,乘起来效果就是>=2。
那么给定N,如何求k呢?
4 * (k + 1) - delta = N - k, where 0 <= delta <= 4
k = (N + delta - 4) / 5 = N / 5
这里需要解释一下。结论就是优先使用4作为乘数,其次用3.
为什么?这里有一个“贡献因子”的理论,也就是说长度为x贡献x-1的乘数,贡献因子是(x-1)^(1/x),
x=2因子为1,x=3因子为1.26,x=4因子为1.316,x=5因子为1.320,x=6因子为1.308.
也就是说acv+的长度尽可能取5,其次是4,反映到乘数里面就是优先用4,其次用3了。

具体代码如下:

# Time:  O(1)
# Space: O(1)
    def maxA(self, N):
        """
        :type N: int
        :rtype: int
        """
        if N < 7: return N
        if N == 10: return 20  # the following rule doesn't hold when N = 10

        n = N // 5 + 1  # n3 + n4 increases one every 5 keys
        # (1) n     =     n3 +     n4
        # (2) N + 1 = 4 * n3 + 5 * n4
        #     5 x (1) - (2) => 5*n - N - 1 = n3
        n3 = 5 * n - N - 1
        n4 = n - n3
        return 3 ** n3 * 4 ** n4

n就是上面的k+1,理想情况下由x3的因子n3和x4的因子n4组成。
然后呢?就看不懂了……

又想了一下,这个意思应该是把平A也假设成为3或者4,否则结果里面没有说不过去。
那为什么只是N+1= 4 * n3 + 5 * n4呢?
因为作者把平A也归纳到n里面去了!换言之,作者把x0转化当成了一个acv+!
想想无论是平A=3还是4,就乘积效果而言相比acv+,都是省1步的!
所以这么写更好理解:N = 4 * n3 + 5 * n4 - 1
之后就一切自然而然了,然而还有一个特例就是10不遵守这个条件……
作者原话是:

10 is special here because it's the only > 6 number where there is no enough factors to share cuts from decrement of the number of 3's which means a 5 has to be introduced.

按照之前的算法n=3,然而即使是最小的组合3+4+4也超了,所以不成立。反映到计算就是n4=-1.
总而言之,这个算法充满神奇,多个大胆的假设,然后最后还是对的。临场还是别抱数学解法的想法吧……

彩蛋:一个基于数学解法的DP……

# Time:  O(n)
# Space: O(1)
class Solution2(object):
    def maxA(self, N):
        """
        :type N: int
        :rtype: int
        """
        if N < 7: return N
        dp = range(N + 1)
        for i in xrange(7, N + 1):
            dp[i % 6] = max(dp[(i - 4) % 6] * 3, dp[(i - 5) % 6] * 4)
        return dp[N % 6]

我一开始不太明白i%6是否有特别含义,对我而言这是滚动DP,按道理3个格子够用了?
想一想,i=7的时候需要dp[2]和dp[3]的值,为了不冲突,所以把值放到dp[1],很合理的解释。
试了一下,把所有%6改成%7,结果不变,说明我的猜测是正确的。事实上可以单独拿3个格子出来装,不过牵涉到初始值的问题麻烦一点。
另外dp = range(N + 1)改成dp = range(7)也不会有任何问题。

总结

数学解法超乎想象。

推荐阅读更多精彩内容