快速放置拼图的方法

时间:2013-10-10 21:38:06

标签: python performance algorithm math time-complexity

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

考虑长度为n的二进制向量,该向量最初全为零。你选择了一个向量并将其设置为1.现在一个进程开始设置从任何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

3 个答案:

答案 0 :(得分:12)

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

如果我们通过templatetypedef和Aleksi Torhamo(更新:此帖子末尾的证据)相信猜测,那么count(n)中可以计算出一个封闭的表单解决方案O(log n) (或O(1)如果我们假设对数和位移是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;
}

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

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

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

1 很快,我的意思是
2 Aleksi Torhamo关于f函数的猜想已经凭n<=100,000,000

进行了实证验证

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

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

这是我的想法,它实现了O(n)时间复杂度。

算法

f(n)

的定义

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

f(n)

请注意,这与我们要查找的值不同,因为初始位可能不是f(1) = 1 f(2) = 1 f(3) = 2 f(4) = 2 f(5) = 3 计算的第一位或最后一位。例如,我们有f(n)而不是4。

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

n

表示f(2n) = f(n)+f(n+1)-1 f(2n+1) = 2*f(n+1)-1 ,因为除了n>=5之外,规则之后的下一位设置将是中间位。然后我们可以将位向量分成两部分,每部分彼此独立,这样我们就可以计算使用n=1,2,3,4设置的位数,如下所示:

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)

我们在公式中有f( floor(n/2) ) + f( ceil(n/2) ) - 1来排除中间位的双重计数。

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

-1

的定义

g(n,i)定义为将在长度为g(n,i)的位向量上设置的位数,遵循问题中的规则,其中初始位位于n位(从1开始)。注意,通过对称,初始位可以是从第一位到第i位的任何位置。对于这些情况,请注意第一位将在第一位和第一位之间的任何位之前设置,最后一位也是如此。因此,第一个分区和第二个分区中设置的位数分别为ceil(n/2)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)的最大值,即初始位的位置:

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

Python代码:

i

复杂性分析

现在,有趣的是,使用上述方法计算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) n/2的值,即初始位的位置。在每次迭代中,我们都会调用if(i)。天真分析将导致f(n+1-i),但实际上我们在O(n * O(f(n)))上使用了memoization,因此它比这快得多,因为f的每个值只计算一次,最多。因此,复杂性实际上是通过计算f(i)的所有值所需的时间来增加的,而f(n)则是O(n + f(n))

那么初始化f(n)的复杂性是什么?

我们可以假设在计算f(n)之前先预先计算g(n)的每个值。请注意,由于递归关系和memoization,生成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
Enter n (1 <= n <= 10,000,000; 0 to stop): 1000000
g(1000000) = 475712, in 1.358s
Enter n (1 <= n <= 10,000,000; 0 to stop): 0
>
> <restarted the program to remove the effect of memoization>
>
> python max_vec_bit.py
Enter n (1 <= n <= 10,000,000; 0 to stop): 10000000
g(10000000) = 4757120, in 13.484s
Enter n (1 <= n <= 10,000,000; 0 to stop): 6745231
g(6745231) = 3145729, in 3.072s
Enter n (1 <= n <= 10,000,000; 0 to stop): 0

作为memoization的副产品,在第一次调用大n之后,n的较小值的计算速度会快得多,您也可以在示例运行中看到。使用更适合数字运算的语言(如C ++),您的运行时间可能会明显加快

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

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

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

> ./a.out
Enter n (1 <= n <= 10,000,000; 0 to stop): 1000000
g(1000000) = 475712, in 0.020s
Enter n (1 <= n <= 10,000,000; 0 to stop): 0
>
> <restarted the program to remove the effect of memoization>
>
> ./a.out
Enter n (1 <= n <= 10,000,000; 0 to stop): 10000000
g(10000000) = 4757120, in 0.198s
Enter n (1 <= n <= 10,000,000; 0 to stop): 6745231
g(6745231) = 3145729, in 0.047s
Enter n (1 <= n <= 10,000,000; 0 to stop): 0

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

价值的猜想
For `n>=1`, prove that:  
f(2n+k) = 2n-1+1 for k=1,2,…,2n-1  ...(1)  
f(2n+k) = k     for k=2n-1+1,…,2n  ...(2)
given f(0)=f(1)=f(2)=1

通过考虑以下四种情况,使用归因关系的归纳可以很容易地证明上述结果:

  • 案例1:(1)for even k
  • 案例2:(1)for odd k
  • 案例3:(2)for even k
  • 案例4:(2)for odd k
Suppose we have the four cases proven for n. Now consider n+1.
Case 1:
f(2n+1+2i) = f(2n+i) + f(2n+i+1) - 1, for i=1,…,2n-1
          = 2n-1+1 + 2n-1+1 - 1
          = 2n+1

Case 2:
f(2n+1+2i+1) = 2*f(2n+i+1) - 1, for i=0,…,2n-1-1
            = 2*(2n-1+1) - 1
            = 2n+1

Case 3:
f(2n+1+2i) = f(2n+i) + f(2n+i+1) - 1, for i=2n-1+1,…,2n
          = i + (i+1) - 1
          = 2i

Case 4:
f(2n+1+2i+1) = 2*f(2n+i+1) - 1, for i=2n-1+1,…,2n-1
            = 2*(i+1) - 1
            = 2i+1

因此通过归纳证明了猜想。

将templatetypedef推测为最佳位置

For n>=1 and k=1,…,2n, prove that g(2n+k) = g(2n+k, 2n+1)
That is, prove that placing the first bit on the 2n+1-th position gives maximum number of bits set.

证据:

First, we have
g(2n+k,2n+1) = f(2n+1) + f(k-1) - 1

Next, by the formula of f, we have the following equalities:
f(2n+1-i) = f(2n+1), for i=-2n-1,…,-1
f(2n+1-i) = f(2n+1)-i, for i=1,…,2n-2-1
f(2n+1-i) = f(2n+1)-2n-2, for i=2n-2,…,2n-1

and also the following inequality:
f(k-1+i) <= f(k-1), for i=-2n-1,…,-1
f(k-1+i) <= f(k-1)+i , for i=1,…,2n-2-1
f(k-1+i) <= f(k-1)+2n-2, for i=2n-2,…,2n-1

and so we have:
f(2n+1-i)+f(k-1+i) <= f(2n+1)+f(k-1), for i=-2n-1,…,2n-1

Now, note that we have:
g(2n+k) = maxi=1..ceil(2n-1+1-k/2)(f(i) + f(2n+k+1-i) - 1)
       <= f(2n+1) + f(k-1) - 1
        = g(2n+k,2n+1)

因此推测证明了这一点。

答案 1 :(得分:4)

因此,在我没有发布算法的常规传统中,我没有证明,我想我应该提一下,对于高达50,000+的数字,并且在O中运行的算法看起来是正确的(log n)时间。这是由于Sophia Westwood,我今天在这个问题上工作了大约三个小时。所有功劳归功于她。从经验来看,它看起来效果很好,并且比O(n)解决方案快得多。

关于这个问题的结构的一个观察是,如果n足够大(n≥5),那么如果你把1放在任何地方,问题会分成两个子问题,一个在1的左边,一个在对。虽然1s可能会在不同的时间放置在不同的一半,但最终的放置方式与分别解决每一半并将它们组合在一起的方式相同。

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

  • 下一个1位于阵列的另一侧。
  • 下一个放在中间。
  • 您现在有两个较小的子问题,大小为2 k-1 + 1.

关于这一点的重要部分是得到的位模式是1和0的交替序列。例如:

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

重要的原因如下:假设数组中有n个元素,并且k是2 k +1≤n的最大可能值。如果你将1放在位置2 k + 1,那么阵列的左边部分到达那个位置将最终得到平铺,交替的1和0,这将放置批次 1s进入数组。

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

唯一的问题是我没有正式证明这是有效的。我所知道的是,我们尝试的前50,000个值是正确的。 : - )

希望这有帮助!

答案 2 :(得分:2)

我会附上我拥有的东西。和你的一样,唉,时间基本上是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