ac之次小生成樹

修路方案

時間限制:3000 ms  |  內存限制:65535 KB
難度:5
描述

南將軍率領着許多部隊,它們分別駐紮在N個不同的城市裏,這些城市分別編號1~N,由於交通不太便利,南將軍準備修路。

現在已經知道哪些城市之間可以修路,如果修路,花費是多少。

現在,軍師小工已經找到了一種修路的方案,能夠使各個城市都聯通起來,而且花費最少。

但是,南將軍說,這個修路方案所拼成的圖案很不吉利,想讓小工計算一下是否存在另外一種方案花費和剛纔的方案一樣,現在你來幫小工寫一個程序算一下吧。

輸入
第一行輸入一個整數T(1<T<20),表示測試數據的組數
每組測試數據的第一行是兩個整數V,E,(3<V<500,10<E<200000)分別表示城市的個數和城市之間路的條數。數據保證所有的城市都有路相連。
隨後的E行,每行有三個數字A B L,表示A號城市與B號城市之間修路花費爲L。
輸出
對於每組測試數據輸出Yes或No(如果存在兩種以上的最小花費方案則輸出Yes,如果最小花費的方案只有一種,則輸出No)
樣例輸入
2
3 3
1 2 1
2 3 2
3 1 3
4 4
1 2 2
2 3 2
3 4 2
4 1 2
樣例輸出
No

Yes


題目分析求次小生成樹一般有兩種辦法:

a.利用克魯斯卡爾先生成一次最小生成樹並保存每條樹上的邊的下標,然後遍歷這些樹上的邊,然後在把每次樹上的邊執行刪除-->重新建樹-->更新最小權值-->恢復的過程,每次刪除樹一條邊後,就破壞了原來的樹,重新建成的樹一定不是原來的樹,重新建成的樹的最小權值就是次小的,注意每次刪邊的時候如果刪掉的是橋從而導致圖的不聯通,所以在克魯斯卡爾中需要利用並查集統計圖中的聯通分量的個數,如果整個圖不聯通返回無窮大,使權值本次更新一定失敗。

複雜度O(n-1*優化後的克魯斯卡爾的複雜度)


#include<iostream>//導入輸入輸出流頭文件
#include<algorithm>//導入頭文件
using namespace std;//導入標準命名空間
typedef struct edge//定義邊的結構體
{
    int v,w,d;//定義一條邊的兩個端點及其邊長
    bool is_delete;//定義是否被刪除標誌
} Edge;
#define MAXN 200007//定義最大邊長
#define MAXNUM 0x3f3f3f3f//定義無窮大值
Edge bian[MAXN];//建立邊的數組
int pre[MAXN];//定義集合父節點數組
int n,e;//定義邊的數目和點的數目
int MST_id[MAXN];//定義樹的下標保存數組
int mid_i;//生成樹數組的下標
int cnt;//定義圖內連同分量的個數
bool cmp(Edge a,Edge b)
{
    return a.d<b.d;
}
int Find(int x)//壓縮路徑
{
    int temp,p=x;
    while(x!=pre[x])
    x=pre[x];//先找到x所屬集合的根節點
    while(p!=x)//更新路徑上的根節點全部壓縮
    {
        temp=pre[p];//從x到根節點路徑遍歷
        pre[p]=x;
        p=temp;
    }
    return x;//返回該集合的根節點
}
int kruskal(int p)//進入克魯斯卡爾算法
{
    int sum=0;//定義最小生成樹權值和sum
    for(int i=0;i<e;i++)//遍歷每條邊
    {
        int p1=Find(bian[i].v);
        int p2=Find(bian[i].w);
        if(bian[i].is_delete==false&&p1!=p2)//如果這條邊沒有被標記刪除,並且兩個點集不一個父節點
        {
            cnt--;//集合數目減一個
            pre[p1]=pre[p2];//父節點改變
            sum+=bian[i].d;//權值和加這條邊的權值
            if(p==1)//如果是第一次生成
            {
                MST_id[mid_i++]=i;//記錄最小生成樹的邊的id
            }
            if(cnt==1)
                break;
        }
    }
    if(cnt==1)//如果最後全圖聯通返回權值和
    return sum;
    else//否則返回無窮大
        return MAXN;
}
void init()//初始化父節點和集合個數cnt
{
    for(int i=0;i<=n;i++)
        pre[i]=i;
        cnt=n;
}
int main()
{
    ios::sync_with_stdio(false);//優化輸入輸出
    int ncase;//定義測試組數
    cin>>ncase;
    while(ncase--)
    {
        cin>>n>>e;//給定頂點數和邊數
       mid_i=0;
        init();//初始化
        for(int i=0; i<e; i++)
        {
            int v,w,d;
            cin>>v>>w>>d;
            bian[i].d=d;
            bian[i].v=v;
            bian[i].w=w;//建圖
            bian[i].is_delete=false;
        }
        sort(bian,bian+e,cmp);//排序
        int sum=kruskal(1);//獲得最小權值並標記最小生成樹的邊
         int tsum=MAXNUM;
        for(int i=0;i<mid_i;i++)
        {
            //cout<<mid_i<<endl;
            init();//每次初始化
            bian[MST_id[i]].is_delete=true;//把最小生成樹的一條邊刪除
            tsum=min(kruskal(0),tsum);//更新最小值
            bian[MST_id[i]].is_delete=false;//恢復這條邊
        }
        if(tsum==sum)//如果數目大於0,就存在
        {
            cout<<"Yes"<<endl;
        }
        else
            cout<<"No"<<endl;
    }
}

第二種是藉助普里找不在樹中最大邊看看原圖中有沒有與之等價的,有就說明不唯一

O(n^2)

#include<iostream>//導入輸入輸出流函數
#include<string.h>//用到memset
using namespace std;//導入標準命名空間
#define MAXN 505//定義最大的定點數
#define MAXNUM 0x3f3f3f3f//定義無窮大
int vis[MAXN];//訪問標記數組
int graph[MAXN][MAXN];//矩陣圖
int n,e;//頂點數,邊數
int low[MAXN];//保存已經建好的樹的頂點到各個點的最小值
int MST[MAXN][MAXN];//i到j路徑上的最小值
int is_MST[MAXN][MAXN];//是否在樹中
int pre[MAXN];//確定的那一點的邊上的另一個節點
int csum=MAXNUM;
int sum=0;
void init()
{
    memset(graph,0x3f,sizeof(graph));//初始化圖
    for(int i=0;i<=n;i++)
        graph[i][i]=0;//將自己和自己設置成0
      memset(MST,0,sizeof(MST));//初始化爲0
    memset(is_MST,0,sizeof(is_MST));//初始化爲0
csum=MAXNUM;
 sum=0;
}
bool prim(int s)//prim算法
{


    for(int i=1;i<=n;i++)
    {
        low[i]=graph[s][i];//每個點到s的距離
        pre[i]=s;//每個點的直接前驅是s
        vis[i]=0;//初始化每個點都沒有被訪問過
    }
    vis[s]=1;//把s點設置訪問過
    for(int j=1;j<n;j++)//循環n-1次
    {
        int tmin=MAXN;//
        int fa=0;//
        int ti=-1;
        for(int i=1;i<=n;i++)
        {
            if(vis[i]==0&&low[i]<tmin)
            {
                tmin=low[i];
                ti=i;
            }
        }//找到最小的low以及下標
        sum+=tmin;
            vis[ti]=1;//設置爲已經找到
            fa=pre[ti];//取出ti這個點所在邊的另一個點
            is_MST[ti][fa]=is_MST[fa][ti]=1;//將這條邊標記爲在樹中
            for(int i=1;i<=n;i++)
            {
                if(vis[i]==1&&i!=ti)
                {
                    MST[i][ti]=MST[ti][i]=max( MST[i][fa],low[ti]);//更新最大的從其他已經在樹中點到剛找的點的路徑上的最大邊
                }
                if(!vis[i]&&low[i]>graph[ti][i])//如果沒有被訪問過,s鬆弛low
                {
                    low[i]=graph[ti][i];
                    pre[i]=ti;
                }
            }
        }

        for(int i=1;i<=n;i++)
        {
            for(int j=1;j<i;j++)
            {
                if(is_MST[i][j]==0&&MST[i][j]!=MAXNUM)//如果有邊並且沒在樹裏面
                {
                     //res=min(res,mst-path[i][j]+g[i][j]); 求具體的csum
                     csum=min(csum,sum-MST[i][j]+graph[i][j]);//求次小生成樹
                    if(graph[i][j]==MST[i][j])//如果在原圖中有等價的最大邊則一定存在
                    {
                        //return true;
                    }
                }
            }
        }
        return false;
}
int main()
{
    ios::sync_with_stdio(false);
    int ncase;
    cin>>ncase;
   // ios::sync_with_stdio(false);
    while(ncase--)
    {
        cin>>n>>e;
        init();
        for(int i=0;i<e;i++)
        {
            int v,w,d;
            cin>>v>>w>>d;
            graph[v][w]=d;
            graph[w][v]=d;
          //  cout<<i<<endl;
        }//建圖
prim(1);
    if(sum==csum)//如果原來的和新建的一樣,那麼就輸出yes
    {
        cout<<"Yes"<<endl;
    }
    else
    {
        cout<<"No"<<endl;
    }
    }
    return 0;
}

還是那句話點少用普里母

點多用克魯斯卡爾





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章