Эффективный алгоритм декартовых произведений

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

foreach (int i in is) {
   foreach (int j in js) {
      //Pair i and j
   }
}

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

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

редактировать

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

как-то из существующих ответов я сомневаюсь, есть ли какие-либо трюки, которые могли бы применяться

обновление

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

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

?a :someProperty ?b .
?c :anotherProperty ?d .
?b a :Class .

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

?b a :Class .
?a :someProperty ?b .
?c :anotherProperty ?d .

нам все равно понадобится декартово произведение, чтобы получить окончательные результаты, так как 2-й и 3-й шаблоны все еще непересекаются, но путем переупорядочивания мы ограничиваем размер результатов 2-го шаблона, что означает, что размер нашего декартового произведения будет намного меньше.

есть несколько других оптимизаций, которые мы делаем, но я не собираюсь публиковать их здесь, поскольку он начинает довольно подробное обсуждение внутренних компонентов SPARQL engine. Если кто-то заинтересован в более подробная информация просто оставить комментарий или отправить мне твит @RobVesse

6 ответов


сложность декартова произведения равна O (n2), нет ярлыка.

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

нужен ли вам декартово произведение или где более эффективный алгоритм, зависит от проблемы, которую вы решаете.


вы не можете реально изменить производительность вложенного цикла без некоторых дополнительных знаний, но это было бы специфично для использования. Если у вас есть n элементов is и m элементов js, это всегда будет O (n*m).

вы можете изменить посмотреть этого:

var qry = from i in is
          from j in js
          select /*something involving i/j */;

это все еще O (n*m), но имеет номинальный дополнительно накладные расходы LINQ (вы не заметите его в обычном использовании, хотя).

что ты делать в код случае? Могут быть фокусы...

одно наверняка избегать-это все, что заставляет перекрестное соединение буферизировать -foreach подход прекрасен и не буферизует-но если вы добавляете каждый элемент в List<>, тогда остерегайтесь последствий памяти. Дитто OrderBy etc (если использовано неуместно).


Я не могу предложить ничего лучше, чем O (n^2), потому что это размер выходного, и алгоритм, следовательно, не может быть быстрее.

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

действительно (псевдокод)

bool IsInSet(pair (x,y), CartesianProductSet P)
{
   return IsInHash(x,P.set[1]) && IsInHash(y,P.set[2])
}

где декартово произведение рассчитывается следующим образом:

// Cartesian product of A and B is
P.set[1]=A; P.set[2]=B;

если вы реализуете наборы как хэши, то поиск в декартовом произведении m наборы-это просто поиск в m хэши вы получаете бесплатно. Построение декартова произведения и IsInSet поиск каждого взять O(m), где m - это количество множеств, которые вы умножаете, и это намного меньше, чем n--размер каждого набора.


дополнительная информация была добавлена к вопросу.

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

среда выполнения C# действительно очень быстрая, но для экстремального тяжелого подъема вы можете захотеть перейти в собственный код.

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


Если локальность кэша (или локальная память, необходимая для поддержания j) является проблемой, вы можете сделать свой алгоритм более удобным для кэша, разделив входные массивы рекурсивно. Что-то вроде:

cartprod(is,istart,ilen, js,jstart,jlen) {
  if(ilen <= IMIN && jlen <= JMIN) { // base case
    for(int i in is) {
      for(int j in js) {
        // pair i and j
      }
    }
    return;
  }
  if(ilen > IMIN && jlen > JMIN) { // divide in 4
    ilen2= ilen>>1;
    jlen2= jlen>>1;
    cartprod(is,istart,ilen2,            js,jstart,jlen2);
    cartprod(is,istart+ilen2,ilen-ilen2, js,jstart,jlen2);
    cartprod(is,istart+ilen2,ilen-ilen2, js,jstart+jlen2,jlen-jlen2);
    cartprod(is,istart,ilen2,            js,jstart+jlen2,jlen-jlen2);
    return;
  }
  // handle other cases...
}

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


Я не знаю, как писать Java-подобные-итераторы на C#, но, возможно, вы знаете и можете передать мое решение здесь в C# самостоятельно.

Это может быть интересно, если ваши комбинации слишком велики, чтобы держать их полностью в памяти.

однако, если вы фильтруете по атрибуту над коллекцией, вы должны фильтровать перед построением комбинации. Пример:

Если у вас есть числа от 1 до 1000 и случайные слова и объединить их, а затем отфильтруйте те комбинации, где число делится на 20, а слово начинается с "d", вы можете иметь 1000*(26*x)=26000*X комбинаций для поиска.

или вы сначала фильтруете числа, что дает вам 50 чисел и (если равномерно распределены) 1 символ, которые в конце концов составляют только 50*x элементов.