达神上次给我提了一个建议,对算法进行数学证明,我也是这么想的,但是这样的话,我就需要在自学一遍算法学了,不过学习嘛,就是这样
所以,再过几期之后我会开始自学算法学,尽力将数学证明过程添加到讲解里面,让算法变的更加的“原来如此”
步入正题:
最小生成树算法汇总
1.Krustral(贪心+并查集+排序)
1.算法的思路:
Krustral算法通过边集数组来保存图中的边的信息,我们通过对边集数组按照边的权重进行排序后,按照从小到大的顺序每次选择一条边(头和尾不在不在一个集合中(并查集判断))知道我们选择完了n-1条边之后,最小生成树酒构造完毕(算法的贪心的证明我以后会给出)
2.算法举例描述(无情的粘自百度百科,毕竟人家有图言卵):
3.代码示例:
#include"iostream"
#include"cstdio"
#include"cstring"
using namespace std;
typedef struct node
{
int x,y;
int weight;
}e;
e edge[100];
int fa[100];
int deep[100];
int n,m;
int sum=0;
void init()
{
for(int i=1;i<=n;i++)
{
fa[i]=i;
deep[i]=1;
}
}
int find(int x)
{
if(x==fa[x]) return x;
else return fa[x]=find(fa[x]);
}
void unit(int x,int y)
{
x=find(x);
y=find(y);
if(x==y) return ;
else
{
if(deep[x]>deep[y]) fa[y]=x;
else
{
fa[x]=y;
if(deep[x]==deep[y]) deep[y]++;
}
}
}
bool same(int x,int y)
{
return find(x)==find(y);
}
void quicksort(int left,int right)
{
if(left>right) return ;
else
{
int i=left;
int j=right;
e t;
e temp=edge[left];
while(i!=j)
{
while(i<j&&edge[j].weight>=temp.weight) j--;
while(i<j&&edge[i].weight<=temp.weight) i++;
if(i<j)
{
t=edge[i];
edge[i]=edge[j];
edge[j]=t;
}
}
edge[left]=edge[i];
edge[i]=temp;
quicksort(left,i-1);
quicksort(i+1,right);
return ;
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++) cin>>edge[i].x>>edge[i].y>>edge[i].weight;
init();
quicksort(1,m);
for(int i=1;i<=n-1;)
{
if(!same(edge[i].x,edge[i].y))
{
unit(edge[i].x,edge[i].y);
sum+=edge[i].weight;
i++;
}
}
cout<<sum<<endl;
return 0;
}
4.数据结构的选择:
因为我们要涉及到边集的排序,所以我们选择边集数组算是一个不错的选择
5.总结:
Krustral算法通过利用贪心的策略,每一次都是选择相对小最小的权值的边,直至构建出来最小生成树,在这里,排序算法我推荐快排,冰茶记得作用是通过判断是否具有仙童的父亲来判断这条边是否会构成回路,从而方便我们进行选择。
2.Prim(贪心+扩展)
1.算法的思路:
在这里Prim算法思路和Dijstra算法的思路有一些类似,我们都通过开辟内存空间记录距离,但是区别在于,Dijstra算法记录的是单元最短路距离,而,Prim算法的记录数组记录的是点到生成树的最短距离
2.算法的过程:
初始的时候选择一个开始源点,此时生成树中只有她一个源点,所以此时dis数组的含义就是其他店到该生成树上的最短距离,然后我们选择最近的一个加入到生成树中,形成新的生成树,然后进行dis数组的维护,知道所有的点都已经在生成树中为止
3.代码示例:
3.1》》朴素Prim:
#include"iostream"
#include"cstdio"
#include"cstdlib"
using namespace std;
int n,m;
int dis[100];
int u[100];
int v[100];
int w[100];
int first[100];
int nextk[100];
int book[100];
int sum=0;
int inf=99999999;
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
dis[i]=inf;
first[i]=-1;
book[i]=0;
}
memset(nextk,0,sizeof(nextk));
dis[1]=0; //我把 1 当作初始扩展节点
book[1]=1;
for(int i=1;i<=m;i++) cin>>u[i]>>v[i]>>w[i];
for(int i=m+1;i<=2*m;i++) //无向图
{
u[i]=v[i-m];
v[i]=u[i-m];
w[i]=w[i-m];
}
for(int i=1;i<=2*m;i++)
{
nextk[i]=first[u[i]];
first[u[i]]=i;
}
int k=first[1];
while(k!=-1)
{
dis[v[k]]=w[k];
k=nextk[k];
}
for(int i=1;i<=n-1;i++)
{
int minpoint;
int mink=inf;
for(int j=1;j<=n;j++) //寻找最近的节点
{
if(book[j]==0&&dis[j]<mink)
{
mink=dis[j];
minpoint=j;
}
}
book[minpoint]=1;
sum+=dis[minpoint];
k=first[minpoint];
while(k!=-1) //松弛出边
{
if(dis[v[k]]>w[k]) dis[v[k]]=w[k];
k=nextk[k];
}
}
cout<<sum<<endl;
return 0;
}
3.2》》堆优化:
#include"iostream"
#include"cstdio"
#include"cstring"
#include"cstdlib"
using namespace std;
int n,m;
int u[100];
int v[100];
int w[100];
int first[100];
int nextk[100];
int dis[100];
int heap[100];
int pos[100];
int sum=0;
int inf=99999999;
int heapnum=0;
void swap(int x,int y)
{
int t=heap[x];
heap[x]=heap[y];
heap[y]=t;
t=pos[heap[x]]; //同步调整
pos[heap[x]]=pos[heap[y]];
pos[heap[y]]=t;
}
void siftdown(int i)
{
int t,flag=0;
while(i*2<=heapnum&&flag==0)
{
if(dis[heap[i]]>dis[heap[i*2]]) t=i*2;
else t=i;
if(i*2+1<=heapnum&&dis[heap[i*2+1]]<dis[heap[t]]) t=i*2+1;
if(t!=i)
{
swap(i,t);
i=t;
}
else flag=1;
}
}
void siftup(int i)
{
int t,flag=0;
while(i!=1&&flag==0)
{
if(dis[heap[i]]<dis[heap[i/2]])
{
swap(i,i/2);
i=i/2;
}
else flag=1;
}
}
int pop()
{
int t=heap[1];
swap(1,heapnum);
heapnum--;
siftdown(1);
return t;
}
int main()
{
cin>>n>>m;
memset(first,-1,sizeof(first));
memset(nextk,0,sizeof(nextk));
for(int i=1;i<=n;i++)
{
dis[i]=inf;
heap[i]=pos[i]=i;
}
for(int i=1;i<=m;i++) cin>>u[i]>>v[i]>>w[i];
for(int i=1+m;i<=2*m;i++)
{
u[i]=v[i-m];
v[i]=u[i-m];
w[i]=w[i-m];
}
for(int i=1;i<=2*m;i++)
{
nextk[i]=first[u[i]];
first[u[i]]=i;
}
int k=first[1];
dis[1]=0; //同样以 1 为初始节点
heapnum=n;
while(k!=-1)
{
dis[v[k]]=w[k];
k=nextk[k];
}
for(int i=n/2;i>=1;i--) siftdown(i); //堆初始化
pop();
for(int i=1;i<=n-1;i++)
{
int d=pop(); //弹出节点
sum+=dis[d];
k=first[d];
while(k!=-1)
{
if(dis[v[k]]>w[k])
{
dis[v[k]]=w[k];
siftup(pos[v[k]]); //按照dis数组进行堆调整
}
k=nextk[k];
}
}
cout<<sum<<endl;
return 0;
}
4.数据结构的选择:
因为我们要涉及到对节点出边松弛,所以说我们用数组链表(邻接表)可以很简单的实现
5.堆优化:
通过对来进行优化,我们可以降低选边的复杂度,从而加快prim算法的速度
3.路径记录的策略:
其实我们都可以用二维数组的方式来记录两个顶点之间是否存在边,但是二维数组的空间消耗太大,所以引出下面的方法
1.Krustral算法:
Krustral算法应用了边集数组,所以我们可以直接记录边就可以记录下来最小生成树中的边
2.Prim算法:
Prim算法因为是采用了邻接表还不断进进行了变得优化,看样子好像无法记录,但是实际上,我们可以开辟一个前驱数组记录在最小生成树中直接连
#include"iostream"
#include"cstdio"
#include"cstring"
#include"cstdlib"
using namespace std;
int n,m;
int u[100];
int v[100];
int w[100];
int first[100];
int nextk[100];
int dis[100];
int heap[100];
int pos[100];
int sum=0;
int inf=99999999;
int heapnum=0;
int pre[100];
typedef struct node
{
int x,y;
int weight;
}e;
e edge[100];
void swap(int x,int y)
{
int t=heap[x];
heap[x]=heap[y];
heap[y]=t;
t=pos[heap[x]]; //同步调整
pos[heap[x]]=pos[heap[y]];
pos[heap[y]]=t;
}
void siftdown(int i)
{
int t,flag=0;
while(i*2<=heapnum&&flag==0)
{
if(dis[heap[i]]>dis[heap[i*2]]) t=i*2;
else t=i;
if(i*2+1<=heapnum&&dis[heap[i*2+1]]<dis[heap[t]]) t=i*2+1;
if(t!=i)
{
swap(i,t);
i=t;
}
else flag=1;
}
}
void siftup(int i)
{
int t,flag=0;
while(i!=1&&flag==0)
{
if(dis[heap[i]]<dis[heap[i/2]])
{
swap(i,i/2);
i=i/2;
}
else flag=1;
}
}
int pop()
{
int t=heap[1];
swap(1,heapnum);
heapnum--;
siftdown(1);
return t;
}
int main()
{
cin>>n>>m;
memset(first,-1,sizeof(first));
memset(nextk,0,sizeof(nextk));
for(int i=1;i<=n;i++)
{
dis[i]=inf;
heap[i]=pos[i]=pre[i]=i; //开始每个点的前驱都是自己,因为 1 是厨师的节点所以1的前驱永远是自己,这一点反而可以用作我们判断
}
for(int i=1;i<=m;i++) cin>>u[i]>>v[i]>>w[i];
for(int i=1+m;i<=2*m;i++)
{
u[i]=v[i-m];
v[i]=u[i-m];
w[i]=w[i-m];
}
for(int i=1;i<=2*m;i++)
{
nextk[i]=first[u[i]];
first[u[i]]=i;
}
int k=first[1];
dis[1]=0; //同样以 1 为初始节点
heapnum=n;
while(k!=-1)
{
dis[v[k]]=w[k];
pre[v[k]]=1;
k=nextk[k];
}
for(int i=n/2;i>=1;i--) siftdown(i); //堆初始化
pop();
for(int i=1;i<=n-1;i++)
{
int d=pop(); //弹出节点
sum+=dis[d];
k=first[d];
while(k!=-1)
{
if(dis[v[k]]>w[k])
{
dis[v[k]]=w[k];
pre[v[k]]=u[k]; //修改前驱
siftup(pos[v[k]]); //按照dis数组进行堆调整
}
k=nextk[k];
}
edge[i].x=pre[d];
edge[i].y=d;
edge[i].weight=dis[d];
}
for(int i=1;i<=n-1;i++)
{
cout<<edge[i].x<<'-'<<edge[i].y<<'-'<<edge[i].weight<<endl;
}
cout<<sum<<endl;
return 0;
}
4.次小生成树(动态规划+最小生成树)
1.算法描述:
我们发现在最小生成树上加一条边必然会构成一个环,那么如果我枚举出来所有的环得情况,然后都删除掉环中
第二大的边,找到最小的那种变更情况不就是次小生成树了吗,在这里我要对第二大进行一下简单的解析,如果加了一条不在生成树上的边,那么这条边必然是加后环里面最大的边,为什么呢,如果这条边在不再生成树里面,说明它比环的和还要大,所以必然是最大得,我们只要删除第二大的就好,因为这样才可以保证是次小的
我们用反证法来证明一下,构成环的那一个新家的边一定是乘的环里面最大的一条边
我们加上这条不在生成树中的边,构成了环,如果该边比环中的其中一条边的权值小,那么我们完全可以删除比他大的边构成一个更小的最小生成树,但是实际上我们意境的到了最小生成树,所以我们的假设是错误的
即新的加的那一条边必定是新城的环中最大的一条边
2.算法的实现:
在这里,我们用到了动态规划的思想,我们开辟所谓的dp二位数组,dp[i][]j用来记录从i到j的路径中最大的那条边的权值
在这里动态转移方程是dp[i][j]=max(dp[i][j],min) 【min的含义是目前我们在进行最小生成树的构建过程中,挑选出来的最短的边的权值】
最后我们只要枚举出所有不在最小生成树上的点之后,统一计算一下新的生成树的权值求出最小的那就是次小生成树的权值
在这里如果我们要记录边的话,需要再开辟一个记录内存保存当前的最小的次小生成树的边的状态,如果遇到更小的,用原来的最小生成树的替换一下就好(还有好方法吗?)
3.注意要点:
因为这里我们需要枚举边,所以说我们最好用二维数组来保存二者之间是否存在边,空间换时间,方便我们遍历操作
4.代码示例:
#include"iostream"
#include"cstdio"
#include"cstring"
using namespace std;
int n,m;
int book[100][100]; //记录生成树上的存边
int sum=0;
int u[100];
int v[100];
int w[100];
int first[100];
int nextk[100];
int dp[100][100]; //为了节约脑力,就不写堆优化了
int dis[100];
int inf=99999999;
int vis[100];
int pre[100];
int main()
{
memset(vis,0,sizeof(vis));
cin>>n>>m;
for(int i=1;i<=n;i++)
{
dis[i]=inf;
pre[i]=i;
}
dis[1]=0;
vis[1]=1;
memset(first,-1,sizeof(first));
memset(book,0,sizeof(book));
memset(dp,0,sizeof(dp));
for(int i=1;i<=m;i++) cin>>u[i]>>v[i]>>w[i];
for(int i=1+m;i<=m*2;i++)
{
u[i]=v[i-m];
v[i]=u[i-m];
w[i]=w[i-m];
}
for(int i=1;i<=2*m;i++)
{
nextk[i]=first[u[i]];
first[u[i]]=i;
}
int k=first[1];
while(k!=-1)
{
dis[v[k]]=w[k];
pre[v[k]]=u[k];
k=nextk[k];
}
for(int i=1;i<=n-1;i++)
{
int mink=inf;
int minpoint;
for(int j=1;j<=n;j++)
{
if(vis[j]==0&&dis[j]<mink)
{
mink=dis[j];
minpoint=j;
}
}
book[pre[minpoint]][minpoint]=book[minpoint][pre[minpoint]]=1; //记录最小生成树上的边
vis[minpoint]=1;
sum+=dis[minpoint];
for(int j=1;j<=n;j++) //动态规划
{
if(vis[j]==1)
{
dp[minpoint][j]=dp[j][minpoint]=max(dis[minpoint],dp[j][pre[minpoint]]);
}
}
k=first[minpoint];
while(k!=-1)
{
if(dis[v[k]]>w[k])
{
dis[v[k]]=w[k];
pre[v[k]]=minpoint;
}
k=nextk[k];
}
}
int high=inf;
for(int i=1;i<=n;i++)
{
int p=first[i];
while(p!=-1)
{
if(book[u[p]][v[p]]==0)
{
high=min(high,sum-dp[u[p]][v[p]]+w[p]);
book[u[p]][v[p]]=book[v[p]][u[p]]=1; //杜绝下次无用的判断,把判断过的边锁死
}
p=nextk[p];
}
}
cout<<high<<endl;
return 0;
}
5.待解决的问题:
1.存在不存在求解k小生成树的算法呢
2.记录次小生成树上的边有没有更好的方法
3.堆优化Prim的判断与分析,算法的复杂度分析学习
6.POJ例题解决(次小生成树):
AC代码如下(本体描述有问题,在下将变得大小扩大至1000则AC,意欲AC这道题的小朋友注意一下,不要相信题的100那个数据,是1000):
#include"iostream"
#include"cstdio"
#include"cstring"
#include"cstdlib"
#include"cmath"
#define N 1005
using namespace std;
int n,m;
int u[N*20];
int v[N*20];
int w[N*20];
int first[N];
int nextk[N*20];
int sum=0;
int high=0;
int dp[N][N];
int pre[N];
int book[N][N];
int dis[N];
int inf=99999999;
int vis[N];
int number[N];
int num=0;
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
scanf("%d%d",&n,&m);
sum=0;
num=0;
number[++num]=1;
memset(first,-1,sizeof(first));
for(int i=1;i<=n;i++)
{
dis[i]=inf;
pre[i]=i;
}
dis[1]=0;
memset(book,0,sizeof(book));
memset(vis,0,sizeof(vis));
for(int i=1;i<=m;i++) scanf("%d%d%d",&u[i],&v[i],&w[i]);
for(int i=m+1;i<=2*m;i++)
{
u[i]=v[i-m];
v[i]=u[i-m];
w[i]=w[i-m];
}
for(int i=1;i<=2*m;i++)
{
nextk[i]=first[u[i]];
first[u[i]]=i;
}
int k=first[1];
while(k!=-1)
{
dis[v[k]]=w[k];
pre[v[k]]=1;
k=nextk[k];
}
vis[1]=1;
for(int i=1;i<=n-1;i++)
{
int mink=inf;
int minpoint;
for(int j=1;j<=n;j++)
{
if(vis[j]==0&&dis[j]<mink)
{
mink=dis[j];
minpoint=j;
}
}
vis[minpoint]=1;
sum+=dis[minpoint];
book[minpoint][pre[minpoint]]=book[pre[minpoint]][minpoint]=1;
for(int j=1;j<=num;j++) dp[minpoint][number[j]]=dp[number[j]][minpoint]=max(dp[number[j]][pre[minpoint]],dis[minpoint]);
number[++num]=minpoint;
k=first[minpoint];
while(k!=-1)
{
if(dis[v[k]]>w[k])
{
dis[v[k]]=w[k];
pre[v[k]]=u[k];
}
k=nextk[k];
}
}
high=inf;
for(int i=1;i<=n;i++)
{
k=first[i];
while(k!=-1)
{
if(book[u[k]][v[k]]==0)
{
high=min(high,sum+w[k]-dp[u[k]][v[k]]);
book[u[k]][v[k]]=book[v[k]][u[k]]=1;
}
k=nextk[k];
}
}
if(high==sum) printf("Not Unique!\n");
else printf("%d\n",sum);
}
return 0;
}
7.算法分析:
1.Prim算法我们得分析会发现,我们每次要对每个点的出边就你行维护以及查询每个的最小出边,都需要遍历一遍所有的出度
但是我们始终没有考虑边的分类,所以说我们在图很稠密的时候,图中的边非常的多的时候,我们尽量采用Prim算法,时间复杂度是O(n^2),n是点的数目
2.Krustral算法:
我们会发现Krustral算法的话,我们是对边的操作,先排序,我们在挑选变,所以说在边少的时候我们采用Krustral算法,时间复杂度是O(n*logn),n是边的数目,适用于稀疏图