I'm looking for a algorithm that counts number of all possible combinations that gives certain product. I have a list of perfect squares [1,4,9,16,..,n] and I have two values a, b where a - number of elements that we can use in multiplication to get perfect square, b - maximum value of each element that can be used in multiplication For example, if a = 3 and b = 6 then for perfect square 36 we can have combinations such as [1,6,6], [2,3,6], [4,3,3] and so on (order matters [1,6,6] and [6,6,1] are different). NOTE we can not use [1,9,4] combination because 9 > b
I've tried to use combinations of all divisors for each perfect square from itertools and after that I checked each product of combination and if x1x2x3 == 36 I added 1 to my counting for perfect square = 36. This algorithm works but it requires significant amount of time for long multiplication.
Can we make it faster than look at each combination for each perfect square?
def get_divisors(n):
result = []
for i in range(1, n//2 + 1):
if n % i == 0:
result.append(i)
result.append(n)
return result
a = 2
b = 3
count = 0
all_squares = [1,4,9]
for i in all_squares:
divisors = get_divisors(i)
for r in divisors:
if r > b:
divisors.remove(r)
for j in (itertools.product(divisors, repeat=a)):
if numpy.prod(j) == i:
count += 1
print(count)
Here's a mostly straightforward solution using recursive generators. No element larger than
bshows up because no element so large is ever considered. Duplicates never show up because, by construction, this yields lists in non-increasing order of elements (i.e., in lexicographically reverse order).I don't know what squares have to do with this. The function couldn't care less whether
targetis a square, and you didn't appear to make any use of that property in what you wrote.and then, e.g.,
If
targetcan grow large, the easiest "optimization" to this would be to precoumpute all its non-unit factors <= b, and restrict the"for largest in ..."loop to look only at those.To just count the number, use the above like so:
EDIT
BTW, as things go on, mounds of futile search can be saved by adding this at the start:
However, whether that speeds or slows things overall depends on the expected characteristics of the inputs.
OPTIMIZING
Running the above:
That took close to 30 seconds on my box. What if we changed the function to just return the count, but not the solutions?
Returns the same total, but took closer to 20 seconds.
Now what if we memoized the function? Just add two lines before the
def;Doesn't change the result, but cuts the time to under half a second.
So the bulk of the recursive calls were duplicates of calls already resolved, and were satisfied by the hidden cache without executing the body of the function. OTOH, we're burning memory for a hidden dict holding about 32K entries.
Season to taste. As a comment noted, the usual alternative to memoizing a recursive function is to do heavier thinking to come up with a "dynamic programming" approach that builds possible results "from the bottom up", typically using explicit lists instead of hidden dicts.
I won't do that here, though. It's already thousands of times faster than I have any actual use for ;-)
Computing # of permutations from a canonical
Here's code for that:
Counting distinct permutations
This may be what you really want. The code is simpler because it's not being "clever" at all, treating all positions exactly the same (so, e.g., all 3 distinct ways of permuting
[6, 6, 1]are considered to be different).You absolutely need to memoize this if you want it to complete a run in your lifetime ;-)
The second way uses the original function, and applies the earlier
numperms()code to deduce how many distinct permutations each canonical solution corresponds to. It doesn't benefit from caching, though, so is very much slower. The first way took several seconds under CPython, but 1 under PyPy.Of course the universe will end before any approach using itertools to consider all possible arrangements of divisors could compute a result so large.
Moving to DP
For pedagogical purposes, I think it's worth the effort to show a dynamic programming approach too. The result here is easily the fastest of the bunch, which is typical of a DP approach. It's also typical - alas - that it takes more up-front analysis.
This is much like recursive memoization, but "bottom up", starting with the simplest cases and then iteratively building them up to fancier cases. But we're not waiting for runtime recursion to figure out which simpler cases are needed to achieve a fancier result - that's all analyzed in advance.
In the
dimple()code,bis invariant. What remains can be viewed as entries in a large array,M[i, a], which gives the number of ways to express integerias a product ofafactors (all <=b).There is only one non-zero entry in "row" 0:
M[1, 0] = 1. The empty product (1) is always achievable.For row 1, what can we obtain in one step? Well, every divisor of
targetcan be reduced to the row 0 case by dividing it by itself. So row 1 has a non-zero entry for every divisor oftarget<=b, with value 1 (there's only one way to get it).Row 2? Consider, e.g.,
M[6, 2]. 6 can be gotten from row 1 via multiplying 1 by 6, or 6 by 1, or 2 by 3, or 3 by 2, soM[6, 2] = M[1, 1] + M[2, 1] + M[3, 1] + M[6, 1] = 4,And so on. With the right preparation, the body of the main loop is very simple.
Note that each row
i's values depend only on rowi-1, so saving the whole array isn't needed. The code only saves the most recent row, and builds the next row from it. In fact, it's often possible to update to the next row "in place", so that memory for only one row is needed. But offhand I didn't see an efficient way to do that in this particular case.In addition, since only divisors of
targetare achievable, most of a denseMwould consist of 0 entries. So, instead, a row is represented as adefaultdict(int), so storage is only needed for the non-zero entries.Most of the code here is in utility routines to precompute the possible divisors.
EDIT: simplified the inner loop by precomputing invariants even before the outer loop starts.
Note that the amount of memory needed is proportional to the number of divisors of
target, and is independent ofa. When memoizing a recursive function, the cache never forgets anything (unless you implement and manage it yourself "by hand").