Pull to refresh

Вперед, на поиски палиндромов

Reading time 7 min
Views 22K
Не так давно на Хабре была статья про codebattle от hexlet.io. Ну и затянуло же нас с друзьями, это как наркотик! Вроде пытаешься на работу отвлечься, а руки прям сами тянутся зайти на сайт, и все мысли — об оптимизации решений.

И вот однажды попалась мне задачка, звучала она так: «The decimal number 585 is 1001001001 in binary. It is palindromic in both bases. Find n-th palindromic number». А если по-русски, то так: «десятичное число 585 в двоичном виде выглядит как 1001001001. Оно является палиндромом в обеих системах счисления. Найдите n-ый подобный палиндром». Она совсем не сложная и решена была быстро.

function is_palindrome($num) {
   return $num == strrev($num);
}
function solution($num) {
   $count = $i = 0;
   while($count<$num) {
      $i++;
      // Проверяем по порядку все числа, являются ли они палиндром в десятичном и двоичном виде
      if (is_palindrome($i) && is_palindrome(decbin($i))){
         $count++;
      }
   }
   return $i;
}

Но вот незадача. Примерно в то время на сайт напал хабраэффект, и тесты ни в какую не хотели проходить, отваливались по timeout. В местном чате началось обсуждение по оптимизации решения, но никто дельного совета так и не дал. Потом сайт отпустило, все тесты прошли, но желание оптимизировать осталось…

Генерируем десятичные палиндромы


Для начала мне стало интересно, сколько же я смогу найти палиндромов на своей машинке. Хоть в чате и не советовали искать больше 22-х, я спокойно нашел 27 всего за 5 секунд, а вот следующий пришел только через 4 с лишним минуты. Дальше я уж ждать не стал — долго что-то. Чтобы начать оптимизацию, я решил поподробнее узнать палиндромы.

Сгенерировал себе несколько штук и начал рассматривать.

Палиндромы
  1. 1
  2. 3
  3. 5
  4. 7
  5. 9
  6. 33
  7. 99
  8. 313
  9. 585
  10. 717
  11. 7447
  12. 9009
  13. 15351
  14. 32223
  15. 39993
  16. 53235
  17. 53835
  18. 73737
  19. 585585
  20. 1758571


По сути числовой палиндром — это некое число, которое взяли, отзеркалили и прикрепили в конец этого же числа. Т.е. взяли число 235, отзеркалили, получили 532 и соединили — получился прекрасный палиндром — 235532. И было принято решение: зачем перебирать все числа в поисках десятичного палиндрома, если можно их просто генерировать. Сказано — сделано!

function solution($num) {
   $count = $i = 0;
   while($count<$num) {
      $i++;
      //Берем числа по порядку и соединяем с собой же в перевернутом виде
      $palindrome = $i . strrev($i);
      // И проверяем является ли наш десятичный палиндром таким же в двоичном виде.
      if (is_palindrome(decbin($palindrome))) {
         $count++;
      }
   }
   return $palindrome;
}

Запустив, я увидел, что пропущены однознаковые палиндромы, и добавил простой цикл на первые девять чисел.

for ($i = 1; $i < 10; $i++) {
   if (is_palindrome(decbin($i))) {
      $count++;
      if ($count == $num) return $i;
   }
}

Запустив ещё раз, я увидел, что ошибка моя была гораздо сильнее. Я ведь совсем забыл про палиндромы с нечетным количеством знаков, таких как 313, 585 или 717! И вот тут мне пришлось крепко задуматься. Посмотрев на список полученных палиндромов, можно увидеть, что палиндромы с нечетным количеством знаков — это те же четные палиндромы, только с центровым знаком. Т.е. берем четный палиндром, вставляем в центр цифры 1-9 и вуаля — нечетные палиндромы готовы. Но! Если я буду вставлять их в этом цикле, я потеряю порядок чисел. Поэтому пришлось внести кардинальные изменения в код и отталкиваться от количества знаков.

function solution($num) {
   $count = 0;
   // Одноразрядные палиндромы.
   for ($i = 1; $i < 10; $i++) {
      if (is_palindrome(decbin($i))) {
         $count++;
         if ($count == $num) return $i;
      }
   }
   $p_size = 1;
   while (true) {
      $min = 10**($p_size-1);
      $max = (10**$p_size)-1;
      // $min и max с каждым проходом цикла будут увеличиваться на один разряд
      for ($i = $min; $i <= $max; $i++) {
         // Генерируем палиндром с четным кол-вом знаков
         $palindrome = $i . strrev($i);
         //проверяем является ли он палиндромом в двоичном виде.
         if (is_palindrome(decbin($palindrome))) {
            $count++;
            if ($count == $num) return $palindrome;
         }
      }
      for ($i = $min; $i <= $max; $i++) {
         for ($j = 0; $j < 10; $j++) {
            // Генерируем палиндром с нечетным кол-вом знаков
            $palindrome = $i . $j . strrev($i);
            //проверяем является ли он палиндромом в двоичном виде.
            if (is_palindrome(decbin($palindrome))) {
               $count++;
               if ($count == $num) return $palindrome;
            }
         }
      }
      $p_size++;
   }
}

Собственно, тут всё просто. Берем сначала одноразрядные числа 1-9. Создаем двухразрядные палиндромы. Следом трёхразрядные. Потом просто увеличиваем разряд и берем числа от 10-99. Получатся палиндромы 4-х и 5-тиразрядные. Ну и т.д.

Тестируем!


Для начала запустил посмотреть 28-ой палиндром. Оказалось что для улучшенной функции это абсолютно плёвая задача. 28-ой был получен за 0.15 секунды! А это значит, что скорость была увеличена больше чем в 1500 раз. Я был доволен. За 5 секунд я мог получить уже более 40-а палиндромов. 50-ый был получен за 2.5 минуты.

Обратив внимание на полученные палиндромы, я заметил, что все они нечетные. И ведь правда! Четные десятичные палиндромы в двоичном виде будут заканчиваться на 0, а так как они всегда начинаются с 1, то и палиндромом быть не могут. А это значит, что их даже проверять не нужно. А так как палиндромы мы генерируем, мы можем пропускать все числа с первой четной цифрой.

Простой continue по условию сразу отмёл. Нам даже не имеет смысла крутить на них цикл. Будем их пролистывать. Попробовав несколько вариантов:

if(!(substr($i,0,1))%2)) $i += $min;

if(!((floor($i/$min))%2)) $i += $min;

if (!$limit){
   $limit = $min;
   $i += $min;
}
$limit--;

Я остановился на последнем как на самом быстром и получил вот такой код.

Полный код
function solution($num) {
   $count = 0;
   // Одноразрядные палиндромы.
   for ($i = 1; $i < 10; $i++) {
      if (is_palindrome(decbin($i))) {
         $count++;
         if ($count == $num) return $i;
      }
   }
   $p_size = 1;
   while (true) {
      $min = 10**($p_size-1);
      $max = (10**$p_size)-1;
      // $min и max с каждым проходом цикла будут увеличиваться на один разряд
      $limit = $min;
      for ($i = $min; $i <= $max; $i++) {
         // Условие чтобы перескочить числа с чётной первой цифрой
         if (!$limit){
            $limit = $min;
            $i += $min;
         }
         $limit--;
         // Генерируем палиндром с четным кол-вом знаков
         $palindrome = $i . strrev($i);
         //проверяем является ли он палиндромом в двоичном виде.
         if (is_palindrome(decbin($palindrome))) {
            $count++;
            if ($count == $num) return $palindrome;
         }
      }
      $limit = $min;
      for ($i = $min; $i <= $max; $i++) {
         if (!$limit){
            $limit = $min;
            $i += $min;
         }
         $limit--;
         for ($j = 0; $j < 10; $j++) {
            // Генерируем палиндром с нечетным кол-вом знаков
            $palindrome = $i . $j . strrev($i);
            //проверяем является ли он палиндромом в двоичном виде.
            if (is_palindrome(decbin($palindrome))) {
               $count++;
               if ($count == $num) return $palindrome;
            }
         }
      }
      $p_size++;
   }
}


Этим мы получили ускорение ещё примерно в два раза. 50-ый был получен за 88 секунд и, на мой взгляд, это был отличный результат!

Генерируем двоичные палиндромы


И вот я уже был готов остановиться и порадоваться, как мне пришла в голову мысль попробовать сформировать двоичные палиндромы. А что? Используемых цифр меньше, четных не сгенирируешь. Одни плюсы кругом!

Немного поразмыслив, я быстренько изменил код и получил:

function solution($num) {
   if ($num==1) return 1;
   $count = 1;
   $p_size = 1;
   while (true) {
      $min = 2**($p_size-1);
      $max = (2**$p_size)-1;
      // $min и max с каждым проходом цикла будут увеличиваться на один разряд в двоичном виде
      for ($i = $min; $i <= $max; $i++) {
         $half_palindrome = decbin($i);
         // Генерируем палиндром с четным кол-вом знаков в двоичном виде
         $bin_palindrome = ($half_palindrome) . strrev($half_palindrome);
         //проверяем является ли он палиндромом в десятичном виде.
         $dec_palindrome = bindec($bin_palindrome);
         if (is_palindrome($dec_palindrome)) {
            $count++;
            if ($count == $num) return $dec_palindrome;
         }
      }
      for ($i = $min; $i <= $max; $i++) {
         $half_palindrome = decbin($i);
         for ($j = 0; $j < 2; $j++) {
            // Генерируем палиндром с нечетным кол-вом знаков в двоичном виде
            $bin_palindrome = $half_palindrome . $j . strrev($half_palindrome);
            //проверяем является ли он палиндромом в десятичном виде.
            $dec_palindrome = bindec($bin_palindrome);
            if (is_palindrome($dec_palindrome)) {
               $count++;
               if ($count == $num) return $dec_palindrome;
            }
         }
      }
      $p_size++;
   }
}

После тестов я понял, что всё сделал правильно. 28-ой был получен за 0.05 секунды. 50-ый за 48 секунд. Когда брался за эту задачу, такого результата я совсем не ожидал.

Тут я уже решил, что хватит: я и так превзошел все свои ожидания. Хотя вру, конечно. Потом ещё пытался придумать как можно увеличить скорость ещё больше, но уже ничего в голову не пришло. Устал уже, да и ночь на дворе.

Ну и напоследок сводная таблица:

Сводная таблица
Палиндром Перебор (сек) Генерация dec палиндромов (сек) Генерация bin палиндромов (сек) кол-во бит
1 1 6.9141387939453E-6 5.0067901611328E-6 1
2 3 5.1975250244141E-5 4.887580871582E-5 4.2200088500977E-5 2
3 5 5.8889389038086E-5 5.5074691772461E-5 6.0081481933594E-5 3
4 7 6.4849853515625E-5 6.103515625E-5 6.6041946411133E-5 3
5 9 6.9856643676758E-5 6.6041946411133E-5 7.4148178100586E-5 4
6 33 8.2969665527344E-5 7.1048736572266E-5 9.0122222900391E-5 6
7 99 0.00011205673217773 8.6069107055664E-5 0.00010299682617188 7
8 313 0.00020503997802734 0.00010395050048828 0.00012612342834473 9
9 585 0.00033092498779297 0.00012397766113281 0.00014519691467285 10
10 717 0.0003969669342041 0.00013089179992676 0.0001521110534668 10
11 7447 0.0026609897613525 0.0001828670501709 0.00027918815612793 13
12 9009 0.0031960010528564 0.00020384788513184 0.00030112266540527 14
13 15351 0.0053460597991943 0.0002899169921875 0.00034999847412109 14
14 32223 0.011164903640747 0.00038385391235352 0.00047707557678223 15
15 39993 0.013840913772583 0.00048685073852539 0.00052118301391602 16
16 53235 0.018357038497925 0.00053596496582031 0.00057101249694824 16
17 53835 0.018585920333862 0.00054693222045898 0.0005891323089599 16
18 73737 0.025254964828491 0.00066089630126953 0.00065517425537109 17
19 585585 0.19646406173706 0.0011670589447021 0.0015511512756348 20
20 1758571 0.59263801574707 0.0026059150695801 0.0024890899658203 21
21 1934391 0.65274286270142 0.002892017364502 0.0026500225067139 21
22 1979791 0.66831588745117 0.0029850006103516 0.0027000904083252 21
23 3129213 1.0589859485626 0.00323486328125 0.0032520294189453 22
24 5071705 1.7217099666595 0.0046730041503906 0.0040431022644043 23
25 5259525 1.7863478660583 0.0049829483032227 0.0041420459747314 23
26 5841485 1.9860379695892 0.0059189796447754 0.0043931007385254 23
27 13500531 4.5991010665894 0.0097908973693848 0.0064051151275635 24
28 719848917 254.43427205086 0.074897050857544 0.046326160430908 30
29 910373019 0.090998888015747 0.051257133483887 30
30 939474939 0.096122026443481 0.05202817916870 30
31 1290880921 0.11239194869995 0.061146974563599 31
32 7451111547 0.16925406455994 0.15515112876892 33
33 10050905001 0.19922494888306 0.18062520027161 34
34 18462126481 0.36378288269043 0.2389931678772 35
35 32479297423 0.4427649974823 0.33302116394043 35
36 75015151057 0.88776993751526 0.48717308044434 37
37 110948849011 1.20951795578 0.60900402069092 37
38 136525525631 1.2637610435486 0.69635009765625 37
39 1234104014321 2.6941239833832 2.0280501842499 41
40 1413899983141 3.0781800746918 2.1862101554871 41
41 1474922294741 3.2089228630066 2.2403671741486 41
42 1792704072971 3.8874368667603 2.5199501514435 41
43 1794096904971 3.8904230594635 2.521210193634 41
44 1999925299991 4.3287780284882 2.7018330097198 41
45 5652622262565 7.9812479019165 4.4443550109863 43
46 7227526257227 9.285425901413 5.1428310871124 43
47 7284717174827 9.4120988845825 5.1688570976257 43
48 9484874784849 12.100240945816 5.998220205307 44
49 34141388314143 16.401521921158 11.614565134048 45
50 552212535212255 87.537031888962 49.897539138794 49
51 933138363831339 134.56801986694 62.49614405632 50
52 1793770770773971 171.45103907585 90.226871013641 51
53 3148955775598413 180.56048107147 119.85724520683 52
54 10457587478575401 293.4983189106 224.45361399651 54
55 10819671917691801 303.29767894745 227.38862919807 54
56 18279440804497281 506.18344306946 287.77621507645 55
57 34104482028440143 410.04529714584 55
58 37078796869787073 428.8955411911 56
59 37629927072992673 431.15429711342 56
60 55952637073625955 506.2887160778 56


PS. Продолжение оптимизации от ilyanik смотрите в этой статье
Tags:
Hubs:
+19
Comments 15
Comments Comments 15

Articles