连接所有岛屿的最低费用是多少?


84

有一个大小为N x M的网格。一些单元格是用“ 0”表示的,其他单元格是。每个水电池上都有一个数字,表示在该水电池上建造一座桥的成本。您必须找到可以连接所有孤岛的最低成本。如果一个单元共享一条边或一个顶点,则该单元将连接到另一个单元。

可以使用什么算法解决此问题?如果N,M的值非常小(例如NxM <= 100),可以用作暴力破解方法?

示例:在给定的图像中,绿色单元格表示岛,蓝色单元格表示水,浅蓝色单元格表示应在其上架桥的单元格。因此,对于以下图像,答案将是17

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

最初,我想到将所有岛屿标记为节点,并通过最短的桥梁连接每对岛屿。然后可以将问题简化为最小生成树,但是在这种方法中,我错过了边缘重叠的情况。例如,在下图中,任何两个岛之间的最短距离为7(以黄色标记),因此,通过使用最小生成树,答案将为14,但答案应为11(以浅蓝色标记)。

image2


您在问题中描述的解决方案方法似乎是正确的。您能否详细说明“我错过了边缘重叠的情况”的意思?
2015年

@Asad:我添加了一张图片来说明MST方法中的问题。
Atul Vaibhav,2015年

“通过最短的桥梁连接每两个岛”-如您所见,这显然是一种糟糕的方法。
卡洛里·霍瓦斯

1
您能否分享您当前正在使用的代码?这将使答案更容易一些,并且还可以向我们确切显示您当前的方法。
2015年

7
这是Steiner树问题的一个变体。单击链接到Wikipedia可获得一些见解。简而言之,也许无法在多项式时间内找到确切的解,但是最小的生成树并不是一个很坏的近似。
加萨2015年

Answers:


67

为了解决这个问题,我将使用整数编程框架并定义三组决策变量:

  • x_ij:用于指示是否在水位(i,j)建桥的二进制指标变量。
  • y_ijbcn:用于指示水位(i,j)是否是将岛b链接到岛c的第n个位置的二进制指示器。
  • l_bc:用于指示岛b和岛c是否直接链接的二进制指示符变量(也就是,您只能在从b到c的桥梁正方形上行走)。

对于桥梁建造成本c_ij,要最小化的目标值为sum_ij c_ij * x_ij。我们需要向模型添加以下约束:

  • 我们需要确保y_ijbcn变量有效。如果我们在那里建造一座桥,我们总是只能到达一个水广场,因​​此y_ijbcn <= x_ij对于每个水位置(i,j)。此外,y_ijbc1如果(i,j)不与岛屿b毗邻,则必须等于0。最后,对于n> 1,y_ijbcn仅当在步骤n-1中使用了相邻的水位时才可以使用。定义N(i, j)为与(i,j)相邻的水方块,等效于y_ijbcn <= sum_{(l, m) in N(i, j)} y_lmbc(n-1)
  • 我们需要确保仅在b和c链接时才设置l_bc变量。如果我们定义I(c)为与岛屿c接壤的位置,则可以使用完成l_bc <= sum_{(i, j) in I(c), n} y_ijbcn
  • 我们需要确保所有岛屿都直接或间接相连。这可以通过以下方式实现:对于每个非空的孤岛S子集,要求S中的至少一个孤岛与S的补数中的至少一个孤岛链接,我们将其称为S'。在约束中,我们可以通过为每个大小小于等于K / 2的非空集S(其中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地图开始,该地图底部有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 ""

使用纸浆包装中的默认求解器(CBC求解器)运行需要1.4秒,并输出正确的解决方案:

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;约束总流入在广场上查看是否购买)。
David Eisenstat 2015年

1
@DavidEisenstat感谢您的建议-我只是尝试了一下,不幸的是,对于这些问题实例,它解决了很多问题。
josliber

8
正是我开始赏金时一直在寻找的东西。如此微不足道的描述问题如何给MIP解决者带来如此艰巨的时间,这让我感到惊讶。我想知道以下情况是否正确:连接两个岛的路径是最短路径,并且具有必须通过某个像元(i,j)的附加约束。例如,Gurobi解决方案中的左上和中间岛与受约束以穿过单元格(6,5)的SP链接。不知道这是否是正确的,但在某些时候会对此进行调查。谢谢你的回答!
Ioannis

@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-bridge的答案(如果存在)将是O(nm)而不是O(2 ^ nm)-这是一个巨大的改进。
  • 一旦超出范围bestCost,您就可以避免搜索(通过停止递归;也称为“修剪”),因为继续关注毫无意义。如果不能变得更好,请不要继续挖掘。
  • 如果您在查看“不良”候选者之前先查看“良好”候选者,则上述修剪效果会更好(因为实际上,所有单元格都是按照从左到右,从上到下的顺序查看的)。一个好的启发式方法是将靠近几个未连接组件的单元的优先级视为比未连接单元的单元更高的优先级。但是,添加启发式搜索后,搜索开始类似于A *(并且您还需要某种优先级队列)。
  • 避免重复的桥梁和无处可去的桥梁。如果删除了所有不断开孤岛网络的网桥,则是多余的。

尽管找到更好的启发式方法并非易事,但诸如A *之类的常规搜索算法允许更快的搜索速度。对于特定于问题的方法,可以使用@Gassa建议的在Steiner树上使用现有结果的方法。但是请注意,根据Garey和Johnson的论文,在正交网格上构建Steiner树的问题是NP-Complete 。

如果“足够好”就足够了,只要您对首选的桥梁位置添加一些关键的启发式方法,遗传算法就可以快速找到可接受的解决方案。


“尝试所有2 ^(n * m)个组合”呃,2^(13*14) ~ 6.1299822e+54迭代。如果我们假设您每秒可以进行一百万次迭代,那将只需要... 194380460000000000000000000000000000000000000000年。这些优化是非常必要的。
Mooing Duck 2015年

OP确实要求“如果N,M的值非常小,例如NxM <= 100,则采用蛮力方法”。假设有20座桥就足够了,并且您使用的唯一优化是上面的限制桥,那么最优解将在O(2 ^ 20)中找到,这在您的假设计算机的范围内。
tucuxi

在添加修剪,迭代加深等等之前,大多数回溯算法的效率都非常差。这并不是说它们是无用的。例如,国际象棋引擎经常使用这些算法击败大师级(被允许的-他们使用书中的所有技巧来积极地修剪)
tucuxi

3

这个问题是Steiner树的一种变体,称为节点加权Steiner树,专门用于某种图。紧凑地,给定节点加权的Steiner树,给定节点加权的无向图,其中某些节点为终端,可以找到最便宜的节点集,其中包括诱发连接子图的所有终端。可悲的是,在一些粗略的搜索中,我似乎找不到任何求解器。

要将其表示为整数程序,请为每个非终端节点制作一个0-1变量,然后对从起始图中删除来断开两个终端的非终端节点的所有子集,要求子集中变量的总和为至少1.这会导致太多的约束,因此您必须使用有效的节点连接算法(基本上是最大流量)来懒惰地强制执行它们,以检测到最大程度违反的约束。抱歉,缺少详细信息,但是即使您已经熟悉整数编程,也很难实施。


-1

鉴于此问题发生在网格中,并且您有明确定义的参数,我将通过创建最小生成树来系统地消除问题空间,从而解决该问题。这样做对我来说很有意义,如果您使用Prim的算法来解决这个问题。

不幸的是,您现在遇到了将网格抽象化以创建一组节点和边的问题...因此,本文的真正问题是如何将nxm网格转换为{V}和{E}?

乍一看,这种转换过程很可能是NP-Hard,因为组合的数量可能非常多(假设所有水路成本都相同)。要处理路径重叠的实例,应考虑制作一个虚拟岛。

完成此操作后,运行Prim's Algorithm,您将获得最佳解决方案。

我不认为动态编程可以在这里有效运行,因为没有最优的可观察原理。如果我们找到两个岛之间的最小成本,那并不一定意味着我们可以找到这两个岛与第三个岛之间的最小成本,或者另一个岛子集将是(根据我的定义,可以通过Prim找到MST)连接的。

如果您希望通过代码(伪或其他方式)将网格转换为一组{V}和{E},请给我发送一条私人消息,我将研究将实现方式拼接在一起。


所有水成本都不相同(请参见示例)。由于Prim没有创建那些“虚拟节点”的概念,因此您应该考虑使用一种算法:Steiner树(其中您的虚拟节点称为“ Steiner点”)。
tucuxi

@tucuxi:提及最坏情况的分析时,必须提到所有水路成本可能是相同的,因为这是使搜索空间膨胀至最大潜力的条件。这就是为什么我提出来。关于Prim,我假设负责解决此问题的程序员认识到Prim不会创建虚拟节点,而是在实现级别进行处理。我尚未见过Steiner树(仍是本科生),因此,感谢您学习新材料!
karnesJ.R 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.