【问题标题】:Programming problem - Game of Blocks编程问题 - 积木游戏
【发布时间】:2010-10-29 00:12:03
【问题描述】:

也许您对如何解决以下problem 有一个想法。

约翰决定给儿子约翰尼买一些数学玩具。他最喜欢的玩具之一是不同颜色的积木。约翰决定购买不同颜色的 C 块。对于每种颜色,他都会购买 googol (10^100) 块。所有相同颜色的块具有相同的长度。但是不同颜色的块的长度可能会有所不同。 Jhonny 决定使用这些积木制作一个 1 x n 的大积木。他想知道有多少种方法可以做到这一点。如果存在颜色不同的位置,则认为两种方式不同。该示例显示了一个大小为 5 的红色块、大小为 3 的蓝色块和大小为 3 的绿色块。它表明有 12 种方法可以制作一个长度为 11 的大块。

每个测试用例都以整数 1 ≤ C ≤ 100 开始。下一行包含 c 个整数。第 i 个整数 1 ≤ leni ≤ 750 表示第 i 个颜色的长度。下一行是正整数 N ≤ 10^15。

对于 T MOD 100000007(质数)。

可以推导出为矩阵求幂问题,使用Coppersmith-Winograd算法和快速求幂可以在O(N^2.376*log(max(leni)))中相对高效地解决。但似乎需要一种更有效的算法,因为 Coppersmith-Winograd 意味着一个很大的常数因子。你还有其他建议吗?它可能是数论或分治问题

【问题讨论】:

    标签: algorithm puzzle matrix-multiplication


    【解决方案1】:

    首先注意你拥有的每种颜色的块数是一个完整的红鲱鱼,因为 10^100 > N 总是。所以每种颜色的块数实际上是无限的。

    现在请注意,在每个位置,p(如果有有效的配置,则不留空格等)必须有一个颜色块,c。这个块有len[c] 的方式可以撒谎,所以它仍然在这个位置上,p

    我的想法是在固定位置尝试所有可能的颜色和位置(N/2,因为它将范围减半),然后对于每种情况,在此固定颜色块之前有 b 单元格,之后有 a这个固定色块。因此,如果我们定义一个函数ways(i),它返回平铺i 单元格的方法数(使用ways(0)=1)。那么在某个位置用固定色块平铺多个单元格的方法数为ways(b)*ways(a)。将所有可能的配置相加得出ways(i) 的答案。

    现在我选择了固定位置为N/2,因为它将范围减半,您最多可以将范围减半ceil(log(N)) 次。现在,由于您要移动一个关于N/2 的块,您必须从N/2-750 计算到N/2-750,其中750 是一个块可以具有的最大长度。因此,您必须计算大约 750*ceil(log(N))(由于方差而更多)长度才能得到最终答案。

    因此,为了获得良好的性能,您必须完成记忆,因为这本质上是一种递归算法。

    所以用Python(因为我很懒,不想写大数的类):

    T = int(raw_input())
    
    for case in xrange(T):
        #read in the data
        C = int(raw_input())
        lengths = map(int, raw_input().split())
        minlength = min(lengths)
        n = int(raw_input())
    
        #setup memoisation, note all lengths less than the minimum length are
        #set to 0 as the algorithm needs this
        memoise = {}
        memoise[0] = 1
        for length in xrange(1, minlength):
           memoise[length] = 0
    
        def solve(n):
            global memoise
            if n in memoise:
                return memoise[n]
    
            ans = 0
            for i in xrange(C):
                if lengths[i] > n:
                    continue
                if lengths[i] == n:
                    ans += 1
                    ans %= 100000007
                    continue 
                for j in xrange(0, lengths[i]):
                    b = n/2-lengths[i]+j
                    a = n-(n/2+j)
                    if b < 0 or a < 0:
                        continue
                    ans += solve(b)*solve(a)
                    ans %= 100000007
            memoise[n] = ans
            return memoise[n]
        solve(n)
        print "Case %d: %d" % (case+1, memoise[n])
    

    请注意,我没有对此进行详尽的测试,但如果您将此算法翻译成 C++ 或类似的语言,我很确定它会满足 20 秒的时间限制。

    编辑:使用N = 10^15 和长度为750 的块运行测试我知道memoise 包含大约60000 元素,这意味着solve(n) 的非查找位被调用的次数大致相同.

    【讨论】:

    • 我认为一个非常相似的解决方案。答案中有一些我不明白的地方。 So you will have to calculate about 750*ceil(log(N))。不是100*750^2*log^2(N)吗?请注意,在您的代码中,您首先采用每种颜色,然后采用导致100*750 的每个元素。此外,当在步骤 i 将 N 分为 2 部分时,不同块长度的数量将是 i * 750(对于 i >= 1)(对于第一步 [ i = 0 ],只有一个状态 - N),因此log^2(N) * 750。总的来说,这会导致 100 * 750^2 * 60^2 ~ 200 * 10^9(这不足以通过 1 个最大测试)
    • 实际上迭代次数少了60。总的来说,这导致100 * 750^2 * 60。有了60000元素的实验结果,我们可以用60000替换750*60。这导致100 * 750 * 60000 = 4.5 * 10^9。假设每秒大约 100 * 10^6 次迭代,最大测试可能需要大约 30 秒,因为 MOD(相当复杂的操作)将成为 CPU 的瓶颈。
    • 我承认我说错了一点,我的意思是你只需要计算memoise hash-map 中的O(750*log(N)) 元素。运行时间约为O(750*100*750*log(N))。现在想起来有点慢……
    • 解决方案可以通过对小 N 进行 DP 预计算来改进。这应该可以在 10^8 中的 10^6 items 附近进行预计算。因此将log(10^15) 减少到log(10^9)。然而,这仍然是不够的。此外,我们不能除以2,而是说除以10。实验上,这应该可以估计出最佳的分割数。越来越多的划分会导致需要计算的状态数量减少。
    【解决方案2】:

    请注意:在 c=2, len1=1, len2=2 的情况下,答案将是第 N 个斐波那契数,并且斐波那契数(大约)以黄金的增长因子呈指数增长比率,phi ~ 1.61803399。为了 巨大的值 N=10^15,答案将是大约 phi^(10^15),一个巨大的数字。答案将有存储 (ln(phi^(10^15))/ln(2)) / (8 * 2^40) ~ 79 TB 的要求。因为你甚至不能访问 79 TB 在 20 秒内,您不太可能满足这种特殊情况下的速度要求。

    当 C 不太大并且 leni 对所有 i 都很大时,你的最大希望就出现了。在这种情况下,答案将 仍然随着 N 呈指数增长,但增长因子可能要小得多。

    我建议您首先构造整数矩阵 M 来计算 (i+1,..., i+k) 基于 (i, ..., i+k-1) 项的序列中的项。 (只有这个矩阵的第 k+1 行是有趣的)。 “手动”计算前 k 个条目,然后根据重复平方计算 M^(10^15) 技巧,并将其应用于术语(0...k-1)。

    矩阵的(整数)条目将呈指数增长,可能太快而无法处理。如果是这种情况,请执行 对于几个中等大小的素数 p,计算完全相同,但取模 p。这将允许您获得 对于各种 p,您的答案以 p 为模,而不使用 bigints 矩阵。使用足够的素数后,您可以了解他们的产品 大于你的答案,你可以用所谓的“中国剩余定理”来恢复 你从你的 mod-p 答案中得到的答案。

    【讨论】:

    • 斐波那契数 mod P 可以使用O(Log(N)) 中的斐波那契矩阵形式计算,其中N = 10^15。对于C &gt; 2,可以将问题转换为矩阵 for 和您提到的类似矩阵求幂算法,但是矩阵的大小太大,甚至无法进行一次乘法迭代。对于单次乘法,它可以大到750 x 750 - O(N^3)。众所周知,答案需要计算mod 100000007 (prime number),因此中国剩余定理不适用。
    • 我明白了。我建议您在上面的问题陈述中包含 mod 100000007 要求。
    【解决方案3】:

    我想在早期的@JPvdMerwe 解决方案的基础上进行一些改进。在他的回答中,@JPvdMerwe 使用了动态编程/记忆方法,我同意这是解决这个问题的方法。将问题递归地划分为两个较小的问题并记住先前计算的结果是非常有效的。

    我想提出一些可以进一步加快速度的改进:

    1. 不需要遍历所有可以定位中间块的方式,只需遍历前半部分,然后将解乘以 2。这是因为后半部分的情况是对称的.对于奇数长度的块,您仍然需要将居中位置作为单独的情况。

    2. 一般来说,迭代实现可以比递归实现快几个数量级。这是因为递归实现会为每个函数调用带来记账开销。将解决方案转换为其迭代表亲可能是一个挑战,但通常是可能的。 @JPvdMerwe 解决方案可以通过使用堆栈来存储中间值进行迭代。

    3. 模运算成本高昂,乘法运算成本较低。通过将颜色循环与位置循环进行切换,乘法和模数的数量可以减少大约 C=100 倍。这允许您在进行乘法和取模之前将多个调用的返回值添加到solve()。

    测试解决方案性能的一个好方法是使用病态案例。以下内容可能特别令人生畏:长度 10^15、C=100、素数块大小。

    希望这会有所帮助。

    【讨论】:

    • 1. 3. 看起来很合理。 MOD 运算实际上可以减少 750 / 2 倍(对称),因为最大中间结果可以大到 100000007^2,比最大 64 个无符号整数小约 2000 倍。对于 2. 我认为收益不会很大,但尝试一下可能是个好主意。另一个重要的点 4 可能是使用迭代方法预先计算 DP 数组,以获取大约从 1 到 10^6 的值。鉴于已经讨论过的所有优化,现在肯定会尝试一下。谢谢大家!
    • 关于 2:许多编程语言都有这种称为“尾调用优化”的东西,它将递归调用转换为 JMP,除非数据要保存在堆栈上。如果您已经有一个堆栈,为什么还要实现另一个?
    • @billitch:尾调用优化仅适用于递归调用是函数中的最后一条语句的情况。可以(手动或由编译器)重新排列代码以满足该条件。但由于在代码示例中调用了两次solve(n),因此即使可以进行这种重新排列,也总会有至少一个调用不能进行TCO。
    【解决方案4】:

    在上面的回答中

        ans += 1
        ans %= 100000007
    

    如果没有一般的模数可能会快得多:

        ans += 1
        if ans == 100000007 then ans = 0
    

    【讨论】:

    • 这将如何与ans += 2 一起使用?如果ans == 100000008 then ans 0?注意MOD需要取自solve * solve
    【解决方案5】:

    请参阅TopCoder thread 以获得解决方案。没有人足够接近在此线程中找到答案。

    【讨论】:

      猜你喜欢
      • 2011-10-30
      • 1970-01-01
      • 1970-01-01
      • 2021-09-23
      • 1970-01-01
      • 2011-03-05
      • 2012-04-25
      • 1970-01-01
      • 2023-03-14
      相关资源
      最近更新 更多