他们其实都是“图”
图的表示不多说,数组模拟邻接表的时候,一定要初始化h数组都为-1!!!
int h[N],e[N],ne[N],idx;
//idx是边的编号,h是第一条边,e是这条边的终点,ne是下一条边
void add(int a,int b)//把这条边插入a引出的链表最前面
{
e[idx]=b;
ne[idx]=h[a];
h[a]=idx++;
}
//遍历
for(int i=h[x];~i;i=ne[i])
{
int u=e[i];
//blablabla
}
//初始化
memset(h,-1,sizeof h);
还有一个比较重要的数据结构Dancing Links(十字链表)。他是一个能链接前驱边的邻接表,可以解决重复覆盖和精确覆盖问题:给定一个0和1矩阵,找到一个行的集合,使每一列都有1(或者恰好包含一个1)。
没怎么做这类题,贴别人的链接
最短路问题(参考了acwing的基础课)
如图:
朴素dijkstra
#include<iostream>
#include<cstring>
using namespace std;
const int N=510;
int d[N],vis[N];
int w[N][N];
int n,m;
int resv;
int res;
void dijkstra()
{
for(int i=2;i<=n;i++)
{
res=0x3f3f3f;
resv=-1;
for(int j=1;j<=n;j++)
{
if(!vis[j])
{
if(resv==-1||d[j]<res)
{
resv=j;
res=d[j];
}
}
}
for(int j=1;j<=n;j++)
{
d[j]=min(d[j],d[resv]+w[resv][j]);
}
vis[resv]=1;
}
}
int main()
{
cin>>n>>m;
memset(w,0x3f,sizeof w);
while(m--)
{
int a,b,dis;
cin>>a>>b>>dis;
w[a][b]=min(w[a][b],dis);
}
memset(d,0x3f,sizeof d);
d[1]=0;
dijkstra();
if(d[n]>=0x3f3f3f/2)
{
cout<<"-1";
}
else
{
cout<<d[n];
}
return 0;
}
堆优化dijkstra
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N=2e5+10;
int vis[N],dist[N];
int h[N],e[N],ne[N],v[N],idx;
int n,m;
int resv;
int res;
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>> d;
void add(int a,int b,int c)
{
e[idx]=b;
ne[idx]=h[a];
v[idx]=c;
h[a]=idx++;
}
void dijkstra()
{
for(int i=1;i<=n;i++)
{
while(!d.empty())
{
pair<int,int> t=d.top();
d.pop();
int ver=t.second;
int dis=t.first;
if(vis[ver])
continue;
vis[ver]=1;
for(int i=h[ver];i!=-1;i=ne[i])
{
int k=e[i];
if(dist[k]>=dis+v[i])
{
dist[k]=dis+v[i];
d.push({dist[k],k});
}
}
}
}
}
int main()
{
cin>>n>>m;
memset(h,-1,sizeof h);
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
memset(dist,0x3f,sizeof dist);
dist[1]=0;
d.push({0,1});
dijkstra();
if(dist[n]>=0x3f3f3f/2)
{
cout<<"-1";
}
else
{
cout<<dist[n];
}
return 0;
}
有边数限制的最短路
backup作为备份,否则会用两条边更新的结果当成一条边更新。
#include<iostream>
#include<cstring>
using namespace std;
const int N=1e5+10;
struct e{
int a,b,w;
}edges[N];
int n,m,k;
int dist[N],backup[N];
void BF()
{
for(int i=1;i<=k;i++)
{
memcpy(backup,dist,sizeof dist);
for(int j=0;j<m;j++)
{
int aa=edges[j].a;
int bb=edges[j].b;
int ww=edges[j].w;
dist[bb]=min(dist[bb],backup[aa]+ww);
}
}
}
int main()
{
cin>>n>>m>>k;
for(int i=0;i<m;i++)
{
int a,b,w;
cin>>edges[i].a>>edges[i].b>>edges[i].w;
}
memset(dist,0x3f,sizeof dist);
dist[1]=0;
BF();
if(dist[n]>0x3f3f3f/2)
{
cout<<"impossible";
}
else
cout<<dist[n];
return 0;
}
spfa
要改变了才会放入队列不然会死循环
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N=2e5+10;
int h[N],w[N],e[N],ne[N],d[N],idx;
bool vis[N];
void add(int a,int b,int c)
{
e[idx]=b;
w[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
}
void spfa()
{
queue<int> q;
q.push(1);
while(!q.empty())
{
int t=q.front();
q.pop();
vis[t]=false;
for(int i=h[t];i!=-1;i=ne[i])
{
int k=e[i];
if(d[k]>d[t]+w[i])
{
d[k]=min(d[k],d[t]+w[i]);
if(!vis[k])
{
q.push(k);
}
vis[k]=true;
}
}
}
}
int main()
{
int n,m;
cin>>n>>m;
memset(h,-1,sizeof h);
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
memset(d,0x3f,sizeof d);
d[1]=0;
spfa();
if(d[n]>0x3f3f3f/2)
cout<<"impossible";
else
cout<<d[n];
return 0;
}
spfa判断负环
如果一个点进入队列超过n次则存在负环
为了防止不连通,每个点都要入队
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N=2e5+10;
int h[N],w[N],e[N],ne[N],d[N],idx;
bool vis[N];
int cnt[N];
int n,m;
void add(int a,int b,int c)
{
e[idx]=b;
w[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
}
bool spfa()
{
queue<int> q;
for (int i = 1; i <= n; i ++ )
{
vis[i] = true;
q.push(i);
}
while(!q.empty())
{
int t=q.front();
q.pop();
vis[t]=false;
for(int i=h[t];i!=-1;i=ne[i])
{
int k=e[i];
if(d[k]>d[t]+w[i])
{
cnt[k]=cnt[t]+1;
d[k]=min(d[k],d[t]+w[i]);
if(cnt[k]>=n) return true;
if(!vis[k])
{
q.push(k);
}
vis[k]=true;
}
}
}
return false;
}
int main()
{
cin>>n>>m;
memset(h,-1,sizeof h);
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
memset(d,0x3f,sizeof d);
d[1]=0;
if(spfa())
cout<<"Yes";
else
cout<<"No";
return 0;
}
floyd
k在外层,他是由三维降维到二维的,k在外层改变了更新顺序
#include<iostream>
#include<cstring>
using namespace std;
const int N=210;
int INF=0x3f3f3f;
int n,m,k;
int d[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++)
{
d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
}
}
int main()
{
cin>>n>>m>>k;
memset(d,0x3f,sizeof d);
for(int i=1;i<=n;i++)
{
d[i][i]=0;
}
while(m--)
{
int x,y,z;
cin>>x>>y>>z;
d[x][y]=min(d[x][y],z);
}
floyd();
while(k--)
{
int x,y;
cin>>x>>y;
if(d[x][y]>INF/2)
{
cout<<"impossible"<<endl;
}
else
cout<<d[x][y]<<endl;
}
return 0;
}
次短路
Roadblocks
两种算法:
书上的做法:对每个顶点,记录最短距离和次短距离。
分别维护从起点开始的最短路和从终点开始的最短路
枚举每一条边
次短路=从起点开始的最短路+d[i][j]+从终点开始的最短路
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int h[N],w[N],e[N],ne[N],d1[N],d2[N],idx;
bool vis[N];
void add(int a,int b,int c)
{
e[idx]=b;
w[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
}
void spfa(int start,int d[])
{
queue<int> q;
q.push(start);
while(!q.empty())
{
int t=q.front();
q.pop();
vis[t]=false;
for(int i=h[t];i!=-1;i=ne[i])
{
int k=e[i];
if(d[k]>d[t]+w[i])
{
d[k]=min(d[k],d[t]+w[i]);
if(!vis[k])
{
q.push(k);
}
vis[k]=true;
}
}
}
}
int main()
{
int n,m;
cin>>n>>m;
memset(h,-1,sizeof h);
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
memset(d,0x3f,sizeof d);
d[1]=0;
spfa(1,d1);
spfa(n,d2)
int ans=0x3f;
for(int i=1;i<=n;i++)
{
for(int j=head[i];j!=-1;j=ne[j])//枚举了每条边
{
int v=e[j];
int w=w[j];
int t=d1[i]+d2[v]+w;
if(t>d1[n]&&t<ans)
{
ans=t;
}
}
}
cout<<ans;
return 0;
}
第k短路
BFS+A*
BFS扩展到第k层的路径就是第k短路。
还有带各种边权限制的最短路问题,双重权值的最短路问题,要灵活修改。
最小生成树问题
**prim算法求最小生成树**
```cpp
#include<iostream>
#include<cstring>
using namespace std;
const int N=510;
int d[N],g[N][N],vis[N];
int n,m;
int prim()
{
int ans=0;
for(int i=0;i<n;i++)
{
int resv=0x3f3f3f;
int res=-1;
for(int j=1;j<=n;j++)
if(!vis[j])
{
if(res==-1||d[j]<resv)
{
res=j;
resv=d[j];
}
}
if(i&&d[res]>=0x3f3f3f/2) return 0x3f3f3f3f;
if(i) ans+=resv;
for(int j=1;j<=n;j++)
{
if(d[j]>g[res][j])
{
d[j]=g[res][j];
}
}
vis[res]=1;
}
return ans;
}
int main()
{
cin>>n>>m;
memset(g,0x3f,sizeof g);
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
g[a][b]=g[b][a]=min(g[a][b],c);
}
memset(d,0x3f,sizeof d);
int k=prim();
if(k<0x3f3f3f3f/2)
{
cout<<k;
}
else
{
cout<<"impossible";
}
return 0;
}
kruskal算法求最小生成树
#include<iostream>
#include<algorithm>
using namespace std;
const int N=2e5+10;
pair<int,pair<int,int>> p[N];
int n,m;
int f[N];
int find(int x)
{
return f[x]==x?x:f[x]=find(f[x]);
}
int kruskal()
{
int ans=0;
int cnt=0;
for(int i=0;i<m;i++)
{
int a=p[i].second.first;
int b=p[i].second.second;
int aa=find(a);
int bb=find(b);
if(aa!=bb)
{
f[aa]=bb;
ans+=p[i].first;
cnt++;
}
}
if(cnt<n-1) return 0x3f3f3f;
else return ans;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
f[i]=i;
}
for(int i=0;i<m;i++)
{
int a,b,c;
cin>>a>>b>>c;
p[i].first=c;
p[i].second.first=a;
p[i].second.second=b;
}
sort(p,p+m);
int k=kruskal();
if(k>=0x3f3f3f/2)
{
cout<<"impossible";
}
else
{
cout<<k;
}
return 0;
}
最大权森林
Conscription
对每个边取负数,求一个最小生成树,就是最大权森林。
书上的题目:
图论的题目都是模板,最重要的是运用思维建图。
Layout
看上去是个差分约束的题目,其实简单想想就可以联想到怎么建图。
题目给出两种条件:
1.d[BL]-d[AL]<=DL
2.d[BD]-d[AD]>=DD
还有一个隐含条件
3.d[BL]>=d[AL]
要求的是d[N]-d[1]
如果有条件1,就从AL到BL连接一条长度为DL的边;如果有条件3,就从AL到BL连接一条长度为0的边;这个很好理解。
如果有条件2,可以同乘以-1,变为
d[AD]-d[BD]<=-DD
从AD到BD连一条-DD的边。
这样图就建立完成了,由于有负权边,用SPFA计算从1到N的最短路。
为什么题目中求最大距离却求最短路,请看下面的式子:
式子(1)可能是原本就有的条件,式子(2)通过最短路松弛得到另一个条件,最后叠加的结果是取d1和d2中最小的作为条件。也就是x1-x2能取得的最大值。