解决这个难题的最佳算法是什么?

时间:2017-01-25 11:19:28

标签: python algorithm math

所以我遇到了这个问题:

从1到1000有多少个数字不能被数字2,3和5整除?

起初看起来很简单,所以我写了一个快速的python程序来解决它:

#!/user/bin/env python3

import sqlite3

con = sqlite3.connect(':memory:')
cur = con.cursor()
cur.execute('CREATE TABLE test (name, age)')
cur.execute('INSERT INTO test VALUES (:name, :age)', {'name': 'Aaron', 'age': 75})
cur.execute('INSERT INTO test VALUES (:name, :age)', {'name': 'Zebedee', 'age': 5})

cur.execute('SELECT * FROM test ORDER BY age ASC')
results = cur.fetchall()
print('\nGood, but hard coded:\n', results)
# Good, but hard coded:
#  [('Zebedee', 5), ('Aaron', 75)]

cur.execute('SELECT * FROM test ORDER BY :order_by ASC', {'order_by': 'age'})
results = cur.fetchall()
print('\norder_by parameter ignored:\n', results)
# order_by parameter ignored:
#  [('Aaron', 75), ('Zebedee', 5)]

cur.execute('SELECT * FROM test ORDER BY {order_by} ASC'.format(order_by='age'))
results = cur.fetchall()
print('\nRight order, but vulnerable to SQL injection:\n', results)
# Right order, but vulnerable to SQL injection:
#  [('Zebedee', 5), ('Aaron', 75)]

con.close()

我得到了正确的答案(266),但我认为如果我想检查的不仅仅是3个值,那么这样做是很多打字。我也想做一个数学解决方案,所以我遇到了这个:

count = 0 for number in range(1,1000): if number % 2 != 0 and number % 3 != 0 and number % 5 != 0: count += 1 print(count)

我认为这是一个很好的方法,所以我在代码中实现了它:

1000 - ((1000/2 +1000/3 +1000/5) -(1000/2x3 +1000/2x5 + 1000/3x5)+ (1000/2x3x5)) = 1000-((500+333+200) - (166 +100 + 66) + 33) = 1000- 734 = 266

现在我很确定我在第二个函数中的某个地方搞砸了,因为它似乎不适用于更多的数字。例如,如果我试图找到

从1到1000有多少个数字不能被数字2,3,5和7整除?

第一种方法返回def foo(ln = 1000), numbers = [2,3,5]: div = 0 muldiv = 0 totdiv = 1 for n in numbers: div += ln/n for i in numbers: for n in range(numbers.index(i)+1, len(numbers)): muldiv += ln/(i * numbers[n]) for n in numbers: totdiv *= n answer = ln - (div - muldiv + ln/totdiv) print("answer is ", math.floor(answer)) 228返回foo(numbers = [2,3,5,7]) ...我很确定300是正确答案,因为还有一个数字意味着有228较少的因素而不是更多,但我哪里出错了?有没有更好的方法来解决这个问题?

9 个答案:

答案 0 :(得分:5)

你不需要算法,简单的数学就足够了:

假设您要计算从 k 分割的数字1到 N (包括)的数量,这相当于:

地板(N / k)的

因此,在这种情况下可分为3的数字是333。

现在你不能简单地使用计算可分为2,3和5的数字的数量;总结一下,因为有共同点。确实:例如,15和3都可以分割。

您可以使用inclusion-exclusion principle

解决此问题

可分为2,3和5的数字量与

相同
  • 可分割的数量为2
  • 加上可分割为3的数字
  • 加上可分割为5的数字
  • 减去可分割为2和3的数字
  • 减去可分为2和5的数字
  • 减去可分为3和5的数字
  • 加上可分为2,3和5的数字。

因此,为了解决您的第一个问题,您可以简单地说明:

def n_div(N,k):
    return N//k

def n_div235(N):
    return n_div(N,2)+n_div(N,3)+n_div(N,5)-n_div(N,2*3)-n_div(N,2*5)-n_div(N,3*5)+n_div(N,2*3*5)

def not_div235(N):
    return N-n_div235(N)

正如您所看到的,它会生成正确的结果:

>>> not_div235(1000)
266

只要 N 与除数的数量相比非常大,您最好使用包含 - 排除方法:

你可以这样做:

import itertools
from functools import reduce
import operator

def gcd(a, b):
    while b:      
        a, b = b, a % b
    return a

def lcm(a, b):
    return a * b // gcd(a, b)

def lcm_list(ks):
    res = 1
    for k in ks:
        res = lcm(res,k)
    return res

def n_div_gen(N,ks):
    nk = len(ks)
    sum = 0
    factor = 1
    for i in range(1,nk+1):
        subsum = 0
        for comb in itertools.combinations(ks,i):
            subsum += n_div(N,lcm_list(comb))
        sum += factor * subsum
        factor = -factor
    return sum

def not_div_gen(N,ks):
    return N-n_div_gen(N,ks)

对于小 N ,这不会得到回报,但是要说要计算可分为3,5和7的数字从1到1 000 000 000是:

>>> not_div_gen(1000000000,[3,5,7])
457142857

你可以这样做:

>>> sum(i%3!=0 and i%5!=0 and i%7!=0 for i in range(1,1000000001))
457142857

但是计算它需要几分钟,而我们自己的方法使用毫秒。请注意,这仅适用于巨大的N.

答案 1 :(得分:1)

将内置函数sumall与嵌套生成器一起使用:

def f(r=1000, nums=(2,3,5)):
    return sum(all(x%n for n in nums) for x in range(1, r+1))

这将遍历数字范围,检查每个数字是否具有每个指定数字的非零模数,并将这些布尔值相加(False为0且True为1) 。 nums (2,3,5,7)生成228的结果,这与您更短,更简单的代码(令人放心的是,不使用任何浮点运算)一致,作为您的第二个代码块一样)。

答案 2 :(得分:1)

直到N的整数不能被n 1 ,n 2 ,...,n t 整除(假设是(pairwise-coprime)是

最多N 减去的整数数量 (SUM i in 1..t (最多N的整数数除以n i )) plus
(SUM i,j in 1..t,i< j (最多N的整数数除以n i n j ) )减去
(SUM i,j,k in 1..t,i&lt; j&lt; k (最多N的整数个数除以n i n j < / sub> n k )) plus
(SUM i,j,k,l in 1..t,i&lt; j&lt; k&lt; l (最多N的整数数除以n i n < sub> j
n k n l )) minus
...... ...... ...... (SUM i,j,k,l,... q in 1..t,i&lt; j&lt; k&lt; l&lt; 1&lt; ...&lt; q (最多N个整数的整数by n i n j n k n l ... n q ) )

系列继续,直到下标包含原始列表中的所有 t 整数。

对于未知为成对互质的数字,请用最不常见的倍数替换它们的产品。

这就是为什么您的方法仅适用于3个数字的原因。您只计算该系列的前四个成员。

答案 3 :(得分:1)

这是另一个使用包含 - 排除的实现。它比Willem Van Onsem的优秀答案(我在编写此代码之前没有看到)中的代码简单,但只有在除数列表中的数字全部都有效时才会有效彼此相互影响。对于更一般的情况,您需要使用Willem的方法。

from itertools import combinations
from functools import reduce

def prod(seq, mul=int.__mul__):
    return reduce(mul, seq, 1)

def count_coprimes(n, divisors):
    total = n
    sign = -1
    for i in range(1, len(divisors) + 1):
        for k in combinations(divisors, i):
            total += n // prod(k) * sign
        sign = -sign
    return total

print(count_coprimes(1000, [2, 3, 5]))

<强>输出

266

FWIW,这里的算法与&#34; one-liner&#34;相同。 (分成几行以提高可读性)。由于内循环中的(-1)**i,它效率稍低。

def count_coprimes(n, divisors):
    return n + sum(n // prod(k) * (-1)**i
        for i in range(1, len(divisors) + 1)
            for k in combinations(divisors, i))

print(count_coprimes(1000000000, [3, 5, 7]))

<强>输出

457142857

我们可以通过否定除数和使用修改的整数除法函数来摆脱(-1)**i

def div(p, q):
    return p // q if q > 0 else -(p // -q)

def count_coprimes(n, divisors):
    return sum(div(n, prod(k))
        for i in range(len(divisors) + 1)
            for k in combinations([-u for u in divisors], i))

答案 4 :(得分:0)

你可以做一个非常小的改变,大致减少所需的时间,而不是生成从1到1000的所有数字,生成从1到1000的所有奇数:

count = 0
for number in range(1,1001,2):
    if number % 3 != 0 and number % 5 != 0:
        count += 1
print(count)

虽然这不是一个巨大的变化,也不是一个数学解决方案,但它使代码的可读性和效率更低。

考虑到你的其他代码,你可以在if语句中使用列表理解来检查其他数字(注意我也使用第一个数字来生成数字的初始列表,而不是执行模数运算全部1000):

def foo(lst):
    count = 0
    for number in range(1,1001,lst[0]):
        if not any([number % i == 0 for i in lst[1:]]):
            count += 1
    return count

>>> foo([2,3,5])
266
>>> foo([2,3,5,7])
228

答案 5 :(得分:0)

有许多方法可以迭代地解决这个小问题,所有这些方法都具有非常相似的性能,这里有几个例子:

import timeit


def f1(l, h):
    count = 0
    for number in range(l, h):
        if number % 2 != 0 and number % 3 != 0 and number % 5 != 0:
            count += 1

    return count


def f2(l, h):
    return len(filter(lambda x: x % 2 != 0 and x % 3 != 0 and x % 5 != 0, range(l, h)))


def f3(l, h):
    count = 0
    for number in range(l, h):
        if number % 2 == 0:
            continue
        if number % 3 == 0:
            continue
        if number % 5 == 0:
            continue

        count += 1

    return count


def f4(l, h):
    return len([x for x in range(l, h) if x % 2 != 0 and x % 3 != 0 and x % 5 != 0])

a, b, N = 1, 1000, 10000

print timeit.timeit('f1(a,b)', setup='from __main__ import f1, a, b', number=N)
print timeit.timeit('f2(a,b)', setup='from __main__ import f2, a, b', number=N)
print timeit.timeit('f3(a,b)', setup='from __main__ import f3, a, b', number=N)
print timeit.timeit('f4(a,b)', setup='from __main__ import f4, a, b', number=N)

i7-2.6ghz上的时间将是:

0.802361558825
1.46568073638
0.91737188946
0.846404330893

通常这些时间足以在下限/上限(1,1000)相对较小时考虑。现在,如果我们谈论计算不可行的真正高边界(万亿),你可以考虑应用更聪明的inclusion-exclusion principle,这样你就可以解决问题并且你可以获得与您的解决方案保持一致的时间。

答案 6 :(得分:0)

  1. <强>输入

    n为要测试的区间<0,n>d[]={2,3,5,0};为空终止的除数数组

  2. 计算d[]

    的最小公倍数

    最小公倍数是 SoE 重复自身的时间段。 2,3,5的{​​{1}}是lcm=30。在计算它时使用max(d[])作为增量来提高速度......如果LCM太大(LCM>=n),则使用n代替速度。

  3. 计算<0,LCM)

    的SoE

    只需创建 LCM 数字数组,并为不可分a[i]=1i设置a[i]=0以获得可分i

  4. 将SoE转换为不可分割的数字

    只需计算a'[i]=a[0]+a[1]+..a[i]

  5. 计算点数

    算是简单的:

    int(n/LCM)*a'[LCM-1] + a'[n%LCM];
    
  6. 这里简单的 C ++ 示例:

    int non_divisibles(int n,const int *d)  // SoE
        {
        int i,j,cnt,lcm,m,*a;
        for (m=0;d[m];m++); // compute m
        for (cnt=0,i=0;i<m;i++) if (cnt<d[i]) cnt=d[i]; // cnt = max d[] (to speed up LCM)
        // LCM d[]
        a=new int[m]; if (a==NULL) return -1;
        for (i=0;i<m;i++) a[i]=d[i];
        for (lcm=cnt;lcm<=n;lcm+=cnt) // no need to test above `n` even if lcm is bigger
            {
            for (i=0;i<m;i++) for (;a[i]<lcm;) a[i]+=d[i];
            for (i=0;i<m;i++) if (a[i]!=lcm) { i=-1; break; }
            if (i>=0) break;
            }
        delete[] a;
        // SoE <0,LCM)
        a=new int[lcm]; if (a==NULL) return -1;
        for (i=0;i<lcm;i++) a[i]=1;
        for (j=0;d[j];j++)
         for (i=0;i<lcm;i+=d[j])
          a[i]=0;
        // convert to cnt
        for (i=1;i<lcm;i++) a[i]+=a[i-1];
        // compute whole count
        cnt =(n/lcm)*a[lcm-1];
        cnt+=a[n%lcm];
        delete[] a;
        return cnt;
        }
    

    这里有一些测量来比较天真, SoE SoE (最大(n LCM ({{1} })))方法:

    d[]

    正如您所看到的, SoE(n)更好,如果 LCM n=1000000 d[]={ 2 3 5 7 11 13 17 19 } 171021 [ 27.514 ms] naive 171021 [ 12.642 ms] SoE 171021 [ 25.438 ms] LCM+Soe n=1000000 d[]={ 2 3 5 7 11 13 17 } 180524 [ 26.212 ms] naive 180524 [ 11.781 ms] SoE 180524 [ 9.807 ms] LCM+Soe n=1000000 d[]={ 2 3 5 7 11 13 } 191808 [ 24.690 ms] naive 191808 [ 11.512 ms] SoE 191808 [ 0.702 ms] LCM+Soe n=1000000 d[]={ 2 3 5 } 266666 [ 16.468 ms] naive 266666 [ 9.744 ms] SoE 266666 [ 0.006 ms] LCM+Soe n= 1000 d[]={ 2 3 5 } 266 [ 0.012 ms] naive 266 [ 0.012 ms] SoE 266 [ 0.001 ms] LCM+Soe n=1000000 d[]={ 2 3 5 19 23 61 87 10001 } 237662 [ 26.493 ms] naive 237662 [ 10.180 ms] SoE 237662 [ 19.429 ms] LCM+Soe 相比过大(n包含许多素数或大数字)但需要d[]空间。

答案 7 :(得分:0)

一旦你可以分开就可以拯救。

266 not divisible by [2, 3, 5]
228 not divisible by [2, 3, 5, 7]

输出:

WebSocketContainer

答案 8 :(得分:0)

def not_divisible(n = 1000, divisors = [2, 3, 5]):
    count = 0
    for i in range(1, n + 1):
        if all(1 if i % d else 0 for d in divisors):
            count += 1
    return count

第四行的说明:

  • 如果数字i不能被除数d整除,则i%d返回非零 整数。 Python将任何非零数字视为True。
  • 列表推导[如果i%d等于1,则除数为d,则返回1]返回a
    1和0的列表,如果数字不可整除,则值为 1,否则为0。
  • all()函数检查集合中的所有值是否均为True。如果 列表中的所有值均为1(真),表示数字为'not
    被所有除数整除。
  • 如果数字不能被2和3整除,则不能被2整除 6.因此,无需在此处进行检查。

这是一个较小的代码:

def not_divisible(n = 1000, divisors = [2, 3, 5]):
    return sum(1 for i in range(1, n + 1) if all(1 if i % d else 0 for d in divisors))

在这里,我们为无法被所有除数除的每个数字生成一个1的列表,该列表的总和就是答案。