生成樹


生成樹專題

cover by 一堆大佬的博客 百度百科等#%¥%~

反正不是我寫的

首先 讓我們先了解一下生成樹的概念

生成樹

  • 在圖論中,如果連通圖 的一個子圖是一棵包含 的所有頂點的樹,則該子圖稱為G的生成樹(SpanningTree)。

  • 生成樹是連通圖的包含圖中的所有頂點的極小連通子圖。

  • 圖的生成樹不惟一。從不同的頂點出發進行遍歷,可以得到不同的生成樹

通俗的來說,生成樹就是

  • 只要能連通所有頂點而又不產生回路的任何子圖都是它的生成樹

  • 連接圖中所有的點n,並且只有n-1條邊的子圖就是它的生成樹

——————————————————————————————————————

常用的生成樹算法有DFS生成樹、BFS生成樹、PRIM 最小生成樹和Kruskal最小生成樹算法

通常,由深度優先搜索得到的生成樹稱為深度優先生成樹,簡稱為DFS生成樹;由廣度優先搜索得到的生成樹稱為廣度優先生成樹,簡稱為BFS生成樹

深度優先生成樹具體有什么好處我也不太清楚,可能比較好打(口胡)

廣度優先生成樹是所有生成樹中高度最低的(顯然)

接下來進入正題

在所有生成樹中,應用最廣泛的當然是

——————————————————————————————————————

最小生成樹

  • 在生成樹中,我們稱生成樹各邊權值和為該樹的權。對於無向連通圖來說,權值最小的生成樹被成為最小生成樹。

這個也是圖論的基礎,能夠配合圖論其他多中算法使用

在此先引入一個別的概念

———————————————————————————————————————

瓶頸生成樹

無向圖G的一顆瓶頸生成樹是這樣的一顆生成樹T,它最大的邊權
值在G的所有生成樹中是最小的。瓶頸生成樹的值為T中最大權值邊的權。

結論

無向圖的最小生成樹一定是瓶頸生成樹,但瓶頸生成樹
不一定是最小生成樹

怎么證明呢?

可以使用反證法

假設最小生成樹不是瓶頸樹,設最小生成樹T的最大權邊為e,則
存在一棵瓶頸樹Tb,其所有的邊的權值小於w(e)。刪除T 中的e,形成
兩棵數T1,T2,用Tb中連接T1,T2的邊連接這兩棵樹,得到新的生成樹,
其權值小於T,與T是最小生成樹矛盾

通俗的來說呢,就是先假設結論錯誤,即最小生成樹的最大邊比瓶頸生成樹的最大邊大,然后刪掉最小生成樹的最大邊,這時候最小生成樹會被分成兩個部分(兩顆樹),那么,在瓶頸生成樹中肯定存在連接這兩個部分並且比最小生成樹最大邊小的邊(因為畢竟瓶頸生成樹人家也是生成樹,任意兩個部分是肯定有邊相連的),那么用這條邊替換掉最小生成樹的最大邊,就會與最小生成樹的定義矛盾

應該很容易理解才對

接下來,瓶頸生成樹不一定是最小生成樹

不多說,直接上圖

感性理解

——————————————————————————————————————

知道了這個結論之后,像找最短路中最大權值的題應該就很好做了吧

那么,我們如何來求最小生成樹呢?

1.kruskal算法

Kruskal 算法是能夠在O(mlogm) 的時間內得到一個最小生成樹的算
法。它主要是基於貪心的思想:

① 將邊按照邊權從小到大排序,並建立一個沒有邊的圖T。

② 選出一條沒有被選過的邊權最小的邊。

③ 如果這條邊兩個頂點在T 中所在的連通塊不相同,那么將
它加入圖T。

④ 重復②和③直到圖T 連通為止。

由於只需要維護連通性,可以不需要真正建立圖T,可以用並查集
來維護。

觀察一下幾種不同風格的代碼

#include<algorithm>
#include<cstdio>
using namespace std;
int n,m,x,y,z,f1,f2,tot,k,fa[1001];
struct node{
	int x,y,v;
}a[10001];
int cmp(const node &a,const node &b)
{
	return a.v<b.v;
}
int father(int x)
{
	return fa[x]==x?x:fa[x]=father(fa[x]);
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) fa[i]=i;
	for(int i=1;i<=m;i++)
		scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].v);
	sort(a+1,a+m+1,cmp);
	for(int i=1;i<=m;i++)
	{
		x=a[i].x;y=a[i].y;z=a[i].v;
		f1=father(x);f2=father(y);
		if(f1==f2) continue;
		fa[f1]=f2;tot+=z;k++;
		if(k==n-1) break;
	}
	printf("%d",tot);
}

——————————————————————————————

/*
 * 克魯斯卡爾(Kruskal)最小生成樹
 */
void kruskal(Graph G)
{
    int i,m,n,p1,p2;
    int length;
    int index = 0;          // rets數組的索引
    int vends[MAX]={0};     // 用於保存"已有最小生成樹"中每個頂點在該最小樹中的終點。
    EData rets[MAX];        // 結果數組,保存kruskal最小生成樹的邊
    EData *edges;           // 圖對應的所有邊

    // 獲取"圖中所有的邊"
    edges = get_edges(G);
    // 將邊按照"權"的大小進行排序(從小到大)
    sorted_edges(edges, G.edgnum);

    for (i=0; i<G.edgnum; i++)
    {
        p1 = get_position(G, edges[i].start);   // 獲取第i條邊的"起點"的序號
        p2 = get_position(G, edges[i].end);     // 獲取第i條邊的"終點"的序號

        m = get_end(vends, p1);                 // 獲取p1在"已有的最小生成樹"中的終點
        n = get_end(vends, p2);                 // 獲取p2在"已有的最小生成樹"中的終點
        // 如果m!=n,意味着"邊i"與"已經添加到最小生成樹中的頂點"沒有形成環路
        if (m != n)
        {
            vends[m] = n;                       // 設置m在"已有的最小生成樹"中的終點為n
            rets[index++] = edges[i];           // 保存結果
        }
    }
    free(edges);

    // 統計並打印"kruskal最小生成樹"的信息
    length = 0;
    for (i = 0; i < index; i++)
        length += rets[i].weight;
    printf("Kruskal=%d: ", length);
    for (i = 0; i < index; i++)
        printf("(%c,%c) ", rets[i].start, rets[i].end);
    printf("\n");
}

2.prim算法

Prim 算法和Kruskal 算法一樣也是尋找最小生成樹的一種方法:

① 先建立一個只有一個結點的樹,這個結點可以是原圖中任
意的一個結點。

② 使用一條邊擴展這個樹,要求這條邊一個頂點在樹中另一
個頂點不在樹中,並且這條邊的權值要求最小。

③ 重復步驟②直到所有頂點都在樹中。

這里記頂點數v,邊數e

鄰接矩陣:O(v) 鄰接表:O(elog2v)

此為原始的加權連通圖。每條邊一側的數字代表其權值。

頂點D被任意選為起始點。頂點A、B、E和F通過單條邊與D相連。A是距離D最近的頂點,因此將A及對應邊AD以高亮表示。

不可選 C, G 可選A, B, E, F 已選 D

下一個頂點為距離D或A最近的頂點。B距D為9,距A為7,E為15,F為6。因此,F距D或A最近,因此將頂點F與相應邊DF以高亮表示。

不可選 C G,可選B, E, F,已選A,D

算法繼續重復上面的步驟。距離A為7的頂點B被高亮表示。

可選 B,E,G 不可選 C,已選 A D F

在當前情況下,可以在C、E與G間進行選擇。C距B為8,E距B為7,G距F為11。點E最近,因此將頂點E與相應邊BE高亮表示

可選 C E G,不可選 無,已選 A D B F

接下來繼續進行,我就不打具體步驟了

結束

現在,所有頂點均已被選取,圖中綠色部分即為連通圖的最小生成樹。在此例中,最小生成樹的權值之和為39

是不是有點像最短路算法中的迪傑斯特拉?

其實代碼實現也差不多

最簡單的無優化版本

+ View code
#include<cstdio>
#include<cstdlib>
#include<iostream>

using namespace std;
/*最小生成樹Prim未優化版*/

int book[100];//用於記錄這個點有沒有被訪問過
int dis[100];//用於記錄距離樹的距離最短路程
int MAX = 99999;//邊界值
int maps[100][100];//用於記錄所有邊的關系

int main()
{
    int i,j,k;//循環變量
    int n,m;//輸入的N個點,和M條邊
    int x,y,z;//輸入變量
    int min,minIndex;
    int sum=0;//記錄最后的答案
    
    cin>>n>>m;

    //初始化maps,除了自己到自己是0其他都是邊界值
    for (i = 1; i <= n; i++)
    {
        for (j = 1; j <= n; j++)
        {
            if(i!=j)
                maps[i][j] = MAX;
            else
                maps[i][j] = 0;
        }
    }
            
    for (i = 1; i <= m; i++)
    {
        cin>>x>>y>>z;//輸入的為無向圖
        maps[x][y] = z;
        maps[y][x] = z;
    }

    //初始化距離數組,默認先把離1點最近的找出來放好
    for (i = 1; i <= n; i++)
        dis[i] = maps[1][i];

    book[1]=1;//記錄1已經被訪問過了

    for (i = 1; i <= n-1; i++)//1已經訪問過了,所以循環n-1次
    {
        min = MAX;//對於最小值賦值,其實這里也應該對minIndex進行賦值,但是我們承認這個圖一定有最小生成樹而且不存在兩條相同的邊
        //尋找離樹最近的點
        for (j = 1; j <= n; j++)
        {
            if(book[j] ==0 && dis[j] < min)
            {
                min = dis[j];
                minIndex = j;
            }
        }

        //記錄這個點已經被訪問過了
        book[minIndex] = 1;
        sum += dis[minIndex];

        for (j = 1; j <= n; j++)
        {
            //如果這點沒有被訪問過,而且這個點到任意一點的距離比現在到樹的距離近那么更新
            if(book[j] == 0 && maps[minIndex][j] < dis[j])
                dis[j] = maps[minIndex][j];
        }
    }

    cout<<sum<<endl;
}

鏈式前項星存圖

#include<bits/stdc++.h>
using namespace std;
#define re register
#define il inline
il int read()
{
    re int x=0,f=1;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
    while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+(c^48),c=getchar();
    return x*f;
}//快讀,不理解的同學用cin代替即可
#define inf 123456789
#define maxn 5005
#define maxm 200005
struct edge
{
    int v,w,next;
}e[maxm<<1];
//注意是無向圖,開兩倍數組
int head[maxn],dis[maxn],cnt,n,m,tot,now=1,ans;
//已經加入最小生成樹的的點到沒有加入的點的最短距離,比如說1和2號節點已經加入了最小生成樹,那么dis[3]就等於min(1->3,2->3)
bool vis[maxn];
//鏈式前向星加邊
il void add(int u,int v,int w)
{
    e[++cnt].v=v;
    e[cnt].w=w;
    e[cnt].next=head[u];
    head[u]=cnt;
}
//讀入數據
il void init()
{
    n=read(),m=read();
    for(re int i=1,u,v,w;i<=m;++i)
    {
        u=read(),v=read(),w=read();
        add(u,v,w),add(v,u,w);
    }
}
il int prim()
{
    //先把dis數組附為極大值
    for(re int i=2;i<=n;++i)
    {
        dis[i]=inf;
    }
    //這里要注意重邊,所以要用到min
    for(re int i=head[1];i;i=e[i].next)
    {
        dis[e[i].v]=min(dis[e[i].v],e[i].w);
    }
    while(++tot<n)//最小生成樹邊數等於點數-1
    {
        re int minn=inf;//把minn置為極大值
        vis[now]=1;//標記點已經走過
        //枚舉每一個沒有使用的點
        //找出最小值作為新邊
        //注意這里不是枚舉now點的所有連邊,而是1~n
        for(re int i=1;i<=n;++i)
        {
            if(!vis[i]&&minn>dis[i])
            {
                minn=dis[i];
                now=i;
            }
        }
        ans+=minn;
        //枚舉now的所有連邊,更新dis數組
        for(re int i=head[now];i;i=e[i].next)
        {
            re int v=e[i].v;
            if(dis[v]>e[i].w&&!vis[v])
            {
                dis[v]=e[i].w;
            }
        }
    }
    return ans;
}
int main()
{
    init();
    printf("%d",prim());
    return 0;
}

優先隊列+堆優化

#include<cstdio>
#include<queue>
#include<cstring>
#include<algorithm>
#define R register int
using namespace std;

int k,n,m,cnt,sum,ai,bi,ci,head[5005],dis[5005],vis[5005];

struct Edge
{
    int v,w,next;
}e[400005];

void add(int u,int v,int w)
{
    e[++k].v=v;
    e[k].w=w;
    e[k].next=head[u];
    head[u]=k;
}

typedef pair <int,int> pii;
priority_queue <pii,vector<pii>,greater<pii> > q;

void prim()
{
    dis[1]=0;
    q.push(make_pair(0,1));
    while(!q.empty()&&cnt<n)
    {
        int d=q.top().first,u=q.top().second;
        q.pop();
        if(vis[u]) continue;
        cnt++;
        sum+=d;
        vis[u]=1;
        for(R i=head[u];i!=-1;i=e[i].next)
            if(e[i].w<dis[e[i].v])
                dis[e[i].v]=e[i].w,q.push(make_pair(dis[e[i].v],e[i].v));
    }
}

int main()
{
    memset(dis,127,sizeof(dis));
    memset(head,-1,sizeof(head));
    scanf("%d%d",&n,&m);
    for(R i=1;i<=m;i++)
    {
        scanf("%d%d%d",&ai,&bi,&ci);
        add(ai,bi,ci);
        add(bi,ai,ci);
    }
    prim();
    if (cnt==n)printf("%d",sum);
    else printf("orz");
}

這是啥。。。

#include<cstring>
#include<cstdio>
#include<vector>
#include<queue>
using namespace std;
int n,m,x,y,z,tot,ans,k,ds[1001],next[2001],st[1001],to[2001],cost[2001];
bool vis[1001];
void addedge(int x,int y,int z)
{
	next[++tot]=st[x];st[x]=tot;to[tot]=y;cost[tot]=z;
}
struct node
{
	int x,d;
	node(int a,int b):x(a),d(b) {}
	bool operator<(const node&t) const {return d>t.d;}
}; priority_queue<node>q;
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d%d",&x,&y,&z);
	    addedge(x,y,z);addedge(y,x,z); 
	}
	memset(ds,0x7f,sizeof(ds));
	ds[1]=0;q.push(node(1,0));vis[0]=1;
	for(int i=1;i<=n;i++)
	{
		node a(0,0);
		while(vis[a.x]&&!q.empty())
          a=q.top(),q.pop();
		if(vis[a.x]) break;
		ans+=a.d;vis[a.x]=1;k++;
		for(int j=st[a.x];j;j=next[j])
		{
			y=to[j];z=cost[j];
			if(!vis[y]&&z<ds[y])
		      ds[y]=z,q.push(node(y,z));
		}  
	}
	if(k!=n) printf("-1");
	else printf("%d",ans);
}

kruskal和prim的比較


從策略上來說,Prim算法是直接查找,多次尋找鄰邊的權重最小值,而Kruskal是需要先對權重排序后查找的~

所以說,Kruskal在算法效率上是比Prim快的,因為Kruskal只需一次對權重的排序就能找到最小生成樹,而Prim算法需要多次對鄰邊排序才能找到~

prim:該算法的時間復雜度為O(n2)。與圖中邊數無關,該算法適合於稠密圖。

kruskal:需要對圖的邊進行訪問,所以克魯斯卡爾算法的時間復雜度只和邊又關系,可以證明其時間復雜度為O(eloge)。適合稀疏圖

接下來講幾道例題

[UVALive 6437]Power Plant

T組數據,給定一幅帶權圖(n, m), 然后給定k個點, 與圖中存在有若
干條邊。每個點都要至少要和這k個點的一個點直接或間接相連, 問最少
的距離是多少。
1 ≤ T ≤ 100,

k個點,至少一個,明顯的縮點。

[UVA 1151]Buy or Build

平面上有n個點,你的任務是讓所有n個點連通,為此,你可以新建
一些邊,費用等於兩個端點的歐幾里得距離的平方。另外還有q個套餐,
可以購買,如果你購買了第i個套餐,該套餐中的所有結點將變得相互
連通,第i個套餐的花費為ci。求最小花費。

1 ≤ n ≤ 1000, 0 ≤ q ≤ 8。

枚舉選擇哪個套餐后再求最小生成樹即可。

[UVA 10369]Arctic Network

南極有n個科研站,要用衛星或無線電把他們連起來,無線電的費
用隨着距離增加而增加,並且長傳播距離為d,現在有s個衛星,任意兩
個安裝了衛星的設備無論距離多遠都可以直接通信,求一個方案使
得d最小。

s ≤ 1時求最小生成樹即可

s ≥ 2時,等於孤立了s − 1個區域,即s − 1條邊置為0,當然是最小
生成樹中最大的s − 1條。
kruskal的過程中直接計算即可。

[BZOJ 1601][Usaco2008 Oct]灌水

Farmer John已經決定把水灌到他的n(1¡=n¡=300)塊農田,農田被數
字1到n標記。把一塊土地進行灌水有兩種方法,從其他農田飲水,或者
這塊土地建造水庫。建造一個水庫需要花費wi
,連接兩塊土地需要花
費pij。計算Farmer John所需的最少代價。

1 ≤ N ≤ 300, 1 ≤ wi ≤ 105
, 1 ≤ pij ≤ 105。

每個水庫要么選擇自己這里建造水庫,要么選擇連一條邊到已建成
的水庫。
假設所有的水庫最終都選擇好了一個決策的話,那么整個圖就是,
分成m塊,每一塊有一個點是自己建造水庫的。其他都是順着邊連到這
個點的。也就是在這個子圖當中做最小生成樹。

加一個超級源,每個點向源連花費wi邊。然后在整個圖中做最小生
成樹。超級源的連通保證了至少有一個點建造了水庫

還有一些比較難的題,等我自己搞懂了再補吧~

完結撒花


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM