图的两个基本元素是点和边,与此对应,有两种方法可以构造最小生成树T。这两种算法都基于贪心算法,因为MST问题满足贪心算法的“最优性原理”,即全局最优包含局部最优。prim算法的原理是“最近的邻居一定在MST”上,kruskal算法的原理是“最短的边一定在MST上”。
第一次出现kruskal算法或prim算法
的题目,很多描述大多是紫书上的。


hdu 1233


题意
有n个村庄需要修通道路,已知每两个村庄之间的距离,问怎么修路,使得所有村庄都连通(但不一定有直接相连的公路,只要能间接的通过公路到达即可),并且道路总成最小?请计算最小的公路总长度。
前导知识
1.对边进行排序。可以用sort函数排序,排序后,依次把最短路的边加入到T中。
2.判断图,处理连通性问题。这个问题用并查集简单而高效,主要是判断所选的边与已经选了的边是否会构成环,如果会构成环就不选,并查集和kruskal算法是绝配。
思路
kruskal算法+并查集。
代码

#include<bits/stdc++.h>
using namespace std;
int fa[105];
struct Edge{
    int u,v,w;
    bool operator<(const Edge&a)const{
        return w<a.w;
    }
}edge[101*101];
int find(int u) {
    return fa[u]==-1?u:(fa[u]=find(fa[u]));
}
int n,m;
int kruskal() {
    int ans=0;
    memset(fa,-1,sizeof fa);
    sort(edge+1,edge+1+m);
    for(int i=1;i<=m;++i) {
        int b=find(edge[i].u);
        int c=find(edge[i].v);
        if(b==c) continue;
        fa[c]=b;
        ans+=edge[i].w;
    }
    return ans;
}
int main() {
    while(scanf("%d",&n),n) {
        m=n*(n-1)/2;
        for(int i=1;i<=m;++i)
            scanf("%d%d%d",&edge[i].u,&edge[i].v,&edge[i].w);
        printf("%d\n",kruskal());
    }
}

hdu 1102


简单题
题意
3
0 990 692
990 0 179
692 179 0
1
1 2
一共3个村子,下面是第i行j列 i村子和j村子之间建路需要的距离
下面是一个k
代表有k条路已经修好了,1村子和2村子之间已修路。问怎么修路,使得所有村庄都连通(但不一定有直接相连的公路,只要能间接的通过公路到达即可),并且道路总成最小?请计算最小的公路总长度。
前导知识
prim算法:对点进行贪心操作。从任意一个点u(这里假设从点u=1开始,起点dis[u]=0,更新dis[i]为i到u的距离)开始,把距离它最小的点v加入到T中;下一步,把距离{u,v}最近的点w加入到加入到T中(并更新dis[i]为到u或v的最短距离);继续这个过程,直到所有的点都在T中。
思路
prim算法。
Code

#include<bits/stdc++.h>
using namespace std;
const int maxn = 105;
int n,q,a,b;
int dis[maxn], vis[maxn], e[maxn][maxn];
int Prim() {
    int ans=0;
    for(int i=0; i<=n; ++i) dis[i]=100000, vis[i]=0; //初始化
    dis[1]=0; //prim算法任选起点,于是可以选择1为起点
    for(int i=1; i<=n; ++i) {
        int u=0; //注意dis[0]已经初始化为无穷大了,不用管了
        for(int j=1; j<=n; ++j) if(!vis[j]) {
            if(dis[j]<dis[u]) u=j;
        }
        vis[u]=1; ans+=dis[u]; //更新答案啦
        for(int j=1; j<=n; ++j) if(!vis[j]) { //拿u点来更新新拓展的边
            if(dis[j]>e[u][j]) dis[j]=e[u][j];
        }
    }
    return ans;
}
void solve() {
    for(int i=1; i<=n; ++i)
        for(int j=1; j<=n; ++j) scanf("%d", &e[i][j]);
    scanf("%d",&q);
    for(int i=0;i<q;++i) {
        scanf("%d%d",&a,&b);
        e[a][b]=e[b][a]=0;
    }
    printf("%d\n", Prim());
}
int main() {
    while(~scanf("%d", &n)) {
        solve();
    }
}

hdu 3938


离线算法+kruskal算法+并查集
题意:给你n个顶点,m条边,以及q次询问。
之后给出m条边的起末点及边权值,在q次询问,每次给你一个价值wal,问 这个价值你能在这个图里走多少种路。每一条路的权值是这条路中最大边的权值。
思路:用结构体存每次询问的价值和序号,按价值从小到大排序,因为小价值能走的路大价值也一定能走。每次加入一条边会连通两个集合,路径数就等于两个集合中元数的乘积,并查集可以做到存该集合的元数个数,kruskal算法选边是从小到大,所以放入的当前边一定是两个集合构成的所有路径的权值(该路径的起点、终点分别在不同的集合,如果在一个集合就会和前面的计算重了)。把目前算出的方案总数赋值给价值最小的询问,直到询问的最小价值小于当前的边,才把方案数赋值给价值大一点的(注意:因为可能询问价值大一点点也的还是小于当前的边,所以要把现在询问价值的路径数赋值给大一点的询问价值,如果大一点点的价值大于当前的边,就用现在路径总数跟新它),一直执行这个操作,直到边都选完了,如果还有询问的价值没有答案,说明价值偏大了,把现在的路径总数赋值给没有答案的询问,最后按顺序输出。
Code

#include<bits/stdc++.h>
using namespace std;
int size[100005],fa[100005],sum[100005];
struct node{
    int id,val;
    bool operator<(node a) const{
        return val<a.val;
    }
}q[100005];
struct edge{
    int u,v,val;
    bool operator<(edge a) const{
        return val<a.val;
    }
}f[100005];
int find(int x) {
    return x==fa[x]?x:(fa[x]=find(fa[x]));
}
int n,m,Q,ans,t;
void solve() {
    for(int i=1;i<=m;++i) {
        int x=find(f[i].u),y=find(f[i].v);
        if(x!=y) {
            ans+=size[x]*size[y];
            fa[x]=y;
            size[y]+=size[x];
            while(q[t].val<f[i].val) {
                ++t;    sum[q[t].id]=sum[q[t-1].id];
                //cout<<q[t].id<<"不配"<<sum[q[t].id]<<endl;
            }
            sum[q[t].id]=ans;
            //cout<<q[t].id<<" "<<sum[q[t].id]<<endl;
        }
    }
    while(t<Q) sum[q[++t].id]=ans;
}
int main() {
    while(scanf("%d%d%d",&n,&m,&Q)==3) {
        for(int i=1;i<=n;++i) {
            fa[i]=i;
            size[i]=1;
        }
        for(int i=1;i<=m;++i)
            scanf("%d%d%d",&f[i].u,&f[i].v,&f[i].val);
        sort(f+1,f+1+m);
        for(int i=1;i<=Q;++i) {
            scanf("%d",&q[i].val);
            q[i].id=i;
        }
        sort(q+1,q+1+Q);
        ans=0,t=1;
        solve();
        for(int i=1;i<=Q;++i) printf("%d\n",sum[i]);
    }
}

poj 2377


最小生成树的扩展,最大生成树。
题意:n个点m条边,接下来m行表示连接i和j的花费val,将所有的点连接,要保证不能成环,花费最大,所有的点都在一个连通块里。如果所有的点不在一个连通块就输出-1,否则输出最大花费。
思路:因为要判断是否所有的点都在一个连通块,所以用kruskal算法+并查集最好,把边按从大到小排序。记录合并了多少条边,如果所有的点在一个连通块,连接的边应该有n-1个。
Code

#include<stdio.h>  
#include<algorithm>  
using namespace std;
int n,m,ans=0,cnt=0,f[1005];
struct Node{
    int x,y,w;
    bool operator<(Node a) const{
        return w>a.w;
    }
}node[20005];
int find(int x){return x==f[x]?x:f[x]=find(f[x]);}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)f[i]=i;
    for(int i=1;i<=m;i++)
        scanf("%d%d%d",&node[i].x,&node[i].y,&node[i].w);
    sort(node+1,node+1+m);
    for(int i=1;i<=m;i++){
        int fx=find(node[i].x),fy=find(node[i].y);
        if(fx!=fy)f[fx]=fy,cnt++,ans+=node[i].w;
    }
    if(cnt==n-1)printf("%d\n",ans);
    else puts("-1");
}

hdu 5627


注意:和poj2377一样的思路,题目也差不多,就是如果全部的点不在一个连通块里就输出-1,而且最大生成树的值是每个边异或的值,不是每个边的和。这两个题目不建议都写,浪费时间。
Code

#include<bits/stdc++.h>
using namespace std;
int t,n,m,ans,cnt,f[300005];
struct node{
    int x,y,w;
    bool operator<(node a) const{
        return w>a.w;
    }
}q[300005];
int find(int x){return x==f[x]?x:f[x]=find(f[x]);}
int main(){
    scanf("%d",&t);
    while(t--) {
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;i++)f[i]=i;
        for(int i=1;i<=m;i++)
           scanf("%d%d%d",&q[i].x,&q[i].y,&q[i].w);
            sort(q+1,q+1+m);
            ans=cnt=0;
           for(int i=1;i<=m;i++){
            int fx=find(q[i].x),fy=find(q[i].y);
            if(fx!=fy)f[fx]=fy,cnt++,ans^=q[i].w;
        }
        if(cnt==n-1)printf("%d\n",ans);
        else puts("0");
    }
}

hdu 4081


次小生成树
题意:给n个位置的坐标以及每个坐标的人数,让所有的城市联通,并且我们还有一次施放魔法的机会(连接任意两个城市),让我们求A/B的最大值,A表示两个城市的人数 B表示剩余城市全部联通所需修建的路长。
思路:我们会发现如果我们要去掉的边g[i][j]在最小生成树上,那么我们直接用最小生成树的值减去我们要去掉的边的g[i][j],如果不在,那么直接用最小生成树的值减去path[i][j](表示加上g[i][j]这条边后要生成次小生成树要去的边的权值--次小是相对与这个限制条件而言,即加上边g[i][j]和保证树的值最小)。
那么path[][]怎么求:假设这时最小生成树新加的点是u,u和pre[u]是一条边上的端点,j已经在最小生成树里面了,那么可得path[j][u]=path[u][j]=max(path[j][pre[u]],dis[u])。
为什么u和j连线要去的边一定和u的另一个端点pre[u](dis[u]就是u和pre[u]的边)有关呢。
图片说明
比如求path[1][4],u=4,j=1,如果连接1-4,会形成一个环,而path[1][4]的值就是环中除了加入的边中权值最大的边,可以看成是pre[4]=2和1成环后要去的边(因为1-2是最小生成树上的,所有path[1][2]=0)或者u和pre[u]的边,两者之间取最大值。为什么不是max(path[1][5],dis[4])呢,显然会出现不连通的情况。
上面我解释不清,下面再扯一点。
最小生成树生成后,假设加入我们要连1-4(1假设设是不等于u和pre[u]的任意点,2假设是pre[u],4假设是u),那么将就会生成一个环,我们先把某条权值最大的边去掉,1和2会被分成两个连通块或者只要去掉2-4(也就是u-pre[u])就可以。如果1和2被分成两个连通块,由于1-4和2-4的存在,所有的点又将联通,如果是去掉2-4,由于1-4的存在,所有的点又将联通。所有只有这样的表达式才能保证在求得的path[j][u]尽可能大的情况下所有的点是连通的。
code

#include<bits/stdc++.h>
#define    scanf1(a) scanf("%d",&a)
#define    scanf2(a,b) scanf("%d%d",&a,&b)
#define mes(a,b)    memset(a,b,sizeof a)
#define inf 0x3f3f3f3f
using namespace std;
double g[1005][1005],path[1005][1005],pos[1005][2],dis[1005],cost[1005],mincost,ans;
int fa[1005],val[1005],pre[1005],used[1005][1005];
int t,n;
int find(int x) {
    return x==fa[x]?x:(fa[x]=find(fa[x]));
}
void prime() {
    for(int i=1;i<=n;++i)
        for(int j=1;j<=n;++j)    path[i][j]=used[i][j]=0;
    for(int i=1;i<=n;++i) {
        fa[i]=i;
        val[i]=0;
        dis[i]=g[1][i];
        pre[i]=1;
    }
    val[1]=1;
    for(int i=1;i<n;++i) {
        int u=0;
        for(int j=1;j<=n;++j) if(!val[j]){
            if(dis[j]<dis[u])    u=j;
        }
        used[u][pre[u]]=used[pre[u]][u]=1;
        mincost+=g[pre[u]][u];
        val[u]=1;
        for(int j=1;j<=n;++j) {
            if(!val[j]) {
                if(dis[j]>g[u][j]) {
                    dis[j]=g[u][j];
                    pre[j]=u;
                }
            }
            else if(j!=u)
                path[j][u]=path[u][j]=max(path[j][pre[u]],dis[u]);
        }
    }
}
inline double get(double x,double y, double a,double b) {
    return sqrt((x-a)*(x-a)+(y-b)*(y-b));
}
int main() {
    scanf1(t);
    dis[0]=inf;
    while(t--) {
        scanf1(n);
        for(int i=1;i<=n;++i)    scanf("%lf%lf%lf",&pos[i][0],&pos[i][1],&cost[i]);
        for(int i=1;i<n;++i)
            for(int j=i+1;j<=n;++j) g[j][i]=g[i][j]=get(pos[i][0],pos[i][1],pos[j][0],pos[j][1]);
        mincost=ans=0.0;
        prime();
        for(int i=1;i<=n;++i)
        for(int j=1;j<=n;++j) if(i!=j)
            if(used[i][j])
                ans=max(ans,(cost[i]+cost[j])/(mincost-g[i][j]));
            else
                ans=max(ans,(cost[i]+cost[j])/(mincost-path[i][j]));
        printf("%.2f\n",ans);
    }
}

hdu 4126


题意:(次小生成树)
给出一个连通的无向图,每一条边都有权值,现在有q次询问,每次都会更改一条边的cost,问q次询问更改之后的平均最小生成树是多少。(n个城市,m条路,编号0 ~ n-1)
.注意更改的变的cost比原来大。
.所有询问互不干扰
思路:考虑到一条边改变有两种情况
1.改的边不是生成树上的边,对生成树的权值没有影响,最近加上mincost。
2.改的边是原来生成树上的边,把这条边去掉就会形成两棵树,只要找到这两颗树的最小距离就是生成树新边的权值。
树形dp求出非生成树的边最短距离连接两颗断掉的最小生成树,dp[u][v]表示生成树上的边u-v断掉后从u的子树到v子树的最短距离,也就意味着找一条新边替换原来的边。
图片说明
dfs求dp[u][pre[u]]的原理:(以上图为例,画图后真的好理解多了)
1.dfs(5,5,-1)最后一层求得cost=g[5][3]=9,返回上一层时dp[3][4]=min(dp[3][4],上一层的cost)=9,cost=min(上一层返回值cost,g[5][4])=9,继续同一层求得cost=g[5][1]=6,返回到上一层时dp[2][4]=9,dp[2][1]=6(过程不累赘了),cost=min(6,9)=6。最后回到第一层得dp[5][2]=6。注意:为什么dp[3][4]不可以等于遍历过程中边的始端到4到5的距离,而一定要是终端3到5的距离呢?结合dp[u][v]的意义,因为5和4在同一个子树,如果dp[3][4]可以取g[4][5],那么就会成环。
其实就是这样一个过程,从一个点r出发,r的另一个端点是s,现在点的位置是u,边的另一端是v,求得(此时s!=v)g[r][v]中的最小值,最后将最小值复制给dp[5][2],也就是说一个作用是求结点5到结点2所在子树的最短距离(非生成树上的最小边)。但是这样还不够,dp[u][v]是u子树到v子树的最短距离(非生成树上的最小边),从一个点r出发,r的另一个端点是s,现在点的位置是u(u前面的一个点是k),边的另一端是v,求得(此时s!=v)g[r][v]或min(g[r][v],g[r][u]),接着dp[u][v]=min(dp[u][v],g[r][v])或者dp[k][u]=min(min(g[r][v],g[r][u]),dp[k][u]),比如dp[3][4]=9,dp[2][4]=min(9,10)=9,也就是说另一个作用就是可以取到u(比如4)子树上的结点r(比如5)到v(比如3)的距离。
我都不知道自己在说什么了,看不懂不要太纠结,参考就行,多画图,这个图就不错(不是说画画画得好,是这个模型好,没讨论的边没画)。
for(int i=0;i<n;++i) dfs(i,i,-1);递归出口就是取到i结点到边的另一端的子树的最短距离,子递归就是求边的始端子树中的结点i到总端子树的最短距离。所以dfs每一个点就可以取到u子树到v子树的最短距离(非生成树上的最小边)。
Code

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int inf=0x3f3f3f3f;
int n,m,q,e,mincost;
int dis[3000],val[3000],g[3000][3000],dp[3000][3000];
int pre[3000],Next[6000],head[3000],node[6000];
void addedge(int u,int v) {
    node[e]=v;            //属于顶点u的e边的另一个端点是v 
    Next[e]=head[u];    //Next[e]数组记录顶点u在边e之前的一个边; 
    head[u]=e++;        //head[u]更新下一个顶点u的Next[e]数组的取值
    //head[u]的最终取值还是u中最先遍历的边 
}
void prim() {
    for(int i=1;i<n;++i) if(dis[i]>g[0][i]) {
            dis[i]=g[0][i];
            pre[i]=0;
    }
    val[0]=1;    e=mincost=0;
    for(int i=0;i<n-1;++i) {
        int u=-1;
        for(int j=1;j<n;++j)    if(!val[j]&&(u==-1||dis[u]>dis[j])) u=j;
        mincost+=dis[u];
        val[u]=1;
        if(pre[u]!=-1) {
            addedge(u,pre[u]);
            addedge(pre[u],u);
        }
        for(int j=1;j<n;++j) if(!val[j]&&(dis[j]>g[u][j])) {
            dis[j]=g[u][j];
            pre[j]=u;
        }
    }
}
int dfs(int r,int v,int u) {
    int cost=inf;
    for(int i=head[v];i!=-1;i=Next[i]) if(node[i]!=u) {
        int temp=dfs(r,node[i],v);
        cost=min(cost,temp);
        dp[v][node[i]]=dp[node[i]][v]=min(temp,dp[node[i]][v]);
    }
    if(r!=u) cost=min(cost,g[r][v]);
    return cost;
}
int u,v,w;
int main() {
    while(scanf("%d%d",&n,&m)&&n+m) {
        for(int i=0;i<n;++i) {
            for(int j=0;j<i;++j)
            g[i][j]=g[j][i]=dp[i][j]=dp[j][i]=inf;
            pre[i]=-1;
            val[i]=0;
            dis[i]=inf;
        }
        memset(head,-1,sizeof(head));
        for(int i=0;i<m;++i) {
            scanf("%d%d%d",&u,&v,&w);
            g[u][v]=g[v][u]=w;
        }
        prim();
        for(int i=0;i<n;++i)
            dfs(i,i,-1);
        scanf("%d",&q);
        double ans=0.0;
        for(int i=0;i<q;++i) {
            scanf("%d%d%d",&u,&v,&w);
            if(pre[u]==v||pre[v]==u)
                ans+=mincost-g[u][v]+min(w,dp[u][v]);
            else
                ans+=mincost;
        }
        printf("%.4f\n",ans/q);
    }
}