图上路径问题
前置芝士
Dijkstra算法
朴素法O(n^2+m)
//cpp
int n,m;
const int N=1001;
int dist[N];
bool vis[N];
int e[N][N];
int dijkstra(){
memset(dist,0x3f,sizeof(dist));
dist[1]=0;
for(int i=0;i<n-1;i++){
int t=-1;
for(int j=1;j<=n;j++){
if(!vis[j]&&(t==-1||dist[t]>dist[j])){
t=j;
}
}
for(int j=1;j<=n;j++){
dist[j]=min(dist[j],dist[t]+e[t][j]);
}
vis[t]=true;
}
if(dist[n]==0x3f3f3f3f) return -1;
return dist[n];
}
堆优化O(mlogn)
//cpp
typedef pair<int,int> PII;
int n,m;
const int N=1001;
const int M=1000100;
bool vis[N];
struct edge{
int v,w,ne;
}e[M];
int h[N],idx;
int dist[N];
priority_queue<PII,vector<PII>,greater<PII>> hp;
int dijkstra(){
memset(dist,0x3f,sizeof(dist));
dist[1]=0;
hp.push({0,1});
while(hp.size()){
auto t=hp.top();
hp.pop();
int v=t.second,d=t.first;
if(vis[v]) continue;
vis[v]=true;
for(int i=h[v];i;i=e[i].ne){
int j=e[i].v;
if(dist[j]>dist[v]+e[j].w){
dist[j]=dist[v]+e[j].w;
hp.push({dist[j],j});
}
}
}
if(dist[n]==0x3f3f3f3f) return -1;
return dist[n];
}
空间优化版(省略vis)
输出最短路径路线
const ll INF = 1e16;
const int maxn = 1e5+5;
typedef struct Node{
int v; // 点
ll w; // 权值
}node;
bool operator < (const node &a, const node &b){ // 重载 < ,使优先队列按照权值排序
return a.w > b.w;
}
vector<node>mp[maxn]; // 邻图表
ll dis[maxn]; // 1 到 i号点的最短距离
int path[maxn] = {0}; // 路径,x点是从path[x]到达的
void Pr(int x){ // 递归路径
if(x == 1){ // x = 1 时就结束
cout << 1 << " ";
return;
}
Pr(path[x]); // 递推前一个点
cout << x << " ";
}
void solve(){
int n, m;
cin >> n >> m;
int a, b, w;
node c;
for(int i = 1; i <= m; i++){ // 邻图表,存边和边的长度
cin >> a >> b >> w;
c.v = b; c.w = w;
mp[a].push_back(c);
c.v = a; c.w = w;
mp[b].push_back(c);
}
for(int i = 1; i < maxn; i++) dis[i] = INF; //初始化最大值
priority_queue<node>qu; // 算法的核心:优先队列(贪心的实现)
node now, next; // 队列中的 w 是指 1 号点到 v 号点的距离(队列最前端的就是最短距离)
now.v = 1; now.w = 0;
qu.push(now); // 插入 1 号点
while(!qu.empty()){ // 当不能再插入的时候就结束
now = qu.top();
qu.pop();
int len = mp[now.v].size();
for(int i = 0; i < len; i++){ // 遍历当前点可以到达的下一个点
next.v = mp[now.v][i].v;
next.w = now.w + mp[now.v][i].w;
if(next.w < dis[next.v]){ // 如果当前距离小于储存的最短距离时
path[next.v] = now.v; // 更新路径
dis[next.v] = next.w; // 更新最短距离
}
qu.push(next); // 下一个点入列
}
mp[now.v].clear(); // 这个点已经走过了,储存的点清空,在进入这个点的时候就不用再次循环,当这样可以省去一个标记数组
}
if(path[n] == 0) cout << -1 << endl; // 没有可以到达 n 的点
else Pr(n); // 递归路径
}
Bellman-Ford算法
#define inf 0x3f3f3f3f
const int N=10010;
struct edge{int v,w;};
vector<edge> e[N];
int d[N];
int n,m;
bool bellmanford(int s){
memset(d,inf,sizeof(d));
d[s]=0;
bool flag;
for(int i=1;i<=n;i++){
flag=false;
for(int u=1;u<=n;u++){
if(d[u]==inf) continue;
for(auto ed:e[u]){
int v=ed.v;
int w=ed.w;
if(d[v]>d[u]+w){
d[v]=d[u]+w;
flag=true;
}
}
}
if(!flag) break;
}
return flag;
}
SPFA算法
在SPFA算法中,超级源(super source)是一个虚拟节点,它与所有其他节点都有一条边相连,且这些边的权值都为0。超级源节点的作用是将原图转化为一个具有单源最短路径性质的图,从而简化SPFA算法的实现过程。具体来说,超级源节点的引入可以实现以下两个目的:
将原图转化为具有单源最短路径性质的图。在原图中,可能存在多个连通分量,每个连通分量都有自己的最短路径,因此需要对每个连通分量都运行一次SPFA算法,才能计算出图中任意两个节点之间的最短距离值。而通过引入超级源节点,可以将所有节点连接在一起,从而将原图转化为具有单源最短路径性质的图,即从超级源节点出发,可以计算出所有其他节点到超级源节点的最短路径长度。( 核心思想 )
简化SPFA算法的实现过程。通过引入超级源节点,可以省去判断起点的入队次数的过程,因为起点就是超级源节点。同时,可以将超级源节点的最短距离值初始化为0,从而简化算法的初始化过程。
朴素算法:O(km->nm)k为一个常数
[c++]
#define inf 0x3f3f3f3f
const int N=10010;
struct edge{int v,w;};
vector<edge> e[N];
int d[N],cnt[N],vis[N];
queue<int> q;
int n,m;
bool spfa(int s){
memset(d,inf,sizeof(d));
d[s]=0;vis[s]=1;q.push(s);
while(q.size()){
int u=q.front();q.pop();vis[u]=0;
for(auto ed:e[u]){
int v=ed.v,w=ed.w;
if(d[u]>d[u]+w){
d[v]=d[u]+w;
cnt[v]=cnt[u]+1;
if(cnt[v]>=n) return true;
if(!vis[v]) q.push(v),vis[v]=1;
}
}
}
return false;
}
路径记录与递归输出,在松弛的时候记录前驱点。
#define inf 0x3f3f3f3f
const int N=10010;
struct edge{int v,w;};
vector<edge> e[N];
int d[N],cnt[N],vis[N],pre[N];
queue<int> q;
int n,m,s;//n:点,m:边,s:源点
bool spfa(){
memset(d,inf,sizeof(d));
d[s]=0;vis[s]=1;q.push(s);
while(q.size()){
int u=q.front();q.pop();vis[u]=0;
for(auto ed:e[u]){
int v=ed.v,w=ed.w;
if(d[u]>d[u]+w){
d[v]=d[u]+w;
pre[v]=u;
cnt[v]=cnt[u]+1;
if(cnt[v]>=n) return true;
if(!vis[v]) q.push(v),vis[v]=1;
}
}
}
return false;
}
void dfs_path(int u){
if(u==s) {cout<<u<<" ";return;}
dfs_path(pre[u]);
cout<<u<<" ";
}
虚点
分层图
飞行路线(分层图)
[problem pesection]
Alice 和 Bob 现在要乘飞机旅行,他们选择了一家相对便宜的航空公司。该航空公司一共在 \(n\) 个城市设有业务,设这些城市分别标记为 \(0\) 到 \(n-1\),一共有 \(m\) 种航线,每种航线连接两个城市,并且航线有一定的价格。
Alice 和 Bob 现在要从一个城市沿着航线到达另一个城市,途中可以进行转机。航空公司对他们这次旅行也推出优惠,他们可以免费在最多 \(k\) 种航线上搭乘飞机。那么 Alice 和 Bob 这次出行最少花费多少?
[input]
第一行三个整数 \(n,m,k\),分别表示城市数,航线数和免费乘坐次数。
接下来一行两个整数 \(s,t\),分别表示他们出行的起点城市编号和终点城市编号。
接下来 \(m\) 行,每行三个整数 \(a,b,c\),表示存在一种航线,能从城市 \(a\) 到达城市 \(b\),或从城市 \(b\) 到达城市 \(a\),价格为 \(c\)。
[output]
输出一行一个整数,为最少花费。
[样例]
5 6 1
0 4
0 1 5
1 2 5
2 3 5
3 4 5
2 3 3
0 2 100
8
\(2 \le n \le 10^4\),\(1 \le m \le 5\times 10^4\),\(0 \le k \le 10\),\(0\le s,t,a,b < n\),\(a\ne b\),\(0\le c\le 10^3\)。
[solved]
(1)无向图的e数组内存:\({M}\times({K}+1)\times4\)
(2)额外边:如果 s → t 的路径,结点数小于 k,则需要单独建边。则答案才是d[k*n+t]。
for(int i=1; i<=k; i++)
addEdge((i-1)*n+t,i*n+t,0);
时间复杂度:O(nklogmk)
无权图单源最短路BFS算法
[problem pesection]
给出一个 \(N\) 个顶点 \(M\) 条边的无向无权图,顶点编号为 \(1\sim N\)。问从顶点 \(1\) 开始,到其他每个点的最短路有几条。
[input]
第一行包含 \(2\) 个正整数 \(N,M\),为图的顶点数与边数。
接下来 \(M\) 行,每行 \(2\) 个正整数 \(x,y\),表示有一条由顶点 \(x\) 连向顶点 \(y\) 的边,请注意可能有自环与重边。
[output]
共 \(N\) 行,每行一个非负整数,第 \(i\) 行输出从顶点 \(1\) 到顶点 \(i\) 有多少条不同的最短路,由于答案有可能会很大,你只需要输出 $ ans \bmod 100003$ 后的结果即可。如果无法到达顶点 \(i\) 则输出 \(0\)。
\(1\le M\le 2\times 10^6\)
[solved]
const int N=1000010,M=2000010,inf=0x3f3f3f3f,mod=100003;
vector<int>e[N];
int dep[N];
bool vis[N];
int cnt[N];
int n,m;
void solve(){
cin>>n>>m;
for(int i=1;i<=m;i++){
int x,y;
cin>>x>>y;
e[x].push_back(y);
e[y].push_back(x);
}
queue<int> q;
dep[1]=0;
vis[1]=1;
q.push(1);
cnt[1]=1;
while(!q.empty()){
int x=q.front();q.pop();
for(int i=0;i<e[x].size();i++){
int t=e[x][i];
if(!vis[t]){vis[t]=1;dep[t]=dep[x]+1;q.push(t);}
if(dep[t]==dep[x]+1){cnt[t]=(cnt[t]+cnt[x])%mod;}
}
}
for(int i=1;i<=n;i++){
cout<<cnt[i]<<endl;
}
}
到达目的地的最短路方案数
[prolem pesection]
你在一个城市里,城市由 n 个路口组成,路口编号为 0 到 n - 1 ,某些路口之间有 双向 道路。输入保证你可以从任意路口出发到达其他任意路口,且任意两个路口之间最多有一条路。
给你一个整数 n
和二维整数数组 roads ,其中 \(roads[i] = [u_i, v_i, time_i]\) 表示在路口 \(u_i\) 和 \(v_i\) 之间有一条需要花费 \(time_i\) 时间才能通过的道路。你想知道花费 最少时间 从路口 0
出发到达路口 n - 1 的方案数。
请返回花费 最少时间 到达目的地的 路径数目 。由于答案可能很大,将结果对 10**9+ 7 取余 后返回。
[无负权+无重边+无自环+拓扑排序]
求0到其余点的最短路。由于图中边权均为正,所有在最短路上的边构成了一个有向无环图(DAG)。我们在 DAG 上跑拓扑排序,同时计算最短路方案数。由于输入可以是稠密图,这里可以用邻接矩阵存图,且不需要用堆优化的 Dijkstra。 最短路构成了一个 DAG,这里不需要建一个新图,直接根据距离来判断每条边是否在 DAG 上。
[solved]
[python]
def countPaths(self, n: int, roads: List[List[int]]) -> int:
g = [[1e18] * n for _ in range(n)]
for r in roads:
v, w, wt = r[0], r[1], r[2]
g[v][w] = wt
g[w][v] = wt
d = [1e18] * n
d[0] = 0
used = [False] * n
while True:
v = -1
for w, u in enumerate(used):
if not u and (v < 0 or d[w] < d[v]):
v = w
if v < 0:
break
used[v] = True
for w, wt in enumerate(g[v]):
if d[v] + wt < d[w]:
d[w] = d[v] + wt
deg = [0] * n
for v, r in enumerate(g):
for w, wt in enumerate(r):
if d[v] + wt == d[w]:
deg[w] += 1
dp = [0] * n
dp[0] = 1
q = [0]
while q:
v = q.pop(0)
for w, wt in enumerate(g[v]):
if d[v] + wt == d[w]:
dp[w] = (dp[w] + dp[v]) % (10**9 + 7)
deg[w] -= 1
if deg[w] == 0:
q.append(w)
return dp[n - 1]
多连通块判断负环
超级源应用
const int MAXN=10005;
const int INF=0x3f3f3f3f;
struct edge{
int to;
int w;
};
int n,m,T;
vector<edge> g[MAXN+1];
int dis[MAXN+1];
bool vis[MAXN+1];
int cnt[MAXN+1];
void add(int from ,int to,int w){
g[from].push_back({to,w});
}
bool spfa(int s){
memset(dis,INF,sizeof(dis));
memset(vis,0,sizeof(vis));
memset(cnt,0,sizeof(cnt));
vis[s]=1;
dis[s]=0;
cnt[s]=1;
queue<int> q;
q.push(s);
while(!q.empty()){
int u=q.front();
q.pop();
vis[u]=0;
for(int i=0;i<g[u].size();i++){
int v=g[u][i].to;
int w=g[u][i].w;
if(dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
if(!vis[v]){
vis[v]=1;
q.push(v);
cnt[v]++;
}
if(cnt[v]>n+1){
return false;
}
}
}
}
return true;
}
int main(){
cin>>T;
while(T--){
cin>>n>>m;
for(int i=0;i<m;i++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);
}
for(int i=1;i<=n;i++){
add(n+1,i,0);
}
if(spfa(n+1)) cout<<"NO"<<endl;
else cout<<"YES"<<endl;
for (int i = 1; i <= n+1; i++) {
g[i].clear();
}
}
return 0;
}