当前位置: 首页 > news >正文

动态规划求解最小斯坦纳树(证了一天两夜)

最小斯坦纳树

给定点的“最小生成树”问题。

背景

给定无向连通图 G = ( V , E ) G=(V,E) G=(V,E),给出包含 k k k 个结点的点集 S S S,包含点集 S S S 的连通图被称作 斯坦纳树。但我们关注的是如何求出包含点集 S S S最小连通图 G ′ = ( V ′ , E ′ ) G'=(V',E') G=(V,E)最小斯坦纳树。其中 S ⊆ V ′ , E ′ ⊆ E S\subseteq V',E'\subseteq E SV,EE

此处的最小可以指最小的点权和边权和等。

求解最小斯坦纳树是一个 N P − H a r d \color{red}NP-Hard NPHard 问题,所以只会有近似解,并且数据范围不会太大即 k ≤ 10 k\leq10 k10

目前的最好算法的算法时间复杂度为: O ( 3 K N + 2 K M l o g M ) O(3^KN+2^KMlogM) O(3KN+2KMlogM)。其中 N = ∣ V ∣ , M = ∣ E ∣ N=|V|,M=|E| N=V,M=E

动态规划+状态压缩

笔者对着题解思考了这个方法近 1 1 1 天半,并网上查阅了很多资料,再加上自己的一些证明,终于能够彻底搞懂这个算法的精髓。下文将着重论述笔者当时卡住的地方。

我们先假设求的是包含点集 S S S边权和最小的连通块。设权重函数 W : E → N W:E\rarr N W:EN

【起源】

【性质 1 1 1包含点集 S S S边权和最小的子图 G ′ = ( V ′ , E ′ ) G'=(V',E') G=(V,E) 一定是一棵

证明】若 G ′ G' G 中包含环,那么将环中边权最大的边删去后, G ′ G' G 仍连通且边权和变得更小,与前面假设的边权和最小矛盾,故 G ′ G' G 不含环,证毕。

根据性质 1 1 1,我们不难得到一个暴力算法:枚举 G G G 的所有子图 ,对子图进行最小生成树算法。虽然这样一定能够求出包含 S S S 的斯坦纳树,但时间复杂度是 O ( 2 V E l o g V ) O(2^{V}ElogV) O(2VElogV),这是因为有许多子图是没必要枚举的,且每次枚举后还要进行生成树算法,不如我们干脆就从包含 S S S 的树开始转移

d p [ i ] [ S ] dp[i][S] dp[i][S] 表示以 i i i 为根结点且包含 S S S 的树的最小边权和,因为 S S S 是一个集合,且集合内的元素不超过 10 10 10,所以我们考虑用二进制来表示这个集合。

到这里,几乎所有的题解/博客都是直接给出转移方程,然后再贴个代码。但里面的内容却不细讲,可能是觉得大家都学到斯坦纳树了,没必要再写太过详尽。但是却苦了我这个菜鸟。

为了理解透彻我们先从初始化开始讲。

【初始化】

S = { t e r m i n a l [ 1 ] , t e r m i n a l [ 2 ] , ⋯ , t e r m i n a l [ k ] } S=\{terminal[1],terminal[2],\cdots,terminal[k]\} S={terminal[1],terminal[2],,terminal[k]} 。(最初定义斯坦纳树的论文中,管 S S S 中的点叫做终端( t e r m i n a l terminal terminal)点,斯坦纳树中的不属于终端点的其他点叫做斯坦纳点。)那么则有初始化:

for i in (1,2,...,k) :dp[terminal[i]][1<<(i-1)] = 0;

d p [ t e r m i n a l [ i ] ] [ 1 < < ( i − 1 ) ] dp[terminal[i]][1<<(i-1)] dp[terminal[i]][1<<(i1)] 中,根是终端点本身,包含的集合也是终端点本身。因为算的是子树的最小边权和,而只有一个点的树显然没有边,所以赋值为 0 0 0

数组第二维的 ( i − 1 ) (i-1) (i1) 是为了和集合 S S S 的二进制表示所对齐,因为 S S S 的二进制表示是 1 ⋯ 1 ⏟ k 个 1 \underbrace{1\cdots 1}_{k个1} k1 11,即 ( 1 < < k ) − 1 (1<<k)-1 (1<<k)1,而 1 < < ( i − 1 ) 1<<(i-1) 1<<(i1) 正好是 S S S 的二进制中第 i i i 1 1 1

初始化了解完了之后,我们需要了解答案是怎么产生的。

【答案产生】

{ d p [ i ] [ S ] ; ∀ i ∈ S } \{dp[i][S];\forall i\in S\} {dp[i][S];iS} 显然都是正确答案,因为 d p [ i ] [ S ] dp[i][S] dp[i][S] 表示以 i i i 为根结点且包含 S S S 的树的最小边权和,而最小斯坦纳树显然包括 S S S,所以 S S S 中的任意一点为根都可以作为答案。

因为最终答案的 S S S 的二进制表示中有 k k k 1 1 1,但初始化时只有一个 1 1 1,所以 d p dp dp 的转移过程中必然需要子集合的合并。

比如说 d p [ i ] [ 00 ⋯ 10 ] dp[i][00\cdots10] dp[i][0010] d p [ i ] [ 00 ⋯ 01 ] dp[i][00\cdots01] dp[i][0001] 可以合并为 d p [ i ] [ 00 ⋯ 11 ] dp[i][00\cdots11] dp[i][0011] d p dp dp 的第二维是集合的二进制表示)。

看到这里可能会感到好奇,因为初始化时 d p dp dp 数组中根和集合都是终端点本身,即一个点对应一个集合,但上文的 d p [ i ] [ 00 ⋯ 10 ] dp[i][00\cdots10] dp[i][0010] d p [ i ] [ 00 ⋯ 01 ] dp[i][00\cdots01] dp[i][0001] 却是一个点对应两个集合。这是怎么从初始态转移而来的?

我们定义 d ( i , j ) d(i,j) d(i,j) 为原图中 i ⇝ j i\leadsto j ij最短路径长度。我们强行让 j j j 结点和 一棵以 t e r m i n a l [ i ] terminal[i] terminal[i] 为根,包含集合 1 < < ( i − 1 ) 1<<(i-1) 1<<(i1) 的子树连通,那么连通后就有了一棵新树。以 j j j 为根,包含集合 1 < < ( i − 1 ) 1<<(i-1) 1<<(i1) 的子树的最小边权和是:
d p [ j ] [ 1 < < ( i − 1 ) ] = d p [ t e r m i n a l [ i ] ] [ 1 < < ( i − 1 ) ] + d ( t e r m i n a l [ i ] , j ) (1) dp[j][1<<(i-1)] = dp[terminal[i]][1<<(i-1)] + d(terminal[i],j)\tag{1} dp[j][1<<(i1)]=dp[terminal[i]][1<<(i1)]+d(terminal[i],j)(1)
也就是在原来的基础上加上了 t e r m i n a l [ i ] terminal[i] terminal[i] j j j 的最短路。这个式子的正确性是显然的。

接下来我们将演示在一个简单图上的 d p dp dp 过程。

请看下图演示

S = { 1 , 3 } S=\{1,3\} S={1,3}

在这里插入图片描述

初始化

d p [ 1 ] [ 01 ] = 0 , d p [ 3 ] [ 10 ] = 0 dp[1][01]=0,dp[3][10]=0 dp[1][01]=0,dp[3][10]=0 d p dp dp 的第二维是集合的二进制表示, 01 01 01 表示 结点 1 1 1 是第一个终端点, 10 10 10 表示结点 3 3 3 是第二个终端点。)。

强行连通

2 , 3 , 4 2,3,4 2,3,4 d p [ 1 ] [ 01 ] dp[1][01] dp[1][01] 代表的子树连通:

d p [ 2 ] [ 01 ] = 3 , d p [ 3 ] [ 01 ] = 1 , d p [ 4 ] [ 01 ] = 3 dp[2][01]=3,dp[3][01]=1,dp[4][01]=3 dp[2][01]=3,dp[3][01]=1,dp[4][01]=3

1 , 2 , 4 1,2,4 1,2,4 d p [ 3 ] [ 10 ] dp[3][10] dp[3][10] 代表的子树连通:

d p [ 1 ] [ 10 ] = 1 , d p [ 2 ] [ 10 ] = 2 , d p [ 4 ] [ 10 ] = 4 dp[1][10]=1,dp[2][10]=2,dp[4][10]=4 dp[1][10]=1,dp[2][10]=2,dp[4][10]=4

现在所有结点都连通了包含 t e r m i n a l [ i ] terminal[i] terminal[i] 的子树,且包含的集合大小为 1 1 1

合并子树

接下来对于每个结点 v v v ,我们将两棵以 v v v 为根,包含大小为 1 1 1 的集合的子树,合并为一棵 以 v v v 为根,包含大小为 2 2 2 的集合的树。
d p [ v ] [ 11 ] = d p [ v ] [ 10 ] + d p [ v ] [ 01 ] (2) dp[v][11]=dp[v][10]+dp[v][01]\tag{2} dp[v][11]=dp[v][10]+dp[v][01](2)
在这个例子中, S S S 集合大小为 2 2 2,所以此时已经做完了。不妨更普遍一些, ∣ S ∣ > 2 |S|>2 S>2 显然我们要遍历所有的子集合,设 S S S 的子集合为 S ′ S' S,则有:
d p [ v ] [ S ] = m i n ∀ S ′ ⊆ S ( d p [ v ] [ S ] , d p [ v ] [ S ′ ] + d p [ v ] [ S − S ′ ] ) (2’) dp[v][S]=\underset{\forall S'\subseteq S}{min}(dp[v][S],dp[v][S']+dp[v][S-S'])\tag{2'} dp[v][S]=SSmin(dp[v][S],dp[v][S]+dp[v][SS])(2’)
那么我们可以将 ( 1 ) (1) (1) 式的强行连通也写的更具有普遍性(这样可以求出真正的 d p [ v ] [ S ] dp[v][S] dp[v][S]):
d p [ v ] [ S ] = m i n ∀ u ∈ V { d p [ u ] [ S ] + d ( u , v ) } (1’) dp[v][S]=\underset{\forall u\in V}{min}\{dp[u][S]+d(u,v)\}\tag{1'} dp[v][S]=uVmin{dp[u][S]+d(u,v)}(1’)
所以, d p dp dp 过程就是不断的进行三个操作:“强行连通”“合并子树““强行连通”

稍等…为什么合并子树之后还需要进行强行连通?

举个简单的例子: S = 2 S=2 S=2,对于任意一个结点 v v v 而言,合并子树只是令 d p [ v ] [ 11 ] = d p [ v ] [ 01 ] + d p [ v ] [ 10 ] dp[v][11]=dp[v][01]+dp[v][10] dp[v][11]=dp[v][01]+dp[v][10]。但不代表合并之后 d p [ v ] [ 01 ] + d p [ v ] [ 10 ] dp[v][01]+dp[v][10] dp[v][01]+dp[v][10] 就是真正的一棵以 v v v 为根且包含 S S S 的最小边权和子树。

在这里插入图片描述

假设 v = 1 v=1 v=1 S = { 3 , 4 } S=\{3,4\} S={3,4},此时经过一次强行连通之后 d p [ 1 ] [ 01 ] = a + c , d p [ 1 ] [ 10 ] = a + b dp[1][01]=a+c,dp[1][10]=a+b dp[1][01]=a+c,dp[1][10]=a+b,合并后 d p [ 1 ] [ 11 ] = 2 a + b + c dp[1][11]=2a+b+c dp[1][11]=2a+b+c。但实际上 d p [ 1 ] [ 11 ] = d p [ 2 ] [ 11 ] + d ( 1 , 2 ) = a + b + c dp[1][11]=dp[2][11]+d(1,2)=a+b+c dp[1][11]=dp[2][11]+d(1,2)=a+b+c。这也就是我们需要再进行一次强行连通的目的。因为进行完了一次强行连通,我们会对所有的包含集合大小相同的以 v v v 为根的树求出其真正的最小边权和。

这就是为什么我们要在合并子树之后再进行一次强行连通。

但其实,如果我们循环进行两次操作:“合并子树”“强行连通” 也是没问题的。因为合并子树的目的是 “扩大集合”,前提是子树所代表的最小边权和准确。因为初始时只有 d p [ t e r m i n a l [ i ] ] [ 1 < < ( i − 1 ) ] dp[terminal[i]][1<<(i-1)] dp[terminal[i]][1<<(i1)],如果此时我们进行合并子树的话,是不会有任何反应的,因为没有进行连通,所以一个点只对应一个集合。然后再进行强行连通操作。所以每次只要执行两次操作就行了,更省事。

d p dp dp 过程就是不断的进行两个操作:“合并子树”“强行连通”

到这里,我们可以用一个基础版的代码求出给定点集 S S S 的最小斯坦纳树。基础版的时间复杂度是 O ( 3 k N + N 2 l o g M + 2 k N 2 ) O(3^kN+N^2logM+2^kN^2) O(3kN+N2logM+2kN2)。因为我们需要用到最短路,所以考虑用 n n n d i j k s t r a dijkstra dijkstra 求解出任意两点之间的最短路(当且仅当不含负权边,且 N N N 数量级较小时用)。

合并子树:遍历 S S S 的子集 m a s k mask mask + 遍历每个点 + 枚举 m a s k mask mask 的子集 = O ( 3 k N ) O(3^kN) O(3kN)

强行连通:遍历 S S S 的子集 m a s k mask mask + 遍历最终根结点 u u u 和中转根结点 v v v = O ( 2 k N 2 ) O(2^kN^2) O(2kN2)

预处理最短路 n n n D i j k s t r a Dijkstra Dijkstra = O ( N 2 l o g M ) O(N^2logM) O(N2logM)

for (int mask = 1; mask < (1 << k); mask ++) {for (int submask = mask; submask; submask = (submask - 1) & mask)for (int u = 0; u < N; u ++)dp[mask][u] = min(dp[mask][u], dp[submask][u] + dp[mask ^ submask][u]);for (int u = 0; u < N; u ++) {for (int v = 0; v < N; v ++)dp[mask][v] = min(dp[mask][v], dp[mask][u] + d[u][v]);}
【至臻版本】

代码来自 J i a n g l y Jiangly Jiangly(进行了一些个人习惯上的修改)。

for (int s = 0; s < (1 << (K - 1)); s++) {for (int t = s; t != 0; t = (t - 1) & s) for (int i = 0; i < N; i++) dp[s][i] = min(dp[s][i], dp[t][i] + dp[s ^ t][i]);priority_queue<PII, vector<PII>, greater<PII>> pq;vector<bool> vis(N, false);for (int i = 0; i < N; i++) if (dp[s][i] != inf) pq.push({dp[s][i], i});while (!pq.empty()) {auto [d, x] = pq.top();pq.pop();if (vis[x]) continue;vis[x] = true;for (auto [y, w] : adj[x]) {if (dp[s][y] > dp[s][x] + w) {dp[s][y] = dp[s][x] + w;pq.push({dp[s][y], y});}}}}

至臻版本和普通版本的区别就是,至臻版本没有提前预处理出任意两结点之间的最短路。而是在需要用到最短路的时候调用 D i j k s t r a Dijkstra Dijkstra

我们将有效值统统插入最小优先队列中,然后进行 D i j k s t r a Dijkstra Dijkstra d p [ S ] [ i ] dp[S][i] dp[S][i] 会在 D i j k s t r a Dijkstra Dijkstra 中松弛为最小值。

【问题】
for (int i = 0; i < N; i++) if (dp[s][i] != inf) pq.push({dp[s][i], i});

这段代码为啥和普通的 D i j k s t r a Dijkstra Dijkstra 不一样?正常的 D i j k s t r a Dijkstra Dijkstra 是令起点为 0 0 0,并且只有一个起点。

这段代码的意思是什么?

往最小堆中插入多个值,其中最小的会在第一次被拿出来。此时最小的值对应的根节点可以看作起点。假设这个点为 u u u,然后用这个起点的值 d p [ s ] [ u ] dp[s][u] dp[s][u],去更新起点周围的点,设为 v v v。此时如果 d p [ s ] [ v ] dp[s][v] dp[s][v] 大于 d p [ s ] [ u ] + w ( u , v ) dp[s][u]+w(u,v) dp[s][u]+w(u,v),那么说明 v v v u u u 转移过来更优(强行连通操作之前, v v v 的合并只会从 v v v 的子树中转移,详细见合并操作)。那么 v v v 就会被松弛成功。更新完 u u u 周围的点,我们重新从最小堆中拿出最小的点作为起点去松弛周围的点。

为什么这样做是对的?

性质 2 2 2】每次从最小堆中取出来的值,设其对应根结点是 u u u,显然 u u u 一定从最优的点转移过来的。

证明】因为进行 D i j k s t r a Dijkstra Dijkstra 的元素一定是同集合大小的,假设 d p [ s ] [ u ] dp[s][u] dp[s][u] 是第一个被取出来的最小值,若它不是最优解,假设 u u u 真正的最优解是 ρ ( s , u ) \rho(s,u) ρ(s,u),且设它是由 v v v 转移而来的,显然有 ρ ( s , u ) = d p [ s ] [ v ] + d ( u , v ) ≤ d p [ s ] [ u ] \rho(s,u)=dp[s][v]+d(u,v) \leq dp[s][u] ρ(s,u)=dp[s][v]+d(u,v)dp[s][u]。也就是说存在 d p [ s ] [ v ] < d p [ s ] [ u ] dp[s][v]<dp[s][u] dp[s][v]<dp[s][u],所以 d p [ s ] [ u ] dp[s][u] dp[s][u] 不是第一个被取出来的最小值,这与假设矛盾。所以第一个被取出的最小值一定是最优解。

假设 d p [ s ] [ v ] dp[s][v] dp[s][v] 是第 t t t 次被取出来的最小值,如果此时 d p [ s ] [ v ] = ρ ( s , v ) dp[s][v]=\rho(s,v) dp[s][v]=ρ(s,v),说明接下来它不会被后续的点再更新。

设后续的点为 x x x,如果 x x x 能更新 v v v,说明 d p [ s ] [ v ] > d p [ s ] [ x ] + d ( v , x ) dp[s][v]>dp[s][x]+d(v,x) dp[s][v]>dp[s][x]+d(v,x),又因为后续出现的点 d p [ s ] [ x ] > d p [ s ] [ v ] dp[s][x]>dp[s][v] dp[s][x]>dp[s][v],这与假设矛盾,所以 x x x 必然是 v v v 或者 v v v 之前出现过的点即前 t − 1 t-1 t1 个点。所以每次从最小堆中取出来的值,一定是最优解。

性质 2 2 2 证毕。

根据性质 2 2 2,我们可以知道当 D i j k s t r a Dijkstra Dijkstra 结束之后, ∀ i ∈ V , d p [ s ] [ i ] = ρ ( s , i ) \forall i\in V,dp[s][i]=\rho(s,i) iV,dp[s][i]=ρ(s,i)。所以算法正确。

p s ps ps:现在是凌晨四点半,11点的时候喝了一杯咖啡,然后奋战到现在,妈呀真的战斗了个痛快。为什么写这一篇文章,主要是因为我实在看不懂网上的题解,有好多地方不清楚,然后又没有人细讲,所以干脆自己写了一篇,供大家参考。当然斯坦纳树还有许多的应用,我后续再补充,应该不会有第一次学这么难。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 1998年考研真题英语二(204)阅读理解翻译
  • C语言宠物系统
  • CnosDB 元数据集群 – 分布式时序数据库的大脑
  • C++(week15): C++提高:(五)Redis数据库
  • 将PPT中的元素保存为高清图片
  • 看图学sql之sql的执行顺序
  • 分布式接口文档聚合,Solon 是怎么做的?
  • Qt编程技巧小知识点(2)GPIB缓存区数据读取
  • 快递批量查询有什么软件安利一下
  • 关于手机中的红外遥控
  • leetcode hot 100(1)
  • 多尺度病理图像纹理特征作为肺腺癌预后预测的新指标|文献精读·24-08-09
  • 【vue2】回车发送,Ctrl+回车换行,shift+回车换行禁用
  • 【动态规划】1、不同路径II+2、三角形最小路径和
  • sql注入靶场sqli-labs常见sql注入漏洞详解
  • 【Amaple教程】5. 插件
  • 2017-09-12 前端日报
  • classpath对获取配置文件的影响
  • Docker入门(二) - Dockerfile
  • Fastjson的基本使用方法大全
  • JavaScript实现分页效果
  • python docx文档转html页面
  • Solarized Scheme
  • Yii源码解读-服务定位器(Service Locator)
  • 极限编程 (Extreme Programming) - 发布计划 (Release Planning)
  • 聊聊directory traversal attack
  • 让你的分享飞起来——极光推出社会化分享组件
  • 如何解决微信端直接跳WAP端
  • ​ 轻量应用服务器:亚马逊云科技打造全球领先的云计算解决方案
  • #NOIP 2014# day.1 T2 联合权值
  • (1)Map集合 (2)异常机制 (3)File类 (4)I/O流
  • (C语言)编写程序将一个4×4的数组进行顺时针旋转90度后输出。
  • (附源码)springboot金融新闻信息服务系统 毕业设计651450
  • (每日持续更新)信息系统项目管理(第四版)(高级项目管理)考试重点整理第3章 信息系统治理(一)
  • (数据结构)顺序表的定义
  • (四)JPA - JQPL 实现增删改查
  • (万字长文)Spring的核心知识尽揽其中
  • (一)Linux+Windows下安装ffmpeg
  • (转)shell中括号的特殊用法 linux if多条件判断
  • (自用)交互协议设计——protobuf序列化
  • .gitignore文件—git忽略文件
  • .NET Core 和 .NET Framework 中的 MEF2
  • .NET/C# 使用反射注册事件
  • .Net8 Blazor 尝鲜
  • .Net接口调试与案例
  • [Asp.net MVC]Bundle合并,压缩js、css文件
  • [C#]C# winform实现imagecaption图像生成描述图文描述生成
  • [Codeforces] combinatorics (R1600) Part.2
  • [imx9]DDR test Tool for imx9
  • [JS] 常用正则表达式集(一)
  • [leetcode]beautiful-arrangement. 优美的排列
  • [Linux] Apache的配置与运用
  • [NOIP2015] 运输计划
  • [Oh My C++ Diary]\t \n \r的用法
  • [one_demo_6]逆置整数