SRM596 DIV1 250
开始以为是DP然后用了一个时间复杂度数量级为\(10^7\)的DP来做,结果case 35超时,后来发现就是一个逻辑分析题,就知道有贪心策略的,250分的题怎么可能是DP呢。
分析
首先考虑一个数的情况,任意一个数a的形成可以通过如下构造来表示:
\(\begin{equation}\begin{split}a&=((((0+n_0)\times2+n_1)\times2+n_2)\times2+...+n_{k-1})\times2+n_{k}\\&=\sum_{i=0}^{k} n_i\times2^{k-i} (n^i \in N) \end{split}\end{equation}\)
将连续的\(+1\)操作看成\(+n\)不难得出以上变换,因此a的变换可以看成一个特殊的二进制数,跟一般的二进制数不同之处在于每位的数字可以超过1。
将a重新表示成:
\(a=\overline{n_0n_1n_2...n_k}\quad(例: 10=\overline{210}=\overline{0106}=\overline{1010})\)
注意该种表示法的以下特点:
1. a的前缀零意味着开始一直在做Doubling操作
2. 各种结构可以通过进位(当前位-2,高一位+1)和借位(当前位+2,高一位-1)操作相互变换
3. k就是Doubling操作的次数
从0变换到a所需的操作数s等于:
\(s(\overline{n_0n_1n_2...n_k})=k+\sum_{i=0}^{k}n_i\quad(\overline{n_0n_1n_2...n_k}=a)\)
当a表示为一般二进制数且没有多余前缀零的时候可令s最小。注意到任何一次进位操作可以使s变小或保持不变,所以尽可能的进位最后得出的数形就是一般二进制数。
现在考虑多个数\(a_i\)的情况,每个数可以转变为各自的特殊二进制数,但是由于Doubling操作的存在使得它们的位数必须相等(前缀零也要占位,如果有的话)。
令Doubling操作的次数为t,若已知t,除去总的一起做的Doubling操作,最优解一定是限制条件下每个数单独的最优解之和,单独最优解\(s(a,t)\)可以是将a尽可能进位的结果,即:
\( \begin{equation}\begin{split} s(a,t)_{min} &= s(\overline{n_0n_1...n_t})_{min}\quad&(a=\overline{n_0n_1...n_t}) \\ &= s(\overline{n_0n_1...n_t})-t\quad&(i\geq1, n_i < 2)(注意t要被省去) \\ &= \sum_{i=0}^{t}n_i \\ &= a // 2^t + \sum_{i=1}^{t}n_i \quad&(n_0=a // 2^t)\end{split}\end{equation} \)
最后,枚举t计算所有可能情况下的最小值,总步骤数tot:
\(tot(t)=\{t+\sum_{i}s(a_i, t)_{min}|t\in[0,10]\}\)
\(tot_{min}=min(tot(t))\)
优化
进一步可以证明当 \(t+1\) 为所有数的二进制位数的最大值的时候,\(tot\) 取最小值 \(tot_{min}\),证明如下:
令 \(len(a)\) 表示数a的一般二进制数的位数减一,若 \(t < max(len(a_i))\) 则:
\(\begin{equation}\begin{split}tot(t+1)-tot(t)&=1+\sum_{i}^{len(a_i)>t}(s(a_i, t+1)-s(a_i, t)) \\ &=1+\sum_{i}^{len(a_i)>t}(a_i//2^{t+1}+a_1-a_i//2^t) \quad &(a_1是t+1位的情况) \\ &=(1+\sum_{i}^{len(a_i)>t}a_1)+(\sum_{i}^{len(a_i)>t}(-a_i//2^{t+1})) \\ &\leq 1-a_k//2^{t+1} \leq 0 \quad &(len(a_k)=max(len(a_i))) \end{split}\end{equation}\)
所以有 \( tot(t+1) \leq tot(t)\)
则:
令 \(sum(a)\) 为a的一般二进制数的各位数字之和:
\(tot_{min}=max(len(a_i)) + \sum_{i}sum(a_i)\)
注意这道题如果没有考虑到前缀零也可以占位的情况就会直接得出「t可以取所有数的一般二进制数的位数的最小值」的错误结论,好在题目里有测试点可以测出这个bug,所以不会被无情challenge
class IncrementAndDoubling: def getMin(self, a): x = max(a) maxk = 0 while True: maxk += 1 x = x // 2 if x == 0: break s = maxk - 1 for x in a: for i in range(maxk): s += x >> i & 1 return s # test o = IncrementAndDoubling() # test 0 assert(o.getMin((0,)) == 0) # test case assert(o.getMin((2,1)) == 3) assert(o.getMin((16,16,16)) == 7) assert(o.getMin((100,)) == 9) assert(o.getMin((0, 0, 1, 0, 1)) == 2) assert(o.getMin((123, 234, 345, 456, 567, 789)) == 40) assert(o.getMin((7,5,8,1,8,6,6,5,3,5,5,2,8,9,9,4,6,9,4,4,1,9,9,2,8,4,7,4,8,8,6,3,9,4,3,4,5,1,9,8,3,8,3,7,9,3,8,4,4,7)) == 84) print('ok')