图的两个基本元素是点和边,与此对应,有两种方法可以构造最小生成树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);
}
} 
京公网安备 11010502036488号