Как стать автором
Обновить

Комментарии 49

Довольно интересно! Еще бы умножение за O(1) работало
Можно взять простое число, не меньшее k, для которого двойка является порождающим элементом мультипликативной группы (например, для k=32 это p=37) и посчитать остаток от деления 2^n на p. Все остатки будут различны, и найти логарифм можно по табличке.
Ещё на закуску не хватает задачи «найти по числу m его максимальный делитель вида 2^n за время O(1)»
m&-m
Оу, так, конечно, красивее, а мой способ работает и для unsigned.
-m тоже прекрасно работает для unsigned
Не на всех компиляторах. Undefined behavior.
Вроде вполне определенное С++11 [basic.fundamental]:
Unsigned integers, declared unsigned, shall obey the laws of arithmetic modulo 2^n where n is the number
of bits in the value representation of that particular size of integer.
Но -m тоже под этот пункт попадает тогда…
Зачем простое? Достаточно нечётное… Ну и чтоб размер мультипликативной группы >n. Но здесь же обычный бинпоиск;)
Надо, чтобы порядок двойки в группе был не меньше k. Для составного p это сразу увеличивает его минимальное значение более, чем вдвое: если p=q*r, то его мультипликативная группа — Z_{q-1} x Z_{r-1}, и порядок любого элемента не больше LCM(q-1,r-1)<=(q-1)*(r-1)/2. Так что простое лучше.
В таком графе, в каждую вершину входит два ребра и выходит два ребра, а значит существует Эйлеров цикл

Зануда во мне говорит, что граф еще должен быть связным.

А вообще спасибо за статью, интересно.
Как из наличия эйлерова цикла следует наличие гамильтонова?
Там в качестве вершин надо брать подстроки длиной k-1. Тогда каждое ребро будет соответствовать подстроке длины k, и эйлеров цикл даст как раз нужную последовательность. Парочка рёбер окажется петлями, но это неважно.
Есть карточный фокус, который основан на де Брёйне: www.math.ubc.ca/~anstee/math443/DeBruijnCardTrick.pdf
Предположение, что умножение n-битных чисел выполняется за O(1) означает, что n ограничено.
Но тогда и традиционный двоичный поиск — решение за O(1) (с коэффициентом log n).
Если n небольшое, составляем таблицу из 2^n,
результатов, затем за O(1) выбираем в этой таблице
по входному числу как индексу.
Очевидно, n=32 или 64. Таблицу уже не составить.
А количество бит лучше считать как-нибудь так:
    n=(n&0x55555555)+((n>>1)&0x55555555);
    n=(n&0x33333333)+((n>>2)&0x33333333);
    n=(n+(n>>4))&0x0f0f0f0f;
    n+=n>>8;
    count=(n+(n>>16))&0xff;
Пока кто-нибудь не изобретёт 64-битный процессор.
НЛО прилетело и опубликовало эту надпись здесь
А потом кто-то изобретёт 128-битный процессор. На нём этот код будет продолжать компилироваться без ошибок и предупреждений, но работать не всегда правильно. Как код от Ариан-4 на Ариан-5.

А приведённый автором код из книжки Кернигана и Ричи будет продолжать работать без изменений на любом процессоре, от PDP-11 до самых современных. Вне зависимости от набора инструкций. И скорость выполнения у него та же O(1).
Такая совместимость со всеми возможными процессорами нужна наверно разве что ядру линукса. Большинство программ пишется и умирает гораздо быстрее чем живёт одно поколение процессоров.
Скорее наоборот, такого рода оптимизация имеет смысл разве что в ядре операционной системы или в компиляторе (которые всё равно пишутся или по крайней мере подгоняются под конкретный процессор).

А в прикладных программах эти трюки попадают под определение premature optimization.
Зависит от частоты применения подобного рода функции в прикладной программе. Если я использую это, скажем, для расчёта весов нейросети из тысяч нейронов, оптимизация лишней не будет.
На таком уровне оптимизация имеет смысл на компиляторах, а не на компилируемых программах. Так, поддержка popcount есть как в GCC, так и в Clang. А вот ни один из вышеприведённых способов ни GCC, ни Clang довести до инструкции POPCNT не смогли.
А потом кто-то изобретёт 128-битный процессор.
К этому есть какие-то предпосылки? Если 64 бита ещё как-то были оправданы адресацией памяти и расчётами с числами двойной точности, то применения 128-битам в ближайшие лет 20 я лично придумать не могу (даже с учётом предположения о росте требуемых объёмов памяти пропорционально росту вычислительных возможностей по закону Мура).
А всякая криптография? С какой разрядностью ей приходится работать?
64 бит не хватает даже чтобы написать «наивную» арифметику по модулю больше, чем 2^32. Или в других ситуациях, когда надо посчитать a*b/c, но не хочется уходить от целых чисел.
А «всякая криптография» у нас уже либо достаточно заоптимизирована, чтобы не замечать её выполнение на текущих процессорах, либо вынесена в отдельные железные модули в случае, если требуется высокая пропускная способность при низкой нагрузке на CPU. Потому для конечных пользователей эта проблема стоит не очень остро даже с учётом запросов бизнеса.
вводить 128 бит будут маркетологи, они оправдают, без оглядки на Ваше мнение. Хотя как по мне — давно пора, есть ряд алгоритмов, которые значительно упростятся, имей они такое виртуальное адресное пространство.
НЛО прилетело и опубликовало эту надпись здесь
Как же не привёл? Вот он:

while (n) {
++count;
n &= (n-1);
}

Прямо из Кернигана и Ричи. Процессоров с переменной разрядностью я пока не встречал, так что это константа.
НЛО прилетело и опубликовало эту надпись здесь
Для процессоров без popcnt. За пределами intel тоже есть жизнь.
А для арма есть _popcountsi2. Вот поэтому я и писал выше, что это задача компилятора, а не программиста. В GCC и Clang для этого есть __builtin_popcount. Пусть компилятор соберёт оптимальный набор инструкций.
Это может быть и кодом для библиотеки поддержки компилятора в том числе. Хотя таблично population count считается несколько быстрее.
Вообще говоря, сложность какого-то алгоритма имеет смысл только при указании вычислительной модели. Вы посчитали, как я понимаю, для модели RAM, но, увы, для реальных компьютеров все немного хуже — с длиной числа нужно будет считаться (по сути, количество бит числа будет тем самым n). А за счет умножения у вас даже линейной сложности не выйдет.
Тем не менее, для n=32 или 64 этот алгоритм может работать быстрее «стандартного» на тех процессорах, которые умеют умножать быстро.
И это не совсем верно. Важно понимать, что 64-битные процессоры не смогут быстро и верно перемножить два 64-битных числа в общем случае, ведь их произведение будет занимать больше, чем 64 бита. С уверенностью мы можем говорить лишь про n <= 15 и n <= 31 соответственно.

Для таких чисел мы бинпоиском можем определить степень за, соответственно, 4 и 5 сравнений. Вы уверены, что это медленней чем все действия по алгоритму?
Наверное, это будет для вас сюрпризом — но операция mul/imul, примененная к 64х-разрядным операндам, дает 128и-битный результат. Тот факт, что старшая часть чаще всего отбрасывается, обусловлен системой типов языков высокого уровня, но на низком уровне такой проблемы не стоит.
под разрядностью процессора как правило понимают разрядность его интерфейса — разрядность адресуемого пространства и разрядность шины. Внутри процессора может твориться все что угодно. Более того, оно там творится, и уже достаточно давно(по крайней мере для x86 семейства)
Исключение — 8-битные CPU, которые чаще всего имеет физическую 16-битную шину адреса и адресуют 64KB
НЛО прилетело и опубликовало эту надпись здесь
А мог бы кто-нибудь обяснить что такое «за О(1)»?
Асимптотическая оценка времени для алгоритма. O(1) означает асимптотическую константу — т.е. время работы алгоритма ограничено сверху.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории