连接所有岛屿的最低成本是多less?
有一个大小为N×M的网格。 一些细胞是由“0”表示的岛 ,其他是水 。 每个水细胞上都有一个数字,表示在该细胞上制造的桥梁的成本。 你必须find所有岛屿可以连接的最低成本。 如果单元格共享边或顶点,则该单元格将连接到另一个单元格。
什么algorithm可以用来解决这个问题?
编辑:如果N,M的值非常小,比如说NxM <= 100,那么什么可以用作蛮力方法呢?
例如 :在给定的图像中,绿色的单元格表示岛屿,蓝色的单元格表示水,浅蓝色的单元格表示应在其上创build桥梁的单元格。 因此对于下面的图片,答案将是17 。
起初我想把所有的岛屿都标记为节点,并用最短的桥梁连接每一对岛屿。 然后问题可以减less到最小生成树,但在这种方法中,我错过了边缘重叠的情况。 例如 ,在下图中,任意两个岛之间的最短距离是7 (用黄色标记),所以使用最小生成树的答案是14 ,但答案应该是11 (用浅蓝色标记)。
为了解决这个问题,我将使用一个整数编程框架并定义三组决策variables:
- x_ij :是否在水位(i,j)build立桥梁的二元指示variables。
- y_ijbcn :水位(i,j)是连接岛b到岛c的第n个位置的二元指示符。
- l_bc :一个二元指示符variables,用于判断岛屿b和c是否直接相连(也就是说,你只能从b到c的桥梁方块上走路)。
对于桥梁build设成本c_ij ,最小化的sum_ij c_ij * x_ij
是sum_ij c_ij * x_ij
。 我们需要将以下约束添加到模型中:
- 我们需要确保y_ijbcnvariables是有效的。 如果我们在那里build一座桥,我们总是可以到达一个水广场,所以
y_ijbcn <= x_ij
为每个水位(i,j)。 此外,如果(i,j)不与岛b接壤,则y_ijbc1
必须等于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_bcvariables。 如果我们将
I(c)
定义为与岛c相邻的位置,则可以用l_bc <= sum_{(i, j) in I(c), n} y_ijbcn
。 - 我们需要确保所有的岛屿直接或间接地连接起来。 这可以通过以下方式来完成:对于每个非空的适当子集S,要求S中的至less一个岛与S中的至less一个岛连接,我们称之为S'。 在约束中,我们可以通过为大小小于
sum_{b in S} sum_{c in S'} l_bc >= 1
K / 2(其中K是岛数)的每个非空集合S添加一个约束来实现这一点,sum_{b in S} sum_{c in S'} l_bc >= 1
。
对于具有K个岛,W个水平方和指定的最大path长度N的问题实例,这是具有O(K^2WN)
variables和O(K^2WN + 2^K)
约束的混合整数规划模型。 显然,随着问题规模变大,这将变得棘手,但对于您所关心的尺寸来说,这可能是可以解决的。 为了获得可伸缩性的感觉,我将使用纸浆包在python中实现它。 我们首先从问题底部的3个岛屿的较小的7×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求解器)中的默认解算器运行,并输出正确的解决scheme:
II - - - - - II - - B - - - B - - - - - B - B - - - - - - - B - - - - - - - - B - - - - - - - - B - - - - - - - III - - -
接下来,考虑问题顶部的全部问题,这是一个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求解器通常会相对快速地获得好的解决scheme,然后花费大量的时间试图certificate解决scheme的最优性。 使用与上述相同的解算器代码,程序在30分钟内不能完成。 但是,您可以提供求解器的超时以获得近似解决scheme:
mod.solve(pulp.solvers.PULP_CBC_CMD(maxSeconds=120))
这产生了具有目标值17的解决scheme:
II - - - - - II - - IIIII - - - - - II - - - I - II - - - - - I - B - B - - - - B - - - B - - - B - - - - - - B - B - - - - II - - - - - - B - - - - - II - - - - - - - B - - - - - B - - - - - - - B - I - - - - B - - - - - B - III - - B - - II - B - - - I - - - - B - III - - - - - - - - - - BIII - - - - - II - - - II - - - - - - - IIIIII
为了提高您获得的解决scheme的质量,您可以使用商业化的MIP求解器(如果您在学术机构中,这是免费的,否则可能不会免费)。 例如,这里是Gurobi 6.0.4的性能,同样有2分钟的时间限制(尽pipe从解决scheme日志中我们看到解算器在7秒内find了当前最好的解决scheme):
mod.solve(pulp.solvers.GUROBI(timeLimit=120))
这实际上find了一个客观价值的解决scheme16,比OP手工find的更好!
II - - - - - II - - IIIII - - - - - II - - - I - II - - - - - I - B - B - - - - B - - - - - - - B - - - - - - B - - - - - - II - - - - - - B - - - - - II - - - - - - - B - - BB - - - - - - - - - B - I - - B - - - - - - - B - III - - B - - II - B - - - I - - - - B - III - - - - - - - - - - BIII - - - - - II - - - II - - - - - - - IIIIII
这个问题是Steiner树的一个变种,称为节点加权Steiner树 ,专门化了一类图。 紧凑地说,节点加权Steiner树是给定节点加权的无向图,其中一些节点是terminal,find包括所有引起连接子图的terminal的最便宜的节点集合。 可悲的是,我似乎无法在粗略的search中find解决方法。
为了制定一个整数程序,对每个非terminal节点做一个0-1variables,那么对于从起始图中去掉两个terminal的非terminal节点的所有子集,要求子集中的variables之和为至less1.这导致太多的约束,所以你必须强制执行它们,使用节点连接(最大stream量,基本上)的有效algorithm来检测最大违反约束。 对于缺乏细节感到抱歉,即使您已经熟悉整型编程,这也将是一个很难实现的过程。
伪代码中的暴力方法:
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
包含产生它的桥的模式。 然而,这是非常缓慢的。
优化:
- 通过使用“桥接限制”,并运行增加最大桥接数的algorithm,可以在不探索整棵树的情况下find最小的答案。 find一个1桥答案,如果它存在,将是O(nm),而不是O(2 ^ nm) – 一个显着的改进。
- 一旦你超过了
bestCost
,你可以避免search(通过停止recursion;这也被称为“修剪”),因为继续观察是没有意义的。 如果不能变好,不要继续挖掘。 - 如果你在查看“坏”的候选人之前先看看“好”的候选人,那么上面的修剪会更好。(因为它是从左到右,从上到下的顺序)。 一个好的启发就是将几个未连接组件附近的单元看作比没有连接的单元更高的优先级。 但是,一旦您添加启发式,您的search开始类似于A * (并且您也需要某种优先级队列)。
- 重复的桥梁和桥梁无处可逃。 任何不拆除孤岛networking的网桥如果被移除是多余的。
A *等通用searchalgorithm允许更快的search,尽pipe寻找更好的启发式algorithm不是一件简单的事情。 对于更具针对性的方法,正如@Gassa所build议的那样,使用斯坦纳树上现有的结果是一条路。 但是请注意,根据Garey和Johnson的论文,在正交网格上构buildSteiner树的问题是NP完全的。
如果“足够好”就足够了,一个遗传algorithm可能很快find可接受的解决scheme,只要你添加一些关键的启发式就优先桥放置。
鉴于这个问题发生在一个网格中,并且你有明确的参数,我将通过创build一个最小生成树来系统地消除问题空间来处理这个问题。 这样做,如果你用Prim的algorithm来解决这个问题,这对我来说是有意义的。
不幸的是,你现在遇到了抽象网格的问题,以创build一组节点和边…这个职位的真正问题是如何将我的nxm网格转换为{V}和{E}?
由于可能的组合数量庞大(假定所有水路成本相同),这个转换过程一眼就可能是NP-Hard。 要处理path重叠的情况,您应该考虑制作虚拟岛屿。
完成后,运行Primalgorithm,您应该达到最佳解决scheme。
我不相信dynamic规划可以在这里有效运行,因为没有可观察的最优性原则。 如果我们find两个岛屿之间的最低成本,这并不一定意味着我们可以find这两个岛屿之间的最小成本,或者是另一个岛屿的最小成本(我的定义是通过Prim来findMST)连接的。
如果你想要代码(伪或其他)将网格转换为{V}和{E}的集合,请给我一个私人消息,我会考虑拼接一个实现。
我同意这是一个旅行推销员的问题,但它可以是暴力强制与7 =。 计算每个岛之间的最小成本path,只有n(n-1)/ 2个解= 21个计算