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


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)місця розташування, що межують з островом с, це можна зробити за допомогою l_bc <= sum_{(i, j) in I(c), n} y_ijbcn.
  • Нам потрібно забезпечити зв’язок усіх островів, прямо чи опосередковано. Це може бути здійснено наступним чином: для кожної непорожньої належної підмножини S островів потрібно вимагати, щоб принаймні один острів в S був пов'язаний принаймні з одним островом у додатку S, який ми будемо називати S '. В обмеженнях, ми можемо реалізувати це, додавши обмеження для кожного непорожньої безлічі S розміру <= К / 2 (де До числа острівців), 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 х 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 х 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, краще, ніж ОП вдалося знайти вручну!

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 ++ це можна записати як

// 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, згідно з цією роботою Гарі та Джонсона .

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


"спробувати всі 2 ^ (n * m) комбінації" е-е, 2^(13*14) ~ 6.1299822e+54ітерації. Якщо ми припустимо, що ви можете зробити мільйон ітерацій за секунду, це займе лише ... ~ 194380460000000000000000000000000000000000` років. Ці оптимізації дуже необхідні.
Mooing Duck

OP було просити «грубої сили підхід , якщо значення N, M дуже малі, скажімо NXM <= 100». Припускаючи, що, скажімо, 20 мостів достатньо, і єдиною оптимізацією, яку ви використовуєте, є міст, що обмежує міст, наведене вище, оптимальне рішення буде знайдено в O (2 ^ 20), який знаходиться в межах вашого гіпотетичного комп’ютера.
tucuxi

Більшість алгоритмів зворотного відстеження жахливо неефективні, поки ви не додасте обрізку, ітеративне поглиблення тощо. Це не означає, що вони марні. Наприклад, шахові двигуни регулярно перемагають гросмейстерів за допомогою цих алгоритмів (надається - вони використовують усі трюки в книзі для агресивної обрізки)
tucuxi

3

Ця проблема - варіант дерева Штейнера, званого вузловим зваженим деревом Штейнера , що спеціалізується на певному класі графіків. Порівняно, зважене за вузлами дерево Штейнера, отримуючи найвагоміший орієнтований графік, де деякі вузли є терміналами, знаходить найдешевший набір вузлів, включаючи всі термінали, що викликає підключений підграф. На жаль, я не можу знайти жодних вирішувачів у деяких побіжних пошуках.

Щоб сформулювати як цілочисельну програму, зробіть змінну 0-1 для кожного нетермінального вузла, тоді для всіх підмножин нетермінальних вузлів, видалення яких із початкового графіка відключає два термінали, потрібно, щоб сума змінних у підмножині була на рівні щонайменше 1. Це викликає занадто багато обмежень, тому вам доведеться застосовувати їх ліниво, використовуючи ефективний алгоритм підключення вузлів (в основному, максимальний потік) для виявлення максимально порушеного обмеження. Вибачте за відсутність деталей, але це буде складним завданням, навіть якщо ви вже знайомі з цілочисельним програмуванням.


-1

Враховуючи, що ця проблема відбувається в сітці, і у вас є чітко визначені параметри, я підійшов би до проблеми із систематичним усуненням проблемного простору шляхом створення мінімального дерева, що охоплює. При цьому мені має сенс, якщо ви підходите до цієї проблеми за допомогою алгоритму Прима.

На жаль, зараз ви стикаєтесь із проблемою абстрагування сітки для створення набору вузлів і ребер ... ерго, справжня проблема цієї публікації полягає в тому, як я можу перетворити свою сітку nxm у {V} та {E}?

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

Коли це буде зроблено, запустіть Алгоритм Прима, і ви повинні дійти до оптимального рішення.

Я не вірю, що тут можна ефективно запустити динамічне програмування, оскільки не існує принципу оптимальності, який можна спостерігати. Якщо ми знайдемо мінімальну вартість між двома островами, це не обов'язково означає, що ми можемо знайти мінімальну вартість між цими двома та третім островом, або іншою підмножиною островів, які будуть (за моїм визначенням, знайти MST через Prim) підключений.

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


Усі витрати на воду не однакові (див. Приклади). Оскільки Prim не має поняття створення цих "віртуальних вузлів", вам слід розглянути алгоритм, який має: дерева Штейнера (де ваші віртуальні вузли називаються "точками Штейнера").
tucuxi

@tucuxi: Згадка про те, що всі витрати на водних шляхах можуть бути однаковими, необхідна для аналізу найгіршого випадку, оскільки це умова, яка роздуває простір пошуку до максимального потенціалу. Ось чому я це вигадав. Що стосується Prim, я припускаю, що програміст, відповідальний за реалізацію Prim для цієї проблеми, визнає, що Prim не створює віртуальних вузлів і обробляє це на рівні реалізації. Я ще не бачив дерев Штайнера (все ще перероблений), тож дякую за новий матеріал для вивчення!
karnesJ.R
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.