图上路径问题

前置芝士

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;
}
posted @ 2023-10-10 15:30  White_Sheep  阅读(6)  评论(0编辑  收藏  举报