图论
图论题目,数据量是选择算法的重要依据。
一、图的存储
不同问题使用的存储方式不同,比如区分有向图和无向图,顶点数是否多,图是否稠密等。
邻接矩阵
二维数组保存图 ,行数i
与列数j
是否有通路、权值大小。
int graph[NUM][NUM];
//初始化
//有向图
//无向图
适用于顶点数较少,稠密图
邻接表
1.数组模拟
#include<bits/stdc++.h>
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>
那么i
必在j
前面,满足这样的序列称为拓扑序。
入度为0的为起点,出度为0才有可能是终点。
算法实现
从入度为0的顶点x
开始,消去以x
为起点的所有边(<x,y>……<x,z>)
,如果消去这些边后,有些顶点y
入度为0,那么将y
加入入度为0的集合中。
重复此操作直到所有顶点入度均为0,否则不存在拓扑序(可能是有回路)。
需要借助队列来确保生成的是拓扑序。
#include<bits/stdc++.h>
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<bits/stdc++.h>
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="<<x<<" y="<<y<<" w="<<w<<" dp[x]="<<dp[x]<<" dp[y]"<<dp[y]<<endl;
dp[y]=max(dp[y],dp[x]+w);
in[y]--;
if(!in[y]) q.push(y);
}
//for(int i=1;i<=n;i++) cout<<dp[i]<<' ';
//cout<<endl;
}
if(dp[n]!=0xcfcfcfcf) cout<<dp[n];
else cout<<-1;
}
USACO Cow Contest S
Floyd(任意两点连通性计算祖先个数)+拓扑排序
#include<bits/stdc++.h>
using namespace std;
int n,m,x,y,val,Q;
int d[310][310],g[310][310];//存在路径<i……j> 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也不能求解最长路,贪心的局部最优并不能达到整体最优。
以邻接链表存储,不带堆优化的Dijkstra算法实现。
AcWing-Dijkstra求最短路
貌似该题被活动覆盖了,不能做了。
#include<bits/stdc++.h>
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<bits/stdc++.h>
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被更新了不止一次,然后随之加入堆中多次,不过只取最优的即可
//同时如果最短路中<begin……x,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,图中所有边<x,y,z>
如果都有 dist[x]+z>=dist[y]
。那么整张图就不需要再更新。
算法流程
建立队列,开始只有起点start
入队。
取队首元素x
,扫描x
所有边,边<x,y,z>
如果
dist[x]+z<dist[y]
且 y
不在队列中将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 个结点来连接最短路,这个循环可以理解为最外层的阶段,然后枚举要连接的两个结点。
一定要理解阶段( 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<bits/stdc++.h>
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<bits/stdc++.h>
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<bits/stdc++.h>
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]);
//答案栈的数据是回溯生成的,所以逆序输出才是答案
}
看牛
因为图的无向边被当做两条有向边存储,比如欧拉回路,搜索遍历到边<x,y>
,该条边因为更新了head[x]
不会再被遍历到,此时我们需要标记逆向有向边<y,x>
,这样不会重复经过。如果是正反经过两次就不需要每次标记逆向边,只通过更新head[x]
消去边。
洛谷模板欧拉路径
判定欧拉路,S到T
欧拉路要么所有结点入度等于出度,要么仅起点出度大于入度1,中间结点入度等于出度,终点入度大于出度1。
因为输出最小字典序,所以用vector
存储然后排序来保证先遍历到字典序小的边。
其他未学到的算法
树上问题(树的直径、最近公共祖先)、SAT问题、最大流(残留网络、增广路)、最小割、最小费用最大流、二分图匹配