Оптимизация декартовых запросов с аффинными затратами - PullRequest
9 голосов
/ 10 сентября 2009

У меня есть запрос на оптимизацию затрат, который я не знаю, как, если есть литература. Это немного сложно объяснить, поэтому я заранее прошу прощения за длину вопроса.

Есть сервер, к которому я обращаюсь, который работает следующим образом:

  • запрос сделан на записи (r1, ... rn) и поля (f1, ... fp)
  • Вы можете запросить только декартово произведение (r1, ..., rp) x (f1, ... fp)
  • Стоимость (время и деньги), связанная с таким запросом, является аффинной в размере запроса:

T((r1, ..., rn)x(f1, ..., fp) = a + b * n * p

Без потери общности (просто путем нормализации) мы можем предположить, что b=1 так что стоимость:

T((r1, ...,rn)x(f1,...fp)) = a + n * p

  • Мне нужно только запросить подмножество пар (r1, f(r1)), ... (rk, f(rk)), запрос от пользователей. Моя программа действует как посредник между пользователем и сервером (который является внешним). У меня много таких запросов (десятки тысяч в день).

Графически мы можем рассматривать ее как разреженную матрицу n x p, для которой я хочу покрыть ненулевые значения прямоугольной подматрицей:

   r1 r2 r3 ... rp
   ------      ___
f1 |x  x|      |x|
f2 |x   |      ---
   ------
f3
..    ______
fn    |x  x|
      ------

Имея:

  • количество подматриц, сохраняемых разумным из-за постоянных затрат
  • все 'x' должны находиться в подматрице
  • общая покрытая площадь не должна быть слишком большой из-за линейных затрат

Я назову g коэффициент разреженности моей задачи (количество необходимых пар на общее количество возможных пар, g = k / (n * p). Я знаю коэффициент a.

Есть несколько очевидных наблюдений:

  • если a мало, лучшее решение - запросить каждую пару (запись, поле) независимо, а общая стоимость: k * (a + 1) = g * n * p * (a + 1)
  • если a большое, лучшим решением будет запрос всего декартова произведения, а общая стоимость: a + n * p
  • второе решение лучше, как только g > g_min = 1/ (a+1) * (1 + 1 / (n * p))
  • Конечно, заказы в декартовых произведениях не важны, поэтому я могу транспонировать строки и столбцы моей матрицы, чтобы сделать ее более удобной, например:
   f1 f2 f3
r1  x    x
r2     x 
r3  x    x

можно изменить как

   f1 f3 f2
r1  x  x
r3  x  x
r2       x

И есть оптимальное решение, которое можно запросить (f1,f3) x (r1,r3) + (f2) x (r2)

  • Испытывать все решения и искать более низкую стоимость не вариант, потому что комбинаторика взрывается:
for each permutation on rows: (n!)
   for each permutation on columns: (p!)
       for each possible covering of the n x p matrix: (time unknown, but large...)
           compute cost of the covering

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

Чтобы запомнить некоторые цифры, мой n находится где-то между 1 и 1000, а мой p где-то между 1 и 200. Структура покрытия действительно «блочная», потому что записи приходят в классах, для которых запрашиваемые поля похожи , К сожалению, я не могу получить доступ к классу записи ...

Вопрос 1 : Есть ли у кого-нибудь идея, умное упрощение или ссылка на статью, которая может быть полезной? Поскольку у меня много запросов, то алгоритм, который хорошо работает в среднем , - это то, что я ищу (но я не могу позволить, чтобы он работал очень плохо в некоторых экстремальных случаях, например, для запроса всей матрицы когда n и p большие, а запрос действительно довольно редкий).

Вопрос 2 : На самом деле проблема еще более сложна: стоимость на самом деле больше похожа на форму: a + n * (p^b) + c * n' * p', где b - это константа <1 (после запроса записи поле, не слишком дорого запрашивать другие поля), а <code>n' * p' = n * p * (1 - g) - это количество ячеек, которые я не хочу запрашивать (потому что они недействительны, и при запросе недопустимых вещей взимается дополнительная плата). Я даже не могу мечтать о быстром решении этой проблемы, но все же ... идея кого-нибудь?

Ответы [ 6 ]

5 голосов
/ 10 сентября 2009

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

То, что вы разрешаете переставлять строки и столбцы, не такая большая проблема, потому что вы можете просто рассмотреть несвязанные подматрицы. Строка 1, столбцы с 4 по 7 и строка 5, столбцы 4, 2, 7 являются допустимым набором, поскольку можно просто поменять местами строку 2 и строку 5 и получить подключенную подматрицу строки 1, столбец 4 - строку 2, столбец 7. Конечно, это добавит некоторые ограничения - не все наборы действительны при всех перестановках - но я не думаю, что это самая большая проблема.

В статье в Википедии приведены результаты неприемлемости того, что проблема не может быть решена за полиномиальное время лучше, чем с коэффициентом 0.5 * log2(n), где n - это число множеств. В вашем случае 2^(n * p) - это (довольно пессимистичная) верхняя граница для числа множеств и выходов, для которых вы можете найти решение с коэффициентом 0.5 * n * p только за полиномиальное время (кроме N = NP и игнорирования переменных затрат) .

Оптимистическая нижняя граница для числа множеств, игнорирующих перестановки строк и столбцов, равна 0.5 * n^2 * p^2, что дает гораздо лучший коэффициент log2(n) + log2(p) - 0.5. Следовательно, вы можете ожидать, что вы найдете решение в худшем случае n = 1000 и p = 200 с коэффициентом 17 в оптимистическом случае и с коэффициентом 100.000 в пессимистическом случае ( по-прежнему игнорируя различные расходы).

Таким образом, лучшее, что вы можете сделать, - это использовать эвристический алгоритм (статья в Википедии упоминает почти оптимальный алгоритм жадности) и признать, что будет случай, когда алгоритм работает (очень) плохо. Или вы идете другим путем и используете алгоритм оптимизации и пытаетесь найти хорошее решение, используя больше времени. В этом случае я бы предложил использовать A * search .

1 голос
/ 10 сентября 2009

Хорошо, мое понимание вопроса изменилось. Новые идеи:

  • Хранить каждую строку в виде длинной строки битов. И пары битовых строк вместе, пытаясь найти пары, которые максимизируют количество 1 бит. Вырастите эти пары в большие группы (сортируйте и пытайтесь сопоставить действительно большие пары друг с другом). Затем создайте запрос, который попадет в наибольшую группу, а затем забудьте обо всех этих битах. Повторяйте, пока все не будет сделано. Может быть, иногда переключаться между строками и столбцами.

  • Поиск всех строк / столбцов с нулем или несколькими точками в них. «Удалить» их временно. Теперь вы смотрите на то, что покрывается запросом, который их не учитывает. Теперь, возможно, примените один из других методов, а затем разберитесь с игнорируемыми строками / столбцами. Еще один способ думать об этом: сначала разберитесь с более плотными точками, а затем перейдите к более узким.

1 голос
/ 10 сентября 2009

Я уверен, что где-то есть действительно хороший алгоритм для этого, но вот мои собственные интуитивные идеи:

  1. Подбрасывание некоторых прямоугольников:

    • Определите «приблизительно оптимальный» размер прямоугольника на основе a .
    • Разместите эти прямоугольники (возможно, случайным образом) над необходимыми точками, пока все точки не будут покрыты.
    • Теперь возьмите каждый прямоугольник и максимально уменьшите его, не «теряя» никаких точек данных.
    • Найдите прямоугольники рядом друг с другом и решите, будет ли их объединение дешевле, чем хранить их отдельно.
  2. Расти

    • Начните с каждой точки в своем собственном прямоугольнике 1x1.
    • Найдите все прямоугольники в n строках / столбцах (где n может основываться на a ); посмотрите, можете ли вы объединить их в один прямоугольник без каких-либо затрат (или отрицательных затрат: D).
    • Повторите.
  3. термоусадочную

    • Начните с одного большого прямоугольника, который охватывает ВСЕ точки.
    • Найдите под прямоугольник, который разделяет пару сторон с большой, но содержит очень мало точек.
    • Вырежьте его из большого, получив два меньших прямоугольника.
    • Повторите.
  4. Quad

    • Разделите плоскость на 4 прямоугольника. Для каждого из них посмотрите, получите ли вы лучшую цену, вернувшись дальше или просто включив весь прямоугольник.
    • Теперь возьмите свои прямоугольники и посмотрите, сможете ли вы объединить любой из них с небольшими затратами / бесплатно. \

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

0 голосов
/ 20 сентября 2009

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

Идея тривиальна: мы начинаем с того, что пытаемся выполнить один запрос на запись, и делаем наиболее достойное слияние до тех пор, пока не останется ничего, достойного слияния. У этого алгоритма есть очевидный недостаток, заключающийся в том, что он не допускает перекрывающихся запросов, но я ожидаю, что он будет хорошо работать в реальном случае (с функцией стоимости + + 1003 *):

# given are
# a function cost request -> positive real
# a merge function that takes two pairs of sets (f1, r1) and (f2, r2) 
# and returns ((f1 U f2), (r1 U r2))

# initialize with a request per record

requests = [({record},{field if (record, field) is needed}) for all needed records]
costs = [cost(request) for request in requests]

finished = False

while not finished: # there might be something to gain
    maximum_gain = 0
    finished = True
    this_step_merge = empty

    # loop onto all pairs of request
    for all (request1, request2) in (requests x request) such as request1 != request2:
        merged_request = merge(request1, request2)
        gain = cost(request1) + cost(request2) - cost(merged_request)

        if gain > maximum_gain:
            maximum_gain = gain
            this_step_merge = (request1, request2, merged_request)

    # if we found at least something to merge, we should continue
    if maximum_gain > 0:
        # so update the list of requests...
        request1, request2, merged_request = this_step_merge
        delete request1 from requests
        delete request2 from requests
        # ... and we are not done yet
        insert merged_request into requests
        finished = False

output requests

Это O (n3 * p), потому что:

  • после инициализации мы начинаем с n запросов
  • цикл while удаляет ровно один запрос из пула на каждой итерации.
  • внутренний цикл for выполняет итерацию по (ni^2 - ni) / 2 различным парам запросов, причем ni идет от n к единице в худшем случае (когда мы объединяем все в один большой запрос).

    1. Может кто-нибудь помочь мне указать очень плохие случаи алгоритма. Это звучит разумно, чтобы использовать этот?
    2. Это O (n ^ 3), которое слишком дорого для больших входов. Есть идеи по его оптимизации?

Заранее спасибо!

0 голосов
/ 10 сентября 2009

Поскольку ваши значения редки, может ли быть так, что многие пользователи запрашивают аналогичные значения? Является ли кэширование в вашем приложении вариант? Запросы могут быть проиндексированы с помощью хэша, который является функцией позиции (x, y), так что вы можете легко идентифицировать кэшированные наборы, которые попадают в правильную область сетки. Например, хранение кэшированных наборов в дереве позволит вам найти минимальные кэшированные наборы, которые очень быстро охватывают диапазон запросов. Затем вы можете выполнить линейный поиск для подмножества, которое является небольшим.

0 голосов
/ 10 сентября 2009

Я бы рассмотрел n записей (строк) и p полей (столбцов), упомянутых в пользовательском запросе, как n точек в p-мерном пространстве ({0,1} ^ p) с i-й координатой, равной 1, если имеет X и идентифицирует иерархию кластеров с самым грубым кластером в корне, включая все X. Для каждого узла в иерархии кластеризации рассмотрите продукт, который охватывает все необходимые столбцы (это строки (любой подузел) x cols (любой подузел)). Затем решите снизу вверх, объединять ли дочерние покрытия (оплачивая все покрытие) или оставить их как отдельные запросы. (покрытия состоят не из смежных столбцов, а именно из тех, которые нужны; то есть, подумайте о битовом векторе)

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

...