packages feed

data-findcycle-0.1.0.0: src/Data/FindCycle.hs

{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}

{- |
  Module: Data.FindCycle
  Description: Find cycles in periodic functions (and lists)
  Copyright: (c) 2025 Florian Ragwitz
  License: MIT

  Any function @f :: a -> a@ where the type @a@ has finitely many values
  eventually has to be cyclic when iterated from some initial @a@.

  This module provides a number of common algorithms and utilities to identify
  and work with such cycles.
-}
module Data.FindCycle (
    -- * Typical Usage

    {- |
       The value of iterating @someCyclicFunc@ for \(10^{100}\) times from
       @startingValue@, using the 'brent' algorithm for cycle detection:

       > let fastCyclicFunc = cycleExp brent someCyclicFunc startingValue
       > fastCyclicFunc (10^100)

       The length of the non-repeating prefix and the length of the cycle, as
       determined using the 'nivash' algorithm:

       > let (mu, lambda) = findCycle nivash someCyclicFunc startingValue

       The same two lengths, plus two lists containing the values of the prefix and
       cyclic parts of the sequence using the 'naiveOrd' algorithm:

       > let (mu, lambda, (pre, cyc)) = findCycleExtract naiveOrd someCyclicFunc startingValue

       When you already have a list of values created by iterating a cyclic
       function:

       > let xs = iterate someCyclicFunc startingValue
       > let (mu, lambda, (pre, cyc)) = unsafeFindCycleFromList brent xs
    -}

    -- * CycleFinder type
    CycleFinder,

    -- * Algorithms

    {- |
       Cycles are typically described with a pair \((\mu, \lambda)\), where
       \(\mu\) represents the length of the non-cyclic prefix of the sequence, and
       \(\lambda\) represents the length of the repeating cycle of the sequence.

       The cycle finding algorithms provided by this module return such a pair as
       a result, but some might return an upper bound \(\tilde{\mu}\) instead of
       the minimal \(\mu\) in order to avoid the computational cost of finding the
       minimal value. This approximation is acceptable in many practical cases,
       such as when using 'cycleExp', which uses the cyclic behavior of a function
       to efficiently compute \(f^n(x)\) for large \(n\).

       When a minimal \(\mu\) is needed, it can be computed from a t'CycleFinder'
       returning a non-minimal \(\tilde{\mu}\) using 'minimalMu'.

       All algorithms always provide a minimal \(\lambda\) as opposed to a
       multiple of the true cycle length.

       In practice, you'll usually want to use 'nivash', 'brent', or one of the
       naive variants. If performance matters and you're not sure what to choose,
       compare the alternatives by benchmarking for your usecase.
    -}

    -- ** Naive

    {- |
       These algorithms use Map-like structures to store the index of the first
       occurrence of each value in the sequence until a duplicate is found.

       They always produce the minimal \((\mu, \lambda)\).

       They never iterate the sequence further than \(\mu + \lambda\) elements.

       They never compute an element at a given position in the sequence more than
       once.

       They use memory approximately proportional to \(\mu + \lambda\).

       'naiveHashable' tends to perform slightly better and uses slightly less
       memory. Both are provided for completeness and for cases where you might
       not have a 'Hashable' instance or don't want to write one.
    -}
    naiveOrd,
    naiveHashable,

    -- ** Constant Memory

    {- |
       These algorithms use a constant amount of memory, at the cost of having to
       potentially evaluate values in the sequence more than once.

       'brent' is always better than 'floyd', and the latter is only present for
       completeness and as a baseline for testing. You shouldn't use 'floyd'.

       They always compute a minimal \(\lambda\), but only an upper bound
       \(\tilde{\mu}\) for the cycle length. Combine with 'minimalMu' if the
       minimal \(\mu\) is needed.
    -}
    brent,
    floyd,

    -- ** Memory/Time Compromise
    nivash,

    -- * Running algorithms
    findCycle,
    findCycleExtract,
    cycleExp,
    cycleExp',
    unsafeFindCycleFromList,

    -- * Utilities
    minimalMu,
) where

import Control.Applicative ((<*>), (<|>))
import Data.Functor ((<$), (<$>))
import qualified Data.HashMap.Strict as HM
import Data.Hashable (Hashable)
import qualified Data.Map.Strict as M
import Data.Maybe (fromJust, fromMaybe)
import Prelude hiding ((<$), (<$>), (<*>))

data Input s a = Input
    { inpUncons :: s -> Maybe (a, s)
    , inpAdvance :: Int -> s -> s
    }

funcInput :: (a -> a) -> Input a a
funcInput f = Input (\x -> Just (x, f x)) advance
  where
    advance 0 a = a
    advance n a = advance (n - 1) (f a)

listInput :: Input [a] a
listInput = Input uncons drop
  where
    uncons [] = Nothing
    uncons (x : xs) = Just (x, xs)

{- |
  An algorithm to find the cycle in a function from @a@ to @a@ (or a list of @a@s).
-}
newtype CycleFinder a = CycleFinder
    { runCycleFinder :: forall s. Input s a -> s -> (Int, Int)
    }

{- |
  Runs a t'CycleFinder' algorithm for the given function and starting value,
  returning a pair \((\mu, \lambda)\) representing the length of the
  non-cyclic prefix and the length of the cycle of the sequence.
-}
findCycle :: CycleFinder a -> (a -> a) -> a -> (Int, Int)
findCycle alg f = runCycleFinder alg (funcInput f)

extract :: Int -> Int -> [a] -> ([a], [a])
extract mu lambda = fmap (take lambda) . splitAt mu

{- |
  Like 'findCycle', but also returns a third value @(pre, cyc)@ such that

  > pre ++ cycle cyc == iterate f x

  In addition to extracting the prefix and cyclic part of the list, this can
  also be used to cache some function calls to @f@ which the specified
  t'CycleFinder' might make, as the results of all calls to @f@ in the sequence
  are memorised in a lazy list which is later used to extract @pre@ and @cyc@.

  If you're only interested in caching calls to @f@ but don't need the two
  parts of the list, just don't evaluate the last part of the return value to
  not pay the cost of those parts being computed.
-}
findCycleExtract :: CycleFinder a -> (a -> a) -> a -> (Int, Int, ([a], [a]))
findCycleExtract alg f x = (mu, lambda, extract mu lambda xs)
  where
    xs = iterate f x
    (mu, lambda) = runCycleFinder alg listInput xs

{- |
  Runs the t'CycleFinder' for a given input list.

  This function is provided as a convenience for when you already have a list
  of values you'd like to find a cycle in. It's referred to as "unsafe", because
  it might lead to surprising results when the input doesn't satisfy the
  invariants that different algorithms assume.

  All algorithms assume that the sequence they're searching can be constructed
  by repeated function application from a starting value. Many sequences can't
  be, such as @[1,2,1,3,1,4,1,5,...]@ (because there can only be one unique
  successor of @1@).

  Algorithms also assume the input sequence to be infinite, and they will
  commonly consume more than \(\mu + \lambda\) (or \(\tilde{\mu} + \lambda\))
  elements from it. If you provide a finite input list, cycles might not be
  identified correctly if the chosen algorithm runs into the end of it, even
  though the input does technically contain an identifiable cycle.

  If an assumption is violated, algorithms might wrongly identify cycles or
  never terminate. Try to stick to 'findCycle', 'findCycleExtract', 'cycleExp',
  or 'cycleExp'' if possible, or only pass infinite lists generated via
  @iterate f x@ (or equivalent) to 'unsafeFindCycleFromList'.

  Similar to 'findCycleExtract', just don't evaluate the last part of the
  return value if you don't need it and want to avoid the cost of computing it.
-}
unsafeFindCycleFromList :: CycleFinder a -> [a] -> (Int, Int, ([a], [a]))
unsafeFindCycleFromList alg xs = (mu, lambda, extract mu lambda xs)
  where
    (mu, lambda) = runCycleFinder alg listInput xs

{-# INLINE cycleExpWith #-}
cycleExpWith :: CycleFinder a -> Input s a -> s -> Integer -> a
cycleExpWith alg inp@Input{..} s n =
    fst . fromJust . inpUncons $ inpAdvance (fromIntegral ix) s
  where
    (mu, lambda) = runCycleFinder alg inp s
    (mu', lambda') = (fromIntegral mu, fromIntegral lambda)
    ix
        | n < mu' = n
        | otherwise = mu' + ((n - mu') `mod` lambda')

{- |
  Constructs an efficient evaluator for a cyclic function by "exponentiating" it.
  Given a t'CycleFinder' for a function @f@ and an initial value @x@, it returns
  a function of type @Integer -> a@ which computes the nth iterate (i.e. the
  value of \(f^n(x)\)).

  Using the pair \((\mu, \lambda)\) obtained by the t'CycleFinder', this
  function computes

  \[ f^n(x) = \begin{cases}
       f^n(x)                                 & \text{if } n < \mu, \\
       f^{\mu + ((n - \mu) \bmod \lambda)}(x) & \text{if } n \ge \mu.
     \end{cases} \]

  which allows \(f^n(x)\) to be computed for very large \(n\) without requiring
  \(n\) function applications.

  Note that this function will use a lazy list generated by @iterate f x@. This
  list will only be evaluated up to \(\mu + \lambda\) elements and is shared
  between the cycle finding phase and the computation of the value after @n@
  iterations, but might still require a significant amount of memory. Use
  'cycleExp'' if you'd rather re-evaluate @f@ many more times but use less
  memory at the expense of more time.

  The lazy list might also be evaluated further than \(\mu + \lambda\)
  depending on the cycle finding algorithm chosen ('brent', 'floyd').

  >>> f x = x^42 `mod` 1000003 -- cycle (1, 83333)
  >>> g = cycleExp nivash f 23
  >>> g 0 -- after 0 iterations
  > 23
  >>> -- after a googol iterations, but finishes in less than the current
  >>> -- age of the universe
  >>> g (10^100)
  > 671872
-}
cycleExp :: CycleFinder a -> (a -> a) -> a -> Integer -> a
cycleExp alg f x = cycleExpWith alg listInput (iterate f x)

-- | Like 'cycleExp', but doesn't cache. Probably not very useful in practice.
cycleExp' :: CycleFinder a -> (a -> a) -> a -> Integer -> a
cycleExp' alg f = cycleExpWith alg (funcInput f)

data NaiveContainer m a = NaiveContainer
    { emptyC :: m
    , lookupC :: a -> m -> Maybe Int
    , insertC :: a -> Int -> m -> m
    }

naive :: NaiveContainer m a -> Input s a -> s -> (Int, Int)
naive NaiveContainer{..} Input{..} = go 0 emptyC . inpUncons
  where
    go i _ Nothing = (i, 0)
    go i m (Just (x, xs))
        | Just j <- lookupC x m = (j, i - j)
        | otherwise = go (i + 1) (insertC x i m) (inpUncons xs)

naiveOrd' :: (Ord a) => Input s a -> s -> (Int, Int)
naiveOrd' = naive (NaiveContainer M.empty M.lookup M.insert)

-- | Naive cycle finding algorithm using t'Data.Map.Strict.Map'.
naiveOrd :: (Ord a) => CycleFinder a
naiveOrd = CycleFinder naiveOrd'

naiveHashable' :: (Eq a, Hashable a) => Input s a -> s -> (Int, Int)
naiveHashable' = naive (NaiveContainer HM.empty HM.lookup HM.insert)

-- | Naive cycle finding algorithm using t'Data.HashMap.Strict.HashMap'.
naiveHashable :: (Eq a, Hashable a) => CycleFinder a
naiveHashable = CycleFinder naiveHashable'

{-# INLINE brent' #-}
brent' :: (Eq a) => Input s a -> s -> (Int, Int)
brent' Input{..} = maybe (0, 0) (uncurry (findLambda 1 1)) . inpUncons
  where
    findLambda pow lambda t hs =
        maybe (pow + lambda - 1, 0) (uncurry go) (inpUncons hs)
      where
        go h hs'
            | t == h = (pow, lambda)
            | pow == lambda = findLambda (2 * pow) 1 h hs'
            | otherwise = findLambda pow (1 + lambda) t hs'

{- |
  Brent's cycle finding algorithm.

  Evaluates at most \(2(\mu + \lambda)\) elements of the sequence.

  Always better than floyd.

  * [Brent, R. P. "An improved Monte Carlo factorization algorithm", BIT Numerical Mathematics, 20(2):176–184, 1980.](https://maths-people.anu.edu.au/~brent/pd/rpb051i.pdf)
  * <https://en.wikipedia.org/wiki/Cycle_detection#Brent's_algorithm>
-}
brent :: (Eq a) => CycleFinder a
brent = CycleFinder brent'

{-# INLINE nivash' #-}
-- TODO: add a variant with stack partitioning, probably requiring a partitioning
--       function as an extra argument. this version can use (const 0).
nivash' :: (Ord a) => Input s a -> s -> (Int, Int)
nivash' Input{..} = go 0 []
  where
    go i stack = maybe (i, 0) (uncurry go') . inpUncons
      where
        go' x s
            | (sx, si) : _ <- stack', sx == x = (si, i - si)
            | otherwise = go (i + 1) ((x, i) : stack') s
          where
            stack' = dropWhile ((> x) . fst) stack

-- TODO: Gosper? maybe not really that useful in practice.

{- |
  Nivash's cycle finding algorithm.

  Never computes an element at a given position in the sequence more than once.

  Might use memory proportional to \(\mu + \lambda\) in the worst case of an
  ascending sequence, but commonly uses much less for reasonably "random"
  sequences.

  Can often be faster than 'brent' while not using nearly as much memory as
  'naiveOrd' or 'naiveHashable'.

  * [G. Nivasch, "Cycle detection using a stack", Information Processing Letters 90/3, pp. 135-140, 2004.](https://drive.google.com/file/d/16H_lrjeaBJqWvcn07C_w-6VNHldJ-ZZl/view)
-}
nivash :: (Ord a) => CycleFinder a
nivash = CycleFinder nivash'

-- TODO: Sedgewick, Szymanski, Yao

{-# INLINE floyd' #-}
floyd' :: (Eq a) => Input s a -> s -> (Int, Int)
floyd' Input{..} s = detectCycle 0 s s
  where
    detectCycle n ts hs =
        fromMaybe (2 * n, 0) $
            go <$> inpUncons ts <*> (inpUncons . snd =<< skipped)
                <|> (2 * n + 1, 0) <$ skipped
      where
        skipped = inpUncons hs
        go (t, ts') (h, hs')
            | t == h = (n, findLambda 1 t ts')
            | otherwise = detectCycle (n + 1) ts' hs'
    findLambda n m ms =
        maybe n (uncurry go) (inpUncons ms)
      where
        go x xs
            | m == x = n
            | otherwise = findLambda (n + 1) m xs

{- |
  Floyd's / Tortoise and Hare cycle finding algorithm.

  Always worse than 'brent'. Don't use this.

  * <https://en.wikipedia.org/wiki/Cycle_detection#Floyd's_tortoise_and_hare>
-}
floyd :: (Eq a) => CycleFinder a
floyd = CycleFinder floyd'

{- |
  Compute a minimal result \((\mu, \lambda)\) from a partial result
  \((\tilde{\mu}, \lambda)\).

  This involves re-traversing the sequence from the start and from \(\lambda\)
  which might be expensive for large \(\mu\). This should largely be negligible
  if you're running the t'CycleFinder' using any of the functions which cache
  the sequence of values (any but 'findCycle' and 'cycleExp'').
-}
minimalMu :: (Eq a) => CycleFinder a -> CycleFinder a
minimalMu alg = CycleFinder go
  where
    go inp@Input{..} s = maybeFindMu (runCycleFinder alg inp s)
      where
        maybeFindMu r@(_, lambda)
            | lambda == 0 = r
            | otherwise = (findMu 0 s (inpAdvance lambda s), lambda)
        findMu mu ts ms =
            fromMaybe mu $ go' <$> inpUncons ts <*> inpUncons ms
          where
            go' (t, ts') (m, ms')
                | t == m = mu
                | otherwise = findMu (mu + 1) ts' ms'