1

Let's say we have numbers factors, for example 1260:

>>> factors(1260)
[2, 2, 3, 3, 5, 7]

Which would be best way to do in Python combinations with every subproduct possible from these numbers, ie all factorings, not only prime factoring, with sum of factors less than max_product?

If I do combinations from the prime factors, I have to refactor remaining part of the product as I do not know the remaining part not in combination.

I can also refine my divisors function to produce pairs of divisors instead of divisors in size order, but still it will cost me to do this for number with product upto 12000. The product must remain always the same.

I was linked to divisor routine, but it did not look worth the effort to prove them to adopt to my other code. At least my divisor function is noticably faster than sympy one:

def divides(number):
    if number<2:
        yield number
        return
    high = [number]
    sqr = int(number ** 0.5)
    limit = sqr+(sqr*sqr != number)
    yield 1
    for divisor in xrange(3, limit, 2) if (number & 1) else xrange(2, limit):
        if not number % divisor:
            yield divisor
            high.append(number//divisor)
    if sqr*sqr== number: yield sqr
    for divisor in reversed(high):
        yield divisor

Only problem to reuse this code is to link the divisors to factoring sieve or do some kind of itertools.product of divisors of the divisors in pairs, which I would give out as pairs instead of sorting to order.

Example results would be:

[4, 3, 3, 5, 7] (one replacement of two)
[5, 7, 36] (one replacement of three)
[3, 6, 14, 5] (two replacements)

Probably I would need some way to produce sieve or dynamic programming solutions for smaller divisors which could be linked to numbers whose divisors they are. Looks difficult to avoid overlap though. I do have one sieve function ready which stores biggest prime factor for each number for speeding up the factoring without saving complete factorings of every number... maybe it could be adapted.

UPDATE: The sum of factors should be near the product, so probably there is high number of factors of <= 10 in answer (upto 14 factors).

UPDATE2: Here is my code, but must figure out how to do multiple removals recursively or iteratively for parts > 2 long and dig up the lexical partitioning to replace the jumping bit patterns which produce duplicates (pathetic hit count only for one replacement, and that does not count the passing of 'single element partionings' inside the single_partition):

from __future__ import print_function
import itertools
import operator
from euler import factors

def subset(seq, mask):
    """ binary mask of len(seq) bits, return generator for the sequence """
    # this is not lexical order, replace with lexical order masked passing duplicates
    return (c for ind,c in enumerate(seq) if mask & (1<<ind))


def single_partition(seq, n = 0, func = lambda x: x):
    ''' map given function to one partition  '''
    for n in range(n, (2**len(seq))):
        result = tuple(subset(seq,n))
        others = tuple(subset(seq,~n))
        if len(result) < 2 or len(others) == 0:
            #empty subset or only one or all
            continue
        result = (func(result),)+others
        yield result


if __name__=='__main__':
    seen,  hits, count = set(), 0, 0
    for f in single_partition(factors(13824), func = lambda x: reduce(operator.mul, x)):
        if f not in seen:
            print(f,end=' ')
            seen.add(f)
        else:
            hits += 1
        count += 1
    print('\nGenerated %i, hits %i' %(count,hits))

REFINEMENT I am happy to get only the factorings with max 5 factors in the non-prime factor parts. I have found by hand that non-decreasing arrangements for up to 5 same factors follow this form:

partitions of 5    applied to 2**5
1  1  1   1  1     2  2   2   2  2
1  1  1     2      2  2   2    4
1  1  1  3         2  2      8
1   2       2      2    4      4 
1       4          2      16
  2      3           4       8

THE SOLUTION I do not remove the accepted answer from fine solution down, but it is over complicated for the job. From Project Euler I reveal only this helper function from orbifold of NZ, it works faster and without needing the prime factors first:

def factorings(n,k=2):
    result = []
    while k*k <= n:
        if n%k == 0:
            result.extend([[k]+f for f in factorings(n/k,k)])
        k += 1
    return result + [[n]]

The relevant solution for problem 88 of his run in Python 2.7 in 4.85 s by my timing decorator and after optimizing the stop condition by found counter 3.4 s in 2.6.6 with psyco, 3.7 s in 2.7 without psyco . Speed of my own code went from 30 seconds with code in accepted answer (sorting added by me removed) to 2.25 s (2.7 without psyco) and 782 ms with psyco in Python 2.6.6.

Tony Veijalainen
  • 5,447
  • 23
  • 31

3 Answers3

1

What you are looking for is more commonly called a divisor. This answers to this question may help you.

Community
  • 1
  • 1
President James K. Polk
  • 40,516
  • 21
  • 95
  • 125
  • Yes, and I have quite effective routine for that, but the problem is that my routine does not produce subsets of factors only pairs of divisors which overlap in their use of factors. I would need to loop to produce all possible arrangements for numbers less than 12000. At least with the smallest factors. – Tony Veijalainen Mar 05 '11 at 23:36
1
from __future__ import print_function
import itertools
import operator

def partition(iterable, chain=itertools.chain, map=map):
    # http://code.activestate.com/recipes/576795/
    # In [1]: list(partition('abcd'))
    # Out[1]: 
    # [['abcd'],
    #  ['a', 'bcd'],
    #  ['ab', 'cd'],
    #  ['abc', 'd'],
    #  ['a', 'b', 'cd'],
    #  ['a', 'bc', 'd'],
    #  ['ab', 'c', 'd'],
    #  ['a', 'b', 'c', 'd']]    
    s = iterable if hasattr(iterable, '__getslice__') else tuple(iterable)
    n = len(s)
    first, middle, last = [0], range(1, n), [n]
    getslice = s.__getslice__
    return [map(getslice, chain(first, div), chain(div, last))
            for i in range(n) for div in itertools.combinations(middle, i)]

def product(factors,mul=operator.mul):
    return reduce(mul,factors,1)

def factorings(factors,product=product,
               permutations=itertools.permutations,
               imap=itertools.imap,
               chain_from_iterable=itertools.chain.from_iterable,
               ):
    seen=set()
    seen.add(tuple([product(factors)]))
    for grouping in chain_from_iterable(
        imap(
            partition,
            set(permutations(factors,len(factors)))
            )):
        result=tuple(sorted(product(group) for group in grouping))
        if result in seen:
            continue
        else:
            seen.add(result)
            yield result

if __name__=='__main__':
    for f in factorings([2,2,3,3,5,7]):
        print(f,end=' ')

yields

(3, 420) (9, 140) (28, 45) (14, 90) (2, 630) (3, 3, 140) (3, 15, 28) (3, 14, 30) (2, 3, 210) (5, 9, 28) (9, 10, 14) (2, 9, 70) (2, 14, 45) (2, 7, 90) (3, 3, 5, 28) (3, 3, 10, 14) (2, 3, 3, 70) (2, 3, 14, 15) (2, 3, 7, 30) (2, 5, 9, 14) (2, 7, 9, 10) (2, 2, 7, 45) (2, 3, 3, 5, 14) (2, 3, 3, 7, 10) (2, 2, 3, 7, 15) (2, 2, 5, 7, 9) (2, 2, 3, 3, 5, 7) (5, 252) (10, 126) (18, 70) (6, 210) (2, 5, 126) (5, 14, 18) (5, 6, 42) (7, 10, 18) (6, 10, 21) (2, 10, 63) (3, 6, 70) (2, 5, 7, 18) (2, 5, 6, 21) (2, 2, 5, 63) (3, 5, 6, 14) (2, 3, 5, 42) (3, 6, 7, 10) (2, 3, 10, 21) (2, 3, 5, 6, 7) (2, 2, 3, 5, 21) (4, 315) (20, 63) (2, 2, 315) (4, 5, 63) (4, 9, 35) (3, 4, 105) (7, 9, 20) (3, 20, 21) (2, 2, 9, 35) (2, 2, 3, 105) (4, 5, 7, 9) (3, 4, 5, 21) (3, 3, 4, 35) (3, 3, 7, 20) (2, 2, 3, 3, 35) (3, 3, 4, 5, 7) (7, 180) (3, 7, 60) (2, 18, 35) (2, 6, 105) (3, 10, 42) (2, 3, 6, 35) (15, 84) (12, 105) (3, 5, 84) (5, 12, 21) (7, 12, 15) (4, 15, 21) (2, 15, 42) (3, 5, 7, 12) (3, 4, 7, 15) (2, 6, 7, 15) (2, 2, 15, 21) (21, 60) (30, 42) (6, 7, 30) (5, 7, 36) (2, 21, 30) (5, 6, 6, 7) (3, 12, 35) (6, 14, 15) (4, 7, 45) (35, 36) (6, 6, 35)
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • Looks nice, I do have one of my own partitioning funcitions around. Looks probable that this one is more efective though. First result should be dropped for proper factors answer though. I hope the regeneration is not much, as I see you use seen set. I think you use it to do 'permutations_with_repetitions' like I have one version picked up from net before. Must see if it is better than my idea of using suitable size binary number (bit per item) for selection and complement it for knowing the left over part from items. – Tony Veijalainen Mar 06 '11 at 01:03
  • @Tony Veijalainen: Yes, there are probably better ways to implement this. If you put a counter in the `if result in seen` block, you'll see over 600 repetitions of already-generated results. – unutbu Mar 06 '11 at 01:17
  • Must try the code for permutation with repeats function. Maybe must count multiplicity and do partitioning of the exponent numbers for each factor. My own partitioning function was also producing repeats and I was using seen set like your solution for this problem when I used it last. Looks like the partitioning you used does not deal also well with repeats. – Tony Veijalainen Mar 06 '11 at 01:32
  • @Tony Veijalainen: I think the repetitions come from situations like this: `[2,2,3,3,5,7]` gets partitioned into `[[2],[2],[3],[3],[5,7]]` and `[2,2,3,3,7,5]` gets partitioned into `[[2],[2],[3],[3],[7,5]]`. Note the `[5,7]` and `[7,5]` both end up forming the same divisor, 35. I haven't thought of a good way to avoid this. – unutbu Mar 06 '11 at 01:39
1

I use a list like [(2, 9), (3, 3)] (for [2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3]) as the base list of unexpanded factors and a list of expanded factors. In each round I pick one factor from the base, decreasing it's count, and

  • add it to the expanded list, increasing it's length, so we have one additional factor in total (until the cufoff)
  • multiply it with all expanded factors, generating all possibilities

With dynamic programming and the cutoff strategy this became extremely fast:

from itertools import groupby

def multiplicies( factors ):
    """ [x,x,x,,y,y] -> [(x,3), (y,2)] """
    return ((k, sum(1 for _ in group)) for k, group in groupby(factors))

def combinate(facs, cutoff=None):
    facs = tuple(multiplicies(facs))

    results = set()
    def explode(base, expanded):
        # `k` is the key for the caching
        # if the function got called like this before return instantly
        k = (base, expanded)
        if k in results:
            return
        results.add(k)

        # pick a factor
        for (f,m) in base:
            # remove it from the bases
            newb = ((g, (n if g!=f else n-1)) for g,n in base)
            newb = tuple((g,x) for g,x in newb if x > 0)

            # do we cutoff yet?
            if cutoff is None or len(newb) + len(expanded) < cutoff:
                explode(newb, tuple(sorted(expanded + (f,))))

            # multiply the pick with each factor in expanded
            for pos in range(len(expanded)):
                newexp = list(expanded)
                newexp[pos] *= f
                explode(newb, tuple(sorted(newexp)))

    explode(facs, ())
    # turn the `k` (see above) into real factor lists
    return set((tuple((x**y) for x,y in bases) + expanded) 
        for (bases, expanded) in results)

# you dont even need the cutoff here!
combinate([2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3])
# but you need it for 
combinate([2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3,5,7,9,11], 5)

Try Psyco if you can (I cant because I only have Py2.7 here), it might speed this up quite a bit too.

Jochen Ritzel
  • 104,512
  • 31
  • 200
  • 194
  • Any special reason defining branch again in every recursion? – Tony Veijalainen Mar 06 '11 at 01:22
  • @Tony Veijalainen: The recursion happens in `branch` not in `combinate`. The function is defined once per input. – Jochen Ritzel Mar 06 '11 at 02:00
  • Your program did well with example in my post but seemed to freeze with factors of 13824, finally giving answer after 3 min 35 s. Must find way to limit the form of factors. Yes you are not calling the combinate recursively, should have looked your code more carefully, sorry! – Tony Veijalainen Mar 06 '11 at 02:08
  • @Tony Veijalainen: Good example, with factors like `[2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3]` its easy to see that my approach generates lots of factors like `[4, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3], [2, 4, 2, 2, 2, 2, 2, 2, 3, 3, 3], [2, 2, 4, 2, 2, 2, 2, 2, 3, 3, 3]` which are all the same. Pretty much the same problem unutbu has. So i guess you'd have to group factors together, but this is getting quite complicated. Maybe tomorrow. – Jochen Ritzel Mar 06 '11 at 02:37
  • If it would be easy, I would not be asking :) I will be able to solve it, but thought ask advice to pass some of the hurdles. See my pathetic try for what I have tried and why I stoped to consider the approach to avoid regeneration. With upto 14 factors (even rearly), mostly small, the permutation explosion comes fast (that is factorial, not exponential even). – Tony Veijalainen Mar 06 '11 at 09:38
  • @Tony Veijalainen: Btw, is this for some math puzzle? – Jochen Ritzel Mar 06 '11 at 15:49
  • Thanks for fine answer. I only added sorted `(sorted(sorted(comb) for comb in combinate([2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3])))` and then must test it in my program. Fine job. Only thing is that I feel now quilty if I use it in my program, so maybe I try to do also solution of my own based on the non-decreasing 5 partitions for the Euler problem 88. Must just add correct number of ones in front and loop the factorings of numbers until 1200 saving the smallest sums for each length. – Tony Veijalainen Mar 06 '11 at 16:21
  • Lookes from forum, that sieve style solution is finally the best to my need as it need to be done preferably until 2*n (I did get correct result even with 1.5*n factorings) which means factoring all numbers until 24000. I did get result but little over one minute, when best solutions in Python took 1.6 s total memo function with psyco. – Tony Veijalainen Mar 07 '11 at 01:30
  • See the best code in end of my post from forum of Project Euler. – Tony Veijalainen Mar 07 '11 at 08:38