Код поиска пути Lua нуждается в оптимизации

Поработав некоторое время над своим кодом, оптимизировав самые очевидные вещи, я получил следующее:

function FindPath(start, finish, path)
    --Define a table to hold the paths
    local paths = {}
    --Make a default argument
    path = path or {start}
    --Loop through connected nodes
    for i,v in ipairs(start:GetConnectedParts()) do
        --Determine if backtracking
        local loop = false
        for i,vv in ipairs(path) do
            if v == vv then
                loop = true
            end
        end
        if not loop then
            --Make a path clone
            local npath = {unpack(path)}
            npath[#npath+1] = v
            if v == finish then
                --If we reach the end add the path
                return npath
            else
                --Otherwise add the shortest part extending from this node
                paths[#paths+1] = FindPath(v, finish, npath) --NOTED HERE
            end
        end
    end
    --Find and return the shortest path
    if #paths > 0 then
        local lengths = {}
        for i,v in ipairs(paths) do
            lengths[#lengths+1] = #v
        end
        local least = math.min(unpack(lengths))
        for i,v in ipairs(paths) do
            if #v == least then
                return v
            end
        end
    end
end

Проблема в том, что указанная строка получает какую-то ошибку тайм-аута игрового сценария (что, как я полагаю, происходит из-за массовой рекурсии без выхода). Я также чувствую, что как только эта проблема будет решена, она, вероятно, будет довольно медленной даже в масштабе доски pacman. Есть ли способ, которым я могу его оптимизировать, или, возможно, лучший метод, который я могу изучить, подобный этому?

ОБНОВЛЕНИЕ: я, наконец, решил отказаться от своего алгоритма из-за его неэффективности и реализовал алгоритм Дейкстры для поиска пути. Для всех, кто интересуется исходным кодом, его можно найти здесь: http://pastebin.com/Xivf9mwv


person warspyking    schedule 19.01.2016    source источник


Ответы (3)


Вы знаете, что Roblox предоставляет вам PathfindingService? Он использует путь C-стороны A * для довольно быстрого расчета. Я бы рекомендовал его использовать

http://wiki.roblox.com/index.php?title=API:Class/PathfindingService

person Henry    schedule 20.01.2016
comment
Я знаю об этом и часто использую его, но я работаю над кодом для друга, которому нужен ИИ для ходьбы по тротуарной системе (PathfindingService заставит их ходить по дорогам). - person warspyking; 20.01.2016
comment
Тогда мы можем получить ваш код FindPath? Я подозреваю, что это может быть эта функция, вызывающая замедление. - person Henry; 20.01.2016
comment
О, это рекурсивно ... В этом случае я ожидаю, что да, проблема в ограничении рекурсии, выраженном ROBLOX. Советую создавать свои узлы по-новому. Возможно, рассчитывайте длину ваших тротуаров, а не соединительных частей. Я бы создал сетку из каждых 5 или около того гвоздей вдоль таких тротуаров, которые создают путь к узлу. - person Henry; 20.01.2016
comment
Если бы я сделал операцию менее дорогой, я мог бы заставить ее работать. - person warspyking; 20.01.2016
comment
У меня есть доска pacman, полная узлов размером 4 * 0,1 * 4. Размер доски 28 на 36 узлов, есть 360 узлов, по которым можно пройти, остальные — стены. Это то, что я использую для тестирования. - person warspyking; 20.01.2016
comment
Можете ли вы обозначить, какие узлы можно пройти, а какие нет? Названы ли они соответственно? - person Henry; 20.01.2016
comment
Подключены только те, которые можно ходить, поэтому функция не имеет проблем между ними. - person warspyking; 20.01.2016
comment
Я настроил его на ожидание () каждые столько итераций. Если я сделаю его слишком высоким, это не сработает, но если я сделаю слишком низкий, это займет слишком много времени. Я действительно думаю, что мне нужно поучить, насколько это дорого. - person warspyking; 20.01.2016
comment
Дай мне немного времени. Я посмотрю, смогу ли я что-нибудь создать. Однако прошло некоторое время с тех пор, как я сделал roblox lua. - person Henry; 20.01.2016
comment
Когда я сказал 28 на 36, я имел в виду 28 на 31, слишком поздно редактировать - person warspyking; 20.01.2016
comment
Хорошо, это было бы здорово, спасибо. Я свяжусь с вами позже/завтра. - person warspyking; 20.01.2016
comment
Попробуйте использовать это: en.wikipedia.org/wiki/A*_search_algorithm Это очень эффективно, и я думаю, это будет работать лучше для того, что вы делаете. попробую сделать что-нибудь на Rbx lua, если будет время - person Henry; 20.01.2016
comment
Я не совсем уверен, подходит ли A*. Каждый узел имеет такое же расстояние/стоимость, как и любой другой, так зачем это учитывать? - person warspyking; 20.01.2016
comment
Тогда попробуйте dijkstra's, может быть, это упрощенный A* /а> - person Henry; 20.01.2016
comment
Потому что вы не ведете список пройденных узлов. Как часть а переходит в часть б, а часть б возвращается в а, и так до бесконечности. - person Henry; 21.01.2016
comment
Я рекомендую вариант a*, потому что он хорошо отслеживает пройденные узлы. - person Henry; 21.01.2016
comment
Я, хотя. Вы заметите, что я перебираю текущий путь, чтобы убедиться, что я не возвращаюсь. - person warspyking; 21.01.2016
comment
Он проходит по пути, но не проверяет предыдущие узлы пути. - person Henry; 22.01.2016
comment
После довольно большой работы, пытаясь сделать алгоритм Дейкстры в коде Lua, мне наконец удалось это: pastebin.com/KawUKbu8Но это все еще кажется слишком тяжелым. Правильно ли я выполнил алгоритм? - person warspyking; 23.01.2016
comment
Подожди, неважно. Мне просто нужно исправить порядок пути, который он возвращает сейчас, но, похоже, он работает (нет тайм-аута). Спасибо, предложите Дейкстру в своем ответе, чтобы я мог принять - person warspyking; 23.01.2016
comment
Боже мой, я ненавижу заливать комментарии (и, возможно, ваши уведомления), но я обнаружил проблему, я использовал #Q, когда реализовывал его как словарь, на случай, если вас интересует конечный продукт: pastebin.com/Xivf9mwv - person warspyking; 23.01.2016
comment
Работает? Надеюсь, тоже быстро? - person Henry; 26.01.2016
comment
предложите Дейкстру в своем ответе, чтобы я мог принять warspyking 23 янв., в 03:21 - person warspyking; 27.01.2016

Попробуйте перестроить свой алгоритм, чтобы использовать хвостовые вызовы. Это отличный механизм, доступный в Lua.

Хвостовой вызов — это тип рекурсии, при котором ваша функция возвращает вызов функции в последнюю очередь. Lua имеет правильную реализацию хвостовых вызовов, и эта рекурсия будет выглядеть как «goto» под сценами, поэтому ваш стек никогда не взорвется.

В этом может помочь передача «путей» в качестве одного из аргументов FindPath.

person Etiene Dalcol    schedule 20.01.2016
comment
Было бы неплохо, если бы Roblox использовал Lua 5.2, но, увы, он использовал Lua 5.1. - person warspyking; 20.01.2016
comment
Этот код предназначен для ROBLOX, где хвостовые вызовы фактически отключены. - person EinsteinK; 10.02.2016
comment
@EinsteinK Они не отключены, а скорее не реализованы. ROBLOX использует модифицированную версию Lua 5.1, но Lua представила хвостовые вызовы только в 5.2. - person warspyking; 10.02.2016
comment
@warspyking Lua 5.1 имеет хвостовые вызовы: lua.org/manual/5.1/manual.html, как показано в разделе 2.5.8 — Вызовы функций. У ROBLOX были хвостовые вызовы, но они были удалены менее 3 лет назад. Вот почти древний пост Страванта на форуме об этом: /ShowPost.aspx?PostID=132422268. Кроме того, откуда вы взяли, что хвостовые вызовы были добавлены только в 5.2? - person EinsteinK; 10.02.2016
comment
@Einstein Я не могу найти, где я это нашел ... Это было в официальном источнике Lua, но я думаю, что это было исправлено. - person warspyking; 11.02.2016
comment
@EtieneDalcol Очевидно, чтобы упростить отладку, поскольку хвостовой вызов повторно использует стек вызовов, что может привести к потере некоторой информации. Это основная причина, по которой нам сказали. - person EinsteinK; 13.02.2016

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

  • ipairs медленнее, чем пары, которые медленнее, чем числовой цикл for. Если важна производительность, никогда не используйте ipairs, а используйте цикл for i=1,#tab
  • Если вы хотите клонировать таблицу, используйте цикл for. Иногда вам приходится использовать распаковку (возвращая динамическое количество завершающих нулей), но это не такой случай. Распаковка тоже медленная функция.

Замена ipairs на pairs или числовые циклы for и использование циклов вместо unpack намного повысит производительность.

Если вы хотите получить наименьшее значение в таблице, используйте этот фрагмент кода:

local lowestValue = values[1]
for k,v in pairs(values) do
    if v < lowestValue then
        lowestValue = k,v
    end
end

Это можно переписать для вашего примера пути так:

local least = #path[1]
for k,v in pairs(path) do
    if #v < least then
        least = v
    end
end

Должен признать, ты очень изобретателен. Мало кто будет использовать math.min(unpack(tab)) (не считая того, что это плохо)

person EinsteinK    schedule 10.02.2016
comment
ipairs БЫСТРЕЕ, чем пары, потому что он выполняет итерацию только по части МАССИВА, а не по хэшу. Не уверен, откуда вы взяли, что пары были быстрее. - person warspyking; 10.02.2016
comment
И почему распаковка таблицы должна использоваться для math.min плохая распаковка предназначена для распаковки таблицы. Использование его для распаковки в качестве аргументов для math.min кажется мне довольно логичным. - person warspyking; 10.02.2016
comment
И я проверил ваш аргумент for loop VS unpack. Я клонировал массив с 7999 элементами, 1000 раз и выводил красным, сколько времени они заняли. Цикл for занял 9 секунд, тогда как распаковка заняла 2. (Конечно, мой тест был на Lua 5.2, но, конечно, тест дал бы примерно такие же результаты на roblox) - person warspyking; 10.02.2016
comment
Я только что протестировал пары VS ipairs, используя массив из 10k и 5M записей. Только в случае с 10k ipairs иногда работал немного быстрее, чем пары. Большую часть времени пары все равно были быстрее, хотя разница почти не стоит упоминания. - person EinsteinK; 10.02.2016
comment
Использование распаковки не так уж плохо, так как на самом деле это быстрее, чем клонирование таблицы с помощью цикла for. Однако Unpack может распаковать только 7999 элементов. Выполнение unpack(tab), когда tab содержит 8k элементов, приведет к ошибке слишком много результатов для распаковки. Получение наименьшего значения в таблице действительно быстрее, чем цикл for (примерно в 10 раз), но ваш код также использует цикл для получения фактического пути из минимального значения. Простое сравнение v == наименее уже сделало бы ваш код медленнее по крайней мере в 50% случаев. В сочетании с #v очевидно, что использование метода распаковки в большинстве случаев будет медленнее. - person EinsteinK; 10.02.2016
comment
У меня странные ipairs всегда появляются быстрее. - person warspyking; 11.02.2016
comment
Хотя у меня никогда не было 8K элементов. И вы сказали, что распаковка была медленной в вашем ответе, а это не так. - person warspyking; 11.02.2016
comment
Я протестировал его с 50 элементами сейчас. В этом случае unpack только тогда (в половине случаев) быстрее, чем цикл for. (снова предполагая, что вам нужно получить путь от наименьшей длины пути). Кроме того, я сказал, что код будет медленнее при использовании _v == наименее по сравнению с #v == наименее. По-видимому, версия длины быстрее, вероятно, из-за простого сравнения чисел. (Вот почему большинство людей, включая меня, предпочитают уже созданный код поиска пути) - person EinsteinK; 11.02.2016
comment
Я думаю, вам нужно пересмотреть/удалить этот ответ. - person warspyking; 11.02.2016