Оптимизация декартовых запросов с аффинными затратами
Автор вопроса: LeMizОтветов: 6
У меня есть запрос на оптимизацию затрат, о котором я не знаю, если есть литература. Это немного сложно объяснить, поэтому заранее извиняюсь за длину вопроса.
Есть сервер, к которому я обращаюсь, который работает следующим образом:
делается запрос по записям (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, для которой я хочу покрыть ненулевые значения прямоугольной подматрицей:
количество подматриц остается разумным из-за постоянной стоимости
все "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 (как только запрашивается запись для поля, она не слишком дорого запрашивать другие поля), а n' * p' = n * p * (1 - g) - это количество ячеек, которые я не хочу запрашивать (потому что они недействительны, а запрос недействительных вещей требует дополнительных затрат). Я даже не мечтаю о быстром решении этой проблемы, но все же... у кого есть идеи?
У вас есть оракул, который говорит вам, какие (строки, столбцы) пусты бесплатно? Jonathan Graehl 10.09.2009
Вы можете указать наборы строк и полей явно, т.е. вам не нужно указывать непрерывный прямоугольник в фиксированной системе координат (конкретные перестановки строк и столбцов)? Jonathan Graehl 10.09.2009
Re: мой первый вопрос, ответ да, если я правильно понимаю запросы, поступающие от части пользователей. Jonathan Graehl 10.09.2009
ответ на ваш второй вопрос тоже да. Если есть перестановка рядов и столбцов, дающая менее разреженное покрытие, то вообще лучше (и я не понял вашего комментария к Артелиусу, ответил, извините. Я ограничен смежными прямоугольниками над перестановками рядов и столбцов). LeMiz 10.09.2009
Выбор подматриц для покрытия запрошенных значений является формой проблемы покрытия набора и, следовательно, NP завершен . Ваша проблема добавляет к этой и без того сложной проблеме, что стоимость наборов различается.
То, что вы позволяете переставлять строки и столбцы, не такая уж большая проблема, потому что вы можете просто рассматривать несвязанные подматрицы. Строка первая, столбцы с четвертого по седьмой и строка пятая, столбцы с четвертого по седьмой являются допустимым набором, потому что вы можете просто поменять местами вторую и пятую строки и получить связанную строку подматрицы с первой, четвертый столбец со второй строкой, седьмой столбец. Конечно, это добавит некоторые ограничения - не все наборы допустимы при всех перестановках - но я не думаю, что это самая большая проблема.
Статья в Википедии дает результаты о неаппроксимации, согласно которым задача не может быть решена за полиномиальное время лучше, чем с коэффициентом 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*.
Ответ 2 · рейтинг 1 · Artelius
Я уверен, что где-то есть действительно хороший алгоритм для этого, но вот мои собственные интуитивные идеи:
Подход с броском прямоугольников:
Determine a "roughly optimal" rectangle size based on a.
Поместите эти прямоугольники (возможно, случайным образом) на требуемые точки, пока все точки не будут покрыты.
Теперь возьмите каждый прямоугольник и максимально уменьшите его, не "теряя" никаких точек данных.
Найдите прямоугольники, расположенные близко друг к другу, и решите, будет ли их объединение дешевле, чем хранение их по отдельности.
Расти
Start off with each point in its own 1x1 rectangle.
Найдите все прямоугольники в n строках/столбцах (где n может быть основано на a); посмотрите, сможете ли вы объединить их в один прямоугольник бесплатно (или с отрицательной стоимостью: D).
Повторить.
Сокращать
Start off with one big rectangle, that covers ALL points.
Найдите подпрямоугольник, у которого пара общих сторон с большим, но в котором очень мало точек.
Вырежьте его из большого, получив два меньших прямоугольника.
Повторить.
Четырехместный
Divide the plane into 4 rectangles. For each of these, see if you get a better cost by recursing further, or by just including the whole rectangle.
Теперь возьмите свои прямоугольники и посмотрите, сможете ли вы объединить любой из них с небольшими/бесплатными затратами.\
Кроме того: имейте в виду, что иногда лучше иметь два перекрывающихся прямоугольника, чем один большой прямоугольник, являющийся их надмножеством. Например. случай, когда два прямоугольника просто пересекаются в одном углу.
Ответ 3 · рейтинг 1 · Artelius
Хорошо, мое понимание вопроса изменилось. Новые идеи:
Сохраняйте каждую строку как длинную битовую строку. И пары битовых строк вместе, пытаясь найти пары, которые максимизируют количество битов 1. Объедините эти пары в более крупные группы (сортируйте и старайтесь сопоставить действительно большие пары друг с другом). Затем создайте запрос, который будет охватывать самую большую группу, а затем забудьте обо всех этих битах. Повторяйте, пока все не сделаете. Может быть, иногда переключаться со строк на столбцы.
Ищите все строки/столбцы с нулем или несколькими точками в них. "Удалите" их временно. Теперь вы смотрите на то, что покрывается запросом, который не включает их. Теперь, возможно, примените один из других методов и впоследствии обработайте игнорируемые строки/столбцы. Другой способ думать об этом: сначала работать с более плотными точками, а затем переходить к более разреженным.
Ответ 4 · ire_and_curses
Поскольку ваши значения редки, может ли быть так, что многие пользователи запрашивают аналогичные значения? Возможно ли кэширование в вашем приложении? Запросы могут быть проиндексированы хэшем, который является функцией положения (x,y), чтобы вы могли легко идентифицировать кэшированные наборы, которые попадают в правильную область сетки. Хранение кэшированных наборов в дереве, например, позволит вам очень быстро найти минимальные кэшированные подмножества, которые покрывают диапазон запроса. Затем вы можете выполнить линейный поиск по небольшому подмножеству.
Ответ 5 · Jonathan Graehl
Я бы рассмотрел n записей (строк) и p полей (столбцов), упомянутых в запросе пользователя, заданных как n точек в p-мерном пространстве ({0,1}^p) с i-й координатой, равной 1, если он имеет X , и определить иерархию кластеров, где самый грубый кластер находится в корне, включая все домены X. Для каждого узла в иерархии кластеризации рассмотрите продукт, который покрывает все необходимые столбцы (это строки (любой подузел) x столбцы (любой подузел)). Затем решите снизу вверх, объединять ли дочерние покрытия (оплачивая все покрытие) или оставить их как отдельные запросы. (покрытия состоят не из смежных столбцов, а именно из тех, которые необходимы, т.е. подумайте о битовом векторе)
Я согласен с Артелиусом в том, что перекрывающиеся запросы продуктов могут быть дешевле; мой иерархический подход нуждался бы в улучшении, чтобы включить это.
Ответ 6 · Community
Я немного поработал над этим, и вот очевидный, O (n ^ 3) жадный алгоритм нарушения симметрии (записи и поля обрабатываются отдельно) в псевдокоде, похожем на python.
Идея тривиальна: мы начинаем с того, что пробуем один запрос на запись, и делаем самое достойное слияние до тех пор, пока не останется ничего достойного для слияния. У этого алгоритма есть очевидный недостаток, заключающийся в том, что он не допускает перекрывающихся запросов, но я ожидаю, что он будет работать достаточно хорошо в реальном случае (с функцией стоимости a + n * (p^b) + c * n * p * (1 - g)):
# 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 до единицы в худшем случае (когда мы объединяем все в один большой запрос).
Can someone help me pointing the very bad cases of the algorithm. Does it sound reasonnable to use this one ?
Это O (n ^ 3), что слишком дорого для больших входных данных. Любая идея, чтобы оптимизировать его?