Планирование заданий перестановки с частичными доступными машинами

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

проблема

Я хотел бы провести несколько тестов на батареях, чтобы увидеть, как они реагируют на нагрузку. И выполните эти тесты в кратчайшие сроки, чтобы завершить все тесты. Две важные переменные здесь:

  • State-of-Charge (SoC) количество оставшейся энергии в аккумуляторе от 100% до 0%. Мы будем тестировать 99%, 75%, 50% и 25% (4 варианта). (позже объяснил, почему 99%, а не 100%). Предположим, что потери SoC при расслаблении равны 0.
  • Релаксация количество расслабления батареи в часах. Мы знаем, что теоретически 24 часов должно хватить, так что это максимум. Мы будем тестировать разное время, например: 5 минут, 15 минут, 30 минут, 1 час, 2 часа, 6 часов, 24 часа (7 вариантов).

Всего комбинаций: 4 x 7 = 28 для одной батареи

Порядок, в котором должен проходить тест, следующий: зарядка до 100%, разрядка до нужного SoC, расслабление, разрядка до нового SoC во время измерения.

Пример: мы хотим посмотреть, как батарея реагирует на разрядку с 75% до 50%, отдыхая 2 часа.

  1. Аккумулятор имеет неизвестную SoC (методы измерения недостаточно точны)
  2. Зарядить до 100%
  3. Разрядка до 75%
  4. Расслабьтесь 2 часа
  5. Разрядка во время измерения, остановка на 50%

Теперь батарея может снова расслабиться и начать измерение с 50% до 25%. Его НЕ нужно перезаряжать до 100% снова.

ситуации/состояния

Теперь я опишу некоторые ситуации, которые могут возникнуть и что нужно делать в таком случае.

инициализация

Проблема может быть инициализирована с помощью уже выполненных тестов (это важно, потому что мы можем захотеть перенести их на полпути). Если батареи имеют известное состояние (SoC/расслабление), мы можем его использовать. Если SoC неизвестен, аккумулятор необходимо перезарядить. Если релаксация неизвестна, но известен SoC, то аккумулятор должен быть релаксирован не менее чем на 24 часа.

перезарядка

Установка аккумулятора в зарядное устройство должна производиться вручную. Оставить батарею в зарядном устройстве не проблема. Зарядка занимает около 2,5 часов. Каждая батарея имеет свое собственное зарядное устройство, но в будущем у нас может быть больше батарей, чем зарядных устройств, поэтому алгоритм должен иметь возможность принимать переменное количество зарядных устройств.

расслабление (расслабление)

Расслабление можно просто сделать, ни к чему не подключая аккумулятор, поэтому для него не нужно никакого специального оборудования. Перед началом периода релаксации батарея должна быть нагружена (= подключена к разряднику). Мы не знаем точно, сколько времени продлится стрессовый период, но предполагаем, что периода разрядки батареи на 1% будет достаточно. Таким образом, 99% будет первой SoC, где мы сможем точно определить время релаксации.

разрядка

На данный момент имеется только один разрядник, но алгоритм должен иметь возможность принимать переменное количество разрядников. Установка батареи в разрядник должна производиться вручную (также и извлечение). ОДНАКО размещение аккумулятора в разряднике не обязательно сразу же разрядит аккумулятор. Время может быть установлено, чтобы начать в определенное время. И разрядник может автоматически останавливаться, когда было разряжено достаточно энергии. Оценить время разрядки можно по справочной таблице. Это нелинейно, поэтому от 75% до 50% не нужно столько же времени, сколько от 25% до 0%. Поиск довольно точен (около 5 минут разницы за 2,5 часа).

ожидающий

Батарея может подождать, если все разрядники сняты, но ожидание разрядника увеличивает время релаксации. Таким образом, если время релаксации становится больше, чем время релаксации, необходимое для выполнения измерений, то аккумулятор должен либо разрядиться до более низкого уровня заряда и снова расслабиться, либо снова зарядиться.
Аккумулятор может разрядиться. подождите, если все зарядные устройства взяты безопасно, здесь нет штрафа / недостатка, кроме потери времени на ожидание.

ограничения

То, что нужно делать вручную, можно делать только в рабочее время (понедельник-пятница с 8:30 до 17:00). Так, например, установка батареи в разрядник должна выполняться вручную. Затем в установленное время ночью (после того, как батарея достаточно отдохнет) можно включить разрядник по таймеру, а на следующее утро по приходу в офис поставить батарею в зарядное устройство.

мысли для решения

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

Последовательность задач имеет значение, потому что другая последовательность может привести к большему или меньшему времени ожидания, чем другая последовательность. Таким образом, для одной батареи с 28 тестами это будет перестановка 28! что довольно большое число. Поэтому полный перебор проблемного пространства невозможен. Единственный известный мне тип алгоритма, который может дать достаточно хороший результат в подобных задачах, — это генетический алгоритм. Хотя со всеми ограничениями и возможностями я не могу просто использовать классический генетический алгоритм. Я читал некоторые (исследовательские) статьи, и, в конце концов, описание проблемы планирования Flowshop Permutation (PFSP) вызвало наибольший резонанс (различные источники). Хотя упомянутая Расширенная проблема планирования работы (EJSSP) здесь тоже была интересной.

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

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

  1. какие алгоритмы подходят для этого типа задач?
  2. как установить особые условия для алгоритмов?
  3. как я могу сделать функцию кроссовера/селекции/мутации?
  4. мне нужно разбить эту проблему на подзадачи и включить это в более крупный алгоритм? Какие подзадачи оптимально решать в первую очередь?
  5. как будет выглядеть псевдокод?

person Flip    schedule 30.11.2014    source источник
comment
Нужно ли вам присутствовать в начале и/или конце измерения? Если нет, то можно ли запрограммировать серию разрядов и периодов релаксации в разряднике? Например. в вашем примере вы могли бы вручную поместить только что заряженный аккумулятор в разрядник и запрограммировать его на разрядку до 75%, затем расслабиться на 2 часа, затем продолжить разрядку до 50%, пока он измеряет, затем расслабиться на 5 минут, затем разрядить до 25% пока меряет?   -  person j_random_hacker    schedule 04.12.2014
comment
Сколько батарей? Также было бы полезно примерно знать, сколько времени занимает полная (от 100% до 0%) разрядка. И что касается того, что если релаксация неизвестна, но известен SoC, то аккумулятор должен быть разряжен не менее 24 часов: означает ли это, что аккумулятор с известным SoC, но неизвестным релаксацией нельзя просто перезарядить сразу ?   -  person j_random_hacker    schedule 04.12.2014
comment
Я думаю, что лучше задать вопрос в разделе CS Theory, поскольку переполнение стека в основном носит технический характер.   -  person Khaled.K    schedule 04.12.2014
comment
@j_random_hacker порядок такой: поместите аккумулятор в разрядник, начните измерение, завершите измерение, выньте аккумулятор из разрядника. Мне нужно присутствовать на 1-м и 4-м шагах. Ваше предложение оставить ОДНУ и ту же батарею в разряднике и иметь несколько серий разрядки/расслабления ВОЗМОЖНО. На данный момент есть 2 батареи, но это должно быть переменной. с известным SoC, но неизвестную релаксацию нельзя просто сразу зарядить? правильно, нужно подождать 24 часа.   -  person Flip    schedule 04.12.2014
comment
@khaledAKhunaifer да, я сомневался между этим и CS. После периода вознаграждения я могу попросить модератора переместить вопрос или я могу удалить и задать новый вопрос.   -  person Flip    schedule 04.12.2014
comment
OK. Все еще немного смущен этим 24-часовым расслаблением: я предполагаю, что 24-часовое расслабление также требуется перед перезарядкой, даже если вы знаете уровень расслабления, но он превышает необходимый вам уровень? Например. если батарея уже разряжалась в течение 5 часов, но ваш следующий запланированный тест рассчитан на 2-часовую релаксацию, вам нужно дать ей отдохнуть еще 19 часов (5 + 19 = 24) перед перезарядкой, верно?   -  person j_random_hacker    schedule 04.12.2014
comment
Также примерно сколько времени занимает полная (от 100% до 0%) разрядка?   -  person j_random_hacker    schedule 04.12.2014
comment
@j_random_hacker, если вы знаете уровень релаксации, и это больше часов, чем вам нужно, вам нужно снова перезарядить / разрядить. Если это меньше, чем вам нужно, вам просто нужно подождать, пока не пройдет достаточно времени. Таким образом, батарея отдыхала 5 часов, сбрасывалась зарядкой/разрядкой, а затем начинала считать от 0 до 2 часов во время отдыха. Разряд оценивается в 12 часов, но эта длительность не должна определять тип алгоритма.   -  person Flip    schedule 05.12.2014
comment
Не ответ на ваш вопрос, но, пожалуйста, не удаляйте вопрос сейчас ... кто-то уже приложил много усилий, пытаясь ответить на вопрос. И вопрос, и ответ интересны мне как минимум :)   -  person Vikas Gupta    schedule 06.12.2014
comment
обязательно ли использовать генетический алгоритм? можно ли использовать другой метаэвристический алгоритм?   -  person Adem    schedule 07.12.2014


Ответы (2)


Обзор

Хотя вы явно отметили evolutionary-algorithm, я бы посоветовал вам взглянуть на набор алгоритмов, обобщенных под названием Приближенное динамическое программирование (ADP). На мой взгляд, хорошей вводной книгой является книга Уоррена Б. Пауэлла. Он содержит множество таких задач распределения ресурсов, а также несколько других практически важных вещей, которые часто называют оптимальным управлением (пример: управление автотранспортной компанией, имеющей несколько тысяч грузовиков).

Одним из преимуществ ADP по сравнению с непосредственным применением эволюционных алгоритмов, имитации отжига и т. д. является то, что он представляет не конкретный алгоритм, а скорее структуру для моделирования марковских задач принятия решений, зависящих от времени. В этих рамках можно свободно использовать множество подходящих алгоритмов.

Одним из ключей к применению АДП является правильное математическое моделирование данной проблемы. Центральное значение имеет state системы, actions, которые можно принять в заданном состоянии, а также cost, которые требуются для этих действий и которые нужно минимизировать — ваши затраты здесь определяются продолжительностью теста. После построения подходящей модели задача состоит в том, чтобы (приблизительно) решить уравнение Беллмана, для которого существует несколько алгоритмов.

Далее я приведу пример того, как здесь можно действовать. Естественно, это не готовая к использованию модель, так как ее создание может занять довольно много времени - на самом деле я думаю, что это была бы хорошая задача для магистерской или даже докторской диссертации. Тем не менее, я постараюсь сделать его исчерпывающим на первом этапе, чтобы позже можно было ввести приближения.

Моделирование состояния одной батареи

Сначала мы создадим грубую модель для одной батареи. Здесь полезно, чтобы ваша проблема была такой же детерминированной, как вы описали, то есть что здесь нет стохастических компонентов (например, что все длительности фиксированы, а не взяты случайным образом из некоторого статистического распределения).

  • Одно состояние: Как вы написали, одна батарея определяется состоянием

    S = {SoC, Relax}      where SoC   \in {UNKNOWN, 0%, 25%, 50%, 75%, 100%}
                          and   Relax \in {UNKNOWN, 0m, 5m, 15m, 30m, 1h, 2h, 6h, 24h}
    

    Я добавил 0% и 0m для удобства, хотя здесь они, возможно, и не нужны.

    Обратите внимание, что здесь я уже сделал большое упрощение, так как, например, состояние заряда также может быть 79%. Предположение, которое оправдывает это, следующее: как только вы начинаете свой эксперимент в первый раз (или также заново через долгое время), все батареи разумно находятся в состоянии {UNKNOWN,UNKNOWN}. Тогда, согласно вашему описанию, первое, что нужно сделать, это сделать полную перезарядку, которая устанавливает все в состояние {100%,0m} (и которая стоит 2,5h). Отсюда выполняются только квалифицированные изменения состояния — разрядка выполняется только для определенных SoC, а перезарядка — только для 100% (это я предположил на основании вашего описания). Обратите внимание, что это становится сложнее в более естественной стохастической структуре, где, например, батарея SoC ведет себя не так хорошо.

  • Действия и затраты: для конкретных состояний с одной батареей связан набор возможных действий (плюс соответствующие затраты). Соберем их в следующем:

    State                  Possible Actions                       Cost
    -------------------------------------------------------------------------------
    {UNKNOWN, Relax}  ->   RECHARGE TO {100%,0m}                  2,5h
    
    {SoC, 0m}         ->   RELAX TO {SoC,5m},                     5m          
    
    {SoC, Relax}      ->   RECHARGE TO {100%,0m},                 2,5h           
                           DISCARGE TO {SoC-1},                   C(SoC, SoC-1)
                           DISCHARGE-MEASURE TO {SoC-1},          C(SoC, SoC-1)
                           RELAX TO {SoC, RELAX+1}                C(Relax, Relax+1)
    
    {SoC, UNKNOWN}    ->   RECHARGE TO {100%,0m},                 2,5h
                           DISCARGE TO {SOC-1,0m},                C(SoC,SoC-1)   
                           RELAX {SoC, 24h}                       24h
    

    SoC-1 здесь означает следующее возможное состояние, тогда как C(SoC, SoC-1) означает «время перехода от SoC к SoC-1, например, с 75% до 50%. Теперь ваша очередь проверить эту таблицу, соответствует ли она вашей модели. Если нет, у вас есть исправить или расширить его.

    Обратите внимание, что я снова сделал упрощение, разрешив только переходы в следующее возможное состояние SoC-1 (например, с 75% до 50%). Это разумно, так как стоимость считается дополнительной. Например, переход от 75 % к 25 % занимает столько же времени, сколько переход сначала к 50 %, а затем к 25 %.

    Кроме того, все действия RECHARGE и DISCHARGE осуществимы только в рабочие часы, которые не учитывались в приведенной выше таблице (но должны быть включены в модель позже).

Объедините вышеперечисленное в модель полной системы

Теперь предположим, что у вас есть батареи N, зарядные устройства M и разрядники K (где мы можем предположить M<=N и K<=N), все они идентичны. Далее, допустим, цель состоит в том, чтобы выполнить каждый тест только один раз.

  • Состояние теста. Состояние теста Test представляет собой вектор размерности 4*7 из 0 и 1, содержащий информацию о том, был ли уже выполнен конкретный тест. Обратите внимание, что это соответствует 2^28 возможным состояниям, поэтому здесь определенно нужно ввести приближение.

  • Состояние нескольких батарей. Суммарное состояние всех батарей B является декартовым произведением состояния одной батареи, т. е. находится в пространстве {SoC,Relax}^N. Это просто означает, что нужно учитывать N состояния одной батареи.

    B={SoC_1, Relax_1}, ..., {SoC_N, Relax_N}
    

    Опять же, размер этого пространства будет очень большим для средних чисел N.

  • Время работы. Кроме того, нам нужно указать время суток T. Делая это точно, мы получаем 24*60m / 5m = 288 возможных пятиминутных слотов.

  • Многобатарейные действия. Точно так же многобатарейные действия задаются N-мерным декартовым произведением одномерных действий. RECHARGE и DISCHARGE возможно только для T в рабочее время и при наличии достаточного количества Re- и Dischargers (сумма всех RECHARGE/ DISCHARGE не должна превышать M/K).

Подводя итог, полное состояние S задается комбинацией

S = {Test, T, B}

Размерность пространства состояний составляет около 2^28 * (6*9)^N * 288, что быстро становится огромным.

Далее, для каждого состояния есть соответствующий набор разрешенных действий, который уже должен быть ясен.

Итак, теперь, когда модель системы более или менее определена (исправьте ее, если нужно!), мы можем продолжить ее решение.

Уравнение Беллмана

Уравнение Беллмана является центральным уравнением (приближенного) динамического программирования. В качестве хорошего введения взгляните на книгу Саттона и Барто, которая находится в свободном доступе в сети.

Идея довольно проста: для каждого возможного состояния введите функцию значения V(S), которая говорит вам, насколько хорошо находиться в состоянии S. Эта функция значения в принципе содержит время, необходимое для завершения тестов, когда вы находитесь в состоянии S. В Чтобы определить это значение для задачи с конечным горизонтом, как она есть, нужно начинать с конечного состояния и возвращаться к началу, то есть, по крайней мере, если это позволяет размер задачи. Но давайте сделаем это схематично в следующем и посмотрим:

Конечное состояние — это состояние, в котором Test содержит только единицы, т. е. все 28 проверки выполнены. Установите V(S)=0 для всех этих состояний (независимо от T и состояния нескольких батарей), так как вам больше не нужно время.

Шаг назад. Теперь рассмотрим все состояния, в которых Test содержит только 27 единиц и один ноль, т. е. еще нужно выполнить один тест. Для каждого возможного состояния нескольких батарей B и момента времени T можно автоматически определить самую быструю альтернативу. Установите V(S) равным этой стоимости.

Еще один шаг назад. Далее рассматриваются все состояния, в которых Test содержит только 26 единиц и два нуля (предстоит провести еще два теста). Теперь для каждого возможного состояния B с несколькими батареями и момента времени T выбирают действие a таким образом, чтобы минимизировать стоимость действия плюс значение состояния, к которому приводит действие. С точки зрения, вы должны выбрать a так, чтобы минимизировать C(a) + V(S'), где a ведет от S к S'. Если вы нашли это действие, установите состояние равным V(S) = C(a) + V(S').

И так далее. Это делается для всех возможных состояний и сохраняется каждое оптимальное значение V(S), полученное таким образом - для небольшого количества батарей N это может быть даже осуществимо на практике.

Как только вы будете готовы к этому, вы получите оптимальное действие в каждом состоянии. Для этого, если кто-то находится в состоянии S, он следует тому же рецепту, что и выше, и всегда выбирает действие a, которое минимизирует C(a) + V(S'). Это также можно сделать только один раз, когда сохраняется лучшее действие для каждого состояния.

И тогда все готово — вы полностью решили свою проблему. Или скажем, по крайней мере теоретически, потому что на практике размер задачи требует слишком много усилий и памяти, чтобы выполнить вышеупомянутую обратную рекурсию и сохранить ее в таблице (для задачи, как указано выше, я бы сказал, что этот режим начинается, когда N~3 и больше ). Поэтому необходимо ввести приближения.

приближения

При использовании приближений обычно жертвуют «оптимальным решением» в пользу «хорошо работающего решения». Поскольку это можно сделать несколькими способами, именно здесь начинается искусство. Или, цитируя Пауэлла, глава 15.7: «Успешный алгоритм ADP для важного класса задач, по нашему мнению, является патентоспособным изобретением». В связи с этим я только набросаю некоторые возможные шаги, которые можно сделать здесь.

  • Один класс методов аппроксимации, называемый агрегированием, использует упрощенную модель переменной состояния. Например, вместо того, чтобы включать время T в 5m фрагментах в переменной состояния (288 состояний), можно использовать 1h фрагментов или даже логическое значение, указывающее, является ли это рабочим временем. Или вы можете использовать 5m чанков только в рабочее время плюс одно состояние вне рабочего времени. И так далее... масса возможностей. Здесь всегда получается меньшее табличное представление

  • Другой класс методов аппроксимации использует параметризованное представление функции ценности, например модель линейной регрессии или нейронную сеть. В приведенной выше итерационной схеме функции значений не хранятся в таблице, а скорее используются в качестве входных данных для подбора параметров. Это заменяет часто огромное табличное представление гораздо меньшим количеством параметров, но недостатком является то, что процедура подбора обычно является более сложной. (Обратите внимание, что на этом этапе естественно могут применяться эволюционные алгоритмы).

  • В другом методе используются базовые функции, фиксирующие важные состояния системы. Например, в игре в крестики-нолики вам не нужны все возможные игровые состояния, но достаточны базисные состояния, которые указывают, кто занимает центр и количество занятых ребер.

  • Далее, вместо того, чтобы пытаться выполнить полную итерацию по всем состояниям, можно использовать методы Монте-Карло для случайного исследования многих, но не всех возможных состояний. Это тем эффективнее, чем лучше существуют эвристики, которые позволяют алгоритму исследовать значимые состояния.

Другие идеи, а также их практическое применение можно найти в упомянутых выше книгах.


Хорошо, это получилось длинно, но я надеюсь, что это поможет дать вам некоторое представление об одном из возможных подходов. Я бы предложил вам спроектировать небольшую модель, используя, скажем, только одну батарею, и попытаться реализовать описанную выше обратную итерацию самостоятельно. Кроме того, в обеих процитированных книгах вы найдете несколько игрушечных задач, где вы можете ознакомиться с такими задачами. Удачи с аккумуляторами!

person davidhigh    schedule 05.12.2014
comment
Было очень интересно читать, уровень объяснения точно правильный. Не слишком абстрактно/академично, что я не знаю, как это реализовать, и не учитываю некоторые детали реализации, которые, я думаю, я могу понять самостоятельно. Я рад, что вы подумали о чем-то другом, кроме генетического алгоритма. Вы частично соответствуете критерию 1, рассматривая один альтернативный алгоритм, отличный от предложенного. Конечно, этот ответ также соответствует критерию 2, который заключается в моделировании решения. Я постараюсь реализовать это в течение нескольких дней и свяжусь с вами. - person Flip; 05.12.2014
comment
@Flip: только что снова наткнулся на это ... ты решил свою проблему тогда? И если да, то каким методом? - person davidhigh; 14.06.2017
comment
Я пытался реализовать это сам, но в то время мне пришлось сдаться, однако я дорожу этим ответом и обязательно буду нуждаться в нем в будущем. Я приму ответ, как только я его проверю. - person Flip; 14.06.2017
comment
@Flip: дело не в принятии, мне просто было любопытно. Если вам нужна дополнительная помощь, просто дайте мне знать. - person davidhigh; 14.06.2017

Поскольку это StackOverflow, и поскольку вы собираетесь потратить fkn 500 своих с трудом заработанных баллов, и поскольку я сам очень заинтересован в этом, я написал вам пример кода, который реализует рецепт, который я представил несколько дней назад. Хотя это игрушечная модель, которая там решается, она содержит большинство вопросов вашей исходной проблемы и — при наличии достаточной мощности компьютера — может быть легко расширена для решения последней.

Модельная проблема

Рассмотрим следующую модельную задачу:

  • N=2 аккумуляторы, M=2 зарядные устройства, K=1 разрядник.

  • Время T продолжается кратно 6 часам: 0:00, 6:00, 12:00, 18:00. Часы работы все кроме 0:00 ночи.

  • Состояния батареи:

    • SoC in {0%,50%,100%}
    • Relax in {0h,6h,12h,18h,24h}

    Аккумуляторы изначально находятся в состоянии {0%,0h}, т.е. незаряжены на момент начала проведения теста (Расслабление не имеет значения, так как все равно сначала нужно подзарядить).

  • Доступные действия:

    • RELAX: do nothing. Increases Relax by 6 hours.
    • RECHARGE: увеличивает SoC до следующего состояния и устанавливает Relax в 0h.
    • MEASURE: уменьшает SoC до предыдущего состояния, а также устанавливает Relax в 0h.

    Каждое действие «стоит» 6 часов.

    Обратите внимание, что MEASURE ведет себя почти так же, как то, что вы назвали DISCHARGE. В частности, если ИЗМЕРЕНИЕ применяется к состоянию батареи, которое не является тестовым случаем, это просто РАЗРЯДКА. Однако, и это единственная разница, когда MEASURE применяется к одному из тестовых случаев, также обновляется состояние Test.

  • Батареи необходимо измерять один раз для каждого состояния {SoC,Relax} в {{50%,6h},{100%,6h},{50%,24h},{100%,24h}}. Таким образом, получается вектор Test размерности четыре, содержащий либо нули, либо единицы.

Как только вы ознакомитесь с приведенной ниже реализацией, вы сможете легко изменить все эти варианты.

Описание модели

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

{Test, T, {SoC_1,Relax_1}, {SoC_2,Relax_2}}

На самом деле это не лучшая переменная состояния (переменная состояния всегда должна быть максимально компактной). Например, не учитывается симметрия аккумуляторов. В «жестких» моделях подобных проблем следует избегать, когда это возможно.

Кроме того, я упростил ситуацию, предположив, что все происходит с временными интервалами в 6 часов. При этом можно просто увеличивать переменную T на 6 часов после каждого действия. Если бы были действия продолжительностью, скажем, 12 часов, нужно было бы добавить дополнительные переменные к переменной состояния, которые указывают, заблокирована ли батарея и когда она снова будет доступна. Не говоря уже о том, что была бы акция, которая длилась, например, 5 часов. Итак, вы видите, что время рассматривается здесь довольно просто.

Реализация

Я использовал C++ для реализации, и я надеюсь, что вам это удобно. Сам код уже довольно длинный, но я постарался его хоть немного прокомментировать. Вот основные моменты реализации:

  • Во-первых, что касается основных элементов: более элементарные задаются enums, например

    enum struct StateOfCharge
    {
        P0=0,
        P50,
        P100,
        END_OF_LIST
    };
    

    Целые числа тоже сработали бы хорошо, но я полагаю, что с перечислениями становится легче читать. Для этих перечислений я также предоставил operator<< для вывода на экран, а также operator++ и operator-- для увеличения/уменьшения состояния. Два последних оператора являются шаблонами функций, принимающими любое перечисление (по крайней мере, если оно содержит состояние END_OF_LIST).

  • Более продвинутые элементы, такие как состояния и действия, являются классами. В частности, класс State содержит много логики: он может определить, разрешено ли данное действие через его член.

    bool isActionAllowed(Action const&) const
    

    Кроме того, он может дать вам следующее состояние для данного действия через

    State nextState(Action const&) const
    

    Эти функции занимают центральное место в итерации значений, описываемой далее.

  • Есть класс BatteryCharger, выполняющий реальный алгоритм динамического программирования. Он содержит контейнер std::map<State,int> для хранения значения данного состояния (помните, что значение здесь — это просто время, необходимое для завершения, которое, естественно, должно быть сведено к минимуму). Для того, чтобы map работало, также есть operator<, сравнивающий две переменные State.

  • Наконец, несколько слов о схеме Итерация ценности. В ответе выше я написал, что это можно сделать с помощью обратной итерации. Это верно, но это может стать довольно сложным, потому что вам нужно найти обратный путь, чтобы были покрыты все возможные состояния (и в лучшем случае только один раз). Хотя это было бы возможно здесь, можно также просто перебирать все состояния произвольным образом. Однако это необходимо делать итеративно, потому что в противном случае вы будете использовать значения состояния, которые вы еще не оптимизировали. Итерация здесь заканчивается, когда все значения сходятся.

    Дополнительную информацию об итерации значения см. в книге Саттона и Барто.

Код

Вот код. Это не особенно хорошо написано (--глобальные переменные и т.д.), но оно работает и может быть легко расширено:

#include<iostream>
#include<array>
#include<map>
#include<type_traits>
#include<algorithm>

template<typename T, typename = typename std::enable_if<std::is_enum<T>::value >::type>
bool isLast(T const& t)
{
    return t == static_cast<T>(static_cast<int>(T::END_OF_LIST) - 1);
}

template<typename T, typename = typename std::enable_if<std::is_enum<T>::value >::type>
bool isFirst(T const& t)
{
    return t == static_cast<T>(0);
}



template<typename T, typename = typename std::enable_if<std::is_enum<T>::value >::type>
T& operator++(T& t)
{
    if (static_cast<int>(t) < static_cast<int>(T::END_OF_LIST) - 1)
    {
        t = static_cast<T>(static_cast<int>(t)+1);
    }
    return t;
}

template<typename T, typename = typename std::enable_if<std::is_enum<T>::value >::type>
T operator++(T& t, int)
{
    auto ret = t;
    ++t;
    return ret; 
}


template<typename T, typename = typename std::enable_if<std::is_enum<T>::value >::type>
T& operator--(T& t)
{
    if (static_cast<int>(t) > 0)
    {
        t = static_cast<T>(static_cast<int>(t)-1);
    }
    return t;
}


template<typename T, typename = typename std::enable_if<std::is_enum<T>::value >::type>
T operator--(T& t, int)
{
    auto ret = t;
    --t;
    return ret;
}



const int Nbattery = 2;
const int Nrecharger = 2;
const int Ndischarger = 1;
const int Ntest = 4;

enum struct StateOfCharge
{
    P0=0,
    P50,
    P100,
    END_OF_LIST
};

//screen output
std::ostream& operator<<(std::ostream& os, StateOfCharge const& s)
{
    if(s==StateOfCharge::P0) os << "P0";
    else if (s==StateOfCharge::P50) os << "P50";
    else if (s == StateOfCharge::P100) os << "P100";
    return os;
}


enum struct StateOfRelax
{
    H0=0,
    H6,
    H12,
    H18,
    H24,
    END_OF_LIST
};

//screen output
std::ostream& operator<<(std::ostream& os, StateOfRelax const& s)
{
    if(s==StateOfRelax::H0) os << "H0";
    else if (s == StateOfRelax::H6) os << "H6";
    else if (s == StateOfRelax::H12) os << "H12";
    else if (s == StateOfRelax::H18) os << "H18";
    else if (s == StateOfRelax::H24) os << "H24";
    return os;
}


struct SingleBatteryState
{
    //initialize battery as unfilled
    StateOfCharge SoC;
    StateOfRelax Relax;
    SingleBatteryState(StateOfCharge _SoC = static_cast<StateOfCharge>(0), StateOfRelax _Relax = static_cast<StateOfRelax>(0))
        : SoC(_SoC)
        , Relax(_Relax)
    {}


    //loop over state
    bool increase()
    {
        //try to increase Relax
        if (!isLast(Relax))
        {
            ++Relax;
            return true;
        }
        //if not possible, reset Relax
        else if (!isLast(SoC))
        {
            ++SoC;
            Relax = static_cast<StateOfRelax>(0);
            return true;
        }

        //no increment possible: reset and return false
        SoC = static_cast<StateOfCharge>(0);
        Relax = static_cast<StateOfRelax>(0);
        return false;
    }
};

std::ostream& operator<<(std::ostream& os, SingleBatteryState const& s)
{
    os << "("<<s.SoC << "," << s.Relax << ")";
    return os;
}


bool operator<(SingleBatteryState const& s1, SingleBatteryState const& s2)
{
    return std::tie(s1.SoC, s1.Relax) < std::tie(s2.SoC, s2.Relax);
}

bool operator==(SingleBatteryState const& s1, SingleBatteryState const& s2)
{
    return std::tie(s1.SoC, s1.Relax) == std::tie(s2.SoC, s2.Relax);
}

//here specify which cases you want to have tested
std::array<SingleBatteryState, Ntest> TestCases = { SingleBatteryState{ StateOfCharge::P50, StateOfRelax::H6 }
                                                  , SingleBatteryState{ StateOfCharge::P50, StateOfRelax::H24 }
                                                  , SingleBatteryState{ StateOfCharge::P100, StateOfRelax::H6 }
                                                  , SingleBatteryState{ StateOfCharge::P100, StateOfRelax::H24 } };


// for a state s (and action MEASURE), return the entry in the array Test
// which has to be set to true
int getTestState(SingleBatteryState const& s)
{
    auto it = std::find(std::begin(TestCases), std::end(TestCases), s);

    if(it==std::end(TestCases))
        return -1;
    else
        return it - std::begin(TestCases);
}


enum struct SingleAction
{
    RELAX = 0,
    RECHARGE,
    MEASURE,
    END_OF_LIST
};

std::ostream& operator<<(std::ostream& os, SingleAction const& a)
{
    if(a== SingleAction::RELAX) os << "RELAX";
    else if (a == SingleAction::RECHARGE) os << "RECHARGE";
    else if (a == SingleAction::MEASURE) os << "MEASURE";

    return os;
}


enum struct TimeOfDay
{
    H0 = 0,
    H6,
    H12,
    H18,
    END_OF_LIST
};


//here set office times
std::array<TimeOfDay,3> OfficeTime = {TimeOfDay::H6, TimeOfDay::H12, TimeOfDay::H18};

bool isOfficeTime(TimeOfDay t)
{
    return std::find(std::begin(OfficeTime), std::end(OfficeTime),t) != std::end(OfficeTime);
}

//screen output
std::ostream& operator<<(std::ostream& os, TimeOfDay const& s)
{

    if(s==TimeOfDay::H0) os << "0:00 h";
    else if (s == TimeOfDay::H6) os << "6:00 h";
    else if (s == TimeOfDay::H12) os << "12:00 h";
    else if (s == TimeOfDay::H18) os << "18:00 h";
    return os;
}


struct Action
{
    SingleAction& operator[](int i) { return A[i]; }
    SingleAction const& operator[](int i) const { return A[i]; }

    std::array<SingleAction, Nbattery> A{};

    bool increase()
    {
        for (int i = Nbattery - 1; i >= 0; --i)
        {
            if (!isLast(A[i]))
            {
                ++A[i];
                return true;
            }
            else
            {
                A[i] = static_cast<SingleAction>(0);
            }       
        }
        return false;
    }
};


//screen output
std::ostream& operator<<(std::ostream& os, Action const& A)
{
    os << "(";
    for (int i = 0; i < Nbattery-1 ; ++i)
    {
        os << A[i] << ",";
    }
    os << A[Nbattery-1] << ")";

    return os;
}



struct State
{
    std::array<bool, Ntest> Test{};
    TimeOfDay T = TimeOfDay::H0;
    std::array<SingleBatteryState, Nbattery> B{};

    State()
    {
        for (int i = 0; i < Ntest; ++i)
        {
            Test[i] = true;
        }
    }

    bool isSingleActionAllowed(SingleAction const& a) const
    {
        if ( !isOfficeTime(T) &&  a != SingleAction::RELAX)
        {
            return false;
        }

        return true;
    }

    bool isActionAllowed(Action const& A) const
    {
        //check whether single action is allowed
        for (int i = 0; i < Nbattery; ++i)
        {
            if (!isSingleActionAllowed(A[i]))
                return false;
        }

        //check whether enough Rechargers and Dischargers are available
        int re = 0;
        int dis = 0;

        for (int i = 0; i < Nbattery; ++i)
        {
            //increase re counter
            if (A[i] == SingleAction::RECHARGE)
            {
                ++re;
            }
            //increase dis counter
            else if (A[i] == SingleAction::MEASURE)
            {
                ++dis;
            }

            //check whether ressources are exceeded
            if (re>Nrecharger || dis > Ndischarger)
            {
                return false;
            }
        }

        return true;
    }

    //loop over state
    bool increase()
    {
        //increase time
        if (!isLast(T))
        {
            ++T;
            return true;
        }
        else
        {
            T = static_cast<TimeOfDay>(0);
        }

        //if not possible, try to increase single battery states
        for (int i = Nbattery-1; i >= 0; --i)
        {
            if (B[i].increase())
            {
                return true;
            }
        }

        //if also not possible, try to increase Test state
        for (int i = Ntest-1; i >=0; --i)
        {
            if (Test[i])
            {
                Test[i] = false;
                return true;
            }
            else
            {
                Test[i] = true;
            }
        }

        return false;
    }


    // given a single action and a single-battery state, calculate the new single-battery state.
    // it is assumed the action is allowed
    SingleBatteryState newState(SingleBatteryState s, SingleAction const& a) const
    {
        if (a == SingleAction::RELAX)
        {
            ++s.Relax;
        }
        else if (a == SingleAction::RECHARGE)
        {
            ++s.SoC;
            s.Relax = static_cast<StateOfRelax>(0);
        }
        else if (a == SingleAction::MEASURE)
        {
            --s.SoC;
            s.Relax = static_cast<StateOfRelax>(0);
        }
        return s;
    }

    // calculate new complete state while assuming the action s allowed
    State newState(Action const& a) const
    {
        State ret = *this;

        //increase time
        if (isLast(ret.T))
        {
            ret.T = static_cast<TimeOfDay>(0);
        }
        else
        {
            ret.T++;
        }

        //apply single-battery action
        for (int i = 0; i < Nbattery; ++i)
        {
            ret.B[i] = newState(B[i], a[i]);

            // if measurement is performed, set new Test state.
            // measurement is only possible if Soc=50% or 100%
            // and Relax= 6h or 24h
            if (a[i] == SingleAction::MEASURE
                && getTestState(B[i]) != -1)
            {
                ret.Test[getTestState(B[i])] = true;
            }
        }
        return ret;
    }

    int cost(Action const& a) const
    {
        if (std::find(std::begin(Test), std::end(Test), false) == std::end(Test))
        {
            return 0;
        }
        return 1;
    }

};

//screen output
std::ostream& operator<<(std::ostream& os, State const& S)
{
    os << "{(";
    for (int i = 0; i < Ntest-1; ++i)
    {
        os << S.Test[i]<<",";
    }
    os << S.Test[Ntest-1] << "),"<<S.T<<",";

    for (int i = 0; i < Nbattery; ++i)
    {
        os << "(" << S.B[i].SoC << "," << S.B[i].Relax<<")";
    }
    os << "}";

    return os;
}

bool operator<(const State& s1, const State& s2)
{
    return std::tie(s1.Test, s1.T, s1.B) < std::tie(s2.Test, s2.T, s2.B);
}




struct BatteryCharger
{
    bool valueIteration()
    {
        // loop over all states with one specified Test state
        State S;

        int maxDiff=0;
        do
        {
            int prevValue = V.find(S)->second;
            int minCost = prevValue;

            // loop over all actions
            // and determine the one with minimal cost
            Action A;
            do
            {
                if (!S.isActionAllowed(A))
                {
                    continue;
                }

                auto Snew = S.newState(A);
                int newCost = S.cost(A) + V.find(Snew)->second;

                if (newCost < minCost)
                {
                    minCost = newCost;
                }
            }
            while (A.increase());

            V[S] = minCost;

            maxDiff = std::max(maxDiff, std::abs(prevValue - minCost));
        }
        while (S.increase());

        //return true if no changes occur
        return maxDiff!=0;
    }


    void showResults()
    {
        State S;
        do
        {
            auto Aopt = getOptimalAction(S);
            auto Snew = S.newState(Aopt);
            std::cout << S << "   " << Aopt << "   " << Snew << "   " << V[S] << "   " << V.find(Snew)->second << std::endl;
        }
        while (S.increase());
    }

    Action getOptimalAction(State const& S) const
    {
        Action Aopt;

        Action A;
        int minCost = std::numeric_limits<int>::max();
        do
        {
            if (!S.isActionAllowed(A))
            {
                continue;
            }

            auto Snew = S.newState(A);
            int newCost = S.cost(A) + V.find(Snew)->second;

            if (newCost < minCost)
            {
                minCost = newCost;
                Aopt = A;
            }
        }
        while (A.increase());

        return Aopt;
    }

    BatteryCharger()
    {
        State S;
        do
        {
            int ad = 0;
            for (int i = 0; i < Ntest; ++i)
            {
                if (!S.Test[i])
                    ad += 100;
            }
            V[S] = ad;
        }
        while (S.increase());
    }

    std::map<State, int> V;
};





int main(int argc, char* argv[])
{
    BatteryCharger BC;

    int count = 0;
    while (BC.valueIteration())
    {
        ++count;
    };
    std::cout << "Value Iteration converged after " << count << " iterations\n"<<std::endl;

    //start at 6:00 with no tests at all performed
    State S;
    S.Test[0] = false;
    S.Test[1] = false;
    S.Test[2] = false;
    S.Test[3] = false;
    S.T = TimeOfDay::H6;

    //get sequence of optimal actions
    auto Aopt = BC.getOptimalAction(S);
    while (BC.V[S] != 0)
    {
        std::cout << S << "    " << Aopt << "   " << BC.V[S] << std::endl;

        S = S.newState(Aopt);
        Aopt = BC.getOptimalAction(S);
    }
    std::cout << S << "    " << Aopt << "   " << BC.V[S] << std::endl;

    return 0;
}

Вот DEMO, чтобы поиграть.

Результаты

С помощью приведенного выше кода можно получить следующие результаты, напечатанные на экране:

Value Iteration converged after 8 iterations

{(0,0,0,0),6:00 h,(P0,H0)(P0,H0)}    (RELAX,RECHARGE)   10
{(0,0,0,0),12:00 h,(P0,H6)(P50,H0)}    (RECHARGE,RECHARGE)   9
{(0,0,0,0),18:00 h,(P50,H0)(P100,H0)}    (RECHARGE,RELAX)   8
{(0,0,0,0),0:00 h,(P100,H0)(P100,H6)}    (RELAX,RELAX)   7
{(0,0,0,0),6:00 h,(P100,H6)(P100,H12)}    (MEASURE,RELAX)   6
{(0,0,1,0),12:00 h,(P50,H0)(P100,H18)}    (RELAX,RELAX)   5
{(0,0,1,0),18:00 h,(P50,H6)(P100,H24)}    (RELAX,MEASURE)   4
{(0,0,1,1),0:00 h,(P50,H12)(P50,H0)}    (RELAX,RELAX)   3
{(0,0,1,1),6:00 h,(P50,H18)(P50,H6)}    (RELAX,MEASURE)   2
{(1,0,1,1),12:00 h,(P50,H24)(P0,H0)}    (MEASURE,RELAX)   1
{(1,1,1,1),18:00 h,(P0,H0)(P0,H6)}    (RELAX,RELAX)   0

Видно, что итерация значения сходится после 8 итераций. На моем ПК и в Visual Studio в режиме Release это занимает примерно полсекунды. Таким образом, вы можете смело детализировать задачу и все же ожидать возможного точного ответа.

В строках ниже вы видите пример оптимизированного процесса тестирования. Здесь начальное состояние {(0,0,0,0),6:00 h,(P0,H0)(P0,H0)}, т.е. полный тест начинается в 6 утра с неиспользованными батареями (P0 здесь означает 0%, 6:00 h означает 6 утра или немецкое "6:00 Uhr"). Следующий столбец дает вам оптимизированное действие {RELAX,RECHARGE} (которое приводит к состоянию в следующей строке). Число в третьем столбце — это время (в единицах по 6 часов), которое еще требуется для завершения тестов. Для этого примера требуется в общей сложности 60 часов.

Вот и проблема с моделью решена. Чтобы проверить разные начальные состояния (они все оптимизированы!), просто измените следующие строки в main:

//start at 6:00 with no tests at all performed
State S;
S.Test = {false,false,false,false};
S.T = TimeOfDay::H6;

А теперь, получайте удовольствие! В лучшем случае вы сообщите нам, как вы действовали и насколько это было успешно.

person davidhigh    schedule 06.12.2014
comment
Я просмотрел результаты, и, честно говоря, они выглядят не очень хорошо. Мне пришлось бы потратить первые 24 часа, просто расслабляясь/подзаряжая аккумулятор, и не выполняя измерения. У меня есть сомнения, подходит ли уравнение Беллмана для этой задачи, потому что оно предполагает, что действие с наименьшей продолжительностью является наиболее ценным. Можете ли вы доказать, что не будет ситуации, когда выбор более медленного действия будет более выгодным с учетом ограничений (например, рабочего времени)? Я думаю, что ценность действия можно определить только после того, как будет рассчитана вся временная шкала. - person Flip; 07.12.2014
comment
@Flip: при условии, что я все сделал правильно в реализации (чего я не могу исключить, хотя и сделал это тщательно), эти результаты на самом деле являются оптимальными. Bellman работает с общей идеей затрат, которые необходимо минимизировать. Этими затратами может быть что угодно (например, сочетание времени и денег), но здесь я предположил, что общая продолжительность становится минимальной. И именно поэтому в первый день нет измерений: более медленные действия, такие как ПЕРЕЗАРЯДКА и РАССЛАБЛЕНИЕ, предпочтительнее быстрых измерений. Таким образом, измерения могут быть выполнены по очереди от 100% до 0%. - person davidhigh; 07.12.2014
comment
@Flip: еще один: "I think that the value of an action can only be determined after the entire timeline has been calculated." Верно, и это именно то, что здесь делается. Способ сделать это — динамическое программирование: оптимизируйте всю проблему небольшими шагами. Это, так сказать, идентично задаче о кратчайшем пути из города (0,0,0,0) в город (1,1,1,1), где каждое соединение занимает 6 часов. Наконец, что касается результатов примера: мне они кажутся разумными. Однако это всего лишь простая модель. Используя более реалистичные правила, все может быть иначе, и, возможно, в это будет легче поверить. - person davidhigh; 07.12.2014
comment
Я еще раз взглянул на результаты, и они действительно оформляют заказ. Код, который вы добавили, хорош на C++, но, поскольку он упускает из виду многие вещи, он слишком упрощен, и я не могу сказать, сможет ли он справиться с мельчайшими деталями: выходные, более длинные/более короткие действия (вы зафиксировали 6-часовой действий), как справляться с последовательными действиями по расслаблению. Я все еще в восторге от вашего первого ответа и работаю над собственной реализацией. Быстро закончить его будет сложно, но только тогда можно будет доказать, что метод Беллмана работает для этой задачи. - person Flip; 08.12.2014
comment
@Flip: вы правы, код реализует только игрушечную модель. Тем не менее, уравнение Беллмана, безусловно, является подходящим способом, и оно будет работать и для гораздо более сложных вещей. Некоторые варианты: Выходные: добавить счетчик дней. Более длинные/более короткие действия: установите минимальную продолжительность, равную самому короткому действию. Для более длительных действий добавьте переменную блокировки, которая указывает, когда аккумулятор можно использовать повторно. И т. д. Очевидно, что при этом наступает момент, когда использование приближений становится неизбежным. Мне было бы интересно, насколько мой простой код может быть расширен... - person davidhigh; 08.12.2014