Я удивлен этим. Кажется, что каждый пропустил самый быстрый алгоритм из всех.
Следующий алгоритм в среднем быстрее, а в некоторых случаях значительно быстрее, чем простой цикл while(n%3==0) n/=3;
:
bool IsPowerOfThree(uint n)
{
// Optimizing lines to handle the most common cases extremely quickly
if(n%3 != 0) return n==1;
if(n%9 != 0) return n==3;
// General algorithm - works for any uint
uint r;
n = Math.DivRem(n, 59049, out r); if(n!=0 && r!=0) return false;
n = Math.DivRem(n+r, 243, out r); if(n!=0 && r!=0) return false;
n = Math.DivRem(n+r, 27, out r); if(n!=0 && r!=0) return false;
n += r;
return n==1 || n==3 || n==9;
}
Числовые константы в коде: 3 ^ 10, 3 ^ 5 и 3 ^ 3.
Расчет производительности
В современных процессорах DivRem
часто представляет собой одну инструкцию, которая занимает один цикл. В других случаях он расширяется до div, за которым следует mul и add, что в целом занимает больше трех циклов. Каждый шаг общего алгоритма выглядит длинным, но на самом деле он состоит только из: DivRem, cmp, cmove, cmp, cand, cjmp, add
. Доступно много параллелизма, поэтому на типичном двухстороннем суперскалярном процессоре каждый шаг, вероятно, будет выполняться примерно за 4 такта, обеспечивая гарантированное время выполнения в худшем случае около 25 тактов.
Если входные значения равномерно распределены по диапазону UInt32
, вот вероятности, связанные с этим алгоритмом:
- Возврат в или до первой строки оптимизации: 66% времени
- Возврат во или до второй строки оптимизации: 89% времени
- Возврат в или до первого общего шага алгоритма: 99,998% времени
- Возврат во второй шаг общего алгоритма или до него: 99,99999% времени
- Возврат в или до третьего общего шага алгоритма: 99,999997% времени
Этот алгоритм превосходит простой цикл while(n%3==0) n/=3
, который имеет следующие вероятности:
- Возврат в первой итерации: 66% времени
- Возврат в первые две итерации: 89% времени
- Возврат в первые три итерации: 97% времени
- Возврат в первые четыре итерации: 98,8% времени
- Возврат в первые пять итераций: 99,6% времени ... и так далее ...
- Возврат в первые двенадцать итераций: 99,9998% времени ... и далее ...
Что, возможно, еще важнее, этот алгоритм обрабатывает средние и большие степени трех (и их кратные) намного более эффективно: в худшем случае простой алгоритм потребляет более 100 циклов ЦП, поскольку цикл 20 раз (41 раз для 64 бит). Алгоритм, который я здесь представляю, никогда не займет больше 25 циклов.
Расширение до 64 бит
Расширение вышеупомянутого алгоритма до 64 битов тривиально - просто добавьте еще один шаг. Вот 64-битная версия вышеупомянутого алгоритма, оптимизированная для процессоров без эффективного 64-битного разделения:
bool IsPowerOfThree(ulong nL)
{
// General algorithm only
ulong rL;
nL = Math.DivRem(nL, 3486784401, out rL); if(nL!=0 && rL!=0) return false;
nL = Math.DivRem(nL+rL, 59049, out rL); if(nL!=0 && rL!=0) return false;
uint n = (uint)nL + (uint)rL;
n = Math.DivRem(n, 243, out r); if(n!=0 && r!=0) return false;
n = Math.DivRem(n+r, 27, out r); if(n!=0 && r!=0) return false;
n += r;
return n==1 || n==3 || n==9;
}
Новая константа 3 ^ 20. Строки оптимизации опущены в верхней части метода, потому что при нашем предположении, что 64-битное деление медленное, они на самом деле замедляют процесс.
Почему эта техника работает
Скажем, я хочу знать, является ли "100000000000000000" степенью 10. Я мог бы выполнить следующие шаги:
- Я делю на 10 ^ 10 и получаю частное 10000000 и остаток 0. Это добавляет к 10000000.
- Я делю на 10 ^ 5 и получаю частное 100 и остаток 0. Они добавляют к 100.
- Я делю на 10 ^ 3 и получаю частное 0 и остаток 100. Они добавляют к 100.
- Я делю на 10 ^ 2 и получаю частное 1 и остаток 0. Они добавляют к 1.
Поскольку я начинал с степени 10, каждый раз, когда я делил на степень 10, я получал либо нулевой коэффициент, либо нулевой остаток. Если бы я начинал с чего-либо, кроме 10, я бы рано или поздно получил бы ненулевой коэффициент или остаток.
В этом примере я выбрал показатели 10, 5 и 3, чтобы соответствовать коду, предоставленному ранее, и добавил 2 только для этого. Другие показатели также будут работать: существует простой алгоритм выбора идеальных показателей, учитывая ваше максимальное входное значение и максимальную мощность 10, допустимую в выходных данных, но у этого поля недостаточно места для его хранения.
ПРИМЕЧАНИЕ: Вы, возможно, думали на десятой базе в течение всего этого объяснения, но все объяснение выше можно прочитать и понять одинаково, если вы думаете на третьей базе , за исключением показателей выраженные по-разному (вместо «10», «5», «3» и «2» я бы сказал «101», «12», «10» и «2»).