0

So I've been trying to learn Haskell by solving some problems on Codeforce.
And I am getting a lot of TLE (Time Limit Exceed) even though I think my time complexity is optimal.

My question is: is the way I wrote this program that makes it slow?

For example, here is the problem.

Basically the answer is to find an for a given n , where an = 2*an-1 + D(n) and D(n) = the difference of the number of divisors between n and n-1.

(update: the top limit for n is 106).

Below is my program.

import qualified Data.Map.Strict as Map

main = do t <- read <$> getLine
          putStrLn . show $ solve t

solve :: Integer -> Integer
solve 0 = 1
solve 1 = 1
solve n = (2*(solve (n-1)) + (fact n) - (fact (n-1))) `mod` 998244353
    where fact n = foldl (\s -> \t -> s*(snd t + 1)) 1 (Map.toList . factorization  $ n)
    --the number of divisors of a number

--copied from Internet,infinite prime list
primes :: [Integer]
primes = 2: 3: sieve (tail primes) [5,7..]
  where
    sieve (p:ps) xs = h ++ sieve ps [x | x <- t, x `rem` p /= 0]
      where (h,~(_:t)) = span (< p*p) xs

--make factorization of a number
factorization :: Integer -> Map.Map Integer Integer
factorization 1 = Map.fromList []
factorization x = Map.insertWith (+) factor 1 (factorization (x `div` factor))
    where factor = head $ filter (\s -> (x `mod` s) == 0) ls
          ls = primes

This program failed to solve in the time limit.

So could anyone point me out where did I do wrong and how to fix it?
Or it just impossible to solve this problem using Haskell in time limit?

Will Ness
  • 70,110
  • 9
  • 98
  • 181
timliao
  • 27
  • 2
  • There are some ways to improve your code, but I think the problem fundamentally is the algorithm you've chosen. I'm not an expert, but a quick search yields this article: https://www.geeksforgeeks.org/count-divisors-n-on13/. Maybe a simpler, but less efficient algorithm is still sufficient, e.g.: https://stackoverflow.com/a/15825953/15207568. – Noughtmare Jun 19 '21 at 09:44
  • 2
    This program is manipulating very big numbers before taking their modulus. Time complexity is not optimal at all. – Li-yao Xia Jun 19 '21 at 10:23
  • 1
    In fact the program can easily get away with using `Int` throughout. – n. m. could be an AI Jun 19 '21 at 10:39
  • @Li-yaoXia May I ask how is that affecting time complexity?Since solve (n-1) have taken module and there just two small addtion,will it take program more time than times 2 , module , addition and module?Or in other word,how is manipulating big integer affecting performance? – timliao Jun 19 '21 at 14:32
  • @Noughtmare the n^(1/3) method is mind-blowing.I was stuck in my own thought. Thanks for your reply. – timliao Jun 19 '21 at 14:39
  • try replacing your `factorization` by [`primeFactors`](https://wiki.haskell.org/Testing_primality#Primality_Test_and_Integer_Factorization) with [`primesTMWE`](https://wiki.haskell.org/Prime_numbers#Tree_merging_with_Wheel) instead of `primes`. your `foldl` based calculation of the number of divisors from number's factorization is much much better than the answer you got the link for, which isn't very good at all. – Will Ness Jun 19 '21 at 18:57
  • also notice that both `solve n` and `solve (n-1)` calculate `fact (n-1)`, so better precalculate all of them..... perhaps a better algorithm exists to find the numbers of divisors for all numbers from `1` to `n` than calculating each separately. – Will Ness Jun 19 '21 at 19:17

2 Answers2

5

There are many ways in which your time complexity is not optimal. The most obvious one is a prime finder using trial division instead of, e.g., a sieve. Maybe it's fine because you only compute the primes once, but it does not inspire confidence.

factorization also has at least one glaring problem. Consider factoring a number like 78893012641, whose prime factorization is 280879^2. You will search each prime number up to 280879: expensive, but pretty much unavoidable. However, at this point you divide by 280879 and then try to factorize 280879, starting from 2 and scanning all the small primes again even though you just found out none of them are a factor!

As Li-yao Xia says in a comment, I would also be suspicious of the multiplication of very large Integers before taking their modulus, instead of taking a modulus after each multiplication.

amalloy
  • 89,153
  • 8
  • 140
  • 205
  • The `factorization` function is even worse when the input is a large prime, it will try all primes until it reaches that prime. And the `primes` list is shared, so it can eat up all your memory even for primes less than 2^64. – Noughtmare Jun 19 '21 at 10:35
  • I completely overlooked that part! I could have made it a square-root better.Thanks for your answer. – timliao Jun 19 '21 at 14:35
  • 1
    the problem's limit for `n` is 10^6. (the OP did not include this limit in the post, it only appears on the linked page) . correct factorization algorithm needs only primes up to 1000 for that. the use of the optimal trial division sieve instead of the sieve of Eratosthenes is perfectly fine here. – Will Ness Jun 19 '21 at 19:47
1

You haven't copied the right piece of code from the "Internet". You should've instead copied primesTMWE for the primes list, but more importantly, primeFactors for the factorization algorithm.

Your foldl based calculation of the number of divisors from a number's factorization is perfectly fine, except perhaps foldl' should be used instead.

Notice that both solve n and solve (n-1) calculate fact (n-1), so better precalculate all of them..... perhaps a better algorithm exists to find the numbers of divisors for all numbers from 1 to n than calculating it for each number separately.

I suspect even with the right algorithms (which I link above) it's going to be tough, time-wise, if you're going to factorize each number independently (O(n) numbers, O(n1/2)) time to factorize each... each prime, at least).

Perhaps the thing to try here is the smallest-factor sieve which can be built in O(n log log n) time as usual with the sieve of Eratosthenes, and once it's built it lets you find the factorization of each number in O(log log n) time (it's the average number of prime factors for a number). It will have to be built up to n though (you can special-case the evens to halve the space requirements of course; or 6-coprimes to save another 1/6th). Probably as an STUArray (that link is an example; better codes can be found here on SO).

The smallest-factor sieve is just like the sieve of Eratosthenes, except it uses the smallest factor, not just a Boolean, as a mark.

To find a number's factorization then we just repeatedly delete by a number's smallest factor, n / sf(n) =: n1, repeating for n1 / sf(n1) =: n2, then n2, etc. until we hit a prime (which is any number which has itself as the smallest factor).

Since you only use those factors to calculate the number's total number of divisors, you can fuse the two calculations together into one joined loop, for extra efficiency.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
  • The factorization part is similar to what I tried this morning.Mine is just calculating the number of factor by recursively cutting down the smallest factor.But it made little improvement to the time. I'm now looking at other people's solution using Haskell where they didn't even build a prime list.I'm doubting that I was wrong on the very first step. – timliao Jun 20 '21 at 12:23
  • you can post new question with the up-to-date code. – Will Ness Jun 20 '21 at 15:25
  • your comment conflates two issues here. one is finding the factorization, the other is calculating the number of distinct divisors from it. you already have the function `fact` that does the second part in _log log n_ time. to efficiently do the first, you need to build the smallest factor sieve (takes _N log log N_ time), and then use it to find the factorization of each number _n_ in it, in _log log n_ time. this is enormously better than either _n^(1/2)_ or _n^(1/3)_. – Will Ness Jun 20 '21 at 15:39