Структура данных дерева в Mathematica

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

Теперь на мой вопрос (ы):

  • Есть ли какой-либо пакет с открытым исходным кодом для доступных структур данных?

  • Какой подход вы использовали в отношении структур данных? Постепенно разрабатывайте свой собственный пакет утилиты?

(Не вопрос, просто замечание. Возможно... отсутствие (много доступных) пакетов с открытым исходным кодом является причиной того, что Mathematica не обладает темпом, которого он заслуживает. Проблема с курицей/яйцом, я боюсь.)

Ответ 1

В Mathematica большая часть того, что вы делаете, основана на выражениях. Естественно, что выражения имеют древовидную структуру. Для переходов по глубине (которые, вероятно, наиболее распространены) вы можете использовать такие функции, как Scan, Map, Cases и т.д. Разница между более традиционными языками заключается в том, что нет простого способа сохранить личность индивидуального node в дереве выражений, поскольку в Mathematica нет указателей. Кроме того, многие операции над выражениями, которые являются идиоматическими в Mathematica, будут копировать все выражение, когда вам нужно только изменить его в нескольких местах, потому что выражения неизменяемы.

Использование неизменяемых выражений Mathematica, поскольку деревья по-прежнему имеют ряд преимуществ. Во-первых, потому что они неизменны, легко понять, что они хранят, просто глядя на них (состояние и поведение не смешиваются). Другим является то, что существуют эффективные и общие функции, такие как Map, MapIndexed или Scan, которые пересекают их. Например, шаблон дизайна посетителя невидимый - это просто Map[f,tree,Infinity], встроенный в langauge. Кроме того, существуют встроенные функции, такие как Cases, Replace, ReplaceAll и т.д., Что позволяет писать очень сжатый и декларативный код для деструкции деревьев, поиска кусков деревьев с определенным синтаксисом или удовлетворения некоторого состояния, и т.д. Поскольку деревья не ограничиваются только созданием из списков и создаются из разных головок, можно эффективно использовать это, чтобы написать очень сжатый код обработки дерева. Наконец, свобода очень легко создавать любую древовидную структуру, которую вы хотите, значительно облегчает выполнение экспериментов и прототипов в духе поисковое и восходящее программирование, что сокращает цикл разработки и в конечном итоге приводит к лучшим проектам.

Тем не менее, вы можете, конечно, реализовать структуру данных "stateful" (изменчивый) дерева. Настоящая причина, по которой это еще не было сделано, - это, как я подозреваю, удар производительности, связанный со строительством, модификацией и перемещением такого дерева, поскольку на каждом этапе он будет проходить полный процесс символической оценки (см. этот для более подробной информации об этом). Для двух примеров того, как, например, двоичное дерево поиска может использоваться в контексте Mathematica для довольно эффективного кода, см. Мои сообщения здесь (общая символьная настройка) и здесь (в контексте Скомпилированного кода). Для общих путей построения структур данных по идиоматике в Mathematica я рекомендую книги Романа Мадера: "Программирование в математике", "Математический программист я и II" и особенно "Компьютерные науки в математике". В последнем он подробно обсуждает, как реализовать двоичное дерево поиска в Mathematica. EDIT Как отметил @Simon, разговор о @Daniel Lichtblau также является отличным ресурсом, который показывает, как создавать структуры данных и сделать их эффективными.

В отношении общих способов реализации структур данных в Mathematica, которые будут включать некоторое состояние, вот простой пример, извлеченный из моего сообщения в this Mathgroup thread - он реализует структуру данных "пары".

Unprotect[pair, setFirst, getFirst, setSecond, getSecond, new, delete];
ClearAll[pair, setFirst, getFirst, setSecond, getSecond, new, delete];
Module[{first, second},
  first[_] := {};
  second[_] := {};
  pair /: new[pair[]] := pair[Unique[]];
  pair /: pair[tag_].delete[] := (first[tag] =.; second[tag] =.);
  pair /: pair[tag_].setFirst[value_] := first[tag] = value;
  pair /: pair[tag_].getFirst[] := first[tag];
  pair /: pair[tag_].setSecond[value_] := second[tag] = value;
  pair /: pair[tag_].getSecond[] := second[tag];
  Format[pair[x_Symbol]] := "pair[" <> ToString[Hash[x]] <> "]";
];
Protect[pair, setFirst, getFirst, setSecond, getSecond, new, delete]; 

Вот как вы могли его использовать:

pr = new[pair[]];
pr.setFirst[10];
pr.setSecond[20];
{pr.getFirst[], pr.getSecond[]}

{10, 20}

Создание списка новых парных объектов:

pairs = Table[new[pair[]], {10}]

{"pair[430427975]", "pair[430428059]", "pair[430428060]", "pair[430428057]",
"pair[430428058]", "pair[430428063]", "pair[430428064]", "pair[430428061]", 
"pair[430428062]", "pair[430428051]"}

Установка полей:

Module[{i},
 For[i = 1, i <= 10, i++,
  pairs[[i]].setFirst[10*i];
  pairs[[i]].setSecond[20*i];]]

Проверка полей:

#.getFirst[] & /@ pairs

{10, 20, 30, 40, 50, 60, 70, 80, 90, 100}

#.getSecond[] & /@ pairs

{20, 40, 60, 80, 100, 120, 140, 160, 180, 200} 

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

Существует несколько расширений OOP для Mathematica, таких как пакет classes.m Романа Мадера (источник находится в его книге "Программист Mathematica" ), коммерческий пакет Objectica и несколько других. Но до тех пор, пока сама Mathematica не обеспечит эффективные механизмы (возможно, основанные на каком-то указателе или ссылочном механизме) для создания изменяемых структур данных (если это когда-либо произойдет), вероятно, будет большой удар производительности, связанный с реализацией на высшем уровне таких структур данных в мм. Кроме того, поскольку mma основано на неизменности как одной из основных идей, не так просто сделать изменяемые структуры данных хорошо вписывающимися в другие идиомы программирования Mathematica.

EDIT

Ниже приведен пример реализации дерева с голыми бинами по строкам приведенного выше примера:

Module[{parent, children, value},
  children[_] := {};
  value[_] := Null;
  node /: new[node[]] := node[Unique[]];
  node /: node[tag_].getChildren[] := children[tag];
  node /: node[tag_].addChild[child_node, index_] := 
        children[tag] = Insert[children[tag], child, index];
  node /: node[tag_].removeChild[index_] := 
        children[tag] = Delete[children[tag], index];
  node /: node[tag_].getChild[index_] := children[tag][[index]];
  node /: node[tag_].getValue[] := value[tag];
  node /: node[tag_].setValue[val_] := value[tag] = val;
];

Некоторые примеры использования:

In[68]:= root = new[node[]]

Out[68]= node[$7]

In[69]:= root.addChild[new[node[]], 1]

Out[69]= {node[$8]}

In[70]:= root.addChild[new[node[]], 2]

Out[70]= {node[$8], node[$9]}

In[71]:= root.getChild[1].addChild[new[node[]], 1]

Out[71]= {node[$10]}

In[72]:= root.getChild[1].getChild[1].setValue[10]

Out[72]= 10

In[73]:= root.getChild[1].getChild[1].getValue[]

Out[73]= 10

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

Ответ 2

Я использовал математику в основном как верстак математики и для написания относительно небольших специальных программ.

Mathematica действительно превосходит это.

Какой подход вы использовали в отношении структур данных? Постепенно разрабатывайте свой собственный пакет утилиты?

Я не создаю свои собственные структуры данных в Mathematica, потому что он не может эффективно их обрабатывать. В частности, общие структуры данных в Mathematica имеют тенденцию быть в 10-1 000 × медленнее, чем в других местах, что значительно ограничивает их практическую полезность. Например, Mathematica на 100 × медленнее, чем F # при вычислении диапазона глубин в красно-черном дереве.

Логическое программирование со списками - один из примеров, где Mathematica обычно на порядки медленнее, чем другие скомпилированные языки. Следующая программа Mathematica использует связанные списки для решения проблемы n-queens:

safe[{x0_, y0_}][{x1_, y1_}] := 
 x0 != x1 && y0 != y1 && x0 - y0 != x1 - y1 && x0 + y0 != x1 + y1

filter[_, {}] := {}
filter[p_, {h_, t_}] := If[p[h], {h, filter[p, t]}, filter[p, t]]

search[n_, nqs_, qs_, {}, a_] := If[nqs == n, a + 1, a]
search[n_, nqs_, qs_, {q_, ps_}, a_] := 
 search[n, nqs, qs, ps, 
  search[n, nqs + 1, {q, qs}, filter[safe[q], ps], a]]

ps[n_] := 
 Fold[{#2, #1} &, {}, Flatten[Table[{i, j}, {i, n}, {j, n}], 1]]

solve[n_] := search[n, 0, {}, ps[n], 0]

Вот эквивалент F #:

let safe (x0, y0) (x1, y1) =
  x0<>x1 && y0<>y1 && x0-y0<>x1-y1 && x0+y0<>x1+y1

let rec filter f = function
  | [] -> []
  | x::xs -> if f x then x::filter f xs else filter f xs

let rec search n nqs qs ps a =
  match ps with
  | [] -> if nqs=n then a+1 else a
  | q::ps ->
      search n (nqs+1) (q::qs) (filter (safe q) ps) a
      |> search n nqs qs ps

let ps n =
  [ for i in 1..n do
      for j in 1..n do
        yield i, j ]

let solve n = search n 0 [] (ps n) 0

solve 8

Решение проблемы 8-queens занимает 10,5 с Mathematica и 0,07 с F #. Таким образом, F # в 150 раз быстрее, чем Mathematica.

Вопрос Связанные списки Mathematica и производительность дают более экстремальный пример. Наивный перевод этого кода Mathematica в F # дает эквивалентную программу, которая работает от 4000 до 200 000 × быстрее, чем Mathematica:

let rand = System.Random()
let xs = List.init 10000 (fun _ -> rand.Next 100)
Array.init 100 (fun _ ->
  let t = System.Diagnostics.Stopwatch.StartNew()
  ignore(List.length xs)
  t.Elapsed.TotalSeconds)

В частности, Mathematica занимает от 0,156 до 16 секунд для выполнения одной итерации, тогда как F # занимает от 42 до 86 мкс.

Если я действительно захочу остаться в Mathematica, то я расскажу обо всем, что я делаю, в Mathematica, содержащей встроенные структуры данных. Dispatch.