Как вычислить абсолютную минимальную сумму изменений для преобразования одного порядка сортировки в другой?
Цель
Как кодировать данные, которые описывают, как переупорядочить статический список из одного порядка в другой порядок, используя минимальный объем данных?
У меня есть чувство, что есть алгоритм или термин компьютерной науки, который поможет мне, но прямо сейчас я слишком зациклен на проблеме, чтобы понять другие способы взглянуть на нее.
Фоновая мотивация
У меня есть программа, которая развертывается в удаленном месте, где вся связь осуществляется через прерывистое невероятно дорогое спутниковое соединение. Это небольшое преувеличение, но стоимость данных близка к доллару за килобайт и может произойти только несколько раз в день.
В начале дня пользователям присваивается список элементов, они выходят в поле и делают что-то, но конечный результат - это более или менее тот же список элементов, отсортированных в другом порядке. Там другие данные, но это не важно для этой проблемы.
Сейчас я отправляю запись всех ходов, которые происходят и воспроизводят их по порядку. По мере того как пользователи получают доступ к системе, список записей перемещений начинает приближаться к размеру просто отправки всех элементов самостоятельно, и часто некоторые комбинации ходов приводят к отмене предыдущих.
Предположения
- Начальный список и конечный список состоят из одного и того же набора элементов
- Каждый элемент имеет уникальный идентификатор (32-битное целое число)
- Каждый элемент имеет уникальный вид (32-битное целое число)
- Пользователь будет иметь список от нескольких сотен до тысячи или более элементов.
- Пользователь обычно переупорядочивает около 100 из этих элементов за один день.
- Изменения в порядке могут быть обнаружены, перемещая элемент в новую позицию в списке
- Некоторые "ходы" могут отменить предыдущие
- Ресурсы вычислений для определения оптимальных решений дешевы/неограничены
- Время передачи дорогое
- Отправка назад данных изменений дешевле, чем отправка назад всего списка.
Простейшая структура данных
В целях решения этой проблемы предполагается наличие следующих структур данных.
Вот пример списка. Элементы в каждом списке одинаковы. Обратите внимание, что, хотя только некоторые из элементов были изменены, каждый идентификатор элемента имеет новый порядок сортировки, поэтому вы не можете просто отправить новые пары item_id/sort_order_id.
**List 1: Original List** **List 2: Re-ordered List**
order - id order - id
1. 10 1. 90
2. 20 2. 30
3. 30 3. 40
4. 40 4. 50
5. 50 5. 60
6. 60 6. 10
7. 70 7. 80
8. 80 8. 70
9. 90 9. 20
Как мне закодировать изменения, необходимые для преобразования порядка List 1, в список List 2, используя минимальный объем данных?
Как любопытство можно доказать, что решение является оптимальным?
Обновление
Сотрудник отметил, что "своп" может быть неправильным способом думать об этом. Вы также можете отправить элемент в верхнюю или нижнюю часть списка, что является скорее перемещением, чем свопом. Затем своп становится комбинацией двух ходов.
Спасибо за указатели. Пока я не вижу гарантированного оптимального решения. Плюс проблема просто немного изменилась.
Если я не могу доказать, что какой-либо один метод дает лучший результат, я выясню решение с использованием каждого метода и отправлю обратно это решение с небольшим заголовком, указывающим используемый метод. Продолжайте предлагать решения, хотя я и обновлю этот вопрос своими исследованиями.
Спасибо всем!
Ответы
Ответ 1
Часть Альго:
Переупорядочение списка называется перестановкой. Каждая перестановка может быть разделена на набор циклов, причем каждый цикл из N элементов требует (N - 1) свопов. Например
1, 2, 3, 4, 5, 6 → 3, 2, 4, 1, 6, 5
Это можно разбить на
1 - 4 - 3 (требуется 2 свопа)
2 - 2 (0 свопов)
5 - 6 (1 своп)
Чтобы найти решение, вы можете просто выбрать любой элемент в неправильном положении и поставить его на свое место.
Детали:
Конечно, вы можете использовать меньшие типы данных, RLE или некоторые другие алгоритмы кодирования и т.д.
Очень теоретическая, но не практическая часть.
Все перестановки последовательности из N чисел могут быть лексикографически упорядочены, и одного числа от 0 до (N! - 1) достаточно для представления последовательности. Итак, теоретически лучший ответ: вычислить индекс перестановки, перенести его, воссоздать перестановку по этому индексу.
Ответ 2
Я не уверен, что анализ свопов даст вам что-нибудь; как вы говорите, они могут отменить друг друга и привести к запутывающим результатам.
Я считаю, что ваш лучший вариант - идентифицировать в переупорядоченном списке сегменты этого списка, которые не переупорядочены по отношению к исходному списку, даже если они начинаются в новом месте. В вашем примере это сегмент от 30 до 60. Поэтому в некотором виде кодирования длины пробега я отправил обратно карту сегмента, которая описывает местоположения и длины.
Опять же, используя ваши данные примера: список упорядоченного индекса начала, длина:
{(9, 1), (3, 4), (1, 1), (8, 1), (7, 1), (2, 1)}
кажется наименьшим количеством информации, которую вы можете отправить назад. Сжимаемость данных зависит от количества и размера общих сегментов.
(Edit)
Собственно, мне приходит в голову, что будут некоторые наборы данных, где список подкачки будет короче, если количество свопов невелико. Но, вероятно, будет какая-то точка перекоса, где кодирование длины пробега улучшится; в этом случае я бы сказал, вычислить оба и выбрать меньший.
Ответ 3
Что вы хотите - это перестановка, необходимая для сортировки списка. Вы можете получить это, построив список индексов от 0 до n, а затем отсортировав этот список с помощью специальной функции сравнения, которая сравнивает элементы с соответствующими индексами. Например, в Python:
perm = sorted(range(len(l)), key=lambda x:l[x])
Затем вы можете отправить "perm" по соединению и использовать его для получения отсортированного списка:
for x in perm:
print perm[x]
В качестве дополнительной оптимизации, если большинство элементов остаются неизменными, перестановка будет сильно сжимаемой - либо с использованием обычного сжатия, либо с использованием преобразований, подобных разности (например, хранить каждый элемент как отличие от предыдущего элемента, а не его абсолютную значение), перейти на передний план и кодировать длину пробега.
Ответ 4
Быстрое исправление может заключаться в использовании Zobrist hash для определения случаев, когда вы возвращаетесь к предыдущему порядку. То есть, после каждого свопа, вычисляйте хэш на основе перестановки, которую вы достигаете. Каждый хэш отображает кратчайшую последовательность свопов, найденную до сих пор для этой конкретной перестановки.
Это можно легко расширить с помощью небольшого поискового поиска - изобретатель Zobrist был изобретен как способ оптимизации поиска дерева игр.
Легко дать строгую нижнюю границу числа свопов, конечно - количество предметов, которые не находятся в их требуемых местах. Однако действительно ли эта нижняя граница является достижимой, является более сложной проблемой.
Ответ 5
Если вы действительно пытаетесь свести к минимуму каждый бит данных, проходящих через провод, как вы передаете свои данные? Например, вы каким-то образом сжимаете его? Использование 32-разрядного номера для порядка сортировки, вероятно, слишком велико, если у вас всего несколько тысяч элементов. 16 бит доставят вам 65000 предметов за половину $$$. То же самое касается уникальных идентификаторов.
Ответ 6
Другое возможное решение, игнорируя вашу структуру данных...
Отправьте набор идентификаторов/индексов для измененных элементов (если это полностью случайное разреженное подмножество, просто перечислите их) и номер перестановки, описывающий переупорядочение этого подмножества. Для номера перестановок потребуется большое целочисленное представление - размер должен быть пропорционален log (n!), Где n - количество измененных элементов.
Перестановочный номер определен из массива перестановок, конечно, но эту деталь можно избежать при декодировании. Трюк состоит в том, чтобы закодировать номер перестановки так, чтобы после того, как вы поменяли правильный первый элемент в первый слот, вы также можете получить новый номер перестановки, который является правильным для хвоста массива.
То есть...
while not empty(indexes)
item-to-swap := permutation-no remainder len(indexes)
permutation-no := permutation-no div len(indexes)
if item-to-swap != 0 : swap slot[indexes[0]], slot[indexes[item-to-swap]]
indexes := tail(indexes)
Требуется проверка!= 0, даже если все элементы, требующие изменения в начале - элемент, возможно, был заменен на правильное местоположение ранее в цикле.
Это не пытается оптимизировать количество свопов - элемент может меняться вверх несколько раз перед тем, как его поместить вниз в правильное место. Тем не менее, номер перестановки, вероятно, является представлением оптимального пространства для случайной перестановки массива. Учитывая, что ваша перестановка влияет только на небольшое подмножество полного массива, использование меньшего номера перестановок для этого подмножества имеет большой смысл.
Ответ 7
Предполагая, что:
- Вы можете хранить копии исходных и конечных данных как на ваших полевых устройствах, так и на вашей базовой системе.
- Когда вы говорите о свопах, вы имеете в виду, что два элемента в списке обмениваются друг с другом.
Возможно, ваше лучшее решение:
Вместо того, чтобы хранить список всех свопов, которые вы выполняете по мере их выполнения, сравните свои начальные и конечные данные в конце дня, а затем сгенерируйте свопы, которые вам нужно будет внести в это изменение. Это игнорирует любые местоположения в списке, которые остаются неизменными, даже если они остаются неизменными, потому что ряд свопов "расстегнут" некоторые изменения. Если у вас есть ваши данные в форме a,b,a,b,...
, где a
сообщает вам, что индекс следующих элементов уходит в том же порядке, в котором они находятся, а b
сообщает вам индекс элемента для его замены.
Поскольку вы делаете только свопы вместо смен, вы должны очень редко заканчивать такими данными, как ваши данные образца, где 30, 40 и 50 находятся в одном порядке, но в немного другом месте. Поскольку количество свопов будет между 1/4 и 1/10 количеством исходных элементов в списке, у вас обычно будет большой кусок ваших данных как в том же порядке, так и в том же месте, в котором он был первоначально. Предположим, что были сделаны следующие свопы:
1 <-> 9
4 <-> 2
5 <-> 2
Полученный список будет выглядеть следующим образом:
1. 90
2. 50
3. 30
4. 20
5. 40
6. 60
7. 70
8. 80
9. 10
Таким образом, данные изменения могут быть представлены как:
1,9,2,4,4,5
Это всего шесть значений, которые могут быть представлены в виде 16-разрядных чисел (при условии, что у вас не будет более 16 000 элементов в вашем первоначальном списке). Поэтому каждый "эффективный" своп может быть представлен одним 32-битным числом. И поскольку количество фактических свопов обычно будет на 1/5 до 1/2 размера исходного списка, вы в конечном итоге отправляете от 10% до 20% данных в своем исходном списке по проводу (или меньше, так как число "эффективных" свопов может быть еще меньше, если некоторые из этих свопов отменяют друг друга).
Ответ 8
Как говорит Питер, было бы идеально минимизировать размер каждого целого числа — но на самом деле вы можете сделать это, не устанавливая ограничений на количество элементов. Переменная байтовая кодировка - это способ сжатия последовательностей целых чисел, используя только необходимое количество байтов. Наиболее распространенный способ сделать это - зарезервировать один бит в каждом байте, чтобы указать, является ли этот байт последним в текущем элементе списка.
Может быть полезно сначала использовать дельта-кодирование. Это где вы храните различия между целыми числами, а не самими целями — что означает, что они в конечном итоге сжимаются с переменной байтовой точностью. Конечно, целые числа, хранящиеся (возможно, идентификаторы изменяемых элементов в вашем случае), должны быть отсортированы первыми, но это не похоже, что это будет проблемой для вас.