Какая минимальная стоимость соединения всех островов?

84

Существует сетка размером N х М . Некоторые ячейки представляют собой острова, обозначенные цифрой «0», а другие - воду . На каждой ячейке с водой есть число, обозначающее стоимость моста, построенного на этой ячейке. Вы должны найти минимальную стоимость, по которой можно соединить все острова. Ячейка соединяется с другой ячейкой, если она имеет общее ребро или вершину.

Какой алгоритм можно использовать для решения этой проблемы? Что можно использовать в качестве метода грубой силы, если значения N, M очень малы, скажем, NxM <= 100?

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

http://i.imgur.com/ClcboBy.png

Изначально я подумал о том, чтобы пометить все острова как узлы и соединить каждую пару островов кратчайшим мостом. Тогда проблема может быть сведена к минимальному остовному дереву, но в этом подходе я пропустил случай, когда края перекрываются. Например , на следующем изображении кратчайшее расстояние между любыми двумя островами равно 7 (отмечено желтым цветом), поэтому при использовании минимального связующего дерева ответ будет 14 , но ответ должен быть 11 (отмечен голубым).

image2

Атул Вайбхав
источник
Подход к решению, который вы описали в своих вопросах, кажется правильным. Не могли бы вы пояснить, что вы имеете в виду, говоря «Я пропустил случай, когда края перекрываются»?
Асад Саидуддин
@Asad: Я добавил изображение, чтобы объяснить проблему в подходе MST.
Атул Вайбхав
«Соедините каждые два острова кратчайшим мостом» - как видите, это явно плохой подход.
Кароли Хорват
1
Не могли бы вы поделиться кодом, который вы сейчас используете? Это немного упростит поиск ответа, а также покажет нам, каков ваш текущий подход.
Асад Саидуддин
7
Это вариант задачи о дереве Штейнера . Перейдите по ссылке в Википедию, чтобы узнать больше. Короче говоря, точное решение, возможно, не может быть найдено за полиномиальное время, но минимальное остовное дерево - неплохое приближение.
Гасса

Ответы:

67

Чтобы подойти к этой проблеме, я бы использовал структуру целочисленного программирования и определил три набора переменных решения:

  • x_ij : двоичная индикаторная переменная, указывающая, строим ли мы мост в точке воды (i, j).
  • y_ijbcn : двоичный индикатор того, является ли водное место (i, j) n ^ -м местоположением, связывающим остров b с островом c.
  • l_bc : двоичная индикаторная переменная, указывающая, связаны ли острова b и c напрямую (то есть вы можете ходить только по квадратам моста от b до c).

Для затрат на строительство моста c_ij объективное значение, которое необходимо минимизировать, составляет sum_ij c_ij * x_ij. Нам нужно добавить в модель следующие ограничения:

  • Нам нужно убедиться, что переменные y_ijbcn действительны. Мы всегда сможем добраться до водного квадрата, только если построим там мост, так что y_ijbcn <= x_ijдля каждой водной локации (i, j). Кроме того, y_ijbc1должно быть равно 0, если (i, j) не граничит с островом b. Наконец, для n> 1 y_ijbcnможно использовать только в том случае, если на шаге n-1 использовалось соседнее водное пространство. Определяя N(i, j)как соседние квадраты воды (i, j), это эквивалентно y_ijbcn <= sum_{(l, m) in N(i, j)} y_lmbc(n-1).
  • Нам нужно убедиться, что переменные l_bc устанавливаются только в том случае, если b и c связаны. Если мы определим I(c)местоположения, граничащие с островом c, это можно сделать с помощью l_bc <= sum_{(i, j) in I(c), n} y_ijbcn.
  • Нам необходимо обеспечить, чтобы все острова были связаны прямо или косвенно. Это может быть выполнено следующим образом: для каждого непустого собственного подмножества S островов требуется, чтобы хотя бы один остров в S был связан по крайней мере с одним островом в дополнении S, которое мы назовем S '. В ограничениях мы можем реализовать это, добавив ограничение для каждого непустого множества S размера <= K / 2 (где K - количество островов) sum_{b in S} sum_{c in S'} l_bc >= 1,.

Для примера задачи с K островами, W квадратами воды и заданной максимальной длиной пути N это модель смешанного целочисленного программирования с O(K^2WN)переменными и O(K^2WN + 2^K)ограничениями. Очевидно, что это станет трудноразрешимым, поскольку размер проблемы станет большим, но это может быть решено для размеров, которые вам нужны. Чтобы получить представление о масштабируемости, я реализую его на Python с помощью пакета pulp. Давайте сначала начнем с меньшей карты 7 x 9 с 3 островами в конце вопроса:

import itertools
import pulp
water = {(0, 2): 2.0, (0, 3): 1.0, (0, 4): 1.0, (0, 5): 1.0, (0, 6): 2.0,
         (1, 0): 2.0, (1, 1): 9.0, (1, 2): 1.0, (1, 3): 9.0, (1, 4): 9.0,
         (1, 5): 9.0, (1, 6): 1.0, (1, 7): 9.0, (1, 8): 2.0,
         (2, 0): 1.0, (2, 1): 9.0, (2, 2): 9.0, (2, 3): 1.0, (2, 4): 9.0,
         (2, 5): 1.0, (2, 6): 9.0, (2, 7): 9.0, (2, 8): 1.0,
         (3, 0): 9.0, (3, 1): 1.0, (3, 2): 9.0, (3, 3): 9.0, (3, 4): 5.0,
         (3, 5): 9.0, (3, 6): 9.0, (3, 7): 1.0, (3, 8): 9.0,
         (4, 0): 9.0, (4, 1): 9.0, (4, 2): 1.0, (4, 3): 9.0, (4, 4): 1.0,
         (4, 5): 9.0, (4, 6): 1.0, (4, 7): 9.0, (4, 8): 9.0,
         (5, 0): 9.0, (5, 1): 9.0, (5, 2): 9.0, (5, 3): 2.0, (5, 4): 1.0,
         (5, 5): 2.0, (5, 6): 9.0, (5, 7): 9.0, (5, 8): 9.0,
         (6, 0): 9.0, (6, 1): 9.0, (6, 2): 9.0, (6, 6): 9.0, (6, 7): 9.0,
         (6, 8): 9.0}
islands = {0: [(0, 0), (0, 1)], 1: [(0, 7), (0, 8)], 2: [(6, 3), (6, 4), (6, 5)]}
N = 6

# Island borders
iborders = {}
for k in islands:
    iborders[k] = {}
    for i, j in islands[k]:
        for dx in [-1, 0, 1]:
            for dy in [-1, 0, 1]:
                if (i+dx, j+dy) in water:
                    iborders[k][(i+dx, j+dy)] = True

# Create models with specified variables
x = pulp.LpVariable.dicts("x", water.keys(), lowBound=0, upBound=1, cat=pulp.LpInteger)
pairs = [(b, c) for b in islands for c in islands if b < c]
yvals = []
for i, j in water:
    for b, c in pairs:
        for n in range(N):
            yvals.append((i, j, b, c, n))

y = pulp.LpVariable.dicts("y", yvals, lowBound=0, upBound=1)
l = pulp.LpVariable.dicts("l", pairs, lowBound=0, upBound=1)
mod = pulp.LpProblem("Islands", pulp.LpMinimize)

# Objective
mod += sum([water[k] * x[k] for k in water])

# Valid y
for k in yvals:
    i, j, b, c, n = k
    mod += y[k] <= x[(i, j)]
    if n == 0 and not (i, j) in iborders[b]:
        mod += y[k] == 0
    elif n > 0:
        mod += y[k] <= sum([y[(i+dx, j+dy, b, c, n-1)] for dx in [-1, 0, 1] for dy in [-1, 0, 1] if (i+dx, j+dy) in water])

# Valid l
for b, c in pairs:
    mod += l[(b, c)] <= sum([y[(i, j, B, C, n)] for i, j, B, C, n in yvals if (i, j) in iborders[c] and B==b and C==c])

# All islands connected (directly or indirectly)
ikeys = islands.keys()
for size in range(1, len(ikeys)/2+1):
    for S in itertools.combinations(ikeys, size):
        thisSubset = {m: True for m in S}
        Sprime = [m for m in ikeys if not m in thisSubset]
        mod += sum([l[(min(b, c), max(b, c))] for b in S for c in Sprime]) >= 1

# Solve and output
mod.solve()
for row in range(min([m[0] for m in water]), max([m[0] for m in water])+1):
    for col in range(min([m[1] for m in water]), max([m[1] for m in water])+1):
        if (row, col) in water:
            if x[(row, col)].value() > 0.999:
                print "B",
            else:
                print "-",
        else:
            print "I",
    print ""

Это занимает 1,4 секунды для запуска с использованием решателя по умолчанию из пакета целлюлозы (решателя CBC) и вывода правильного решения:

I I - - - - - I I 
- - B - - - B - - 
- - - B - B - - - 
- - - - B - - - - 
- - - - B - - - - 
- - - - B - - - - 
- - - I I I - - - 

Затем рассмотрим полную проблему в начале вопроса, которая представляет собой сетку 13 x 14 с 7 островами:

water = {(i, j): 1.0 for i in range(13) for j in range(14)}
islands = {0: [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)],
           1: [(9, 0), (9, 1), (10, 0), (10, 1), (10, 2), (11, 0), (11, 1),
               (11, 2), (12, 0)],
           2: [(0, 7), (0, 8), (1, 7), (1, 8), (2, 7)],
           3: [(7, 7), (8, 6), (8, 7), (8, 8), (9, 7)],
           4: [(0, 11), (0, 12), (0, 13), (1, 12)],
           5: [(4, 10), (4, 11), (5, 10), (5, 11)],
           6: [(11, 8), (11, 9), (11, 13), (12, 8), (12, 9), (12, 10), (12, 11),
               (12, 12), (12, 13)]}
for k in islands:
    for i, j in islands[k]:
        del water[(i, j)]

for i, j in [(10, 7), (10, 8), (10, 9), (10, 10), (10, 11), (10, 12),
             (11, 7), (12, 7)]:
    water[(i, j)] = 20.0

N = 7

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

mod.solve(pulp.solvers.PULP_CBC_CMD(maxSeconds=120))

Это дает решение с объективным значением 17:

I I - - - - - I I - - I I I 
I I - - - - - I I - - - I - 
I I - - - - - I - B - B - - 
- - B - - - B - - - B - - - 
- - - B - B - - - - I I - - 
- - - - B - - - - - I I - - 
- - - - - B - - - - - B - - 
- - - - - B - I - - - - B - 
- - - - B - I I I - - B - - 
I I - B - - - I - - - - B - 
I I I - - - - - - - - - - B 
I I I - - - - - I I - - - I 
I - - - - - - - I I I I I I 

Чтобы улучшить качество получаемых решений, вы можете использовать коммерческий решатель MIP (это бесплатно, если вы работаете в академическом учреждении, и, вероятно, не бесплатно в противном случае). Например, вот производительность Gurobi 6.0.4, снова с 2-минутным ограничением времени (хотя из журнала решений мы читаем, что решатель нашел текущее лучшее решение в течение 7 секунд):

mod.solve(pulp.solvers.GUROBI(timeLimit=120))

Это фактически находит решение с объективной ценностью 16, лучше, чем OP смог найти вручную!

I I - - - - - I I - - I I I 
I I - - - - - I I - - - I - 
I I - - - - - I - B - B - - 
- - B - - - - - - - B - - - 
- - - B - - - - - - I I - - 
- - - - B - - - - - I I - - 
- - - - - B - - B B - - - - 
- - - - - B - I - - B - - - 
- - - - B - I I I - - B - - 
I I - B - - - I - - - - B - 
I I I - - - - - - - - - - B 
I I I - - - - - I I - - - I 
I - - - - - - - I I I I I I 
Джослибер
источник
Вместо формулировки y_ijbcn я бы попробовал формулировку, основанную на потоке (переменная для каждого кортежа, состоящего из пары островов и квадрата смежности; ограничения сохранения, с избытком 1 на приемнике и -1 в источнике; связанный общий приток на квадрате по тому, был ли он куплен).
Дэвид Эйзенстат
1
@DavidEisenstat благодарит за предложение - я только что попробовал, и, к сожалению, он решал гораздо медленнее для этих проблемных случаев.
josliber
8
Это именно то , что я искал, когда начинал награждение. Меня поражает, как такая простая для описания проблема может доставить столько трудностей решателям MIP. Мне было интересно, верно ли следующее: путь, соединяющий два острова, - это кратчайший путь с дополнительным ограничением, которое он должен пройти через некоторую ячейку (i, j). Например, верхний левый и средний острова в решении Гуроби связаны с SP, который вынужден проходить через ячейку (6, 5). Не уверен, правда ли это, но в какой-то момент мне придется разобраться в этом. Спасибо за ответ!
Иоаннис
@Ioannis интересный вопрос - я не уверен, что ваша догадка верна, но мне она кажется вполне правдоподобной. Вы можете думать о ячейке (i, j) как о том месте, где должны идти мосты с этих островов для дальнейшего соединения с другими островами, а затем при достижении этой координационной точки вы просто захотите построить самые дешевые мосты из возможных, чтобы соединить остров. пара.
josliber
5

Подход грубой силы в псевдокоде:

start with a horrible "best" answer
given an nxm map,
    try all 2^(n*m) combinations of bridge/no-bridge for each cell
        if the result is connected, and better than previous best, store it

return best

В C ++ это можно было бы записать как

После первого вызова (я предполагаю, что вы преобразовываете свои 2-мерные карты в 1-мерные массивы для упрощения копирования), он bestCostбудет содержать стоимость лучшего ответа и bestобразец мостов, который его дает. Однако это происходит очень медленно.

Оптимизация:

  • Используя «предел мостов» и запуская алгоритм увеличения максимального числа мостов, вы можете найти минимальные ответы, не исследуя все дерево. Ответ на один мост, если бы он существовал, был бы O (нм) вместо O (2 ^ нм) - радикальное улучшение.
  • Вы можете избежать поиска (остановив рекурсию; это также называется «сокращением») после превышения bestCost, потому что нет смысла продолжать поиск после этого. Если не станет лучше, не продолжайте копать.
  • Вышеупомянутая обрезка работает лучше, если вы посмотрите на «хороших» кандидатов перед тем, как смотреть на «плохие» (как таковые, все ячейки просматриваются в порядке слева направо, сверху вниз). Хорошей эвристикой было бы рассматривать ячейки, которые находятся рядом с несколькими несвязанными компонентами, как более приоритетные, чем ячейки, которые не являются таковыми. Однако, как только вы добавляете эвристику, ваш поиск начинает напоминать A * (и вам также нужна какая-то очередь с приоритетом).
  • Следует избегать дублирования мостов и мостов в никуда. Любой мост, который не отключает островную сеть при удалении, является избыточным.

Общий алгоритм поиска, такой как A *, позволяет намного быстрее выполнять поиск, хотя поиск лучших эвристик - непростая задача. Для более специфичного подхода к проблеме использование существующих результатов по деревьям Штейнера , как предлагает @Gassa, является подходящим решением. Обратите внимание, однако, что проблема построения деревьев Штейнера на ортогональных сетках является NP-полной, согласно этой статье Гэри и Джонсона .

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

Tucuxi
источник
"попробуйте все 2 ^ (n * m) комбинаций" ммм, 2^(13*14) ~ 6.1299822e+54итерации. Если предположить, что вы можете делать миллион итераций в секунду, это займет всего ... ~ 194380460000000000000000000000000000000000` лет. Эти оптимизации очень необходимы.
Mooing Duck
OP действительно просил «использовать метод грубой силы, если значения N, M очень малы, скажем, NxM <= 100». Предполагая, что, скажем, 20 мостов достаточно, и единственная оптимизация, которую вы используете, - это ограничивающая мост, описанная выше, оптимальное решение будет найдено в O (2 ^ 20), что находится в пределах досягаемости вашего гипотетического компьютера.
tucuxi
Большинство алгоритмов обратного отслеживания ужасно неэффективны, пока вы не добавите обрезку, итеративное углубление и так далее. Это не значит, что они бесполезны. Например, шахматные движки регулярно побеждают гроссмейстеров с помощью этих алгоритмов (
конечно,
3

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

Чтобы сформулировать целочисленную программу, сделайте переменную 0-1 для каждого нетерминального узла, затем для всех подмножеств нетерминальных узлов, удаление которых из начального графа разъединяет два терминала, потребуйте, чтобы сумма переменных в подмножестве была равна минимум 1. Это приводит к слишком большому количеству ограничений, поэтому вам придется применять их лениво, используя эффективный алгоритм для подключения узлов (в основном, максимальный поток) для обнаружения максимально нарушенного ограничения. Извините за отсутствие деталей, но реализовать это будет сложно, даже если вы уже знакомы с целочисленным программированием.

Дэвид Эйзенстат
источник
-1

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

К сожалению, теперь вы столкнулись с проблемой абстрагирования сетки для создания набора узлов и ребер ... следовательно, настоящая проблема этого поста заключается в том, как мне преобразовать мою сетку nxm в {V} и {E}?

Этот процесс преобразования, на первый взгляд, кажется NP-трудным из-за огромного количества возможных комбинаций (предположим, что все затраты на водные пути идентичны). Чтобы обрабатывать случаи, когда пути перекрываются, вам следует подумать о создании виртуального острова.

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

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

Если вы хотите, чтобы код (псевдо или иначе) преобразовывал вашу сетку в набор {V} и {E}, пожалуйста, отправьте мне личное сообщение, и я постараюсь объединить реализацию.

karnesJ.R
источник
Все затраты на воду не идентичны (см. Примеры). Поскольку Prim не имеет понятия о создании этих «виртуальных узлов», вам следует рассмотреть алгоритм, который это делает: деревья Штейнера (где ваши виртуальные узлы называются «точками Штейнера»).
tucuxi
@tucuxi: Упоминание о том, что все затраты на водные пути могут быть одинаковыми, необходимо для анализа наихудшего случая, потому что это условие, которое увеличивает пространство поиска до его максимального потенциала. Вот почему я поднял это. Что касается Prim, я предполагаю, что программист, отвечающий за реализацию Prim для этой проблемы, понимает, что Prim не создает виртуальные узлы, и обрабатывает это на уровне реализации. Я еще не видел деревьев Штайнера (все еще учусь), так что спасибо за новый материал для изучения!
karnesJ.R