最短路笔记
算法
记号
为了方便叙述,这里先给出下文将会用到的一些记号的含义。
为图上点的数目, 为图上边的数目; 为最短路的源点; 为 点到 点的 实际 最短路长度; 为 点到 点的 估计 最短路长度。任何时候都有 。特别地,当最短路算法终止时,应有 。 为 这一条边的边权。
注:在使用此算法时应默认存在条件:图的边权非负
算法思想
贪心或
算法流程
把当前这个图分为两个集合,一个为已确定最短路的
正确性证明
证明的命题为:每个节点只会更新一次,即每次从
从正面直接攻破貌似很困难,考虑反证法。假设存在路径使得点
case1: 路径 上所有点都在 集合中
设最后路径
case2:路径 上部分点都在 集合中,且第一个在 集合中的点为
因为
故得证,每次从
推理
dijkstra第k次从T集合取出的点为到起点第k小(非严格)的节点
证明:
考虑算法流程,每次从未求出最短路径的点集中取出距离起点最近的点,然后以这个点为跳板刷新其他点才符合贪心(或DP)的证明,所以是的。当然,非严格。
小优化
只要搜到终点后就停止搜索,正确性显然。
时间复杂度分析
朴素的
朴素
void dijkstra(int s)
{
memset(dis,0x3f,sizeof(dis));
dis[s]=0;
for(int i=1;i<=n;i++)
{
int u=0,res=0x3f3f3f3f;
for(int j=1;j<=n;j++)
if(!vis[j]&&dis[j]<res)res=dis[j],u=j;
vis[u]=1;
for(int j=head[u];j;j=nxt[j])
{
int v=to[j];
if(dis[v]>dis[u]+res)dis[v]=dis[u]+res;
}
}
}
我们发现维护最大值可以用优先队列来维护,所以采用
struct node{
int val,pos;
bool operator>(const node &x)const{
return val>x.val;
}
};
priority_queue<node,vector<node>,greater<node> >q;
void dijkstra(int s)
{
memset(dis,0x3f,sizeof(dis));
memset(vis,0,sizeof(vis));
dis[s]=0;
q.push(node{dis[s],s});
while(!q.empty())
{
node t=q.top();q.pop();
if(vis[t.pos])continue;
vis[t.pos]=1;
for(int i=head[t.pos];i;i=nxt[i])
{
int v=to[i];
if(dis[v]>dis[t.pos]+w[i])
{
dis[v]=dis[t.pos]+w[i];
q.push(node{dis[v],v});
}
}
}
return;
}
算法流程
对于边
这么做的含义是显然的:我们尝试用
Bellman–Ford 算法所做的,就是不断尝试对图上每一条边进行松弛。我们每进行一轮循环,就对图上所有的边都尝试进行一次松弛操作,当一次循环中没有成功的松弛操作时,算法停止。
每次循环是
在最短路存在的情况下,由于一次松弛操作会使最短路的边数至少
但还有一种情况,如果从
代码实现
struct edge {
int v, w;
};
vector<edge> e[maxn];
int dis[maxn];
const int inf = 0x3f3f3f3f;
bool bellmanford(int n, int s) {
memset(dis, 63, sizeof(dis));
dis[s] = 0;
bool flag;
for (int i = 1; i <= n; i++) {
flag = false;
for (int u = 1; u <= n; u++) {
if (dis[u] == inf) continue;
for (auto ed : e[u]) {
int v = ed.v, w = ed.w;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
flag = true;
}
}
}
if (!flag) break;
}
return flag;
}
算法优化
考虑只有每次更新操作更中被新的节点才会进入下一轮松弛,所以建一个队列每次从队首取出一个元素进行更新,再将更新成功的节点放入队尾。这个优化称为
代码实现
void spfa(int s)
{
memset(dis,0x3f,sizeof(dis));
memset(vis,0,sizeof(vis));
dis[s]=0;
queue<int> q;
q.push(s);
while(!q.empty())
{
int u=q.front();q.pop();
vis[u]=0;
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];
if(dis[v]>dis[u]+w[i])
{
dis[v]=dis[u]+w[i];
if(vis[v]==0)
{
vis[v]=1;
q.push(v);
}
}
}
}
}
判负环
如果不存在负环,每个节点的最短路径(包括起点和终点)最多
也可以用另一种方式理解:
代码实现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2005,MAXM=20005;
int n,m,cnt0;
int dis[MAXN],vis[MAXN],head[MAXN],cnt[MAXN];
struct Edge{
int nxt,to,dis;
}edge[MAXM];
void add(int u,int v,int w)
{
edge[++cnt0].nxt=head[u];
edge[cnt0].to=v;
edge[cnt0].dis=w;
head[u]=cnt0;
}
bool spfa()
{
queue<int> q;
for(int i=1;i<=n;i++)
{
dis[i]=0x3f3f3f3f;
vis[i]=0;
}
q.push(1);
dis[1]=0;
vis[1]=1;
cnt[1]++;
while(!q.empty())
{
int u=q.front();
q.pop();
vis[u]=0;
for(int i=head[u];i;i=edge[i].nxt)
{
int v=edge[i].to;
if(dis[v]>dis[u]+edge[i].dis)
{
dis[v]=dis[u]+edge[i].dis;
if(vis[v]==0)
{
q.push(v);
vis[v]=1;
cnt[v]++;
if(cnt[v]>n)return 1;
}
}
}
}
return 0;
}
void init()
{
memset(dis,0,sizeof(dis));
memset(vis,0,sizeof(vis));
memset(cnt,0,sizeof(cnt));
memset(edge,0,sizeof(edge));
memset(head,0,sizeof(head));
cnt0=0;
}
int main()
{
int T;
scanf("%d",&T);
init();
for(int a=1;a<=T;a++)
{
init();
scanf("%d %d",&n,&m);
for(int i=1;i<=m;i++)
{
int u,v,w;
scanf("%d %d %d",&u,&v,&w);
add(u,v,w);
if(w>=0)add(v,u,w);
}
if(spfa())printf("YES\n");
else printf("NO\n");
}
return 0;
}
算法思想
采用动态规划的思路。设
考虑如何进行状态更新。显然从
从
从
显然对于第一类路径,最短者的长度就是
然后只需要依次枚举
代码非常简洁,如下所示:
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
dis[i][j][k]=min(dis[i][j][k-1],dis[i][k][k-1]+dis[k][j][k-1]);
时间复杂度
显然
空间复杂度变为
代码实现:
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
你就说它能不能求最短路吧
说白了算法思想还是 如果考场上没想到,也可以用万能的
它是一种由于边权相等的情况下使用,比如题目中常给的边权为
1).边权相等
本质上是类似与
算法流程
每次从队中取出队顶元素,再把更新后的节点放到队尾。这样就可以保证每次从队顶取出的节点一定是
注:在
代码实现:
void bfs(int s)
{
memset(dis,0x3f,sizeof(dis));
memset(vis,0,sizeof(vis));
queue<int> q;
dis[s]=0;
q.push(s);
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];
if(dis[v]>dis[u]+w[i])
{
dis[v]=dis[u]+w[i];
q.push(v);
}
}
}
}
2).边权为 和
算法流程
不说了,直接上模板,大体上就是用个双端队列维护队列中
代码实现
void bfs(int s)
{
memset(dis,0x3f,sizeof(dis));
memset(vis,0,sizeof(vis));
deque<int>q;
dis[s]=0;
q.push(s);
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];
if(dis[v]>dis[u]+w[i])
{
dis[v]=dis[u]+w[i];
if(w[i]==0)q.push_front(v);
else q.push_back(v);
}
}
}
}
时间复杂度
显然一个点只会进队一次,整张图会被遍历一次,故时间复杂度为
题型
常用技巧
- 求所有点到一个点的最短路,可以用反图的方式解决,脑补一下,所有点到一个点,相当于这个点反向建边倒着这走回去。
- 二进制分组
二进制分组处理多源多汇最短路例题:
做法
做法
考虑直接将
那我们看到我们要求
那么最直接的做法是随机讲这
做法
做法
在这之前先引入一个概念二进制分组,因为要使
但值得注意的是本题的边为有向边,所以在划分时有可能把终点的
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <vector>
#include <queue>
#define x first
#define y second
#define mp make_pair
using namespace std;
const int N = 1e5 + 10, M = 5e5 + 10;
typedef long long ll;
typedef pair<ll,int> PLI;
const ll INF = 1e15;
struct node{
ll d;
int x, st;
bool operator > (const node& t) const
{
return d > t.d;
}
};
PLI d[N][2];
int v[N][2], head[N], ver[M], Next[M], edge[M], tot, n, m, k, a[N];
void add(int x, int y, int z)
{
ver[++tot] = y, edge[tot] = z, Next[tot] = head[x], head[x] = tot;
}
void dijkstra()
{
priority_queue <node, vector<node>, greater<node> > q;
for(int i = 1; i <= n; i++) v[i][0] = v[i][1] = 0, d[i][0] = d[i][1] = mp(INF, 0);
sort(a + 1, a + k + 1);
for(int i = 1; i <= k; i++)
{
if(a[i] == a[i - 1]) continue;
int x = a[i];
d[x][0] = mp(0ll, x);
q.push(node({0, x, x}));
}
while(!q.empty())
{
node top = q.top(); q.pop();
int x = top.x, st = top.st; ll dis = top.d;
if((v[x][0] && v[x][1]) || (v[x][0] && st == d[x][0].y)) continue;
if(!v[x][0])
{
v[x][0] = 1;
d[x][0] = mp(dis, st);
}
else
{
v[x][1] = 1;
d[x][1] = mp(dis, st);
}
for(int i = head[x]; i; i = Next[i])
{
int y = ver[i], z = edge[i];
PLI t = mp(dis + z, st);
if(t < d[y][0])
{
if(t.y != d[y][0].y) d[y][1] = d[y][0];
d[y][0] = t;
q.push(node({dis + z, y, st}));
}
else if(t < d[y][1] && st != d[y][0].y)
{
d[y][1] = t;
q.push(node({dis + z, y, st}));
}
}
}
}
int read()
{
int x = 0, t = 1; char ch = getchar();
while(ch < '0' || ch > '9')
{
if(ch == '-') t = -1;
ch = getchar();
}
while(ch >= '0' && ch <= '9')
{
x = x * 10 + ch - '0';
ch = getchar();
}
return x * t;
}
int main()
{
int T = read();
while(T--)
{
tot = 0;
n = read(), m = read();k = read();
for(int i = 1; i <= n; i++) head[i] = 0;
for(int i = 1; i <= m; i++)
{
int x = read(), y = read(), z = read();
add(x, y, z);
}
for(int i = 1; i <= k; i++) a[i] = read();
dijkstra();
ll res = INF;
for(int i = 1; i <= k; i++) res = min(res, d[a[i]][1].x);
printf("%lld\n", res);
}
return 0;
}
判环
判负环
用
判无向图的最小环
考虑
于是,
- 有编号不超过
的节点构成。 - 经过节点
上式中相当于枚举了与
对于所有的整数
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,ans;
const int N=150;
int d[N][N],g[N][N];
signed main()
{
scanf("%lld %lld",&n,&m);
for(int i=1;i<=140;i++)for(int j=1;j<=140;j++)d[i][j]=g[i][j]=2147483648;
for(int i=1;i<=n;i++)g[i][i]=0,d[i][i]=0;
for(int i=1,a,b,w;i<=m;i++)
{
scanf("%lld %lld %lld",&a,&b,&w);
g[a][b]=g[b][a]=min(g[a][b],w);
d[a][b]=g[a][b],d[b][a]=g[b][a];
}
ans=2147483648;
for(int k=1;k<=n;k++)
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(i!=j&&g[i][k]>0&&g[k][j]>0)
ans=min(ans,g[i][k]+g[k][j]+d[i][j]);
d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
}
}
}
if(ans!=2147483648)printf("%lld\n",ans);
else printf("No solution.\n");
return 0;
}
判有向图的最小环
用
最短路计数
前置知识
- 最短路的一个很好的性质:从
到 的最短路上的一个节点 ,都满足 到 的路径是关于 单源最短路的最短路
证明:
反证法,假设
故得证。
- 在
到 的最短路径上一定不存在环
证明:
考虑反证法,因为这个图的边权非负,所以环的权值为非负数,所以一定不会比不经过这个环更优。
故得证。
注: 上述结论是在图的边权非负时才成立的,如果图的边权为负数上述结论就不一定成立了。
例题[HAOI2012] 道路
直接队每一条边进行枚举肯定会
设
很显然,对于一条边
#include<bits/stdc++.h>
using namespace std;
const int N=1550,M=5050;
const int mod=1e9+7;
int n,m,head[N],cnt,to[M],nxt[M],w[M],dis[N],fro[M],in[N],cnt1[N],cnt2[N],s[N],ans[M];
bool vis[N],mark[M];
void add(int u,int v,int f)
{
to[++cnt]=v;
nxt[cnt]=head[u];
head[u]=cnt;
w[cnt]=f;
fro[cnt]=u;
}
struct node{
int val,pos;
bool operator >(const node &x)const{
return val>x.val;
}
};
void dij(int s)
{
priority_queue<node,vector<node>,greater<node> >q;
memset(dis,0x3f,sizeof(dis));
memset(vis,0,sizeof(vis));
memset(mark,0,sizeof(mark));
dis[s]=0;
q.push(node{dis[s],s});
while(!q.empty())
{
node t=q.top();q.pop();
if(vis[t.pos])continue;
vis[t.pos]=1;
for(int i=head[t.pos];i;i=nxt[i])
{
int v=to[i];
if(dis[v]>dis[t.pos]+w[i])
{
dis[v]=dis[t.pos]+w[i];
q.push(node{dis[v],v});
}
}
}
for(int i=1;i<=m;i++)
if(dis[to[i]]==dis[fro[i]]+w[i])mark[i]=true;
return;
}
void topo(int fs)
{
memset(cnt1,0,sizeof(cnt1));
memset(cnt2,0,sizeof(cnt2));
memset(in,0,sizeof(in));
queue<int>q;
for(int i=1;i<=m;i++)
{
if(mark[i]==false)continue;
in[to[i]]++;
}
q.push(fs);
cnt1[fs]=1;
int tag=0;
while(!q.empty())
{
int x=q.front();q.pop();
s[++tag]=x;
for(int i=head[x];i;i=nxt[i])
{
if(mark[i]==false)continue;
in[to[i]]--;
if(in[to[i]]==0)q.push(to[i]);
cnt1[to[i]]+=cnt1[x];
cnt1[to[i]]%=mod;
}
}
for(int i=tag;i;i--)
{
int x=s[i];
cnt2[x]++;
for(int j=head[x];j;j=nxt[j])
{
if(mark[j]==false)continue;
cnt2[x]+=cnt2[to[j]];
cnt2[x]%=mod;
}
}
}
int main()
{
scanf("%d %d",&n,&m);
for(int i=1,u,v,w;i<=m;i++)
{
scanf("%d %d %d",&u,&v,&w);
add(u,v,w);
}
for(int i=1;i<=n;i++)
{
dij(i);topo(i);
for(int i=1;i<=m;i++)
{
// cout<<cnt1[fro[i]]<<" "<<cnt2[to[i]]<<endl;
if(mark[i])
{
ans[i]+=(cnt1[fro[i]]%mod*cnt2[to[i]]%mod)%mod;
ans[i]%=mod;
}
}
}
for(int i=1;i<=m;i++)printf("%d\n",ans[i]);
return 0;
}
时间复杂度
分层图最短路
概述
分层图最短路,如:有
其中
概念理解:分层图最短路往往是与
例题
例
做法
简单来说,本题是在无向图上求出一条从
可以仿照前面所说的方法,用
显然,我们刚才设计的状态转移是有后效性的(因为本题是按照动态规划的思想解决的,动态规划对状态空间的遍历构成一张有向无环图。注意一定是有向无环图!遍历顺序就是该有向无环图的一个拓扑序。 有向无向图中的节点对应问题中的“状态”,图中的边对应状态之间的“转移”,转移的选取就是动态规划中的“决策”。 在本题中,比如有三个点
从最短路径问题的角度去理解,图中的节点也不仅限于“整数编号”,可以扩展到二维,用二元组
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,p,k;
const int N=1000000+10,M=10000000+10;
int head[N],to[M],nxt[M],cnt,w[M],dis[N];
struct node{
int val,pos;
bool operator >(const node &x)const{
return val>x.val;
}
};
void add(int u,int v,int f){
to[++cnt]=v;
w[cnt]=f;
nxt[cnt]=head[u];
head[u]=cnt;
return;
}
priority_queue<node,vector<node>,greater<node> >q;
bool v[N];
void dijkstra(){
memset(dis,0x3f,sizeof(dis));
dis[1]=0;
q.push(node{0,1});
while(!q.empty()){
node t=q.top();q.pop();
if(v[t.pos])continue;
v[t.pos]=1;
for(int i=head[t.pos];i;i=nxt[i]){
int v=to[i],z=max(dis[t.pos],w[i]);
if(dis[v]>z){
dis[v]=z;
q.push(node{dis[v],v});
}
}
}
return;
}
signed main(){
scanf("%lld %lld %lld",&n,&p,&k);
for(int i=1,x,y,z;i<=p;i++){
scanf("%lld %lld %lld",&x,&y,&z);
add(x,y,z);add(y,x,z);
for(int j=1;j<=k;j++){
add(x+(j-1)*n,y+j*n,0);
add(y+(j-1)*n,x+j*n,0);
add(x+j*n,y+j*n,z);
add(y+j*n,x+j*n,z);
}
}
dijkstra();
int ans=1e18;
for(int i=0;i<=k;i++){
ans=min(ans,dis[n+i*n]);
}
if(ans==1e18)printf("-1\n");
else printf("%lld\n",ans);
return 0;
}
例
做法
由于购买机票不需要花钱,所以肯定不会多次重复乘坐同样的航线或者多次访问到同一个城市。如果
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+50,M=2200050;
int head[N*12],cnt,nxt[M],to[M],w[M],n,m,k,s,t,dis[N*12];
void add(int u,int v,int f)
{
to[++cnt]=v;
nxt[cnt]=head[u];
w[cnt]=f;
head[u]=cnt;
}
struct node
{
int val,pos;
bool operator >(const node &x)const{
return val>x.val;
}
};
priority_queue<node,vector<node>,greater<node> >q;
bool b[N*12];
void dij()
{
memset(dis,0x3f,sizeof(dis));
dis[s]=0;
q.push(node{dis[s],s});
while(!q.empty())
{
node t=q.top();q.pop();
if(b[t.pos])continue;
b[t.pos]=1;
for(int i=head[t.pos];i;i=nxt[i])
{
int v=to[i];
if(dis[v]>dis[t.pos]+w[i])
{
dis[v]=dis[t.pos]+w[i];
q.push(node{dis[v],v});
}
}
}
return;
}
int main()
{
scanf("%d %d %d",&n,&m,&k);
scanf("%d %d",&s,&t);
for(int i=1,u,v,w;i<=m;i++)
{
scanf("%d %d %d",&u,&v,&w);
add(u,v,w);add(v,u,w);
for(int j=1;j<=k;j++)
{
add(u+j*n,v+j*n,w);
add(v+j*n,u+j*n,w);
add(u+j*n-n,v+j*n,0);
add(v+j*n-n,u+j*n,0);
}
}
dij();
int ans=0x3f3f3f3f;
for(int j=0;j<=k;j++)
if(dis[t+j*n]<ans)
ans=dis[t+j*n];
printf("%d\n",ans);
return 0;
}
例
考试真题。
做法
这道题就是一个很显然的二维最短路(特殊的分层图最短路),设
很显然可以用
也可以为
还有种情况
答案为
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+50;
int T,cnt;
int n,m,s,t,x;
int head[N];
struct edge{
int to,nxt,w;
}e[N*4];
void add(int u,int v,int f)
{
e[++cnt].to=v;
e[cnt].nxt=head[u];
head[u]=cnt;
e[cnt].w=f;
return;
}
string ss;
bool vis[N][4];
int dis[N][4];
struct node{
int val,pos,type;
bool operator >(const node &x)const
{
return val>x.val;
}
};
void dij(int S)
{
for(int i=1;i<=n;i++){
for(int j=0;j<=2;j++)
{
dis[i][j]=1e18;
vis[i][j]=0;
}
}
if(ss[S]=='L')dis[S][0]=0;
else if(ss[S]=='R')dis[S][1]=0;
else if(ss[S]=='M')dis[S][0]=0,dis[S][1]=0;
priority_queue<node,vector<node>,greater<node> >q;
q.push(node{dis[S][0],S,0});q.push(node{dis[S][1],S,1});
while(!q.empty())
{
node t=q.top();q.pop();
if(vis[t.pos][t.type])continue;
vis[t.pos][t.type]=1;
for(int i=head[t.pos];i;i=e[i].nxt)
{
int v=e[i].to;
int op=-1;
if(ss[v]=='L')op=0;
else if(ss[v]=='R')op=1;
else if(ss[v]=='M')op=2;
if(op==t.type&&dis[v][op]>dis[t.pos][t.type]+e[i].w)
{
dis[v][op]=dis[t.pos][t.type]+e[i].w;
q.push(node{dis[v][op],v,op});
}
else if(op==2&&dis[v][t.type]>dis[t.pos][t.type]+e[i].w)
{
dis[v][t.type]=dis[t.pos][t.type]+e[i].w;
q.push(node{dis[v][t.type],v,t.type});
}
else if(dis[v][op]>dis[t.pos][t.type]+x+e[i].w)
{
dis[v][op]=dis[t.pos][t.type]+e[i].w+x;
q.push(node{dis[v][op],v,op});
}
}
}
return;
}
signed main()
{
scanf("%lld",&T);
while(T--)
{
cnt=0;
scanf("%lld %lld %lld %lld %lld",&n,&m,&s,&t,&x);
for(int i=1;i<=n+50;i++)head[i]=0;
cin>>ss;
ss=" "+ss;
for(int i=1,u,v,w;i<=m;i++)
{
scanf("%lld %lld %lld",&u,&v,&w);
add(u,v,w);add(v,u,w);
}
dij(s);
printf("%lld\n",min(dis[t][0],dis[t][1]));
}
return 0;
}
补图最短路
是一种很套路的考法,一般做法的主题思想都是不变的。
补图的定义
补图是相对于完全图定义的, 对于一个图
通俗的理解即为有
所以补图的有一个性质为点还是原图的点,但边完全不一样。
补图最短路
前置条件:补图中的边权必须相等
补图最短路在补图求最短路,但如果原图的
一种常见的做法为创建两个
然后在原图跑最短路,遍历到一个点
习题 :Sparse Graph
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+50,M=2e4+50;
bool vis[N];
int dis[N],T,n,m,s,head[N],cnt,nxt[M*2],to[M*2],del[N];
set<int> sx[2];
bool f=0;
void add(int u,int v)
{
to[++cnt]=v;
nxt[cnt]=head[u];
head[u]=cnt;
}
void bfs(int x)
{
memset(dis,0x3f,sizeof(dis));
memset(vis,0,sizeof(vis));
dis[x]=0;
queue<int> q;
q.push(x);
while(!q.empty())
{
int u=q.front();q.pop();
if(vis[u])continue;
vis[u]=1;
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];
if(sx[f].find(v)!=sx[f].end())
{
sx[f].erase(v);
sx[!f].insert(v);
}
}
int res=0;
for(auto v:sx[f])
{
// if(dis[v]>dis[u]+1)
// {
dis[v]=dis[u]+1;
q.push(v);
// }
del[++res]=v;
}
for(int i=1;i<=res;i++)sx[f].erase(del[i]);
f=!f;
if(sx[f].size()==0)break;//还没有更新且与u相邻的节点数量为0
}
}
int main()
{
scanf("%d",&T);
while(T--)
{
cnt=0;memset(head,0,sizeof(head));f=0;
scanf("%d %d",&n,&m);
for(int i=1,u,v;i<=m;i++)
{
scanf("%d %d",&u,&v);
add(u,v);add(v,u);
}
for(int i=1;i<=n;i++)sx[f].insert(i);
scanf("%d",&s);
sx[f].erase(s);
bfs(s);
for(int i=1;i<=n;i++)
{
if(i!=s)
{
if(dis[i]==0x3f3f3f3f)printf("-1 ");
else printf("%d ",dis[i]);
}
}
printf("\n");
}
return 0;
}
习题 :神秘力量
算法:二维最短路+补图最短路
和上一题一样,这题做法为在正图上跑最短路,然后再在补图进行更新,很显然,如果上一条边为普通边,那就直接普普通通地进行更新即
为什么可以直接删去?,因为边权为
具体实现过程为:
设
-
如果是通过
出队的,那么对于 的相邻节点 ,如果连接的边为普通边有 ,而如果为特殊边则有 。 -
如果是通过
出队的,那么对于 的相邻节点 ,如果连接的边为普通边有 ,而如果为特殊边则有 ,然后在 里面删去 ,把它放入 中。最后遍历 中的元素,直接进行更新 ,再删去。
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+50,M=2e6+50;
int T,n,m,s,k;
int head[N],to[M],nxt[M],cnt,w[M],type[M],D[N];
long long dis[N][2];
bool vis[N][2];
void add(int u,int v,int f,int t)
{
to[++cnt]=v;
nxt[cnt]=head[u];
head[u]=cnt;
w[cnt]=f;
type[cnt]=t;
}
struct node{
int val,type,pos;
bool operator >(const node &x)const{
return val>x.val;
}
};
void dij(int ss)
{
priority_queue<node,vector<node>,greater<node> >q;
dis[ss][0]=0;
q.push(node{dis[ss][0],0,ss});
bool f=0;
set<int> S[2];
// int res=0x3f3f3f3f,pos=0;
for(int i=1;i<=n;i++)S[f].insert(i);
S[f].erase(ss);
while(!q.empty())
{
node t=q.top();q.pop();
if(vis[t.pos][t.type])continue;
vis[t.pos][t.type]=1;
//cout<<t.pos<<" "<<t.type<<endl;
int type1=t.type,u=t.pos;
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];
if(type1==1&&S[f].find(v)!=S[f].end())
{
S[f].erase(v);
S[!f].insert(v);
}
if(type1==1)
{
if(type[i]==1)
{
if(dis[v][1]>dis[u][type1]+w[i]-k)
{
dis[v][1]=dis[u][type1]+w[i]-k;
q.push(node{dis[v][1],1,v});
}
}
else
{
if(dis[v][0]>dis[u][type1]+w[i]-k)
{
dis[v][0]=dis[u][type1]+w[i]-k;
q.push(node{dis[v][0],0,v});
}
}
}
else
{
if(type[i]==1)
{
if(dis[v][1]>dis[u][type1]+w[i])
{
dis[v][1]=dis[u][type1]+w[i];
q.push(node{dis[v][1],1,v});
}
}
else
{
if(dis[v][0]>dis[u][type1]+w[i])
{
dis[v][0]=dis[u][type1]+w[i];
q.push(node{dis[v][0],0,v});
}
}
}
}
if(type1==1)
{
int res=0;
for(auto x:S[f])
{
if(dis[x][0]>dis[u][type1])
{
dis[x][0]=dis[u][type1];
q.push(node{dis[x][0],0,x});
}
D[++res]=x;
}
for(int i=1;i<=res;i++)S[f].erase(D[i]);
f=!f;
if(S[f].size()==0)break;
}
}
return;
}
int main()
{
scanf("%d",&T);
while(T--)
{
scanf("%d %d %d %d",&n,&m,&s,&k);
cnt=0;
for(int i=0;i<=n+50;i++)
{
head[i]=0;
dis[i][0]=dis[i][1]=1e18;
vis[i][0]=vis[i][1]=0;
}
for(int i=1,u,v,w,t;i<=m;i++)
{
scanf("%d %d %d %d",&u,&v,&w,&t);
add(u,v,w,t);
}
dij(s);
for(int i=1;i<=n;i++)
{
if(dis[i][0]==1e18&&dis[i][1]==1e18)printf("-1 ");
else printf("%lld ",min(dis[i][0],dis[i][1]));
}
printf("\n");
}
return 0;
}
杂题
题 (最短路+拓扑排序)
解法
由题可知,一个城市
具体过程为设
所以先考虑
,其中 为与 相邻的节点。显然 不能更新 ,因为 表示只是到达了 点,但可能还没有进入 点,所以不能这样更新。
然后再来考虑
, 表示保护 的节点编号。如果 表明到达 点可能还没有进入的最早时间比攻破 点的时间早,那么显然此时 应该由 决定。如果 表明到达 点可能还没有进入的最早时间比攻破 点的时间晚,而因为此时保护 点的节点都被攻破了,所以由 来决定,即 。
转移方程想出来了,于是现在考虑用怎样的顺序能正确更新
#include<bits/stdc++.h>
using namespace std;
const int N=3050,M=70050;
long long dis[N],arrive[N],into[N];
int n,m;
int head[N],to[M],w[M],nxt[M],cnt,in[N];
bool vis[N];
vector<int> g[N];
void add(int u,int v,int f)
{
to[++cnt]=v;
nxt[cnt]=head[u];
w[cnt]=f;
head[u]=cnt;
}
struct node{
long long val;int pos;
bool operator >(const node &x)const{
return val>x.val;
}
};
void dij(int s)
{
for(int i=1;i<=n;i++)dis[i]=arrive[i]=1e18;
dis[s]=into[s]=arrive[s]=0;
priority_queue<node,vector<node>,greater<node> >q;
q.push(node{0,s});
while(!q.empty())
{
node t=q.top();q.pop();
if(vis[t.pos])continue;
// cout<<t.pos<<endl;
vis[t.pos]=1;
for(int i=head[t.pos];i;i=nxt[i])
{
int v=to[i];
if(arrive[v]>dis[t.pos]+w[i])
{
arrive[v]=dis[t.pos]+w[i];
if(!in[v])
{
dis[v]=max(into[v],arrive[v]);
q.push(node{dis[v],v});
// cout<<v<<endl;
}
}
}
for(auto v:g[t.pos])
{
in[v]--;
into[v]=max(into[v],dis[t.pos]);
if(!in[v])
{
dis[v]=max(arrive[v],into[v]);
q.push(node{dis[v],v});
}
}
}
return;
}
int main()
{
scanf("%d %d",&n,&m);
for(int i=1,u,v,w;i<=m;i++)
{
scanf("%d %d %d",&u,&v,&w);
add(u,v,w);
}
for(int i=1;i<=n;i++)
{
int x,v;
scanf("%d",&x);
while(x--)
{
scanf("%d",&v);
g[v].push_back(i);
in[i]++;
}
}
dij(1);
printf("%lld\n",dis[n]);
return 0;
}
题 ( 判环+简单 +单源最短路)
解法
建出反图,再在正图上算出以
考虑怎么转移
所以方程为
考虑到这个方程转移边界会很复杂,可以用记忆化搜索来实现。
初始化
#include<bits/stdc++.h>
#define re register int
#define fo(i,a,b) for (re i=a;i<=b;i++)
using namespace std;
const int maxn=200000+50;
const int mxn=100000+50;
int T,n,m,k,p,sum1,sum2;
bool flag;
int head[mxn],rev[mxn],dist[mxn],vis[mxn],wd[mxn][51],f[mxn][51],to1[maxn],to2[maxn],w1[maxn],w2[maxn];
int nxt1[maxn],nxt2[maxn];
void add(int x,int y,int z)
{
to1[++sum1]=y;
nxt1[sum1]=head[x];
w1[sum1]=z;
head[x]=sum1;
}
void addr(int x,int y,int z)
{
to2[++sum2]=y;
nxt2[sum2]=rev[x];
w2[sum2]=z;
rev[x]=sum2;
}
struct node{
int val,pos;
bool operator >(const node &x)const{
return val>x.val;
}
};
void dij()
{
priority_queue<node,vector<node>,greater<node> > q;
dist[1]=0;
q.push(node{dist[1],1});
while (!q.empty())
{
node t=q.top();q.pop();
if(vis[t.pos])continue;
vis[t.pos]=1;
for (int i=head[t.pos];i;i=nxt1[i])
if (dist[to1[i]]>dist[t.pos]+w1[i]){
dist[to1[i]]=dist[t.pos]+w1[i];
q.push(node{dist[to1[i]],to1[i]});
}
}
}
int dfs(int u,int know)
{
if(know>k||know<0)return 0;
if(wd[u][know]){
wd[u][know]=0;
return -1;
}
if(f[u][know]!=-1)return f[u][know];
wd[u][know]=1;
long long sum=0;
for (int i=rev[u];i;i=nxt2[i])
{
int tmp=dist[u]+know-w2[i]-dist[to2[i]];
int val=dfs(to2[i],tmp);
if(val==-1)
{
wd[u][know]=0;
return -1;
}
sum=(sum+val)%p;
}
if(u==1&&know==0)sum++;
wd[u][know]=0;
return f[u][know]=sum;
}
int main()
{
scanf("%d",&T);
while (T--)
{
sum1=0;sum2=0;
flag=false;
scanf("%d%d%d%d",&n,&m,&k,&p);
memset(f,-1,sizeof(f));
for(int i=1;i<=n;i++)
{
head[i]=rev[i]=0;
vis[i]=0;dist[i]=0x3f3f3f3f;
}
for(int i=1,x,y,z;i<=m;i++)
{
scanf("%d %d %d",&x,&y,&z);
add(x,y,z);
addr(y,x,z);
}
dij();
long long ans=0;
for(int i=0;i<=k;i++)
{
int val=dfs(n,i);
if(val==-1)
{
puts("-1");
flag=true;
break;
}
ans=(ans+val)%p;
}
if (!flag)printf("%lld\n",ans);
}
return 0;
}
题 (排序+多次单源最短路)
解法
对所有边进行排序,然后只用取前
然后对于这
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+50;
int n,m,k,dis[N];
bool vis[N],mark[N];
struct edge{
int fr,to,w;
}e[400050];
int head[N],cnt,nxt[4050],to[4050],w[4050];
void add(int u,int v,int f)
{
to[++cnt]=v;
nxt[cnt]=head[u];
head[u]=cnt;
w[cnt]=f;
}
bool cmp(edge a,edge b){return a.w<b.w;}
struct node{
int val,pos;
bool operator >(const node &x)const{
return val>x.val;
}
};
priority_queue<int> ans;
void dij(int s)
{
for(int i=1;i<=n;i++)dis[i]=1e18,vis[i]=0;
dis[s]=0;
priority_queue<node,vector<node>,greater<node> >q;
q.push(node{dis[s],s});
while(!q.empty())
{
node t=q.top();q.pop();
if(vis[t.pos])continue;
if(mark[t.pos]&&t.pos>s&&dis[t.pos]!=1e18)
{
if(ans.size()<k)ans.push(dis[t.pos]);
else{
if(ans.top()>dis[t.pos])
{
ans.pop();
ans.push(dis[t.pos]);
}
}
}
vis[t.pos]=1;
for(int i=head[t.pos];i;i=nxt[i])
{
int v=to[i];
if(dis[v]>dis[t.pos]+w[i])
{
dis[v]=dis[t.pos]+w[i];
q.push(node{dis[v],v});
}
}
}
}
signed main()
{
scanf("%lld %lld %lld",&n,&m,&k);
for(int i=1,u,v,w;i<=m;i++)
{
scanf("%lld %lld %lld",&u,&v,&w);
e[i].fr=u;e[i].to=v;e[i].w=w;
}
sort(e+1,e+m+1,cmp);
for(int i=1;i<=k;i++)
{
mark[e[i].fr]=1,mark[e[i].to]=1;
add(e[i].fr,e[i].to,e[i].w);
add(e[i].to,e[i].fr,e[i].w);
}
for(int i=1;i<=n;i++)
{
if(mark[i])
dij(i);
}
printf("%lld\n",ans.top());
return 0;
}
题 红灯(图论建模)
解法
按照题意模拟即可,注意要把边看成点进行建模。具体地:看每个点连出去的四条边,设当前点的编号为
#include<bits/stdc++.h>
using namespace std;
const int N=5e7+50;
int head[N],cnt,nxt[N],to[N],n,a[500050][5],s1,s2,t1,t2,w[N];
bool vis[N];
long long dis[N];
void add(int u,int v,int f)
{
to[++cnt]=v;
nxt[cnt]=head[u];
head[u]=cnt;
w[cnt]=f;
}
struct node{
long long val,pos;
bool operator >(const node &x)const{
return val>x.val;
}
};
priority_queue<node,vector<node>,greater<node> >q;
void dij(int s)
{
for(int i=0;i<=(2000000)*4;i++)dis[i]=5611686018427387904;
memset(vis,0,sizeof(vis));
dis[s]=0;
q.push(node{dis[s],s});
while(!q.empty())
{
node t=q.top();q.pop();
if(vis[t.pos])continue;
// cout<<endl<<t.pos<<endl;
vis[t.pos]=1;
for(int i=head[t.pos];i;i=nxt[i])
{
int v=to[i];
if(dis[v]>dis[t.pos]+w[i])
{
dis[v]=dis[t.pos]+w[i];
q.push(node{dis[v],v});
}
}
}
}
signed main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d %d %d %d",&a[i][0],&a[i][1],&a[i][2],&a[i][3]);
for(int i=1;i<=n;i++)
{
for(int j=0,u,v;j<=3;j++)
{
if(a[i][j]==0)continue;
for(int x=0;x<=3;x++)
if(a[a[i][j]][x]==i)u=a[i][j]*4+x;
// i->(a[i][(j+1)%4])
for(int x=0;x<=3;x++)
{
if(a[i][(j+1)%4]==a[i][x]&&a[i][(j+1)%4])v=i*4+x,add(u,v,0);
else if(a[i][x])v=i*4+x,add(u,v,1);
}
}
}
scanf("%d %d %d %d",&s1,&s2,&t1,&t2);
// s=s1*4+s2;t=t1*4+t2;
int s,t;
for(int x=0;x<=3;x++)if(a[s1][x]==s2)s=s1*4+x;
for(int x=0;x<=3;x++)if(a[t1][x]==t2)t=t1*4+x;
dij(s);
if(dis[t]!=5611686018427387904)printf("%lld\n",dis[t]);
else printf("-1\n");
return 0;
}
总结
只要是是图论,建模一定排在第一位,其次是算法特征与思想,最后才是套路,三者一结合起来那么你在图论这个版块就一定不会很差。
有时候图论题还需要考察你对模型的抽象化,有可能第一眼看上去和图论一点关系没有,结果最后是图论建模后就解决了,这类题型只能靠见多识广,再无其他门路。
一些图论套路题也要多见识,才能在考场上遇见这些题目不慌张。还有平时打图论模板也要一个字一个字的打,切忌不要直接复制,因为我们是
最后就是不要小看一些看上去复杂度很高很没有优势的算法,它们在特定的题型中还是会发挥它们自己的作用的。比如
当然有时候一个题目需要运用到多种算法,这就很考验一个选手的综合能力与基本功了,所以我们需要及时复习以前学过的算法,以便临近考试时不慌张。
总之一句话,多见识题型,多训练思维,及时回顾算法思想与解题套路,这样就能在
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人