Сортировка 2 списков в Python на основе соотношения отдельных соответствующих элементов или на основе третьего списка

Я пытаюсь написать разные реализации для дробной проблемы с рюкзаком.

Для этого у меня есть 2 массива:

  1. Ценности
  2. Веса

Значение элементов [n] соответствует весам элементов [n]. Таким образом, мы можем рассчитать value_per_unit как:

for I in range(values):
    value_per_unit.append(values[I]/weights[I])
value_per_unit.sort()

Теперь мне нужно, чтобы 2 массива (значения и веса) были отсортированы в соответствии с массивом value_per_unit

eg: If

  • значения = [60, 100, 120]
  • веса = [20, 50, 30]

затем

  • values_per_unit = [3.0, 2.0, 4.0]

  • и поэтому values_per_unit_sorted будет [2.0, 3.0, 4.0]

Мне нужно, чтобы массивы значений и весов стали:

  • значения_отсортированные = [100,60,120]
  • weights_sorted = [50,20,30]

Есть ли способ добиться этого с помощью простых лямбда-функций?

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

weights[(value_per_unit_sorted.index(max(value_per_unit_sorted)))]

person Yogesh    schedule 19.07.2017    source источник


Ответы (4)


В одной строке:

values, weights = zip(*sorted(zip(values, weights), key=lambda t: t[0]/t[1]))

Чтобы объяснить: во-первых, заархивируйте списки, чтобы соединить их.

pairs = zip(values, weights) 
# [(60, 20), (100, 50), (120, 30)]

Затем отсортируйте по отношению стоимости к весу.

sorted_pairs = sorted(pairs, key=lambda t: t[0]/t[1]) 
# [(100, 50), (60, 20), (120, 30)]

Наконец, распакуйте их обратно в отдельные списки.

values, weights = zip(*sorted_pairs)
# (100, 60, 120), (50, 20, 30)

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

ratios, values, weights = zip(*sorted((v/w, v, w) for v, w in zip(values, weights)))

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

И чтобы ответить на комментарий от @TomWyllie, если у вас уже есть список соотношений, вы можете использовать:

ratios, values, weights = zip(*sorted(zip(ratios, values, weights)))

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

person Jared Goguen    schedule 19.07.2017
comment
Довольно небольшая проблема, но вы пересчитываете все отношения с помощью этого решения, теперь ОП выделил жирным шрифтом, что его следует отсортировать в соответствии с массивом value_per_unit, который, я уверен, означает использовать массив (список) и не пересчитывать значения. Хотя ответ хороший :) - person Tom Wyllie; 19.07.2017
comment
@Tom Тогда можно использовать второе решение, и ОП может в первую очередь пропустить построение списка соотношений. - person Jared Goguen; 19.07.2017
comment
Я согласен, что это, вероятно, разумно, но ОП не просил пропустить этап построения коэффициентов; вполне возможно, что ему все равно понадобится этот list для чего-то еще, и поэтому ему нужно решение, которое позволяет избежать пересчета. - person Tom Wyllie; 19.07.2017
comment
@TomWyllie Это упрощает задачу, я добавил решение для этого сценария. - person Jared Goguen; 19.07.2017

Элегантный способ сделать это — составить многомерный список со значениями и весами:

for i in range(len(values)):
    values_and_weights.append([values[i], weights[i])
# The resulting list is [[60, 20], [100, 50], [120, 30]]

Затем используйте метод сортировки со значением, деленным на вес, в качестве ключа.

values_and_weights.sort(key=(lambda x: x[0]/x[1]))
person jmcampbell    schedule 19.07.2017
comment
Первую часть можно упростить, используя zip, а затем сократить до моего второго предложения. - person Jared Goguen; 19.07.2017
comment
@JaredGoguen Великие умы думают одинаково! Моя причина сделать это таким образом заключается в том, что это более явно и более очевидно, что происходит. - person jmcampbell; 19.07.2017
comment
Я бы сказал, что использование встроенной функции zip более похоже на Python, но каждому свое. - person Jared Goguen; 19.07.2017

Для более явного (но, возможно, менее питонического) решения создайте список индексов, отсортированных по значению этого индекса в value_per_unit, и измените порядок values и weights соответственно.

sorted_indices = [index for index, value in 
                  sorted(enumerate(value_per_unit), key=lambda x: x[1])]
values = [values[i] for i in sorted_indices]
weights = [weights[i] for i in sorted_indices]

print(values, weights)

Выходы:

([100, 60, 120], [50, 20, 30])

Вы можете привести это в порядок, устранив ненужные дополнительные циклы, используя zip и выражение генератора;

values, weights = zip(*((values[i], weights[i]) for i, value in
                  sorted(enumerate(value_per_unit), key=lambda x: x[1])))
print(values)
print(weights)

Какие выходы;

(100, 60, 120)
(50, 20, 30)

Обратите внимание, что эти окончательные значения равны tuples, а не lists. Если вам действительно нужно, чтобы на выходе был список, достаточно простого values, weights = map(list, (values, weights)). Вы могли бы даже обернуть это в один вкладыш, хотя к этому моменту, вероятно, становится довольно сложно следить за тем, что происходит.

person Tom Wyllie    schedule 19.07.2017

Проблема, с которой вы столкнулись, связана с использованием вычисляемого поля для каждого элемента (элемент I будет иметь вычисленное значение values[I]/weights[I]). Чтобы решить эту проблему и при этом сохранить ее чрезвычайно простой для понимания, вы можете превратить ее в кортеж следующей формы: ( calculated_value, (value, weight) ) для каждого элемента.

Такой подход упрощает чтение и понимание. Посмотрите на следующее решение:

values = [60, 100, 120]
weights = [20, 50, 30]
value_per_unit = []

for I in range(len(values)):
    value_per_unit.append( (values[I]/weights[I], (values[I], weights[I])) )
sorted_value_per_unit = sorted(value_per_unit, key=lambda x: x[0])

sorted_values = []
sorted_weights = []
for I in range(len(values)):
    (value, weight) = sorted_value_per_unit[I][1]
    sorted_values.append(value)
    sorted_weights.append(weight)

print(str(sorted_values))
print(str(sorted_weights))

Также обратите внимание, что я изменил цикл из вашего исходного кода:

range(values) было изменено на range(len(values))

Поскольку диапазону потребуется длина списка, а не сам список.

person Ori    schedule 19.07.2017