Nearly Square Divisors, Meet In The Middle

September 6, 2016

The nearly square divisors exercise has generated a considerable amount of interest, and several excellent solutions in the comments. We looked previously at Matthew Arcus’ solution using a knapsack algorithm, with logarithms to reduce the problem from multiplication to addition, thus allowing languages like C to solve the problem using their native data types instead of switching to big integers.

The knapsack solution works like this: To find the nearly square divisor of n, factor n and form the list of divisors ds. Then use the subset sum algorithm of a previous exercise (a variant of knapsack), taking products rather than sums, to find the maximal product of divisors less than the square root of n. There are several ways to solve the subset sum problem. The standard solution uses dynamic programming. Another solution splits the problem space into two parts. Both solutions require exponential time, though the meet-in-the-middle solution has better asymptotic time of O(n2n/2). We also studied a solution that takes polynomial time to produce an approximate answer to the subset sum problem, although that solution is not helpful to us because it already takes exponential time to calculate all the divisors.

Today we look at another solution buried in the comments, this one from Paul Hofstra. Here is his solution:

def divisors(facs):
    factors = [(1,) + tuple(accumulate(g, mul)) for _, g in groupby(facs)]
    div = [1]
    for g in factors:
        div = [d * f for d in div for f in g]
    return div
def nsd5(number):
    """ output: nearly square divisor
        method: split factors in 2 equal parts
                create divisors with small factors and sort (descending)
                create divisors with large factors (<= ulimit) and sort
                loop over large divisors and small divisors and search
                   for highest product <= ulimit
    ulimit = isqrt(number)
    facs = rho_factors(number)
    mid = len(facs) // 2
    descending = reversed(sorted(divisors(facs[:mid])))
    ascending = iter(sorted(divisors(facs[mid:])))
    best = 0
    desc = next(descending)
    while True:
        for asc in ascending:
            prod = asc * desc
            if prod > best:
                if prod <= ulimit:
                    best = prod
            break  # while
        for desc in descending:
            prod = asc * desc
            if prod <= ulimit:
                if prod > best:
                    best = prod
            break  # while
    return best

With this solution we are back to using big integers, and we are using the meet-in-the-middle variant of the subset sum solution to calculate the answer. The solution finds the factors, splits them into two halves, calculates the divisors of each half, then uses subset sum to find the maximal divisor less than the square root. Note that we don’t have to compute all the divisors, only the divisors of each of the two halves.

Your task is to implement Hofstra’s solution to the nearly square divisor problem and use it to calculate the nearly square divisor of the product of the primes less than 190. When you are finished, you are welcome to read or run a suggested solution, or to post your own solution or discuss the exercise in the comments below:


Pages: 1 2

One Response to “Nearly Square Divisors, Meet In The Middle”

  1. matthew said

    I’d come across this way of solving the problem but have to say I’m impressed with how much faster than branch-and-bound it is.

    How about a simulated annealing solution?

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: