Проект Эйлера Вопрос 14 (проблема Коллатца) - PullRequest
13 голосов
/ 15 апреля 2010

Следующая итерационная последовательность определена для набора натуральных чисел:

n -> n / 2 (n четно) n -> 3n + 1 (n нечетно)

Используя приведенное выше правило и начиная с 13, мы генерируем следующую последовательность:

13 40 20 10 5 16 8 4 2 1 Можно видеть, что эта последовательность (начиная с 13 и заканчивая 1) содержит 10 членов. Хотя это еще не доказано (проблема Коллатца), считается, что все начальные числа заканчиваются на 1.

Какое начальное число, меньше одного миллиона, дает самую длинную цепочку?

ПРИМЕЧАНИЕ. После запуска цепочки условия могут превышать миллион.

Я пытался кодировать решение этой проблемы в C, используя метод bruteforce. Однако, похоже, что моя программа останавливается при попытке вычислить 113383. Пожалуйста, сообщите:)

#include <stdio.h>
#define LIMIT 1000000

int iteration(int value)
{
 if(value%2==0)
  return (value/2);
 else
  return (3*value+1);
}

int count_iterations(int value)
{
 int count=1;
 //printf("%d\n", value);
 while(value!=1)
 {
  value=iteration(value);
  //printf("%d\n", value);
  count++;
 }
 return count;
}

int main()
{
 int iteration_count=0, max=0;
 int i,count;


 for (i=1; i<LIMIT; i++)
 {
  printf("Current iteration : %d\n", i);
  iteration_count=count_iterations(i);
  if (iteration_count>max)
   {
   max=iteration_count;
   count=i;
   }

 }

 //iteration_count=count_iterations(113383); 
 printf("Count = %d\ni = %d\n",max,count);

}

Ответы [ 8 ]

14 голосов
/ 15 апреля 2010

Причина, по которой вы останавливаетесь, состоит в том, что вы проходите через число, большее 2^31-1 (он же INT_MAX); попробуйте использовать unsigned long long вместо int.

Я недавно писал об этом ; обратите внимание, что в C наивный итеративный метод более чем быстр. Для динамических языков вам может потребоваться оптимизировать, запоминая, чтобы подчиняться правилу одной минуты (но здесь это не так).


Упс, я сделал это снова (на этот раз рассматривал дальнейшие возможные оптимизации с использованием C ++).

9 голосов
/ 15 апреля 2010

Обратите внимание, что ваше решение для грубой силы часто вычисляет одни и те же подзадачи снова и снова. Например, если вы начнете с 10, вы получите 5 16 8 4 2 1; но если вы начнете с 20, вы получите 20 10 5 16 8 4 2 1. Если вы кешируете значение в 10 после того, как оно вычислено, и тогда вам не придется вычислять его заново.

(Это известно как динамическое программирование .)

1 голос
/ 15 апреля 2010

Я решил проблему некоторое время назад и, к счастью, у меня все еще есть код. Не читайте код, если вам не нужен спойлер :

#include <stdio.h>

int lookup[1000000] = { 0 };

unsigned int NextNumber(unsigned int value) {
  if ((value % 2) == 0) value >>= 1;
  else value = (value * 3) + 1;
  return value;
}

int main() {
  int i = 0;
  int chainlength = 0;
  int longest = 0;
  int longestchain = 0;
  unsigned int value = 0;
  for (i = 1; i < 1000000; ++i) {
    chainlength = 0;
    value = i;
    while (value != 1) {
      ++chainlength;
      value = NextNumber(value);
      if (value >= 1000000) continue;
      if (lookup[value] != 0) {
        chainlength += lookup[value];
        break;
      }
    }

    lookup[i] = chainlength;

    if (longestchain < chainlength) {
      longest = i;
      longestchain = chainlength;
    }
  }
  printf("\n%d: %d\n", longest, longestchain);
}

time ./a.out

[don't be lazy, run it yourself]: [same here]

real    0m0.106s
user    0m0.094s
sys     0m0.012s
1 голос
/ 15 апреля 2010

Только что протестировав его в C #, кажется, что 113383 является первым значением, когда 32-битный тип int становится слишком маленьким, чтобы хранить каждый шаг в цепочке.

Попробуйте использовать unsigned long при обработке этих больших чисел;)

0 голосов
/ 25 апреля 2014

Исправление проблемы неподписанного int в исходном вопросе.

Добавлен массив для хранения предварительно вычисленных значений.

include <stdio.h>                                                                                                                                     
#define LIMIT 1000000

unsigned int dp_array[LIMIT+1];

unsigned int iteration(unsigned int value)
{
 if(value%2==0)
  return (value/2);
 else
  return (3*value+1);
}

unsigned int count_iterations(unsigned int value)
{
 int count=1;
 while(value!=1)
 {
 if ((value<=LIMIT) && (dp_array[value]!=0)){
   count+= (dp_array[value] -1);
   break;
  } else {
   value=iteration(value);
   count++;
  }
 }
 return count;
}

int main()
{
 int iteration_count=0, max=0;
 int i,count;
 for(i=0;i<=LIMIT;i++){
  dp_array[i]=0;
 }

 for (i=1; i<LIMIT; i++)
 {
//  printf("Current iteration : %d \t", i);
  iteration_count=count_iterations(i);
  dp_array[i]=iteration_count;
//  printf(" %d \t", iteration_count);
  if (iteration_count>max)
   {
   max=iteration_count;
   count=i;
   }
//  printf(" %d \n", max);

 }

 printf("Count = %d\ni = %d\n",max,count);

}

о / р: Количество = 525 я = 837799

0 голосов
/ 24 апреля 2014

Мои усилия в C #, время выполнения <1 секунда с использованием LinqPad: </p>

var cache = new Dictionary<long, long>();
long highestcount = 0;
long highestvalue = 0;
for (long a = 1; a < 1000000; a++)
{
    long count = 0;
    long i = a;
    while (i != 1)
    {
        long cachedCount = 0;
        if (cache.TryGetValue(i, out cachedCount)) //See if current value has already had number of steps counted & stored in cache
        {
            count += cachedCount; //Current value found, return cached count for this value plus number of steps counted in current loop
            break;
        }
        if (i % 2 == 0)
            i = i / 2;
        else
            i = (3 * i) + 1;
        count++;
    }
    cache.Add(a, count); //Store number of steps counted for current value
    if (count > highestcount)
    {
        highestvalue = a;
        highestcount = count;
    }
}
Console.WriteLine("Starting number:" + highestvalue.ToString() + ", terms:" + highestcount.ToString());
0 голосов
/ 04 ноября 2012

Решение на Haskell, время выполнения 2 секунды.

thomashartman@yucca:~/collatz>ghc -O3 -fforce-recomp --make collatz.hs
[1 of 1] Compiling Main             ( collatz.hs, collatz.o )
Linking collatz ...
thomashartman@yucca:~/collatz>time ./collatz
SPOILER REDACTED
real    0m2.881s

- Возможно, я мог бы получить это немного быстрее, используя хэш вместо карты.

import qualified Data.Map as M
import Control.Monad.State.Strict
import Data.List (maximumBy)
import Data.Function (on)

nextCollatz :: Integer -> Integer
nextCollatz n | even n = n `div` 2
               | otherwise = 3 * n + 1

newtype CollatzLength = CollatzLength Integer
  deriving (Read,Show,Eq,Ord)

main = print longestCollatzSequenceUnderAMill 
longestCollatzSequenceUnderAMill = longestCollatzLength [1..1000000]
-- sanity checks
tCollatzLengthNaive = CollatzLength 10 == collatzLengthNaive 13 
tCollatzLengthMemoized = (CollatzLength 10) == evalState (collatzLengthMemoized 13) M.empty

-- theoretically could be nonterminating. Since we're not in Agda, we'll not worry about it.
collatzLengthNaive :: Integer -> CollatzLength
collatzLengthNaive 1 = CollatzLength 1
collatzLengthNaive n = let CollatzLength nextLength = collatzLengthNaive (nextCollatz n)
                       in  CollatzLength $ 1 + nextLength

-- maybe it would be better to use hash here?
type CollatzLengthDb = M.Map Integer CollatzLength
type CollatzLengthState = State CollatzLengthDb 

-- handy for testing
cLM :: Integer -> CollatzLength
cLM n = flip evalState M.empty $ (collatzLengthMemoized n) 

collatzLengthMemoized :: Integer -> CollatzLengthState CollatzLength
collatzLengthMemoized 1 = return $ CollatzLength 1
collatzLengthMemoized n = do
  lengthsdb <- get
  case M.lookup n lengthsdb of 
    Nothing -> do let n' = nextCollatz n 
                  CollatzLength lengthN' <- collatzLengthMemoized n'
                  put $ M.insert n' (CollatzLength lengthN') lengthsdb
                  return $ CollatzLength $ lengthN' + 1
    Just lengthN -> return lengthN

longestCollatzLength :: [Integer] -> (Integer,CollatzLength)
longestCollatzLength xs = flip evalState M.empty $ do 
  foldM f (1,CollatzLength 1) xs
  where f maxSoFar@(maxN,lengthMaxN) nextN = do
          lengthNextN <- collatzLengthMemoized nextN
          let newMaxCandidate = (nextN,lengthNextN)
          return $ maximumBy (compare `on` snd) [maxSoFar, newMaxCandidate]

=============================================== =================================

А вот еще одно решение haskell, использующее пакет monad-memo. К сожалению, у этого есть ошибка стекового пространства, которая не затрагивает вышеупомянутый мемоизер roll-my-own.

. / CollatzMemo + RTS -K83886080 -RTS # это дает ответ, но было бы лучше устранить утечку пространства

{-# Language GADTs, TypeOperators #-} 

import Control.Monad.Memo
import Data.List (maximumBy)
import Data.Function (on)

nextCollatz :: Integer -> Integer
nextCollatz n | even n = n `div` 2
               | otherwise = 3 * n + 1

newtype CollatzLength = CollatzLength Integer
  deriving (Read,Show,Eq,Ord)

main = print longestCollatzSequenceUnderAMill 
longestCollatzSequenceUnderAMill = longestCollatzLength [1..1000000]

collatzLengthMemoized :: Integer -> Memo Integer CollatzLength CollatzLength
collatzLengthMemoized 1 = return $ CollatzLength 1
collatzLengthMemoized n = do
  CollatzLength nextLength <- memo collatzLengthMemoized (nextCollatz n)
  return $ CollatzLength $ 1 + nextLength 
{- Stack space error
./collatzMemo
Stack space overflow: current size 8388608 bytes.
Use `+RTS -Ksize -RTS' to increase it.

Stack error does not effect rolled-my-own memoizer at
/1802469/proekt-eilera-vopros-14-problema-kollattsa
-}
longestCollatzLength :: [Integer] -> (Integer,CollatzLength)
longestCollatzLength xs = startEvalMemo $ do
  foldM f (1,CollatzLength 1) xs
  where f maxSoFar nextN = do
          lengthNextN <- collatzLengthMemoized nextN
          let newMaxCandidate = (nextN,lengthNextN)
          return $ maximumBy (compare `on` snd) [maxSoFar, newMaxCandidate]

{-
-- sanity checks
tCollatzLengthNaive = CollatzLength 10 == collatzLengthNaive 13 
tCollatzLengthMemoized = (CollatzLength 10) ==startEvalMemo (collatzLengthMemoized 13) 

-- theoretically could be nonterminating. Since we're not in Agda, we'll not worry about it.
collatzLengthNaive :: Integer -> CollatzLength
collatzLengthNaive 1 = CollatzLength 1
collatzLengthNaive n = let CollatzLength nextLength = collatzLengthNaive (nextCollatz n)
                       in  CollatzLength $ 1 + nextLength
-}

=============================================== ===

еще один, более приятный факторинг. не бежит так быстро, но все еще намного меньше минуты

import qualified Data.Map as M
import Control.Monad.State
import Data.List (maximumBy, nubBy)
import Data.Function (on)

nextCollatz :: Integer -> Integer
nextCollatz n | even n = n `div` 2
               | otherwise = 3 * n + 1

newtype CollatzLength = CollatzLength Integer
  deriving (Read,Show,Eq,Ord)

main = print longestCollatzSequenceUnderAMillStreamy -- AllAtOnce                                                                                                                                                                                                         

collatzes = evalState collatzesM M.empty
longestCollatzSequenceUnderAMillAllAtOnce = winners . takeWhile ((<=1000000) .fst) $ collatzes
longestCollatzSequenceUnderAMillStreamy = takeWhile ((<=1000000) .fst) . winners  $ collatzes


-- sanity checks                                                                                                                                                                                                                                                          
tCollatzLengthNaive = CollatzLength 10 == collatzLengthNaive 13
tCollatzLengthMemoized = (CollatzLength 10) == evalState (collatzLengthMemoized 13) M.empty

-- maybe it would be better to use hash here?                                                                                                                                                                                                                             
type CollatzLengthDb = M.Map Integer CollatzLength
type CollatzLengthState = State CollatzLengthDb

collatzLengthMemoized :: Integer -> CollatzLengthState CollatzLength
collatzLengthMemoized 1 = return $ CollatzLength 1
collatzLengthMemoized n = do
  lengthsdb <- get
  case M.lookup n lengthsdb of
    Nothing -> do let n' = nextCollatz n
                  CollatzLength lengthN' <- collatzLengthMemoized n'
                  put $ M.insert n' (CollatzLength lengthN') lengthsdb
                  return $ CollatzLength $ lengthN' + 1
    Just lengthN -> return lengthN

collatzesM :: CollatzLengthState [(Integer,CollatzLength)]
collatzesM = mapM (\x -> do (CollatzLength l) <- collatzLengthMemoized x
                            return (x,(CollatzLength l)) ) [1..]

winners :: Ord b => [(a, b)] -> [(a, b)]
winners xs = (nubBy ( (==) `on` snd )) $ scanl1 (maxBy snd) xs

maxBy :: Ord b => (a -> b) -> a -> a -> a
maxBy f x y = if f x > f y then x else y
0 голосов
/ 15 апреля 2010

Как уже было сказано, самый простой способ - это получить некоторую памятку, чтобы избежать повторного вычисления вещей, которые еще не были вычислены. Возможно, вам будет интересно узнать, что цикла нет, если вы из числа менее одного миллиона (еще не было обнаружено ни одного цикла, и люди исследовали гораздо большие числа).

Чтобы перевести это в код, вы можете подумать, что Python выглядит так:

MEMOIZER = dict()

def memo(x, func):
  global MEMOIZER
  if x in MEMOIZER: return MEMOIZER[x]

  r = func(x)

  MEMOIZER[x] = r

  return r

Мемоизация - очень общая схема.

Что касается гипотезы Коллатце, вы можете столкнуться с небольшой трудностью, потому что числа действительно могут расти и, следовательно, вы можете взорвать доступную память.

Это традиционно обрабатывается с использованием кэширования, вы кэшируете только последние n результаты (приспособленные для того, чтобы занимать определенный объем памяти), и когда у вас уже есть n кэшированных элементов и вы хотите добавить более новый, вы отбрасываете старший.

Для этой гипотезы может быть доступна другая стратегия, хотя и сложнее в реализации. Основная идея заключается в том, что у вас есть только способы набрать определенное число x:

  • из 2*x
  • от (x-1)/3

Поэтому, если вы кешируете результаты 2*x и (x-1)/3, нет смысла кешировать x больше >>, он никогда не будет вызываться (кроме случаев, когда вы хотите напечатать последовательность в конце. .. но это только один раз). Я оставляю вам возможность воспользоваться этим, чтобы ваш кэш не рос слишком сильно:)

...