【问题标题】:Fast way to place bits for puzzle快速放置拼图的方法
【发布时间】:2013-10-18 20:35:37
【问题描述】:

我正在编写代码来解决一个难题,如下所示。

考虑一个长度为 n 的二进制向量,最初全为零。您选择向量的一个位并将其设置为 1。现在一个进程开始将与任何 1 位的最大距离设置为 $1$(或者如果有多个位,则任意选择最远位)。这种情况反复发生,规则是没有两个 1 位可以彼此相邻。当没有更多空间放置 1 位时,它终止。目标是放置初始的 1 位,以便在终止时将尽可能多的位设置为 1。

假设 n = 2。那么无论我们设置哪个位,我们最终都会设置一个位。

对于 n = 3,如果我们设置第一位,我们最终会得到 101。但是如果我们设置中间位,我们会得到 010,这不是最优的。

对于 n = 4,无论我们设置哪个位,最终都会得到两个集合。

对于 n = 5,设置第一个给我们 10101,最后设置三个位。

对于 n = 7,我们需要设置第三位才能得到 1010101 似乎。

我已经编写了代码来找到最佳值,但它不能很好地扩展到较大的 n。我的代码在 n = 1000 左右开始变慢,但我想解决 n 大约 100 万的问题。

#!/usr/bin/python
from __future__ import division
from math import *

def findloc(v):
    count = 0
    maxcount = 0
    id = -1
    for i in xrange(n):
        if (v[i] == 0):
            count += 1
        if (v[i] == 1):
            if (count > maxcount):
                maxcount = count
                id = i
            count = 0

#Deal with vector ending in 0s
    if (2*count >= maxcount and count >= v.index(1) and count >1):
        return n-1
#Deal with vector starting in 0s
    if (2*v.index(1) >= maxcount and v.index(1) > 1):
        return 0
    if (maxcount <=2):
        return -1    
    return id-int(ceil(maxcount/2))


def addbits(v):
    id = findloc(v)
    if (id == -1):
        return v
    v[id] = 1
    return addbits(v)

#Set vector length
n=21    
max = 0
for i in xrange(n):
    v = [0]*n
    v[i] = 1
    v = addbits(v)
    score = sum([1 for j in xrange(n) if v[j] ==1])
#        print i, sum([1 for j in xrange(n) if v[j] ==1]), v
    if (score > max):
        max = score       
print max

【问题讨论】:

  • 您的 n = 7 示例对我来说很奇怪。你说你会设置第 3 位,但你不想做 0000000 -> 1000000 -> 1000001 -> 1010001 -> 1010101 吗?
  • @ShashankGupta 问题是,一旦你到达 1000001,离任何 1 最远的位置就在中间,所以你最终得到 1001001,这不是最优的。遗憾的是,我不相信有任何简单的封闭式解决方案,这就是我想加快代码速度的原因。
  • @user2179021,有用的澄清!谢谢。
  • 好的,谢谢。我仍然认为可以使用公式int(math.ceil(float(n)/2)) 在 O(1) 时间内找到最佳位数,除非我在这里遗漏了什么?
  • @ShashankGupta 尝试 n = 15。我相信正确答案是 7。

标签: python performance algorithm math time-complexity


【解决方案1】:

我会附上我所拥有的。和你一样,唉,时间基本上是O(n**3)。但至少它避免了递归(等),所以当你接近一百万时不会爆炸;-) 请注意,这会返回找到的最佳向量,而不是计数;例如,

>>> solve(23)
[6, 0, 11, 0, 1, 0, 0, 10, 0, 5, 0, 9, 0, 3, 0, 0, 8, 0, 4, 0, 7, 0, 2]

因此它还显示了选择 1 位的顺序。获取计数的最简单方法是将结果传递给max()

>>> max(solve(23))
11

或者将函数改成返回maxsofar而不是best

如果您想计算一百万数量级的数字,您将需要一些完全不同的东西。你甚至买不起二次时间(更不用说这种方法的三次时间了)。不太可能从更高级的数据结构中获得如此巨大的O() 改进 - 我预计这需要对问题的数学有更深入的了解。

def solve(n):
    maxsofar, best = 1, [1] + [0] * (n-1)
    # by symmetry, no use trying starting points in last half
    # (would be a mirror image).
    for i in xrange((n + 1)//2):
        v = [0] * n
        v[i] = count = 1
        # d21[i] = distance to closest 1 from index i
        d21 = range(i, 0, -1) + range(n-i)
        while 1:
            d, j = max((d, j) for j, d in enumerate(d21))
            if d >= 2:
                count += 1
                v[j] = count
                d21[j] = 0
                k = 1
                while j-k >= 0 and d21[j-k] > k:
                    d21[j-k] = k
                    k += 1
                k = 1
                while j+k < n and d21[j+k] > k:
                    d21[j+k] = k
                    k += 1
            else:
                if count > maxsofar:
                    maxsofar = count
                    best = v[:]
                break
    return best

【讨论】:

    【解决方案2】:

    最新答案(O(log n) 复杂度)

    如果我们相信 templatetypedef 和 A​​leksi Torhamo 的猜想(更新:本文末尾的证明),则在 O(log n)(或 @987654323 @如果我们假设对数和位移是O(1)):

    Python:

    from math import log
    
    def count(n): # The count, using position k conjectured by templatetypedef
        k = p(n-1)+1
        count_left = k/2
        count_right = f(n-k+1)
        return count_left + count_right
    
    def f(n): # The f function calculated using Aleksi Torhamo conjecture
        return max(p(n-1)/2 + 1, n-p(n-1))
    
    def p(n): # The largest power of 2 not exceeding n
        return 1 << int(log(n,2)) if n > 0 else 0
    

    C++:

    int log(int n){ // Integer logarithm, by counting the number of leading 0
        return 31-__builtin_clz(n);
    }
    
    int p(int n){ // The largest power of 2 not exceeding n
        if(n==0) return 0;
        return 1<<log(n);
    }
    
    int f(int n){ // The f function calculated using Aleksi Torhamo conjecture
        int val0 = p(n-1);
        int val1 = val0/2+1;
        int val2 = n-val0;
        return val1>val2 ? val1 : val2;
    }
    
    int count(int n){ // The count, using position k conjectured by templatetypedef
        int k = p(n-1)+1;
        int count_left = k/2;
        int count_right = f(n-k+1);
        return count_left + count_right;
    }
    

    此代码可以立即正确计算n=100,000,000(甚至是Python 中的n=1e24!)的结果1

    我已经用 n 的各种值测试了代码(使用我的 O(n) 解决方案作为标准,请参阅下面的旧答案部分),它们似乎仍然正确。

    此代码依赖于 templatetypedef 和 A​​leksi Torhamo2 的两个猜想。 有人想证明这些吗? =D(更新 2:证明)

    1没有时间,我的意思是几乎立即
    2Aleksi Torhamo 对f 函数的猜想已针对n&lt;=100,000,000

    进行了经验证明

    旧答案(O(n) 复杂度)

    我可以使用 Python 2.7 在 1.358 秒内(在我的 iMac 中)返回 n=1,000,000 的计数(结果是 475712)。 更新:在 C++ 中 n=10,000,000 是 0.198 秒。 =)

    这是我的想法,达到O(n)时间复杂度。

    算法

    f(n)的定义

    f(n) 定义为将在长度为n 的位向量上设置的位数,假设设置了第一个和最后一个位n=2 除外,其中只有第一个或最后一个位被设置)。所以我们知道f(n)的一些值如下:

    f(1) = 1
    f(2) = 1
    f(3) = 2
    f(4) = 2
    f(5) = 3
    

    请注意,这与我们正在寻找的值不同,因为初始位可能不在第一个或最后一个,如f(n) 计算的那样。例如,我们有 f(7)=3 而不是 4。

    请注意,这可以使用递归关系相当有效地计算(摊销O(n) 以计算fn 的所有值):

    f(2n) = f(n)+f(n+1)-1
    f(2n+1) = 2*f(n+1)-1
    

    对于n&gt;=5,因为遵循规则设置的下一个位将是中间位,n=1,2,3,4 除外。然后我们可以将位向量分成两部分,每部分相互独立,这样我们就可以使用f( floor(n/2) ) + f( ceil(n/2) ) - 1计算出所设置的位数,如下图所示:

    n=11 n=13 10000100001 1000001000001 f(6) f(7) f(6) f(7) n=12 n=14 100001000001 10000010000001 f(6) f(7) f(7) f(8)

    我们在公式中有-1 来排除中间位的双重计数。

    现在我们准备计算原始问题的解决方案。

    g(n,i)的定义

    按照问题中的规则,将g(n,i) 定义为将在长度为n 的位向量上设置的位数,其中初始位位于i-th 位(从1 开始)。请注意,通过对称性,初始位可以是从第一位到ceil(n/2)-th 位的任何位置。对于这些情况,请注意第一位将在第一位和初始位之间的任何位之前设置,最后一位也是如此。因此第一个分区和第二个分区设置的位数分别为f(i)f(n+1-i)

    所以g(n,i)的值可以计算为:

    g(n,i) = f(i) + f(n+1-i) - 1
    

    在计算f(n)时遵循这个想法。

    现在,计算最终结果很简单。

    g(n)的定义

    g(n) 定义为在原始问题中查找的计数。然后我们可以取所有可能的i中的最大值,即初始位的位置:

    g(n) = maxi=1..ceil(n/2)(f(i) + f(n+1-i) - 1)

    Python 代码:

    import time
    mem_f = [0,1,1,2,2]
    mem_f.extend([-1]*(10**7)) # This will take around 40MB of memory
    def f(n):
        global mem_f
        if mem_f[n]>-1:
            return mem_f[n]
        if n%2==1:
            mem_f[n] = 2*f((n+1)/2)-1
            return mem_f[n]
        else:
            half = n/2
            mem_f[n] = f(half)+f(half+1)-1
            return mem_f[n]
    
    def g(n):
        return max(f(i)+f(n+1-i)-1 for i in range(1,(n+1)/2 + 1))
    
    def main():
        while True:
            n = input('Enter n (1 <= n <= 10,000,000; 0 to stop): ')
            if n==0: break
            start_time = time.time()
            print 'g(%d) = %d, in %.3fs' % (n, g(n), time.time()-start_time)
    
    if __name__=='__main__':
        main()
    

    复杂性分析

    现在,有趣的是,用上述方法计算g(n) 的复杂度是多少?

    我们首先应该注意,我们迭代了in/2 值,即初始位的位置。在每次迭代中,我们调用f(i)f(n+1-i)。天真的分析会导致O(n * O(f(n))),但实际上我们在f上使用了memoization,所以它比这快得多,因为f(i)的每个值最多只计算一次。因此,计算f(n) 的所有值所需的时间实际上增加了复杂性,而不是O(n + f(n))

    那么初始化f(n)的复杂度是多少?

    我们可以假设在计算g(n) 之前,我们首先预先计算了f(n) 的每个值。请注意,由于递归关系和记忆,生成f(n) 的整个值需要O(n) 时间。下一次调用f(n) 需要O(1) 时间。

    因此,总体复杂度为O(n+n) = O(n),这可以从我的 iMac 中n=1,000,000n=10,000,000 的运行时间证明:

    > python max_vec_bit.py 输入 n(1 > > > python max_vec_bit.py 输入 n(1

    作为记忆化的副产品,在第一次调用大 n 后,n 的较小值的计算会快得多,正如您在示例运行中所见。并且使用更适合数字运算的语言(例如 C++),您可能会获得显着更快的运行时间

    我希望这会有所帮助。 =)

    使用 C++ 的代码,用于提高性能

    C++ 中的结果大约快 68 倍(由clock() 测量):

    > ./a.out 输入 n(1 > > > ./a.out 输入 n(1

    C++ 代码:

    #include <cstdio>
    #include <cstring>
    #include <ctime>
    
    int mem_f[10000001];
    int f(int n){
        if(mem_f[n]>-1)
            return mem_f[n];
        if(n%2==1){
            mem_f[n] = 2*f((n+1)/2)-1;
            return mem_f[n];
        } else {
            int half = n/2;
            mem_f[n] = f(half)+f(half+1)-1;
            return mem_f[n];
        }
    }
    
    int g(int n){
        int result = 0;
        for(int i=1; i<=(n+1)/2; i++){
            int cnt = f(i)+f(n+1-i)-1;
            result = (cnt > result ? cnt : result);
        }
        return result;
    }
    
    int main(){
        memset(mem_f,-1,sizeof(mem_f));
        mem_f[0] = 0;
        mem_f[1] = mem_f[2] = 1;
        mem_f[3] = mem_f[4] = 2;
        clock_t start, end;
        while(true){
            int n;
            printf("Enter n (1 <= n <= 10,000,000; 0 to stop): ");
            scanf("%d",&n);
            if(n==0) break;
            start = clock();
            int result = g(n);
            end = clock();
            printf("g(%d) = %d, in %.3fs\n",n,result,((double)(end-start))/CLOCKS_PER_SEC);
        }
    }
    

    证明

    请注意,为了保持这个答案(已经很长)简单,我跳过了证明中的一些步骤

    Aleksi Torhamo 关于f值的猜想

    对于`n>=1`,证明: f(2n+k) = 2n-1+1 for k=1,2,…,2n-1 .. .(1) f(2n+k) = k for k=2n-1+1,…,2n ...(2) 给定 f(0)=f(1)=f(2)=1

    上面的结果可以很容易地通过对递归关系的归纳来证明,通过考虑四种情况:

    • 案例1:(1)偶数k
    • 案例2:(1)奇数k
    • 案例3:(2)偶数k
    • 案例4:(2)奇数k
    假设我们已经证明了 n 的四种情况。现在考虑n+1。 情况1: f(2n+1+2i) = f(2n+i) + f(2n+i+1) - 1 , 对于 i=1,…,2n-1 = 2n-1+1 + 2n-1+1 - 1 = 2n+1 案例二: f(2n+1+2i+1) = 2*f(2n+i+1) - 1,对于 i=0,…,2n-1-1 = 2*(2n-1+1) - 1 = 2n+1 案例3: f(2n+1+2i) = f(2n+i) + f(2n+i+1) - 1 , 对于 i=2n-1+1,…,2n = i + (i+1) - 1 = 2i 案例4: f(2n+1+2i+1) = 2*f(2n+i+1) - 1,对于 i=2n-1 +1,…,2n-1 = 2*(i+1) - 1 = 2i+1

    所以通过归纳,猜想得到证明。

    templatetypedef关于最佳位置的猜想

    对于 n>=1 和 k=1,…,2n,证明 g(2n+k) = g(2n +k, 2n+1) 也就是说,证明将第一位放在第 2n+1 个位置会给出最大位数。

    证明:

    首先,我们有 g(2n+k,2n+1) = f(2n+1) + f(k-1) - 1 接下来,通过 f 的公式,我们有以下等式: f(2n+1-i) = f(2n+1), 对于 i=-2n-1,…, -1 f(2n+1-i) = f(2n+1)-i, 对于 i=1,…,2n-2支持>-1 f(2n+1-i) = f(2n+1)-2n-2,对于 i=2n-2,…,2n-1 还有以下不等式: f(k-1+i) n-1,…,-1 f(k-1+i) n-2-1 f(k-1+i) n-2,对于 i=2n-2,…,2 n-1 所以我们有: f(2n+1-i)+f(k-1+i) n+1)+f(k-1),对于i=-2n-1,…,2n-1 现在,请注意我们有: g(2n+k) = maxi=1..ceil(2n-1+1-k/2)(f (i) + f(2n+k+1-i) - 1) n+1) + f(k-1) - 1 = g(2n+k,2n+1)

    所以这个猜想被证明了。

    【讨论】:

    • 太棒了!你打败了我——正在研究类似的方法。在我看来听起来不错! :-)
    • 我也打算这样做。很棒的工作!
    • 看看f(i) 产生的模式,似乎你实际上可以用l = int(math.log(i - 2, 2)); p = (1 &lt;&lt; l); s = (p &gt;&gt; 1); result = max(s + 1, i - p) 计算它(未证明,只是“似乎成立”。那个奇怪的-2 在那里,因为我看了在功能略有不同的问题上,不计算末端的 1。此外,使用创意地板/天花板,您可以将 f(i) 的案例组合在一起,如 f((n+1)//2)+f(n//2+1)-1 if n&gt;4 else (n+1)//2。并不是没有任何真正的理由这样做就是这样:P)
    • 我们或许可以推测。希望有人能证明。顺便说一句,做条件是有原因的,就是减少函数调用的次数 =)
    • 请注意,您的原始 f(n) 带有“从头开始”的记忆,需要 O(log(n)) 时间,而不是 O(n)。虽然f(even) 确实导致了两次调用,但分歧在此结束。示例:f(100) 调用 f(50) 和 f(51)。依次调用 f(25)、f(26) 和 f(26):只有 f(25) 和 f(26) 是不同的。依次调用 f(13)、f(13) 和 f(14),这又只是两个不同的值。然后那些调用 f(7)、f(7) 和 f(8):两个值。等等。log(n) 的每个级别都有 2 个不同的调用,总时间为 O(log(n))
    【解决方案3】:

    因此,为了打破我没有证据证明的不发布算法的正常传统,我想我应该提一下,有一种算法似乎对高达 50,000+ 的数字是正确的,并且运行时间为 O(log n) 时间。这要归功于Sophia Westwood,我今天和他一起解决了这个问题大约三个小时。这一切的功劳都归功于她。根据经验,它似乎工作得很好,而且比 O(n) 解决方案快得多。

    关于这个问题的结构的一个观察是,如果 n 足够大(n ≥ 5),那么如果你把 1 放在任何地方,问题就会分成两个子问题,一个在 1 的左边,一个在正确的。尽管 1 可能会在不同的时间被放置在不同的一半中,但最终的放置是相同的,就像您分别解决每一半并将它们重新组合在一起一样。

    接下来的观察是这样的:假设你有一个大小为 2k + 1 的数组,用于某些 k。在这种情况下,假设您在数组的任一侧放置一个 1。那么:

    • 下一个 1 放置在数组的另一侧。
    • 下一个 1 放在中间。
    • 您现在有两个较小的子问题,大小为 2k-1 + 1。

    关于此的重要部分是生成的位模式是 1 和 0 的交替序列。例如:

    • 对于 5 = 4 + 1,我们得到 10101
    • 对于 9 = 8 + 1,我们得到 101010101
    • 对于 17 = 16 + 1,我们得到 10101010101010101

    这很重要的原因如下:假设数组中有 n 个元素,设 k 为 2k + 1 ≤ n 的最大可能值。如果您将 1 放置在位置 2k + 1,则数组的左侧部分直到该位置最终会被交替的 1 和 0 平铺,这会放置一个 lot em> 个 1s 到数组中。

    不明显的是,对于 50,000 以内的所有数字,将 1 位放在那里似乎会产生最佳解决方案!我编写了一个 Python 脚本来检查这个(使用类似于@justhalf 的递归关系),它似乎运行良好。这个事实如此有用的原因是计算这个索引真的很容易。特别是,如果 2k + 1 ≤ n,则 2k ≤ n - 1,所以 k ≤ lg (n - 1)。选择值 ⌊lg (n - 1) ⌋ 作为您对 k 的选择,然后让您通过计算 2k + 1 来计算位索引。这个 k 的值可以在 O(log n) 中计算时间和求幂也可以在 O(log n) 时间内完成,因此总运行时间为 Θ(log n)。

    唯一的问题是我还没有正式证明这是可行的。我所知道的是,它适用于我们尝试过的前 50,000 个值。 :-)

    希望这会有所帮助!

    【讨论】:

    • 这很有趣。让我们一起来看看这个解决方案吧!
    • 我用n=100,000,000 测试过(还有一些数字),你的功能是正确的。看起来很有希望,让我们证明这一点 =D
    • 顺便说一句,你如何计算初始位右侧的总位数?根据你的公式,左边只有 2^(k-1)+1 。但是在右边?是在使用我的f 函数吗?
    猜你喜欢
    • 2019-02-15
    • 2022-08-14
    • 2013-09-16
    • 2018-09-22
    • 1970-01-01
    • 2016-12-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多