最小树形图

好像是一个OI中应用不是不少(不要打脸)的算法c++

算法

朱刘算法了解一下本文就是讲这个的......git

其实主要是我调了好久一道题,而后发现一个智障错误而后过来写博客算法

算法主要解决有向图最小生成树问题.ide

定义

在一个有向图中选择一些有向边集构建一颗树,而后使这棵树的边权和最小。oop

就是根肯定的“最小有向生成树”。spa

前面的东西

Q:如何判断存在性?3d

A:直接拿着根跑dfs便可。code

注意这是一个比较重要的地方无解的状况下会有几个点没有入度,然而这样是非法的,同时咱们筛掉这种状况以后能够保证只有一条入边的最小树形图是最优的并且合法的(若是有两条入边要么能够删掉一条要么是无解状况)blog

Q:复杂度?递归

A:$O(VE)$

咱们认为根$Root$一开始已经给定(后面会将没给定的特殊状况)。

记边权为$w$

如下的证实不必定会严谨......

算法

 首先咱们须要清除自环,由于存在自环的话会使复杂度增高但又没有意义。

操做一

  对于除根之外的全部点选定一条入边,该条入边是全部入边中权值最小的一条。

定理一

  上面的操做以后若是没有环的话那么就是最小树形图。

  证实:

  若是不是最小树形图的话那么必定存在一条边能够替代原有的边,可是因为原图已是一棵树,因此替换一条边只可能:

  1. 破坏树的结构
  2. 换到了一条更大的边

  因此并无能够换的边。

而后若是没有环的话那么就直接跑路输出答案(等下介绍缩了环以后的答案怎么计算),若是有呢?

若是存在一个环的话,那么咱们这样作缩环:

操做二

  这一操做命名为缩环(本身瞎BB的)

  令环上任意一点$u$,指向它的有向边为$v$点,令$u$和$v$之间的边的边权为$ind[u]$,而后咱们新建了一个节点$p$代替这个环,而且与外界有以下联系:

  对于点$u$的入边(起点为$s$)链接$s$到$p$,边权为$w-ind[u]$

  对于点$u$的出边(起点为$t$)链接$p$到$t$,边权为$w$

  例以下面这幅比较原谅的图,假设全部绿色的点为新点$p$所表明的点。左上角的连边说明了一个实例。

定理二

  对于上面的操做,当前这一层的最小树形图=缩环后的最小树形图+环内权值

  证实(解释为何要在缩环的时候换边权):

  因为生成树要求每个点都要走到,包括环内的点,因此最后的答案确定长成这张图的样子(解释),即对于一个环在最终的答案中会被选到的只会有环的 $边数-1$、$一条入边$ (为何只有一条边:由于缩了环以后的图和原图的入边一一对应,而根据最小树形图的算法流程,咱们每次只会选择缩了环以后的图的缩点的一条入边做为答案,因此对应到原图也只有一条边)而可能有 $几条出边$ 。咱们发现环内的1条边确定是走不到的,也就是图中的黄色边(若是$9->2$是入边则为$1->2$这条边,若是$11->7$是入边则$6->7$选不到),也就是$v$到$u$的边,边权为$ind[u]$。也就是说若是有一条路径从$u$进入了这个环,那么环内$v$到$u$的边就会不在生成树中。

  因此咱们只须要在入边的$w$上减去$ind[u]$便可保证这条的排名以及在最终对答案产生的贡献正确。

最后咱们只须要递归地跑算法便可。

而后为何这样递归下去最后获得的必定是最小值呢?

定理二的最优性证实

感谢@ Rite的提醒,大概我是只考虑到了本身不熟悉的地方吧,之后写的时候的确要注意一下

(虽然我有点没有看懂评论)

首先定理一讲述了在没有环的状况下为何是最优的,大体就是一个反证吧。

而后另外一部分咱们干脆加入一个定理三:

定理三

一张图缩了环以后的最小树形图就是缩环前的最小树形图

首先咱们考虑到一个环在最小树形图表明的一个缩掉以后会长什么样子

 

先不考虑蓝色边

 

 

 

而后因为整个环内的全部点都必须选到,因此这个环在至关于选择了一条入边以后有些边是一种“必选”,也就是不管如何都必须跑完全部点,同时无论缩了环的图有多少入边出边都如此,最后缩了环的图确定只有只有一条入边,因此其实环的边的选择在选择了入边以后是固定的

例如正上方的原图中咱们能够选择走$6->10$的边来走到10或者走$13->10$的边来走到10,可是我走到环内的任何一个点它就只能从某个入口进来,而后沿着环走遍历点,能够选的就只有入口以后的点。例如最上面的图,$6->7$黄色边有可能有选择的机会,而后咱们处理了这种边的选择而后再缩点,至关于我获得了一个答案以后而后直接在答案上加一些不影响的答案选择的边,由于这些边必须选择,因此必须加入答案。

咱们用一个公式表达一下

定义$e\ in\ loop$表示环内的边,$in$表示入边,$out$表示出边,$pre$表示环内的不要选的边,$Ans'$表示缩了环以后的图除了被缩环的点周围的边的答案

$Ans = Ans' + \Sigma{w_{e\ in\ loop}} + w_{in} - w_{pre} + \Sigma{w_{out}}$

能够发现右边第一项第二项会不受环的影响,必须累加在答案中,而后$\Sigma{w_{out}}$不会受入边的影响,根据本身的边权决定答案

而后$w_{in} - w_{pre}$须要配套选择,因此咱们把它集中在了一条边上选择

因此在$Ans'$最优下$w_{in} - w_{pre}$和$\Sigma{w_{out}}$选择最小边便可

而后一种更复杂的状况:环套环

就是加了那种蓝色边的

可是因为你会跑一次这个环去找环的边,因此其实你首先会找到而且缩掉那个小环,这个时候长这样,而后就只剩下一个环了

 

其实问题等价

还原

当你递归下去求得答案以后怎么还原选择的边呢?

除了被缩成点的环缩表明的点以外的点之间相连的边都是最终的答案

首先在上面咱们认识到

  1. 一个环只会有一条入边
  2. 在这条入边后面(上面有说这条边是什么)的环上的一条边不会被选

因此咱们还原完正常的边以后而后还原环内除了“后面”的那条边便可

复杂度组成

  每次找最小边$O(E)$,缩点$O(V)$,更新边$O(E)$,因为删掉了自环,全部每次至少删掉一个点,因此递归层数$O(V)$

  总复杂度$O(VE)$

后面的东西

  若是我这个虾皮出题人没有指定根怎么办?让你去枚举根?

  那么咱们能够创建虚拟根$root$(小写),而后向每一个点连出$S>\Sigma{W_i}$的边,而后再跑,最后答案减去S便可。

  可是有一种状况就是发现减了以后答案仍是大于$S$,那么这说明原图不连通(由于若是连通的话确定算法不会智障到去选边权为S边)

  而后咱们找到的最小边的和$root$相连的点就是最小根。

例题

Luogu P2792 [JSOI2008]小店购物

题意

就是去交易买东西,一些货物有原价,若是你先买$x$货物而后再买$y$货物搞很差就有优惠)。

你须要为每一种物品很少很多选$k_i$件。

题解

考虑到若是咱们选择最优方案,那么只有第一个物品是须要考虑折扣的,后面的物品能够直接选取最便宜的。

首先统计后面优惠价的全部答案。

咱们为每个物品都开一个节点,而后再开一个$root$, 从$root$向物品连边,权值为原价

而后对于每一对优惠$x$对$y$,从$x$向$y$连边,边权为折扣价。

最后跑一边算法而后累加到原来的答案内便可。

一开始个人板子出了一个很是......的错误,而后调了几年,而后又被卡精度......

代码以下:

 

  1 #include <cstdio>
  2 #include <cctype>
  3 #include <cassert>
  4 #include <cstring>
  5 
  6 #include <fcntl.h>
  7 #include <unistd.h>
  8 #include <sys/mman.h>
  9 
 10 //User's Lib
 11 
 12 using namespace std;
 13 
 14 char *pc;
 15 
 16 inline void Main_Init(){
 17     static bool inited = false;
 18     if(inited) fclose(stdin), fclose(stdout);
 19     else {
 20         #ifndef ONLINE_JUDGE
 21         freopen("b.in", "r", stdin);
 22         freopen("b.out", "w", stdout);
 23         #endif
 24         pc = (char *) mmap(NULL, lseek(0, 0, SEEK_END), PROT_READ, MAP_PRIVATE, 0, 0);
 25         inited = true;
 26     }
 27 }
 28 
 29 static inline int read(){
 30     int num = 0;
 31     char c, sf = 1;
 32     while(isspace(c = *pc++));
 33     if(c == 45) sf = -1, c = *pc ++;
 34     while(num = num * 10 + c - 48, isdigit(c = *pc++));
 35     return num * sf;
 36 }
 37 
 38 static inline double read_dec(){
 39     double num = 0, decs = 1;
 40     char c, sf = 1;
 41     while(isspace(c = *pc ++));
 42     if(c == '-') sf = -1, c = *pc ++;
 43     while(num = num * 10 + c - 48, isdigit(c = *pc ++));
 44     if(c != '.') return num * sf;
 45     c = *pc ++;
 46     while(num += (decs *= 0.1) * (c - 48), isdigit(c = *pc ++));
 47     return num * sf;
 48 }
 49 
 50 namespace LKF{
 51     template <typename T>
 52     extern inline T abs(T tar){
 53         return tar < 0 ? -tar : tar;
 54     }
 55     template <typename T>
 56     extern inline void swap(T &a, T &b){
 57         T t = a;
 58         a = b;
 59         b = t;
 60     }
 61     template <typename T>
 62     extern inline void upmax(T &x, const T &y){
 63         if(x < y) x = y;
 64     }
 65     template <typename T>
 66     extern inline void upmin(T &x, const T &y){
 67         if(x > y) x = y;
 68     }
 69     template <typename T>
 70     extern inline T max(T a, T b){
 71         return a > b ? a : b;
 72     }
 73     template <typename T>
 74     extern inline T min(T a, T b){
 75         return a < b ? a : b;
 76     }
 77 }
 78 
 79 //Source Code
 80 
 81 const int MAXN = 111;
 82 const int MAXM = 555;
 83 const int INF = 0x3f3f3f3f;
 84 
 85 int n, m;
 86 int ind[MAXN], pre[MAXN], id[MAXN], vis[MAXN];
 87 struct Edge{
 88     int u, v, w;
 89     Edge(){}
 90     Edge(int _u, int _v, int _w) : u(_u), v(_v), w(_w){}
 91 }edge[MAXM];
 92 
 93 inline int MA(){
 94     int ret = 0, root = n, num;
 95     while(true){
 96         memset(ind, 0x3f, sizeof(ind));
 97         for(int i = 1; i <= m; i++)
 98             if(edge[i].u != edge[i].v && ind[edge[i].v] > edge[i].w)
 99                 ind[edge[i].v] = edge[i].w, pre[edge[i].v] = edge[i].u;
100         for(int i = 1; i <= n; i++)
101             if(i != root && ind[i] == INF)
102                 return -1;
103         memset(id, -1, sizeof(id)), memset(vis, -1, sizeof(vis));
104         num = ind[root] = 0;
105         for(int i = 1; i <= n; i++){
106             int v = i;
107             ret += ind[i];
108             while(vis[v] != i && v != root)
109                 vis[v] = i, v = pre[v];
110             if(v != root && id[v] == -1){
111                 id[v] = ++ num;
112                 for(int j = pre[v]; j != v; j = pre[j])
113                     id[j] = num;
114             }
115         }
116         if(!num) return ret;
117         for(int i = 1; i <= n; i++)
118             if(id[i] == -1)
119                 id[i] = ++ num;
120         for(int i = 1; i <= m; i++){
121             int ori = edge[i].v;//ori
122             edge[i].u = id[edge[i].u], edge[i].v = id[edge[i].v];
123             if(edge[i].u != edge[i].v) edge[i].w -= ind[ori];
124         }
125         n = num, root = id[root];
126     }
127 }
128 
129 int cost[MAXN], ks[MAXN], pos[MAXN];
130 
131 inline void Re(int &tar){
132     if(tar % 10 != 0){
133         tar ++;
134         //printf("%d\n", tar);
135     }
136 }
137 
138 int main(){
139     Main_Init();
140     n = read();
141     for(int i = 1, j = 1; i <= n; i++, j++){
142         Re(cost[i] = double(read_dec()) * 100.0), ks[i] = read();
143         if(!ks[i]) i --, n --;
144         else ks[i] --,  pos[j] = i;
145     }
146     n ++;
147     for(int i = 1; i < n; i++)
148         edge[++ m] = Edge(n, i, cost[i]);
149     int t = read();
150     for(int i = 1; i <= t; i++){
151         int x = pos[read()], y = pos[read()], w;
152         Re(w = double(read_dec()) * 100.0);
153         if(!(x && y)) continue;
154         LKF::upmin(cost[y], w);
155         edge[++ m] = Edge(x, y, w);
156     }
157     int ans = 0;
158     for(int i = 1; i < n; i++)
159         ans += cost[i] * ks[i];
160     int ret = MA();
161     assert(ret != -1);
162     printf("%.2lf", (ret + ans) / 100.0);
163     Main_Init();
164     return 0;
165 }
Source Code

 

参考文献|资料:

我也找不到原论文,算法是由朱永津与刘振宏提出的,对此表示敬意。

图是本身画的......