最小樹形圖

好像是一個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

 

參考文獻|資料:

我也找不到原論文,算法是由朱永津與劉振宏提出的,對此表示敬意。

圖是本身畫的......

相關文章
相關標籤/搜索