czl蒟蒻的XJOI类型训练2

来源:互联网 发布:匕首cad图纸数据 编辑:程序博客网 时间:2024/06/05 22:55

  • -XJOI类型训练2-
    • 最小生成树
      • T1-Prim算法
        • 包含知识点-最小生成树
        • 知识点讲解
        • 代码实现
      • T2-Kruskal算法
        • 包含知识点-最小生成树
        • 知识点讲解
        • 代码实现
    • 单源最短路问题
      • T3-Dijkstra算法
        • 包含知识点-单源最短路
        • 知识点讲解
        • 代码实现
      • T4-Floyed算法
        • 包含知识点-任意两点最短路
        • 知识点讲解
        • 代码实现
      • T5-SPFA算法
        • 包含知识点-有负权的单源最短路
        • 知识点讲解
        • 代码实现

->XJOI类型训练2<-

(*注:今天是最小生成树和单源最短路专场~)

最小生成树:

最小生成树的就是指在一个带权无向图中生成一棵树,要求这棵树的权值最小。同时,生成树的能否构成与图是否连通是等价的,所以通常可以通过这种方法进行判断。

T1-Prim算法

包含知识点-最小生成树

知识点讲解:

Prim算法的主要内容就是先选定一个只有一个顶点T的树,创建一个集合表示这些点是已经加入树的点,然后不断地找到到这个集合最近的点,把这个点加入这个点集,并不断更新相关的最短路径,直到所有的点都被加入这个点集为止。

代码实现:

//Prim#include<bits/stdc++.h>using namespace std;const int maxn=1005;int cost[maxn][maxn];int min_cost[maxn];bool vis[maxn];int n,m;int Prim(){    memset(min_cost,0x7f,sizeof min_cost);    memset(vis,false,sizeof vis);    min_cost[1]=0;    int res=0;    while(true)    {        int v=-1;        for(int u=1;u<=n;u++)        {            if(!vis[u]&&(v==-1||min_cost[u]<min_cost[v]))            v=u;        }        if(v==-1)break;        vis[v]=true;        res+=min_cost[v];        for(int u=1;u<=n;u++)        {            min_cost[u]=min(min_cost[u],cost[v][u]);        }    }    return res;}int main(){    scanf("%d%d",&n,&m);    memset(cost,0x7f,sizeof cost);    for(int i=1;i<=m;i++)    {        int s,t,c;        scanf("%d%d%d",&s,&t,&c);        cost[s][t]=cost[t][s]=c;    }    int ret=Prim();    printf("%d\n",ret);}

T2-Kruskal算法

包含知识点-最小生成树

知识点讲解:

Kruskal算法也是生成最小生成树的一种算法。与Prim算法不同的是,Kruskal算法先通过把每条边按权值升序排序,然后从权值最小的边开始,不断地把这条边加入进去,如果加入一条边之后,即将会形成一个环,就不加入这条边(具体的理由:树是无环的,如果加入这条边,那么必会替代掉这个环中的一条边,而所有的边都是比新加入的这条边要小的,所以加入这条边必定会使得整棵树的权值更大)。直到加完所有的边(或者优化一点:直到加入m-1条边为止)。
由于在中间需要判断两点是否已经联通了所以需要用到并查集来进行整理。

代码实现:

//Kruskal#include<bits/stdc++.h>using namespace std;const int maxn=200005;typedef long long ll;struct edge{    int u;    int v;    ll cost;}e[maxn];bool cmp(edge a,edge b){    return a.cost<b.cost;}int n,m;int fa[maxn];int init(){    for(int i=1;i<=maxn;i++)    {        fa[i]=i;    }}int find(int x){    if(fa[x]==x)return x;    else return fa[x]=find(fa[x]);}void unite(int x,int y){    x=find(x);    y=find(y);    if(x==y)return ;    fa[x]=y;}bool same(int x,int y){    x=find(x);    y=find(y);    if(x==y)return true;    return false;}ll Kruskal(){    ll res=0;    sort(e+1,e+1+m,cmp);    init();    for(int i=1;i<=m;i++)    {        edge x=e[i];        if(!same(x.u,x.v))        {            unite(x.u,x.v);            res+=x.cost;        }    }    return res;}int main(){    scanf("%d%d",&n,&m);    for(int i=1;i<=m;i++)    {        int s,t;        ll c;        scanf("%d%d%lld",&s,&t,&c);        e[i].u=s;        e[i].v=t;        e[i].cost=c;    }    ll ret=0;    ret=Kruskal();    printf("%lld\n",ret);}

单源最短路问题:

单源最短路问题是指固定一个起点,求它到所有定点最短路的问题。如果终点也固定,就成了求两点之间最短路的问题了。然而求这两种问题的时间复杂度是相同的,所以我们把这种问题当做单源最短路来求。单源最短路的变题有很多,最典型的就是让你输出最短路径。

T3-Dijkstra算法

包含知识点-单源最短路

知识点讲解:

*首先要注意,Dijkstra最大的一个特点是它不能够处理负边的情况。
Dijkstra算法是基于Bellman-Ford的基础上进行改进的,所以还不会Bellman-Ford的可先去学习。在Bellman-Ford中d[i]如果不是最短距离,那么即使之后更新了d[i],所更新的值也不会是最短距离,然而在Bellman-Ford中还是会进行枚举,所以会进行很多次重复的枚举。所以我们可以找到最短距离已经确定好了的顶点,从这个顶点出发更新相邻顶点的最短距离,然后就可以把这个顶点抛弃了(因为这个顶点的最短距离已经确定了,所以之后是不会再次更新了)。
在这里,使用邻接矩阵的复杂度是O(n²),所以我们可以用邻接表,并且由于这个算法大部分时间都用在了枚举顶点上,复杂度为O(n),所以我们可以用优先队列进行存储,达到O(1)的访问以及O(log(n))的删除顶点以及O(log(n))的插入顶点来进行优化。
对于输出最短路径,可以记录一个前趋数组,每一次更新时顺带更新前趋数组即可,最后根据终点反过来寻找前趋数组的每一个值直到顶点为止即可。

代码实现:

//Dj#include<stdio.h>#include<iostream>#include<algorithm>#include<cstring>#include<vector>#include<queue>using namespace std;const int maxn=10005;typedef pair<int,int>P;struct edge{    int to;    int cost;    edge(int to,int cost):to(to),cost(cost) {}};int prev[maxn];//前趋数组记录前一个节点vector<edge>g[maxn];vector<int>p;int d[maxn];priority_queue<P,vector<P>,greater<P> >que;void Dj(int s){    memset(d,0x3f,sizeof d);    for(int i=0;i<maxn;i++)prev[i]=-1;    d[s]=0;    que.push(P(0,s));    while(!que.empty())    {        P x=que.top();        que.pop();        int v=x.second;        if(x.first>d[v])continue;        for(int i=0;i<g[v].size();i++)        {            edge e=g[v][i];            if(d[e.to]>d[v]+e.cost)            {                d[e.to]=d[v]+e.cost;                prev[e.to]=v;//更新时同时更新前趋数组的值                que.push(P(d[e.to],e.to));            }        }    }}void get_path(int s){    for(;s!=-1;s=prev[s])p.push_back(s);//反向寻找最短路径的每一个节点    reverse(p.begin(),p.end());//这里是要求我们从起点开始输出,所以用了翻转,没有要求就不需要了}int main(){    int st,to;    int n,m;    scanf("%d%d",&st,&to);    scanf("%d%d",&n,&m);    for(int i=1;i<=m;i++)    {        int s,t,c;        scanf("%d%d%d",&s,&t,&c);        g[s].push_back(edge(t,c));        g[t].push_back(edge(s,c));    }    Dj(st);    get_path(to);//还原路径    printf("%d\n",d[to]);    for(int i=0;i<p.size();i++)    {        printf("%d ",p[i]);    }    return 0;}

T4-Floyed算法

包含知识点-任意两点最短路

知识点讲解:

我们通过DP的思想来解决任意两点最短路的题目。我们考虑只使用顶点0~k,i,j时,i到j的最短路记为d[i][j],而k为中间的一个顶点,所以更新i,j之间的最短路时,只需要通过d[i][k]和d[k][j]的和来更新d[i][j]的值。
对于输出路径,我们则可以用一个vis数组表示该点是否已经被加入最短路径中了。我们每次枚举每一个没有加入的节点,记已经确定的最后一个节点为temp,枚举到的节点为i,起点为s,终点为t,那么当i到temp的最短路加上temp到i的长度再加上i到t的长度刚好等于s到t的最短路时,这个点便是在最短路径上,顺次输出即可。

代码实现:

//Floyed#include<stdio.h>#include<iostream>#include<algorithm>#include<cstring>#include<vector>#include<queue>using namespace std;const int maxn=1005;#define inf 0x3f3f3f3fint d[maxn][maxn];int f[maxn][maxn];bool l[maxn][maxn];bool vis[maxn];int s1,t1,s2,t2;int n,m;int Floyed(){    for(int i=1;i<=n;i++)    {        for(int j=1;j<=n;j++)        {            for(int k=1;k<=n;k++)            {                d[j][k]=min(d[j][k],d[j][i]+d[i][k]);            }        }    }}int main(){    memset(d,0x3f,sizeof d);    memset(l,false,sizeof l);    scanf("%d%d%d%d",&s1,&t1,&s2,&t2);    scanf("%d%d",&n,&m);    for(int i=1;i<=n;i++)d[i][i]=0,f[i][i]=0;    for(int i=1;i<=m;i++)    {        int s,t,c;        scanf("%d%d%d",&s,&t,&c);        d[s][t]=d[t][s]=min(d[s][t],c);        f[s][t]=f[t][s]=d[s][t];        l[s][t]=l[t][s]=1;    }    Floyed();    //下面是路径还原    printf("%d\n",d[s1][t1]);    memset(vis,false,sizeof vis);    int t=s1;    vis[s1]=true;    printf("%d ",s1);    while(t!=t1)    {        for(int i=1;i<=n;i++)        {            if(l[i][t]&&!vis[i]&&d[s1][t]+f[t][i]+d[i][t1]==d[s1][t1])            {                printf("%d ",i);                vis[i]=true;                t=i;                break;            }        }    }    printf("\n");    printf("%d\n",d[s2][t2]);    memset(vis,false,sizeof vis);    t=s2;    vis[s2]=true;    printf("%d ",s2);    while(t!=t2)    {        for(int i=1;i<=n;i++)        {            if(l[i][t]&&!vis[i]&&d[s2][t]+f[t][i]+d[i][t2]==d[s2][t2])            {                printf("%d ",i);                vis[i]=true;                t=i;                break;            }        }    }    return 0;}

T5-SPFA算法

包含知识点-有负权的单源最短路

知识点讲解:

建立一个队列,先把起始点加入队列中,更新与这个点有关的的点的最短路径,进行松弛操作,用队列中的所有的点作为起始点对最短距离进行松弛操作,如果该点松弛成功并且被松弛的点不在队列中,就把被松弛的点加入队列尾部,这样操作直到队列为空。
至于判断负环的方法,由于总共只有n个点,所以每个点的最短距离最多被更新n次,所以一旦更新次数超过了n次,那么说明这个图中存在着负环。
然后是路径还原,和Dijistra算法一样,记录一个前趋数组,更新的时候顺带更新这个前趋数组,最后反推进行路径还原。

代码实现:

#include<bits/stdc++.h>using namespace std;typedef long long ll;const int maxn=100005;struct edge{    int from;    int to;    ll cost;    int next;}e[maxn*2];int prev[maxn],head[maxn];ll d[maxn],cnt[maxn];bool vis[maxn];int tot=0;int s,t,n,m;queue<int >que;vector<int>p;inline void add(int from,int to,ll cost){    e[++tot].from=from;    e[tot].to=to;    e[tot].cost=cost;    e[tot].next=head[from];    head[from]=tot;}ll spfa(int s,int t){    for(int i=0;i<maxn;i++)prev[i]=-1;    memset(vis,false,sizeof vis);    memset(d,0x3f,sizeof d);    memset(cnt,0,sizeof cnt);    que.push(s);//加入起始点    d[s]=0;    vis[s]=true;    while(!que.empty())    {        int x=que.front();        que.pop();        cnt[x]++;        vis[x]=false;        if(cnt[x]>n)//当更新次数超过n次时,说明有负环        {            puts("You show me the wrong map!");            exit(0);        }        for(int i=head[x];~i;i=e[i].next)        {            int to=e[i].to;            ll co=e[i].cost;            if(d[to]>d[x]+co)            {                d[to]=d[x]+co;                prev[to]=x;//更新前趋数组                if(!vis[to])                {                    que.push(to);                    vis[to]=true;                }            }        }    }    return d[t];}void get_path(int t){    for(;t!=-1;t=prev[t])p.push_back(t);//根据前趋数组反推最短路径的点    reverse(p.begin(),p.end());//翻转~}int main(){    for(int i=0;i<maxn;i++)head[i]=-1;    scanf("%d%d%d%d",&s,&t,&n,&m);    for(int i=1;i<=m;i++)    {        int st,to;        ll c;        scanf("%d%d%lld",&st,&to,&c);        add(st,to,c);    }    prev[s]=0;    ll res=spfa(s,t);    printf("%lld\n",res);    get_path(t);    for(int i=0;i<p.size();i++)    printf("%d ",p[i]);}