Как создать сет-лист, в котором нет двух последовательных песен в одном ключе
Это реальная проблема, решение которой я пытаюсь автоматизировать, поэтому я с удовольствием отвечу на любые вопросы или просьбы о разъяснении. Заранее спасибо за чтение и за любые мысли, которые у вас могут возникнуть по этому поводу. :)
Edit: чтобы отличить от возможного дублирующего вопроса, я надеялся на программы Clojure, которые гарантированно возвращают правильный ответ и используют библиотеки ядра и комбинаторики Clojure... и у людей были ответы! Благодарю ты.
Задача поиска допустимых наборов ключей
У меня есть набор n
песен (порядок не имеет значения).
"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"}
]
Очевидно, что не всегда возможно найти ключ-допустимый setlist:
; input 2, with no solution:
[
{:title "Love Song" key "A"}
{:title "Inspirational Song" key "A"}
]
Что я пробовал
Я попытался написать что-то, что будет использовать Clojure group-by
на входе, чтобы сгруппировать по ключу строка подписи (вызов результирующей карты m
), а затем рекурсивная функция с аккумулятором (где я построю окончательный setlist), которая пытается поместить песню из m
в аккумулятор в допустимой позиции.
Идеи
Моя идея выше казалась правдоподобной, но, возможно, потребуется добавить отступление. Я не знаю, с верхней части моей головы, как осуществить это. Другие идеи включают в себя рассмотрение этого как судоку и использование метода на основе ограничений - я был бы заинтересован в более декларативном подходе, использующемcore.logic
, Если кто-то знает, как это сделать.
Будущие соображения:
- используя какую-то рандомизированную стратегию, чтобы быстрее найти решение.
- возвращает все возможные допустимые наборы ключей, если таковые существуют.
- добавление еще одного свойства к песням, например темпа, который должен следовать другое правило (например, темп должен монотонно увеличиваться по всему сет-листу) Получение приближенных решений (т. е. с минимальным количеством последовательных песен в одном ключе), возможно, только тогда, когда не может быть найдено идеальное решение, или, возможно, когда есть другие ограничения, которые должны быть удовлетворены.
core.logic
может помочь), но очевидно, что алгоритм может быть выполнен на любом языке.2 ответа:
Вот способ сделать это с помощью ядра.логика.
Мы определим
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)))
Затем используйте ядро.логика
pred
, Чтобы применить этот предикат к логической переменной:(run 10 [q] (permuteo coll q) (pred q non-consecutive?))
Вы можете сделать это легко, воспользовавшись
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)) )))