Как создать сет-лист, в котором нет двух последовательных песен в одном ключе


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

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

Задача поиска допустимых наборов ключей

У меня есть набор 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"}
]

Очевидно, что не всегда возможно найти ключ-допустимый setlist:

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

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

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

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

Идеи

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

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

  1. используя какую-то рандомизированную стратегию, чтобы быстрее найти решение.
  2. возвращает все возможные допустимые наборы ключей, если таковые существуют.
  3. добавление еще одного свойства к песням, например темпа, который должен следовать другое правило (например, темп должен монотонно увеличиваться по всему сет-листу)
  4. Получение приближенных решений (т. е. с минимальным количеством последовательных песен в одном ключе), возможно, только тогда, когда не может быть найдено идеальное решение, или, возможно, когда есть другие ограничения, которые должны быть удовлетворены.
Я пытаюсь сделать это в Clojure (я думал, что библиотека core.logic может помочь), но очевидно, что алгоритм может быть выполнен на любом языке.
2 3

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, которая просто смотрит на значения, а не на :keys в карте:

(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)) )))