prim算法--prim算法求次小生成树--prim算法求限制K度生成树

在正常求次小生成树的时候相信大家都喜欢kruskal,毕竟因为太大的图,尤其是稀疏图,prim算法并不实用,而且占内存太多,有可能不让开。但是在解决次小生成树和限制度生成树时候prim算法不失为一种很好的方法,因为这两种情况空间复杂度,尤其是图的大小都并不太大。

从一开始学了prim算法,再到拿prim算法解决次小生成树的变形,直到今天彻底搞懂prim算法求解限制k度最小生成树的问题。我感觉无论如何边,唯一不变的一点就是:

把贪心贯彻到底!!!

最小生成树:朴素prim 与dijkstra相同,一直找最小,一直更新插入

次小生成树:在求解完最小生成树后,找一个未被使用的最小边进行判断

限制K度生成树: 在分块求解完最小生成树后,进行每一次从所有树中找一个差值最大的,重新建树,松弛更新

一:

先说一下普通的最小生成树的题,prim算法 的不优化和优化的两种算法。

1.不优化-详细请点http://blog.csdn.net/qq_33951440/article/details/52640783

POJ 1258 Agri-Net题为例

void prim(int cur)  
{  
    int sum=0;  
    for(int i=1;i<=n;i++)  
    {  
        dis[i]=mp[cur][i];  
    }  
    vis[cur]=1;   
      
    for(int i=1;i<n;i++)  
    {  
        int index=-1;  
        int minn=inf;  
        for(int j=1;j<=n;j++)  
        {  
            if(vis[j]==0&&dis[j]<=minn)  
            {  
                minn=dis[j];  
                index=j;  
            }  
        }  
        vis[index]=1;  
        sum+=minn;  
        for(int j=1;j<=n;j++)  
        {  
            if(vis[j]==0)  
            {  
                if(dis[j]>=mp[index][j])  
                {  
                    dis[j]=mp[index][j];  
                }  
            }  
        }  
    }  

之后就是优化后的prim算法了,用优先队列来搞定,复杂度降低成为了(nlogn),然而空间复杂度仍然是一个问题

priority_queue<node>q;
    while(!q.empty())
        q.pop();
    node fir;
    fir.v=cur,fir.val=0;
    q.push(fir);
    while(!q.empty())
    {
        node tp=q.top();
        q.pop();
        int u=tp.v;
        if(!tree[u])
        {
            ans+=tp.val;
            tree[u]=id;
            for(int i=1;i<n;i++)
            {
                if(!tree[i]&&mp[u][i]!=0&&dis[i]>mp[u][i])
                {
                    pre[i]=u;       
                    dis[i]=mp[u][i]; 
                    node temp;
                    temp.val=dis[i];
                    temp.v=i;
                    q.push(temp);
                }
            }
        }
    }
    return ans;

二.次小生成树

题意:在最小生成树的基础上,询问是否可以不经过最小生成树构造时候经过的边,重新更改一条或者多条边,使得权值和仍然与最小生成树的权值和相同。

思路:在构造最小生成树时候,把用到的边进行全部标记掉,然后再从未被标记的边中找出一条边进行更改。

注意:这个时候prim算法就会看出好处了,在空间允许的情况下我们可以直接用二维数组 类似于mp[i][j]=0 的形式直接把边标记点,而不非要同kruskal算法一样重新构造

具体的点击   

次小生成树模板-prim算法


三.限制K度最小生成树

题意:在普通最小生成树的基础上,题意允许你再多加K条边,构造一个 K度的最小生成树。

大体意思就是:

当把最小生成树都构造好(注意有可能不只是一棵树)之后再进行加入m条边,或者少于m条边,使得权值更小。

我不感觉我可以讲理论讲得很好,所以具体的请看大神写得国家队论文,http://wenku.baidu.com/view/8abefb175f0e7cd1842536aa.html

思路:在不加入总目标的那个点之前,把所有的点都尽可能能连起来,也就是构造尽量少的最小生成树,之后再把每一棵树中找到一个最短的边,把树与总目标点相连,如果还有多余的度可以让我们插入,我们就在所有点中找到一个可以松弛最大的,注意不是最小的,而是相比之下松弛更大的。

我在这里只是把代码一步步的注释,记录下自己一步步调试的发现Orz

以题POJ1639为例

#include<iostream>
#include<cstdio>
#include<cstring>
#include<map>
#include<climits>
#include<queue>
using namespace std;
const int N=30;
struct node
{
    int v,cap;
    bool friend operator < ( node a, node b)
    {
        return a.cap>b.cap;
    }
};
map<string,int> mp;
int g[N][N],dis[N],clo[N],pre[N],fst[N],max_side[N];
int n,m,k;
int Prim(int cur,int id)
{
    int ans=0;
    priority_queue<node>q;
    node fir;
    fir.v=cur,fir.cap=0;
    q.push(fir);
    while(!q.empty())
    {
        node tp=q.top();
        q.pop();
        int u=tp.v;
        if(!clo[u])
        {
            ans+=tp.cap;
            clo[u]=id;
            for(int i=1;i<n;i++)
            {
                if(!clo[i]&&g[u][i]!=0&&dis[i]>g[u][i])
                {
                    pre[i]=u;       //在point.v( u ) 起点的基础上进行了松弛,被松弛的边的父亲进行修改
                    dis[i]=g[u][i]; //修改当前树到达i点的最小权值
                    node temp;
                    temp.cap=dis[i];
                    temp.v=i;
                    q.push(temp);
                }
            }
        }
    }
    return ans;

} 

void update(int cur,int last,int maxside) //这一步的DFS是一个关键,因为之前我们已经知道了当前树如果与总根0相连,可以花费的最小权值,以及花费最小全知道的那个点fst[i]
{
    //因此,这个函数的根本意义是在于把当前树所有的点与fst[i]相连成为最小生成树的花费更新出来
    max_side[cur]=maxside>g[cur][last]?maxside:g[cur][last];  //max_side在这棵树上,从cur点开始他自己一直到最上层0中的最大的一条权值边的记录
    for(int i=1; i<n; i++)
        if(i!=last && g[cur][i]!=0 && (pre[cur]==i || pre[i]==cur))// i!=last 防止来回遍历, g[cur][i] 对于cur 与i 这个点是否有还未使用的边,pre[cur]==i||pre[i]==cur 是否cur i存在直接相连的关系
        {
            update(i,cur,max_side[cur]);
        }
}

void solve() //这一个函数是解决限制度生成树的关键
{
    for(int i=0; i<n; i++)
    {
        dis[i]=INT_MAX;
        clo[i]=pre[i]=fst[i]=0; //clo属于第几棵树,加入的标记, pre父节点,fst  在i所在的树中最大边fst[i]
    }
    int res=0,cnt=0;
    for(int i=1; i<n; i++) //对于未被访问过的点进行prim初步构造
    {
        if(!clo[i])
            res+=Prim(i,++cnt);
    }
    for(int i=1; i<n; i++)  //进行最大边的更改
    {
        int id=clo[i];      //第N棵树
        if(g[0][i]!=0&&(!fst[id]||g[0][i]<g[0][fst[id]]))
            fst[id]=i;      //当前树最小的与0连的权值的i节点
    }
    for(int i=1; i<=cnt; i++)   //加入所有边与 根节点(0)所连时花费的权值
    {
        res+=g[0][fst[i]];
        g[0][fst[i]]=g[fst[i]][0]=0;
        update(fst[i],0,0); //每次都重新更新一次树i的fst数组
    }
    k=k-cnt;  //接下来重复操作,直到度数满足条件
    while(k--)  //每次多加一条边,看看是否有一个在树中的点,直接到0,比原来在自己的生成树中的值还小(因为我们当初把所有和0连通的边都删除了,这次依次回代)
    {
        int tmp=0;
        for(int i=1; i<n; i++)  //找一个差值最大的
        {
            if(g[0][i]!=0&&(tmp==0||max_side[tmp]-g[0][tmp]<max_side[i]-g[0][i]))
                tmp=i;
        }
        if(max_side[tmp]<=g[0][tmp])  //比较一下是否比原来短,如果不行,那一定都没有了跳出就行了
            break;
        res=res-max_side[tmp]+g[0][tmp];//重新更新图和树
        g[0][tmp]=g[tmp][0]=0;
        int p=0;
        for(int i=tmp; pre[i]!=0; i=pre[i]) //对于已经找到的可以连接到总根的一个点,进行从原来的树上拆分,并且访问他的上一个节点,判断是否上一个节点可以通过这个点使用更小的权值,间接的与总根相连
            if(p==0||g[p][pre[p]]<g[i][pre[i]])
                p=i;
        pre[p]=0;
        update(tmp,0,0);
    }
    printf("Total miles driven: %d\n",res);
} 
int main()
{
    char s1[15],s2[15];
    cin>>m;
    mp.clear();
    mp["Park"]=0;
    n=1;
    while(m--)
    {
        int d;
        cin>>s1>>s2>>d;
        if(mp.count(s1)==0)
        {
            mp[s1]=n++;
        }
        if(mp.count(s2)==0)
        {
            mp[s2]=n++;
        }
        int u=mp[s1],v=mp[s2];
        if(g[u][v]==0||g[u][v]>d)
        {
            g[u][v]=d;
            g[v][u]=d;
        }
    }
    cin>>k;
    solve();
    return 0;
}

/*.
10
Alphonzo Bernardo 32
Alphonzo Park 57
Alphonzo Eduardo 45
Bernardo Park 19
Bernardo Clemenzi 89
Clemenzi Park 65
Clemenzi Herb 90
Clemenzi Eduardo 30
Park Herb 24
Herb Eduardo 79
3
*/





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值