Как создать сет-лист, в котором нет двух последовательных песен в одном ключе - PullRequest
0 голосов
/ 04 мая 2018

Это реальная проблема, которую я пытаюсь автоматизировать, поэтому я с удовольствием отвечу на любые вопросы или просьбы о разъяснениях. Заранее благодарим за чтение и за любые мысли, которые могут у вас возникнуть по этому поводу. :)

Редактировать: Чтобы отличить от возможного дублирующего вопроса, я надеялся на программы, специфичные для Clojure, которые гарантированно вернут правильный ответ и используют библиотеки ядра и комбинаторики Clojure ... и у людей были ответы! Спасибо.

Проблема поиска допустимых ключей setlists

У меня есть набор n песен (порядок не имеет значения).

Каждая песня имеет ровно одну ключевую подпись, или сокращенно «ключ», который должен быть одной из 12 строк "A" "A#" "B" "C" "C#" "D" "D#" "E" "F" "F#" "G" "G#".

Несколько песен могут быть «в одном ключе» (им назначено одинаковое целое число).

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

; input 1, here given as a list but really an unordered set, not a key-valid setlist because there are a bunch of songs in the key of A consecutively:
[
    {:title "Deep House Track" :key "F#"}
    {:title "Breakup Song" :key "B"}
    {:title "Love Song" :key "A"}
    {:title "Inspirational Song" :key "A"}
    {:title "Summer Song" :key "A"}
    {:title "Summer Song" :key "A"}
    {:title "Power Ballad" :key "D"}
]

; output 1 will be:

[
    {:title "Love Song" :key "A"}
    {:title "Breakup Song" :key "B"}
    {:title "Inspirational Song" :key "A"}
    {:title "Power Ballad" :key "D"}
    {:title "Summer Song" :key "A"}
    {:title "Deep House Track" :key "F#"}
    {:title "Summer Song" :key "A"}
]

Очевидно, что не всегда можно найти правильный список ключей:

; input 2, with no solution:
[
    {:title "Love Song" key "A"}
    {:title "Inspirational Song" key "A"}
]

Что я пробовал

Я пытался написать что-то, что будет использовать group-by Clojure на входе, чтобы сгруппировать по строке ключевой подписи (вызвать полученную карту m), а затем иметь рекурсивную функцию с аккумулятором (где я буду строить окончательный сет-лист), который пытается поместить песню из m в аккумулятор в допустимую позицию.

Однако я не мог убедить себя, что этот метод всегда найдет решение, если оно существует.

Идеи

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

Другие идеи включают обработку этого типа судоку и использование метода, основанного на ограничениях - я был бы заинтересован в более декларативном подходе с использованием core.logic, если кто-нибудь знает, как это сделать.

Будущие соображения:

  1. Использование какой-то рандомизированной стратегии для быстрого поиска решения.
  2. Возвращение всех возможных наборов ключей, допустимых для ключа, если таковые существуют.
  3. Добавление к песне другого свойства, например темп, который должен следовать другому правилу (например, темп должен монотонно увеличиваться во всем сет-листе)
  4. Получение приблизительных решений (т. Е. С минимальным количеством последовательных песен в одном и том же ключе), возможно, только когда не может быть найдено идеальное решение, или может быть, когда есть другие ограничения, которые должны быть удовлетворены.

Я пытаюсь сделать это в Clojure (я думал, что библиотека core.logic может помочь), но, очевидно, алгоритм может быть выполнен на любом языке.

Ответы [ 2 ]

0 голосов
/ 04 мая 2018

Вот способ сделать это с помощью core.logic.

Мы определим secondo (например, firsto) для просмотра второго элемента каждой пары элементов в коллекции в следующей функции.

(defn secondo [l s]
  (fresh [x]
    (resto l x)
    (firsto x s)))   

Мы определим nonconseco для рекурсивной проверки отсутствия последовательных значений:

(defn nonconseco [l]
  (conde
    [(== l ())]
    [(fresh [x] (== l (list x)))]
    [(fresh [lhead lsecond ltail]
       (conso lhead ltail l)
       (secondo l lsecond)
       (project [lhead lsecond] ;; project to get your map keys
         (!= (:key lhead) (:key lsecond)))
       (nonconseco ltail))]))

И функция для поиска первой перестановки coll, которая не имеет последовательных идентичных значений:

(defn non-consecutive [coll]
  (first
    (run 1 [q]
      (permuteo coll q)
      (nonconseco q))))

Это может быть использовано на вашем примере ввода:

(non-consecutive
  [{:title "Deep House Track" :key "F#"}
   {:title "Breakup Song" :key "B"}
   {:title "Love Song" :key "A"}
   {:title "Inspirational Song" :key "A"}
   {:title "Summer Song" :key "A"}
   {:title "Power Ballad" :key "D"}])
=>
({:title "Love Song", :key "A"}
 {:title "Breakup Song", :key "B"}
 {:title "Inspirational Song", :key "A"}
 {:title "Deep House Track", :key "F#"}
 {:title "Summer Song", :key "A"}
 {:title "Power Ballad", :key "D"})

И вот универсальная версия nonconseco, которая просто смотрит на значения вместо :key s на карте:

(defn nonconseco [l]
  (conde
    [(== l ())]
    [(fresh [x] (== l (list x)))]
    [(fresh [lhead lsecond ltail]
       (conso lhead ltail l)
       (secondo l lsecond)
       (!= lhead lsecond)
       (nonconseco ltail))]))

 (non-consecutive [1 1 2 2 3 3 4 4 5 5 5])
 => (3 2 3 4 2 4 5 1 5 1 5)

Обновление: вот более быстрая версия, которая использует функцию предиката, а не реляционную логику:

(defn non-consecutive? [coll]
  (every? (partial apply not=) (partition 2 1 coll)))

Затем используйте core.logic pred, чтобы применить этот предикат к логической переменной:

(run 10 [q]
  (permuteo coll q)
  (pred q non-consecutive?))
0 голосов
/ 04 мая 2018

Вы можете сделать это легко, воспользовавшись clojure.math.combinatorics:

(ns demo.core
  (:use tupelo.core)
  (:require
    [clojure.string :as str]
    [schema.core :as s]
    [clojure.math.combinatorics :as combo]))

(def Song {:title s/Str :key s/Str})
(def SongPair [(s/one Song "s1")
               (s/one Song "s2")])

(s/defn valid-pair?
  [song-pair :- SongPair]
  (let [[song-1 song-2] song-pair
        key-1 (grab :key song-1)
        key-2 (grab :key song-2)]
    (not= key-1 key-2)))

(s/defn valid-set-list?
  [set-list :- [Song]]
  (let [song-pairs (partition 2 1 set-list)]
    (every? valid-pair? song-pairs)))

(s/defn valid-sets
  "Return a list of valid sets (song orderings) from songs that can follow the given lead-song."
  [songs :- [Song]]
  (let [all-set-lists   (combo/permutations songs)
        all-set-lists   (mapv vec all-set-lists) ; convert set lists => vectors
        valid-set-lists (set (filter valid-set-list? all-set-lists))]
    valid-set-lists))

Юнит-тесты показывают это в действии:

(dotest
  (let [songs [{:title "A1" :key "A"}
               {:title "B1" :key "B"}]]
    (is= (valid-sets songs)
      #{[{:title "A1", :key "A"} {:title "B1", :key "B"}]
        [{:title "B1", :key "B"} {:title "A1", :key "A"}]})))

(dotest
  (let [songs [{:title "A1" :key "A"}
               {:title "B1" :key "B"}
               {:title "B2" :key "B"}]]
    (is= (valid-sets songs)
      #{[{:title "B2", :key "B"}
         {:title "A1", :key "A"}
         {:title "B1", :key "B"}]
        [{:title "B1", :key "B"}
         {:title "A1", :key "A"}
         {:title "B2", :key "B"}]})))

(dotest
  (let [songs [{:title "A1" :key "A"}
               {:title "B1" :key "B"}
               {:title "C1" :key "C"}]]
    (is= (valid-sets songs)
      #{[{:title "A1", :key "A"}
         {:title "B1", :key "B"}
         {:title "C1", :key "C"}]
        [{:title "B1", :key "B"}
         {:title "A1", :key "A"}
         {:title "C1", :key "C"}]
        [{:title "C1", :key "C"}
         {:title "B1", :key "B"}
         {:title "A1", :key "A"}]
        [{:title "C1", :key "C"}
         {:title "A1", :key "A"}
         {:title "B1", :key "B"}]
        [{:title "A1", :key "A"}
         {:title "C1", :key "C"}
         {:title "B1", :key "B"}]
        [{:title "B1", :key "B"}
         {:title "C1", :key "C"}
         {:title "A1", :key "A"}]})))

Для вашего примера, есть 144 возможных набора песен:

(dotest
  (let [songs [{:title "Deep House Track" :key "F#"}
               {:title "Breakup Song" :key "B"}
               {:title "Love Song" :key "A"}
               {:title "Inspirational Song" :key "A"}
               {:title "Summer Song" :key "A"}
               {:title "Power Ballad" :key "D"}]]
    (is= 144 (count (valid-sets songs)) )))
...