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

Существует сетка размера N x M. Некоторые ячейки - это острова, обозначенные "0", а другие - вода. Каждая ячейка воды имеет номер на ней, обозначающий стоимость моста, сделанного на этой ячейке. Вы должны найти минимальную стоимость, на которую могут быть связаны все острова. Ячейка подключена к другой ячейке, если она разделяет ребро или вершину.

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

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

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

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

image2

Ответ 1

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

  • 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, используя пакет пульпы. Пусть сначала начните с меньшей карты 7 x 9 с тремя островами в нижней части вопроса:

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 

Ответ 2

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

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

В С++ это можно записать как

// map = linearized map; map[x*n + y] is the equivalent of map2d[y][x]
// nm = n*m
// bridged = true if bridge there, false if not. Also linearized
// nBridged = depth of recursion (= current bridge being considered)
// cost = total cost of bridges in 'bridged'
// best, bestCost = best answer so far. Initialized to "horrible"
void findBestBridges(char map[], int nm,
   bool bridged[], int nBridged, int cost, bool best[], int &bestCost) {
   if (nBridged == nm) {
      if (connected(map, nm, bridged) && cost < bestCost) {
          memcpy(best, bridged, nBridged);
          bestCost = best;
      }
      return;
   }
   if (map[nBridged] != 0) {
      // try with a bridge there
      bridged[nBridged] = true;
      cost += map[nBridged];

      // see how it turns out
      findBestBridges(map, nm, bridged, nBridged+1, cost, best, bestCost);         

      // remove bridge for further recursion
      bridged[nBridged] = false;
      cost -= map[nBridged];
   }
   // and try without a bridge there
   findBestBridges(map, nm, bridged, nBridged+1, cost, best, bestCost);
}

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

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

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

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

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

Ответ 3

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

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

Ответ 4

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

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

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

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

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

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

Ответ 5

Я согласен, что это проблема коммивояжера, но она может быть грубо вынуждена с n = 7. Вычислите путь минимальной стоимости между каждым островом, и у вас будет только n (n-1)/2 решений = 21 вычисление