Skip to content

Instantly share code, notes, and snippets.

@dmitriy-kiriyenko
Created May 30, 2012 07:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dmitriy-kiriyenko/2834414 to your computer and use it in GitHub Desktop.
Save dmitriy-kiriyenko/2834414 to your computer and use it in GitHub Desktop.
Find two skipped numbers in a natural sequence
# Run it only with ruby 1.9.
range = (1..(ARGV[0] || 10).to_i)
@g = Random.new
def gen_skips(n, range, ready = [])
return ready if ready.size == n
gen_skips(n, range, ready.push(@g.rand(range)).uniq)
end
skips = gen_skips(2, range).sort
fn = 'data.txt'
puts "Generating...\n\n"
step = [range.first, (range.last - range.first + 1)/100].max.to_i
File.open(fn, 'w') do |f|
range.each do |n|
print '*' if n % step == 0
f.puts(n) unless skips.include?(n)
end
end
puts "\n\nGenerated #{fn} with numbers from #{range.first} to #{range.last} with #{skips.join(', ')} skipped."
@andy128k
Copy link

andy128k commented Jun 5, 2012

Это чисто формально логарифм. Файловый ввод/вывод и так буферизован.
Сложность gets тоже неплохо бы посчитать. Она не меньше O(n).
to_i тоже просканнирует все байты как O(n).
:)

@raven29
Copy link

raven29 commented Jun 5, 2012

gets, to_i и т.п. работает только с одним числом, в котором число байтов = количеству цифр одного числа (1-4 например)
когда считаем сложность - имеем дело с количеством чисел (10000 например)

@andy128k
Copy link

andy128k commented Jun 5, 2012

Если чисел n, то оценка сверху количества байт: O(n_log(n)).
Сложность алгоритма по байтам: O(log(b)) ==> по числам: O(log(n_log(n))) ==> O(log(n) + log(log(n))) ==> O(n).

Это легко проверить эмпирически если считать количество вызовов. Оно линейно растёт относительно количества чисел.

@andy128k
Copy link

andy128k commented Jun 5, 2012

Для сравнения. Реализация на питоне для 1000 чисел работает в два раза быстрее чем эта. Для миллиона чисел выходит разница в два порядка.

@dmitriy-kiriyenko
Copy link
Author

Да нет же. Здесь же не каждое число из файла читается. Бинарный поиск скачет по файлу вперёд-назад, а gets и to_i работают O(log(N)) раз. Или я чего-то не понимаю? А максимальная длина одного числа в байтах lg(N).

P.S. - ответ на предпредыдущий комментарий. С предыдущим поспорить сложно. Будет посмотреть.

@dmitriy-kiriyenko
Copy link
Author

Может быть, seek имеет линейную сложность? Тогда всё плохо - алгоритм был бы логарифмическим, если бы не реализация seek.

Причём, в общем случае, при фрагментированном файле, seek действительно вряд ли константа.

@andy128k
Copy link

andy128k commented Jun 5, 2012

check_range вызывается дважды на каждой итерации (безусловно). Тут экспонента. Она гасится тем, что глубина -- логарифм. Итого: O(n).

@dmitriy-kiriyenko
Copy link
Author

Вполне себе условно. Третья ветка if.

@andy128k
Copy link

andy128k commented Jun 5, 2012

В фибоначчи тоже if. Не тупи. Посчитай сам, если мне не веришь. Добавь счётчик вызовов и посмотри.

@raven29
Copy link

raven29 commented Jun 5, 2012

Я тоже не знаю по какому алгоритму работает seek, и если его сложность O(n), то конечно теряется смысл его использования.
Абсолютный seek(pos) используется только один раз, но этого достаточно чтоб испортить все до O(n*ln(n)).
Но что точно: если вы читаете весь файл подряд, у вас однозначно O(n), никакие буферизации и прочие уловки никак не помогут, и уже нет смысла городить бинарный поиск в полученном массиве, а проще и честнее добавить проверочку соседей по ходу считывания.

@dmitriy-kiriyenko
Copy link
Author

Ух ты ж ёжик, так вот почему здесь не требуется указывать количество чисел!
Так это не мешает читать из файла логарифмически - здесь алгоритм сам по себе линейный. Мдя. Но можно допилить до логарифма, если seek не подгадит.

@dmitriy-kiriyenko
Copy link
Author

уже нет смысла городить бинарный поиск в полученном массиве

Задача-то скорее академическая. Спасибо большое за идею побайтового seek - там точно прячется общий O(log(N)), если не подпортит seek.

@raven29
Copy link

raven29 commented Jun 5, 2012

И вам спасибо - за критику и наоборот! :)

@andy128k
Copy link

andy128k commented Jun 5, 2012

В таком виде не прячется. Деление пополям байтов даёт такую фигню, что числа разбиты неравномерно, правая часть длиннее (там числа плотнее). Получается как с несбалансированным деревом, вместо log(n) вылазит n.

Правый хвост длиннее

Надо бить не пополам.

@raven29
Copy link

raven29 commented Jun 5, 2012

Кстати - с check_range я действительно промахнулся, в проекте у меня было проверять каждый интервал, а не только парный, но в реализации как-то забылось.
А надо бы исправить, если получится.

@dmitriy-kiriyenko
Copy link
Author

С трудом продравшись сквось мрак сишного кода убедился - это вроде как если и O(N), то неявно. В винде вызывается http://msdn.microsoft.com/en-us/library/1yee101t(v=vs.80).aspx, в остальных вызывается вот это:

/* License: Ruby's */
off_t
_lseeki64(int fd, off_t offset, int whence)
{
    long u, l;
    int e;
    HANDLE h = (HANDLE)_get_osfhandle(fd);

    if (!h) {
    errno = EBADF;
    return -1;
    }
    u = (long)(offset >> 32);
    if ((l = SetFilePointer(h, (long)offset, &u, whence)) == -1L &&
    (e = GetLastError())) {
    errno = map_errno(e);
    return -1;
    }
    return ((off_t)u << 32) | l;
}

Мне кажется, что это константа.

В таком виде не прячется. Надо бить не пополам.

@raven29 что-то такое говорил, что это не мешает. Если мешает, как думаешь, с этим можно побороться, @andy128k? Потому что исправить второй недостаток, обязав указывать количество пропущенных чисел, как это делаем мы с тобой, довольно просто.

P.S. Гоню. Это код тоже из винды. Код для других операционок тупо найти не могу поиском по Руби.

@raven29
Copy link

raven29 commented Jun 5, 2012

andy128k

Деление пополям байтов даёт такую фигню, что числа разбиты неравномерно, правая часть длиннее (там числа плотнее). Получается как с несбалансированным деревом, вместо log(n) вылазит n.

raven29

Можно показать, что в самом худшем случае такое разбиение приблизительно дает пропорцию 3/4 : 1/4 (вместо 1/2 : 1/2) и соответственно приводит к сложности ln(2)/ln(4/3)O(ln(n)) = O(ln(n)), а время выполнения увеличивает менее чем в ln(2)/ln(4/3) = 2.4 раза. Здесь занудствовать не буду, но желающим могу предоставить подробное доказательство.

@andy128k
Copy link

andy128k commented Jun 5, 2012

lseek это из слоя POSIX совместимости. Тут тупо перевызывается системный SetFilePointer.

@dmitriy-kiriyenko
Copy link
Author

Ещё одно решение. По дате подано вовремя, и это наш новый стажёр. https://gist.github.com/2850061

@dmitriy-kiriyenko
Copy link
Author

Производительность:

                                          10000    1000000
=============================================================
python andy128kpython.py                   0.046      1.188
sbcl --script andy128k.lisp                0.073      0.675
ruby bronislav.rb                          0.148      0.761
ruby zhck.rb                               0.149      0.739
ruby dmitriy-kiriyenko.rb                  0.152      1.139
ruby raven29.rb                            1.885   3:34.870
mysql test_num < dmitriy-kiriyenko.sql  1:13.460   not measured

@raven29
Copy link

raven29 commented Jun 6, 2012

Доказательство валидности байтового разбиения.

Кстати я вчера ошибся: пессимистическая оценка 3/4 : 1/4 неверная, правильно 5/8 : 3/8, что еще лучше и дает снижение производительности по сравнению с точным рабзиением в ln(2)/ln(8/5) = 1.5

  1. Числа данного разряда присутствуют в полном составе.
    Т.е. в интервал включено 10^n-10^(n-1) n-разрядных чисел. В этом случае присутствие любого количества чисел до этого разряда даст отклонение от 1/2 : 1/2 не более 1/10.
    Действительно среднее количество цифр (m - количество предыдущих цифр, заметим что их m <= 10^(n-1), это используется в доказательстве второго неравенства)
    ((10^n-10^(n-1))_n + ...)/(10^n-10^(n-1)+m) > ((10^n-10^(n-1))_n)/(10^n-10^(n-1)+m) > ((10^n-10^(n-1))_n)/(10^n) = (9/10)_n
    Это означает что во вторую часть разбиения войдут только n-разрядные числа в количестве
    x = ((10^n-10^(n-1))_n + ...)/(2_n) > (10^n-10^(n-1))/2 - середина ряда всех n-значных чисел
    x = ((10^n-10^(n-1))_n + ...)/(2_n) < ((10^n-10^(n-1))_n + 10^(n-1)n)/(2_n) = (10^n-10^(n-1))/2 + 10^(n-1)/2 = (с точностью 1/10) = ((10^n-10^(n-1))/2)(1+10^(n-1)/10^n) = (11/10)*(10^n-10^(n-1))/2
    Т.е. весь хвост предшествующих чисел сдвинет середину не более чем на 1/10
  2. Числа данного разряда присутствуют в неполном составе.
    Тогда за ними следующего разряда уже нет, и важно только влияние предыдущего разряда, т.к. весь остальной хвост предыдущих дает отклонение не более 1/10 (см. 1). В этом случае середина числового ряда определяется количеством m1 n-значных и m2 (n-1)-значных чисел. Для малых и больших m1 оно приближается к байтовой середине и достигает максимального отклонения от байтовой середины при m1=m2(=m). В этом случае вторая часть разбиения содержит только n-разрядные числа в количестве
    x = (m_(n-1)+m_n)/(2_n) = m_(1-1/(2_n))
    Соответственно первая часть содержит
    m+(m-x) = m_(1+1/(2_n)) всего n- и (n-1)-разрядных чисел
    Таким образом, с точностью 1/10 (см. 1) имеем разбиение в отношении (1/2-1/(4_n)) : (1/2+1/(4*n))
    Это отношение с ростом n монотонно приближается к вожделенной 1/2 : 1/2
    При n=1, (n-1)=0 имеем 3/4 : 1/4, но это нереальный случай.
    Реальная пессимистческая оценка начинается с n=2 и дает 5/8 : 3/8.

Полагаю, что доказательство максимума при m1=m2 из пункта 2 приводить необязательно: нужно рассмотреть отдельно случаи m1>m2 и m1<m2, а дальше - максимум кусочно-линейной функции, доступно любому знающему алгебру в объеме 7 классов средней школы.

@raven29
Copy link

raven29 commented Jun 6, 2012

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

@raven29
Copy link

raven29 commented Jun 11, 2012

Все получилось, код здесь:
https://gist.github.com/2908925

типичные средние результаты:

partition algorithm10,0001,000,000
"5/8" partition0.004767s0.009469s
"1/2" partition0.004643s0.009897s

Напомню, что все корректно работает для любого количества пропусков в любых конфигурациях, включая пропуски нескольких чисел подряд и пропуски с начала ряда (это касается функции check_range_pos(), подробности см. ниже).

Дима, а можно проверить это на вашем компе? Сравнить интересно, на правах "вне конкурса", конечно :)

И еще: поскольку для правильного кода потребовалась функция для подсчета числа байт-позиций в заданном целочисленном интервале, захотелось уже и реализовать алгоритм честного деления "1/2". Функция check_range_pos() - деление "5/8" (по байтам), check_range_num() - деление "1/2" (по числам). Эта функция не столь универсальна, т.к. будет глючить в случае нескольких пропущенных чисел подряд.
Что интересно, что их производителдьность практически совпадает, иногда даже "5/8" работает быстрее чем "1/2". Это скорее всего связано с тем, что "1/2" требует больше операций, возможно теоретическая разница проявится на бОльших количествах.

@dmitriy-kiriyenko
Copy link
Author

Я гонял тесты у себя дома, там завтра и запущу ваш. Это очень интересно. Спасибо вам большое. Решение прочитаю точно.

@dmitriy-kiriyenko
Copy link
Author

Деление по числам, в виду не универсальности, не так интересно, особенно с учётом того, что оно не столь однозначно лучше.

@raven29
Copy link

raven29 commented Jun 11, 2012

Спасибо!
С нетерпением жду результата... :)

@dmitriy-kiriyenko
Copy link
Author

Не совсем понимаю, как вам удаётся обойтись ещё и без знания количества пропущенных чисел, а код лёгким к чтению не назовёшь.

Но результаты вполне впечатляют, так что, завтра чтобы понять, буду рефакторить =)

@raven29
Copy link

raven29 commented Jun 11, 2012

Никакой уличной магии. :)
Фишка в том что пропуски сканятся только в парах ближайших соседей с любым отклонением от последовательного порядка, типа [3;5], [2;7].
Ну и начало вычисляется отдельно, учитывая что это не обязательно 1.
За код "с душком" прошу прощения, я пока писал на это не обращал внимания, а теперь отлежаться должно для рефакторинга, так что заранее благодарю.

@dmitriy-kiriyenko
Copy link
Author

Да я всё понимаю - я ж не говорю, что вы плохо написали, я говорю, что так сразу не прочитаешь =) Не за что извиняться.

@dmitriy-kiriyenko
Copy link
Author

Обновлённая производительность:

                                            10000    1000000        100000000
=====================================================================================
  python andy128kpython.py                   0.046      1.188       couldn't wait
  sbcl --script andy128k.lisp                0.073      0.675       heap exchausted
  ruby bronislav.rb                          0.148      0.761       couldn't wait
  ruby zhck.rb                               0.149      0.739       couldn't wait
  ruby dmitriy-kiriyenko.rb                  0.152      1.139       couldn't wait
* ruby raven29_cool.rb                       0.145      0.158       0.161
  ruby raven29.rb                            1.885   3:34.870       not measured
  mysql test_num < dmitriy-kiriyenko.sql  1:13.460   not measured   not measured

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment