Иван в комментариях к предыдущему посту придумал очень быстрый и эффективный способ вычисления пифагоровых троек к нечетному числу. Мне стало интересно, я тоже немного погрузился в тему статьи Матсона.
Правда, той эффективности, что у Ивана, мне достичь не удалось, я пока в процессе. И в этом процессе я обнаружил любопытную вещь.
Мне скучно просто искать совершенный кубоид, поэтому я решил в процессе еще и сохранять все найденные обычные кирпичи Эйлера. Это чуть более сложная задача, потому должна работать чуть медленнее, чем поиск совершенного кубоида. И для её работы нужно уметь эффективно считать целочисленный корень для больших целых чисел, в моем случае 128-битных.
Я решил проверить, сможет ли FPU (или по-другому x87) выполнить эту задачу. Шансы у него были, так как во внутреннем представлении FPU хранит вещественные в расширенном формате, длиной не стандартные 64, а целых 80 бит.
В процессе обнаружил, что точности как бы не хватает. Начал копать и через непродолжительное время выяснил, что по какой-то странной причине точность FPU оказалась занижена до 64 бит. Да, в FPU с помощью флагов можно менять точность внутреннего представления чисел от 32 до 80 бит, что кроме точности влияет и на производительность, понятное дело.
В документации на сайте AMD утверждается, что по умолчанию FPU установлен на максимальную точность. Но возможно, это было давно и в современных версиях процессора уже не действует? Кроме того, точность может менять Windows, а так же компилятор Delphi. Кто из них повинен в такой ситуации, мне не ведомо.
Интереснее другое: зачем это вообще сделано? Лично я вижу лишь одну причину: что бы пользователи не заметили разницу в решениях своих задач на FPU и SSE, иначе кто-нибудь как-нибудь да непременно выскажет претензии. Но это лишь предположение.
Что касается целочисленного корня. При максимальной точности FPU может находить точное целое значения корня из 128-битового беззнакового целого. Но вот точности, что бы определить, целым ли получился результат, уже не хватает, а было бы очень кстати.
Теперь про мою реализацию. Во-первых, я отказался от проверки деления на 19. Конечно, такое деление снижает количество рассматриваемых вариантов, но, если я правильно понял, не так уж значительно по сравнению с делением на 16. В то же время, проверка делимости на 16 происходит гораздо быстрее.
Но возможно здесь я ошибаюсь, так как проверка каждого варианта на то, что он является кирпичом Эйлера стоит гораздо дороже, чем разница в скорости проверок делимости.
Во-вторых, я пока не сделал сохранение найденных делителей для последующего использования, что, конечно, существенно снижает производительность.
Ну и в третьих, надо бы написать какую-нибудь эффективную хэш-функцию для хранения потенциальных значений Y. Я попробовал было совсем простой вариант, но столкнулся таки с коллизиями, которые надо как-то эффективно разрешать. На текущий момент просто использую сортированный массив, даже без двоичного поиска.
Все это, конечно, весьма далеко от гораздо более эффективной реализации Ивана. Но, тем не менее, тоже показывает относительно неплохую производительность: от 100001 до 100000001 по нечетной стороне все варианты кирпичей Эйлера были найдены за час с небольшим, всего 3182002 варианта.
Я решил, что вместо того, что бы проверять на первичность решения в процессе поиска (мне показалось это достаточно затратной операцией), сохранять все найденные варианты. А уже потом быстренько отсеять все составные, ну и плюс тут же проверить и на совершенность.
> Но это лишь предположение.
ОтветитьУдалитьВозможно чтобы соответствовать IEEE 754 спецификации, иногда приходилось дебажить крохотное расхождение между нашей собственной реализации на java, и сторонней ML библиотеки. Я бы очень не хотел чтобы еще и процессор вставлял палки в колеса и считал что-нибудь точнее :) Куда как проще когда x86, arm и программная реализация работают одинаково.
> не так уж значительно по сравнению с делением на 16
У меня разница довольно существенная: +24%. Y кандидатов (размер 2-х множеств) не так много: в среднем 11 до X<10M, но если при делении на 19 перебирать в среднем нужно 53 варианта, то при 16-ти - уже 91.
> но столкнулся таки с коллизиями, которые надо как-то эффективно разрешать
Java не особо парится: просто хранит список и перебирает все варианты. Если коллизий много то одно из 2-х: либо хэш функция плохая, либо таблица маленькая :) Но если с нуля писать я бы наверное попробовал с открытой адрессацией реализовать и слизал хэш функцию с java.
> все варианты кирпичей Эйлера были найдены
Но ведь только у совершенного кирпича одно ребро делится на 16 или 19, для НЕ совершенного оно может не выполняется, например: 51975, 1102000, 1636800 оба y и z делятся на 16, а для 51975, 300960, 793212 оба y и z делятся на 19. Я на это поймался когда проверял мое решение на Вашем x=51975. Или я не правильно понял Ваш алгоритм?
> относительно неплохую производительность
Круто, у меня было существенно медленнее до пред-расчета делителей.
> Но ведь только у совершенного кирпича одно ребро делится на 16 или 19...
ОтветитьУдалитьНасколько я понимаю, этот список требований относится не к совершенному, но к примитивному кирпичу.
Что касается того, что обе четные стороны делятся на 16 - то это нормально. Ведь одна четная сторона должна обязательно делится на четыре, а это значит, что вторая четная сторона, делящяяся на 16, также делится и на 4, поэтому должна участвовать в переборе наравне с теми, что делятся на 4. Тут можно лишь не проверять одну и ту же сторону, делящуюся на 16, в качестве одновременно обоих четных сторон, так как решения такой вариант гарантированно не даст.
По поводу делимости на нечетные числа, для примера, на то же 19. Я так понял, что как минимум одна сторона должна делится на 19, но никто не запрещает и еще одной делится на 19. Главное, что бы третья на 19 не делилась - иначе примитивный кубоид не получится.
Что касается совершенного кубоида. Примитивная его форма должна соответствовать всем требованиям на делимости сторон и диагоналей для примитивного кирпича Эйлера. Но, после того, как (и если) такой кубоид будет найдет, количество его составных форм станет бесконечным.
> Насколько я понимаю, этот список требований относится не к совершенному, но к примитивному кирпичу.
УдалитьЯ так понял что именно к совершенному, вот контр-пример "85, 132, 720" - примитивный, но ни одно из ребер не делится на 19, опять же в файле с примитивными кирпичами полно кубов не делящихся на 19, правда ни одного не делящегося на 16, но вот доказанный ли это факт или очередная нерешенная задачка я не знаю... правила на 16 и 19 в википедии рядышком в секции именно совершенного кирпича. Может конечно вики врет, я смотрел в английской, статья вроде норм с деталями.
> но никто не запрещает и еще одной делится на...
Хм, действительно - это я неправильно понимал правило, реализовал с багом и контр-примеры приводил не правильные. Спасибо за замечание, исправил перебор, хорошо хоть на скорость заметно не повлияло :)
> Примитивная его форма должна соответствовать всем требованиям на делимости сторон и диагоналей для примитивного кирпича Эйлера
Согласен, но как я понял так как требований больше, то и ограничений/правил на кратность сторон можно больше найти.
> Я так понял что именно к совершенному...
УдалитьДа, ты прав, это я был не очень внимателен. В русскоязычной википедии приведены константы именно для примитивного кирпича, а в англоязычной к ним еще добавили для совершенного. Но, я думаю, и этот список не полон, возможно. Ну и в любом случае, дополнительные требования, они необходимые, но не достаточные. По крайней мере, я так понял. Иначе можно было бы просто стороны и диагонали проверить на делимость, без вычисления третьей диагонали. Хотя, может быть, её быстрее рассчитать и проверить наличие в списке, чем проверять все признаки делимости.
> они необходимые, но не достаточные
УдалитьСогласен, меня смутило что Вы писали про поиск всех примитивных кирпичей и упомянули 16/19, вот я и вставил свои 5 копеек :)
> может быть, её быстрее рассчитать и проверить наличие в списке
Думаю для совершенного, да быстрее, одной быстрой проверкой: ySq + zSq in yzSqCandidates мы убиваем 2-х зайцев:
1. ySq + zSq - это тоже квадрат
2. xSq + (ySq + zSq) - это тоже квадрат
Проверка на делимость нужна только чтобы уменьшить перебор всех вариантов отсеивая большинство во внешнем цикле выполняя его по подмножеству кандидатов, но применить больше чем один признак не получится, так как они никак не связаны между собой.
А вот для поиска примитивных - проверка более тяжелая как я понимаю: sqrt(ySq + zSq) - целое, тут возможно дополнительные проверки на делимость до вычисления корня что-нибудь и дадут, но к сожалению если цель найти все примитивные кирпичи, то доказанных признаков не так много остается, ни 16, ни 19 не применимы :(
> ...ни 16, ни 19 не применимы...
УдалитьНу почему же? 16 очень даже применимо: для любого кирпича одна четная сторона должна обязательно делиться на 4, а вторая - на 16.
Я вот только пока доказать не могу: вроде для любой пифагоровой тройки для нечетного числа X, Y получается кратным четырем? Нутром чую, что должно быть так )
Да действительно, я поленился об этом подумать:
Удалить1. Из формулы Евклида для генерации пифагоровой тройки видно что один из катетов кратен 4-м
2. А значит в кирпиче 2 стороны кратны 4-м
3. А так как они тоже часть пифагоровой тройки, то одна из них обязана быть кратна 16-ти.
Забавно что в английской википедии этот факт только в секции совершенного кирпича упоминается, доверяй но проверяй!