【图论-最短路】洛谷官方题单刷题总结

图论

图在线生成器

图论题目,数据量是选择算法的重要依据。

一、图的存储

不同问题使用的存储方式不同,比如区分有向图和无向图,顶点数是否多,图是否稠密等。

邻接矩阵

二维数组保存图 ,行数i与列数j是否有通路、权值大小。

int graph[NUM][NUM];
//初始化
//有向图
//无向图

适用于顶点数较少,稠密图

邻接表

1.数组模拟

#include
using namespace std;
#define NUM 10010
int next_[NUM],head[NUM],edge[NUM],ver[NUM];
//head与next_构成邻接链表
//next_与ver配合,next_记录下一条边指针,ver记录本条边终点
//edge记录权值
int n,tot;
void add(int x,int y,int w){
    ver[++tot]=y;
    //进行头插
    next_[tot]=head[x];//本质上head与next_记录的是ver数组下标
    head[x]=tot;//next可以使用结构体,同时记录ver数据
    edge[tot]=w;
}
int main()
{
    int x,y,w;
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>x>>y>>w;
        add(x,y,w);
    }
    cin>>x;
    for(int i=head[x];i;i=next_[i]){
        cout<<x<<' '<<ver[i]<<' '<<edge[i]<<endl;
    }
}

2.vector模拟

//无权值
vector<int>e[NUM]//只需保存和一个点直接相连的集合,例
e[x].push_back(y);
e[y].push_back(x);//无向图还需记录相反边
//有权值,需要定义结构体记录权值
struct edge{
    int from,to,w;//from起点,to终点,w权值
    //from可以省略
};
vector<edge>e[NUM];
e[x].push_back(edge{x,y,w});

二、图的遍历和连通性

根据存储方式不同遍历方式也不同,就是搜索DFS、BFS。

可以使用dfs()判断连通性。并查集可以做到动态判断(不断添加边)连通性,在最小生成树kruskal()算法中,判断连通性用到的就是并查集。

三、拓扑排序

对于任意一个顶点的序列,如果所有有向边 那么i必在j前面,满足这样的序列称为拓扑序。

入度为0的为起点,出度为0才有可能是终点。

算法实现

从入度为0的顶点x开始,消去以x为起点的所有边(……),如果消去这些边后,有些顶点y入度为0,那么将y加入入度为0的集合中。

重复此操作直到所有顶点入度均为0,否则不存在拓扑序(可能是有回路)。

需要借助队列来确保生成的是拓扑序。

#include
using namespace std;
#define NUM 5010
//初始化数据结构,程序设计中最好定义为全局变量,这样不需要全部初始化入度为0
int n,m,in[NUM];//in入度
vector<int>v[NUM];//vector实现邻接矩阵
//隐藏数据 v[i].size()就是顶点i的出度
queue<int>q;
int main()
{
    int x,y,ans=0,cnt=0;
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        cin>>x>>y;
        v[x].push_back(y);
        in[y]++;
    }
    //将所有入度为0的顶点入队
    for(int i=1;i<=n;i++){
        if(in[i]==0) q.push(i);
    }
    while(!q.empty()){
        //每次取队首元素,不要忘记弹出
        x=q.front();q.pop();
        cout<<x<<endl;
        for(int i=0;i<v[x].size();i++){//消去所有的边实际上就是减少入度
            y=v[x][i];
            in[y]--;
            if(in[y]==0) q.push(y);//仅当入度为0时入队
        }
    }
}

洛谷-最大食物链计数

拓扑排序模板题,增加一个计数数组,也算是线性DP。

洛谷最长路径

#include
using namespace std;
#define NUM 5010
int n,m;
struct Node{
    int from,to,val;
};
int in[NUM],dp[NUM];
vector<Node>v[NUM];
queue<int>q;
int main()
{
    int x,y,w;
    cin>>n>>m;
    memset(dp,0xcf,sizeof(dp));
    for(int i=1;i<=m;i++){
        cin>>x>>y>>w;
        v[x].push_back(Node{x,y,w});
        in[y]++;
    }
    for(int i=2;i<=n;i++){
        if(!in[i]) q.push(i);
    }
    while(!q.empty()){
        x=q.front();q.pop();
        for(int i=0;i<v[x].size();i++){
            y=v[x][i].to;
            in[y]--;
            if(!in[y]) q.push(y);
        }
    }
    q.push(1);dp[1]=0;
    while(!q.empty()){
        x=q.front();q.pop();
        for(int i=0;i<v[x].size();i++){
            y=v[x][i].to;
            w=v[x][i].val;
            //cout<<"x="<
            dp[y]=max(dp[y],dp[x]+w);
            in[y]--;
            if(!in[y]) q.push(y);
        }
        //for(int i=1;i<=n;i++) cout<
        //cout<
    }
    if(dp[n]!=0xcfcfcfcf) cout<<dp[n];
    else cout<<-1;
}

USACO Cow Contest S

Floyd(任意两点连通性计算祖先个数)+拓扑排序

#include
using namespace std;
int n,m,x,y,val,Q;
int d[310][310],g[310][310];//存在路径 d[i][j]=1 g数组保存邻接矩阵
int pre[310];//记录祖先数目
int in[310];//入度
queue<int>q;
void floyd(){//floyd计算所有可连通路径
    for(int k=1;k<=n;k++){
        for(int i=1;i<=n;i++){
            for(int j=1;j<=n;j++){
                d[i][j]|=d[i][k]&d[k][j];
            }
        }
    }
}
void getParent(){//计算祖先数
    floyd();
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            if(d[j][i]) pre[i]++;
        }
    }
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        cin>>x>>y;
        g[x][y]=1;d[x][y]=1;
        in[y]++;
    }
    getParent();
    for(int i=1;i<=n;i++) if(in[i]==0) q.push(i);
    int deletenum=0,ans=0;

    while(!q.empty()){
        int x=q.front();q.pop();
        if(q.empty()&&pre[x]==deletenum) ans++;
        for(int i=1;i<=n;i++){
            if(g[x][i]){
                in[i]--;
                if(!in[i]) q.push(i);
            }
        }
        deletenum++;
    }
    cout<<ans<<endl;
}

四、最短路

Dijkstra算法

Dijkstra算法是基于贪心思想,所以不能处理有负数边的最短路。因为当前最优并非全局最优,当前可能被其他顶点通过一个负数边更新,在此之前做的贪心工作是不是最优的。

同时Dijkstra也不能求解最长路,贪心的局部最优并不能达到整体最优。

【图论-最短路】洛谷官方题单刷题总结_第1张图片

以邻接链表存储,不带堆优化的Dijkstra算法实现。

AcWing-Dijkstra求最短路

貌似该题被活动覆盖了,不能做了。

#include
using namespace std;
#define NUM 10010
int n,m,x,y,z,val,minn=0x3f3f3f3f;
//邻接表计算最短路
int next_[NUM*10],head[NUM],ver[NUM*10],edge[NUM*10],tot;
int d[NUM],v[NUM];

void add(int x,int y,int val){
    ver[++tot]=y;
    edge[tot]=val;
    next_[tot]=head[x];
    head[x]=tot;
}
void dijkstra(int x){
    memset(d,0x3f,sizeof(d));//初始化d[],v[]
    d[x]=0;
    for(int i=1;i<n;i++){
        x=0;
        for(int j=1;j<=n;j++){
            if(!v[j]&&(x==0||d[j]<d[x])) x=j;
        }
        v[x]=1;
        for(int j=head[x];j;j=next_[j]){
            y=ver[j];val=edge[j];
            d[y]=min(d[y],d[x]+val);
        }
    }
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        cin>>x>>y>>val;
        add(x,y,val);
    }
    dijkstra(1);
    if(d[n]!=0x3f3f3f3f) cout<<d[n];
    else cout<<-1;
}

算法需要进行 n-1 次更新,每次更新都需要寻找未被访问且最小的结点x,

dist[y]=min(dist[x][y],dist[x]+val[x][y])

一次时间复杂度为O(N),既然是寻找最小的结点,可以使用堆来优化寻找减低复杂度至O(longN)。

单源最短路模板题

这个题70%数据不需要堆优化,其他数据需要堆优化解决。
也可以试试这一道单元最短路模板-标准版

#include
using namespace std;
#define NUM 10010
#define MAX 2147483647 //设置题目所给的不可达距离值
//或者memset数组0x3f
int n,m,x,y,z,val,minn=0x3f3f3f3f;
//邻接表计算最短路
int next_[NUM*50],head[NUM],ver[NUM*50],edge[NUM*50],tot;
int v[NUM],d[NUM];
priority_queue<pair<int,int> >q;
//大根堆,以负数形式存入变为小根堆
//pair排序优先第一维,这里pair 第一维d[x],第二位x

void add(int x,int y,int val){
    ver[++tot]=y;
    edge[tot]=val;
    next_[tot]=head[x];
    head[x]=tot;
}
void dijkstra(int x){
    for(int i=1;i<=n;i++) d[i]=MAX;
    d[x]=0;
    q.push(make_pair(0,x));
    while(!q.empty()){
        x=q.top().second;q.pop();
        if(v[x]) continue;//因为可能重复入队,需要特判一些,否则复杂度可能会大大增加
        //有可能y被更新了不止一次,然后随之加入堆中多次,不过只取最优的即可
        //同时如果最短路中 那么x一定比y前出堆。
        v[x]=1;
        for(int i=head[x];i;i=next_[i]){
            y=ver[i];val=edge[i];
            if(d[y]>d[x]+val){
                d[y]=d[x]+val;
                q.push(make_pair(-d[y],y));
            }
        }
    }
}
int main()
{
    cin>>n>>m>>z;
    for(int i=1;i<=m;i++){
        cin>>x>>y>>val;
        add(x,y,val);
    }
    dijkstra(z);
    for(int k=1;k<=n;k++) cout<<d[k]<<' ';
}

洛谷最短路计数

这道题是dijkstra算法的变形。

Bellman-Ford算法(SPFA算法)

在Dijkstra,图中所有边 如果都有 dist[x]+z>=dist[y]。那么整张图就不需要再更新。

算法流程

建立队列,开始只有起点start入队。

取队首元素x,扫描x所有边,边 如果

dist[x]+zy 不在队列中将y入队。重复此过程直到队列中全部为空。

与dijkstra不同的,暴力扫描而不是贪心求解,这样一来就有了更多的更新机会,而且权值为负数的情况仍能保证结果正确。实现不同的是仅标记队列顶点(防止重复冗余入队),不标记扫描过的结点。

void spfa(int x){
    for(int i=1;i<=n;i++) d[i]=MAX;
    d[x]=0;
    q.push(make_pair(0,x));
    while(!q.empty()){
        x=q.top().second;q.pop();
        v[x]=0;
        for(int i=head[x];i;i=next_[i]){
            y=ver[i],z=edge[i];
            if(d[y]>d[x]+z){
                d[y]=d[x]+z;
                if(!v[y]){
                    q.push(make_pair(-d[y],y));
                    v[y]=1;
                }
            }
        }
    }
}

数据量大使用vector建立邻接链表可能超时。

Floyd算法

Floyd 算法本质是动态规划,动态转移方程

D[i,j]=min(D[i][k],D[k][j]),不断通过连接中间结点来计算最短路。在下图中,1 与 5 之间最短路的中间结点有 { 2,4,3 },如果连接两个端点可以:

通过结点 2 连接 1,4;通过结点 4 连接 1,3; 通过结点 3 连接 1,5;

也可以:通过结点 2 连接 1,4;通过结点 3 连接 4,5; 通过结点 4 连接 1,5;

可以顺序依次借助 1~n 这 n 个结点来连接最短路,这个循环可以理解为最外层的阶段,然后枚举要连接的两个结点。

【图论-最短路】洛谷官方题单刷题总结_第2张图片

一定要理解阶段( k )与附加状态( i , j ),理解之后代码实现比较简单。

void floyd(){
    for(int k=1;k<=n;k++){
        for(int i=1;i<=n;i++){
            for(int j=1;j<=n;j++){
                d[i][j]=min(d[i][k]+d[k][j],d[i][j]);
            }
        }
    }
}

洛谷模板题邮递员送信

题目描述
邮递员从点1出发,去其他n-1个点送快递,每次只能送一个件,然后回到点1。问送完n-1个件最少花费是多少。
题目思路
题目实际上是要求 d[1~2]+d[2~1]+……+d[1~n]+d[n~1]。用floyd跑一遍,求和。
这道题数据量比较大,用floyd可以通过前四个点。

#include
using namespace std;
#define N 1100
int n,m,x,y,w;
int a[N][N];
void floyd(){
    for(int k=1;k<=n;k++){
        for(int i=1;i<=n;i++){
            for(int j=1;j<=n;j++){
                a[i][j]=min(a[i][k]+a[k][j],a[i][j]);
            }
        }
    }
}
int main()
{
    cin>>n>>m;
    //一定要记得这两步初始化
    memset(a,0x3f,sizeof(a));
    for(int i=1;i<=n;i++) a[i][i]=0;
    for(int i=1;i<=m;i++){
        cin>>x>>y>>w;
        a[x][y]=min(a[x][y],w);
    }
    floyd();
    int ans=0;
    for(int i=2;i<=n;i++){
        ans+=a[1][i];
        ans+=a[i][1];
    }
    cout<<ans<<endl;
}
反向建图

AC 做法,正向建图 可以计算出1其他所有点的距离,反向建图可以求出其他所有点到点1的距离。

#include
using namespace std;
#define N 1100
int n,m,x,y,w,ans;
vector<pair<int,int> >v1[N],v2[N];//v1正向,v2反向
int d[N],v[N];
priority_queue<pair<int,int> >q;


void dijkstra(int s,int p){//p为模式,使用堆优化的dijkstra
    memset(d,0x3f,sizeof(d));
    memset(v,0,sizeof(v));
    q.push(make_pair(0,s));
    d[s]=0;
    while(!q.empty()){
        x=q.top().second;
        q.pop();
        v[x]=1;
        for(int i=0;i<(p?v1[x].size():v2[x].size());i++){
            y=p?v1[x][i].first:v2[x][i].first,w=p?v1[x][i].second:v2[x][i].second;
            if(!v[y]&&d[y]>d[x]+w){
                d[y]=d[x]+w;
                q.push(make_pair(-d[y],y));
            }
        }
    }
    for(int i=2;i<=n;i++) ans+=d[i];
}

int main()
{
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        cin>>x>>y>>w;
        v1[x].push_back(make_pair(y,w));
        v2[y].push_back(make_pair(x,w));
    }
    dijkstra(1,1);//模式1正向
    dijkstra(1,0);//模式0逆向
    cout<<ans<<endl;
}

通过Floyd算法还可以实现二元关系传递闭包。

六、欧拉路

俗称一笔画问题,如果存在从顶点S到顶点T,不重不漏的经过每一个边一次,这个路径称为S到T的欧拉路

如果出发点S,终点也为S,就是从任意点出发经过不重不漏地经过所有边,则称该路径为欧拉回路,有欧拉回路的图称为欧拉图

欧拉图判定

无向图连通,并且每个点度数都是偶数。通过边EA进入一个点就能从该点经边EB再出发,同时不要忘记欧拉路性质需要经过所有的边。

欧拉路判定

仅当无向图连通,并且欧拉路的起点和终点度数为奇数其余全部为偶数。

算法实现

深度优先搜索,标记边的同时,遍历未搜索边。有为如果数据量过大,需要模拟栈实现。

#include
using namespace std;
#define NUM 100010
int n,m,x,y,val;
int head[NUM],ver[NUM],next_[NUM],tot;//邻接矩阵存储图
int s[NUM*10],top,ans[NUM*10],t;//防止数据量过大
bool vis[NUM*10];
void add(int x,int y){
    ver[++tot]=y;
    next_[tot]=head[x];
    head[x]=tot;
}
void euler(){
    s[++top]=1;//从1开始
    while(top>0){
        x=s[top];y=head[x];
        while(y && vis[y]) y=next_[y];
        if(y){
            s[++top]=ver[y];
            vis[y]=vis[y ^ 1]=true;
            //无向图正反两个方向都要标记。因为在输入时接连着添加,
            //无向边(1,2) 有向边<1,2> tot=2 2^1=1有向边<2,1> tot=3 3^1=1
            //lyd这一步绝了
            head[x]=next_[y];//更新表头,无需重头开始查找,与vis[y]同功能,也不再访问该条边
        }else{
            ans[++t]=x;
            --top;
        }
    }
}
int main()
{
    cin>>n>>m;
    tot=1;//方便进行成对变换
    for(int i=1;i<=m;i++){
        cin>>x>>y;
        add(x,y);
        add(y,x);
    }
    euler();
    for(int i=t;i;i--)  printf("%d\n",ans[i]);
    //答案栈的数据是回溯生成的,所以逆序输出才是答案
}

看牛

因为图的无向边被当做两条有向边存储,比如欧拉回路,搜索遍历到边,该条边因为更新了head[x] 不会再被遍历到,此时我们需要标记逆向有向边,这样不会重复经过。如果是正反经过两次就不需要每次标记逆向边,只通过更新head[x]消去边。

洛谷模板欧拉路径

判定欧拉路,S到T欧拉路要么所有结点入度等于出度,要么仅起点出度大于入度1,中间结点入度等于出度,终点入度大于出度1。

因为输出最小字典序,所以用vector存储然后排序来保证先遍历到字典序小的边。

其他未学到的算法

树上问题(树的直径、最近公共祖先)、SAT问题、最大流(残留网络、增广路)、最小割、最小费用最大流、二分图匹配

你可能感兴趣的:(#,图论,算法)