C + + to solve the shortest path problem

Posted by mcclellanfsu on Fri, 12 Nov 2021 04:44:56 +0100

Many conceptual problems are not clearly understood by the author, so many basically the whole article needs to be improved

Let's start with the multi-source shortest path algorithm

1.floyd algorithm (n3):

(Floyd can also solve the problem of single source shortest, which is the shortest set of multiple single sources.)

The core code has only three lines. The text description is: suppose there are n points and there are m edges between points (no independent points). We require each point to have the shortest path to other N-1 points. First, store the graph with the adjacency matrix, initialize INF, read the edges and assign values, and then enumerate each point and take each point K as the transit, See if the distance from other point A to the next point B with K as the transit point is smaller than the original non transit distance. If A -- > k -- > b is less than the original A -- > b distance, we need to update the A -- > b distance to the relaxed distance; In this way, after enumerating n-1 points as transit points for relaxation, the shortest distance from each point to other points can be obtained.

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][k]+dis[k][j],dis[i][j]);

If it is to find an undirected graph, it is also simple. Run it as above and get that the shortest time of a directed graph let dis[i][j]=dis[j][i].

Single source shortest path

1.dijkstra algorithm (based on greedy algorithm, choose the shortest one every time)

Note: dij can only solve the graph with positive weights, but can not solve the graph with negative weights, because each time a new point with the shortest distance is expanded, and all edge weights are positive, this nearest point cannot be shortened by the following relaxation operation, That is, the shortest path of this point will not change later. The negative edge weight may change the characteristics that the shortest path of the updated point will not change (for example, a -- > b is the shortest roadside to all points, the weight is 3, and the edge weight of a -- > C is 5, but if there is an edge C -- > b, if the edge weight is - 10, then it is obvious that the shortest path is from a -- > C -- > b (5-10 = - 5 < 3) , which destroys the correctness of the algorithm)

Find the shortest distance from the source point x to each point. First open a dis [] array for initialization, so the value is the maximum value dis[x]=0. The main idea of the algorithm is to find the point with the shortest distance x from the current shortest path, enumerate the dis array, find the point with the shortest distance x (set this point as a), and then the edge of this point (for example, there is a -- > b, and the weight of this edge is w), Then compare the weight of the original X -- > b with the weight of a -- > b + the weight of X -- > A (DIS [b] > dis [a] + W), that is, dis [b] = min (DIS [b], dis [a] + W). Dis [b] records the shortest distance from X to B, and then perform such relaxation every time. After enumerating n-1 points, the shortest path can be found every time the edges of this point are enumerated

(for example, at the beginning, the dis array only stores itself until it is 0, and the others are infinite. The first solution is to operate on all the outgoing edges of our source point X and relax all the edges of our point X -- > n. after the relaxation, it will not be used behind our source point, that is, it is classified as the shortest path, and this shortest path will not be affected by the later route, and then In the second round, we find another point closest to the source point for the same operation. Each round of operation means that a shortest path is determined. We have n points (including the source point), which means that we can find all the shortest paths only by performing n-1 operations)

The following is an optimized version of adjacency matrix storage graph:

for(int i=1;i<n;i++){
    int minn=0x7ffffff;
    for(int j=1;j<=n;j++){
        if(!f[j]&&min>d[j])//The smallest of the tags was not found
            {
                min=d[j];
                v=j;
            }
     }
    f[v]=1;//sign
     for(j=1;j<=n;j++){
            if(!f[j]&&d[j]>d[v]+a[v][j])//Update path length
            {
                d[j]=d[v]+a[v][j];
            }
     }

 }

We can see from the above process that we can optimize. According to the process, it is not difficult for us to figure out that only the points that we relaxed in the previous round can participate in this round of relaxation operation, that is, only the points that succeeded in the previous round of relaxation can affect other points in this round (we let dis [x] at the beginning) =0 can also be regarded as a relaxation operation on the source point, and then the edge can be relaxed. In the second round, only the edge of the point whose relaxation is successful in the first round can be relaxed. In a round of relaxation, multiple points may be relaxed successfully, and when we select the minimum value in the dis array in each round (excluding the previously selected ones), Naturally, only those points with successful relaxation can be selected. We found that we only need to use the queue to maintain two values (the dis array value of successful relaxation in the previous round and the point of successful relaxation in the previous round). When we use these two values, we need the point with the smallest dis. Here, we can use our priority queue for maintenance (that is, those with small dis value have priority in the next round), In this way, the algorithm can be greatly optimized,

Of course, if we generally use priority queue optimization, we directly use the priority provided in STL_ Queue optimizes the queue, and if there are not too many edges, that is, if the number of edges M is less than the quadratic power of the number of points N, we generally use the adjacency table or static adjacency table storage method (the author uses the static adjacency table, which seems to be also called chain forward star). dijkstra has a unified template for such problems:

(the graph storage time and space complexity of the adjacency table is that O (M) M is the number of edges, and the complexity of the adjacency matrix is O(N2). Therefore, when M < N2, it is more appropriate to use the adjacency table. In the original non optimization solution, there must be o (n) complexity to find the minimum value every time, and the optimization of the minimum heap can reduce the complexity to logn level)

1. For the storage of static adjacency table, we generally need to enter three values, starting edge, ending edge and weight. For these three values, the processing in the static adjacency table is to number each edge according to the input order, and use the array to store the edge exit and weight according to the number. The key point is: we should put together the edges with the same starting point in the edge (this process is realized by a static pointer array), that is, use an array to store the numbers of the edges with the same starting point, The following code is a concrete implementation. It can be seen that we need to go backwards when traversing these edges, and the head array always stores the number of the last edge beginning with u. when traversing the edge beginning with u, we first use the number of the last edge beginning with head[u], and then pass it into e[head[u]].nex with head[u] as a parameter, The value it gets is the number of the previous edge starting with u, and it goes through this way until the end.

struct edge
{
    int go,nex,val;//Start, next, weight
}e[MAX];
int head[MAX],cnt;
void addedge(int u,int v,int w)//u starting edge; v end edge; w weight
{
    e[++cnt].go=v;
    e[cnt].val=w;
    e[cnt].nex=head[u];//If u had recorded a demerit before, what was written down here would point to the previous side
//These two lines are the key to traversal
    head[u]=cnt;//Note the number of the starting edge u
//One is used to judge the edge, and the other is used to traverse the edge?
}

Notations without structure:

void addedge(int u, int v, int w) {
    go[++cnt] = v; val[cnt] = w; from[cnt] = head[u]; head[u] = cnt;
}

2. Use priority_ The priority of the two values to be maintained by the queue priority queue: destination v and weight d. We hope that the priority of this queue is in front of d, that is, the small top (root) heap. Because of the structure used by the author, we also need to overload the comparison operator:

#define M my_pair
struct my_pair
{
    int v,dis;
    my_pair(int x,int y) v(x),dis(y) {}//Initialize member
    
    bool operator < (M right)//Member functions can be defaulted
    {
        return right.dis<dis;
    }
    //When comparing structure members, a < B; Equivalent to A. operator < (b);
    //It can be seen that if true is returned (i.e. A.D > B.D), a < B is true, which is the characteristic of large top heap
    //It will lead to b advanced containers and eventually become a descending order, that is, the small d is in front of {b, a};
    
};

Note that in the priority_queue of stl, by default, it is a large top heap, and the default comparison method is operator <. When using greater < T > (in the header file < functional >), it will become a small top heap, and the comparison method will become operatpr >; By default, stl's sort is arranged in ascending order by default, that is, from small to large. The default comparison method is operator <. When greater < T > is used, it will become a descending order, that is, from large to small, and the comparison method will become operator >

  Of course, we can also overload the > number:

#define M my_pair
struct my_pair
{
    int v,dis;
    my_pair(int x,int y) v(x),dis(y) {}//Initialize member
    
    bool operator > (M right)//Member functions can be defaulted
    {
        return right.dis<dis;
    }
    //In priority_ The queue defaults to the large root heap (in descending order), and the overload number > return remains unchanged
    //As a result, it will still return true (A.D > B.D), that is, a > b is consistent, and a is advanced
    //The result will be {a,b}, but obviously d has a big priority and does not meet the demand, so it should be in priority_ Used in queue
    //The greater < > pseudo function changes the comparison mode of the priority queue to operator >
    
//Or change the value of return without changing the comparison method (this idea is considered wrong by the author later. / / it is not verified)
//(because the default comparison method is the < symbol, without the > symbol, there is no need to return an opposite value to achieve the original correct result)
    
};

3. Traversal implementation of priority queue

After we have processed the input data and processed the priority of the queue, we need to use the priority queue for optimization. We directly use priority_queue generates a priority queue container, which involves going out of the queue and entering the queue, and understanding the meaning of each variable. Note that the data we enter the queue is given priority according to the small weight, so there is no need to traverse the dis [] array to find the nearest point when processing the next point as in the case of non optimization. The basic operation implementation is to queue the point to be found and 0, Because we will traverse its outgoing edge later:

priority_queue<M>Q;//Generate a priority queue container Q of type M
dijkstra(int s)
{
    dis[s]=0;//Note that this dis array can be initialized to the maximum value with another function
    Q.push(M(s,0));//Join the team at the first point
    while(Q.empty()){
    }
}

In the loop body, what we need to do is to judge the outgoing edges of each point. First, the first point is s, find all outgoing edges of S, and then update the dis array. At this time, all outgoing edges of s should be listed and expanded (note that the priority queue is the first outgoing column with small weight). After the expansion of S, we need to relax the edges, This implementation is the same as that not optimized above. Judge the edge weight of a -- > B plus the weight of S -- > A to see whether the weight of S -- > B has been changed:

priority_queue<M>Q;//Generate a priority queue container Q of type M
dijkstra(int s)
{
    dis[s]=0;//Note that this dis array can be initialized to the maximum value with another function
    Q.push(M(s,0));//Join the team at the first point
    while(!Q.empty()){
        M cur=Q.top();                    //Because we didn't rule out the point that is already the shortest path when we joined the team
        if(dis[cur.v]<cur.dis)continue; //Therefore, the weight of the current queue has been greater than that of the record, so there is no need to continue
                            //You can also use a tag array instead. Each time you expand the next point, you
                            //Judge whether the array has been marked, because it is the shortest after marking
        
        //Here is to traverse the adjacency table. nex stores the next edge of the current point
        //When you point to the last edge, it is zero, that is, you have traversed all the outgoing edges
        for(int i=head[cur.v];i;i=e[i].nex){
                   int y=e[i].v;//Reduce code size
            if(dis[y]>cur.dis+e[i].val){//dis is the point representing s -- > current edge exit
                                            //If you come in, it means success
                dis[y]=cur.dis+e[i].val;//to update
                Q.push(y,dis[y]);//Push the updated points into the queue
                                        //The next train is the one closest to s
            }
        }
    }
}

Complete implementation:

#include<bits/stdc++.h>
#define M my_pair
#define LL long long
using namespace std;
const int MAX=1e7;
const int INF=1e9;
int dis[MAX],h[MAX];
int n,m,cnt;
struct edge
{
    int go;
    int nex;
    LL val;

}e[MAX];
struct my_pair
{
    int v;
    LL dis;
    M (int x,LL y):v(x),dis(y){}
    //Initialization, equivalent to
    /*
        struct M (int x,int y)
        {
           v=x;//this->v=x;
           dis=y; //this->dis=y;
        }
    */
    bool operator < (M r)const{
        return r.dis<dis;//r.d<this->dis;
    }//Overloaded operator<

};
void addadge(int u,int v,int w)
{
    e[++cnt].go=v;
    e[cnt].val=w;
    e[cnt].nex=h[u];//Note u the number of the previous side and store the next one below
    h[u]=cnt;//Note the number of the current edge of the u point. / / it is always the last edge
}
priority_queue<M>Q;

void dijkstra(int s)
{
    for(int i=1;i<=n<<1;i++){
        dis[i]=INF;
    }
    dis[s]=0;
    Q.push(M(s,0));
    while(!Q.empty()){
        M cur=Q.top();
        Q.pop();
        if(dis[cur.v]<cur.dis)continue;
        for(int i=h[cur.v];i;i=e[i].nex){
                int y=e[i].go;
            if(dis[y]>cur.dis+e[i].val){
                dis[y]=cur.dis+e[i].val;
                Q.push(M(y,dis[y]));
            }
        }
    }

}
int main()
{
    int u,v,w,ans=0;
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        cin>>u>>v>>w;
        addadge(u,v,w);
    }
    dijkstra(1);
    for(int i=1;i<=n;i++){
        ans+=dis[i];//Here is the total length of the shortest path of a single source
    }

    cout<<ans<<endl;
    return 0;
}

2. Bellman Ford & the optimized Bellman Ford, namely spfa algorithm

Bellman Ford algorithm can solve the negative edge weight problem that dijkstra algorithm can't solve (it'S said that dij can also be used to solve the negative edge problem, but I won't!). The main idea of the algorithm is that all points must go through the relaxation of all edges (in fact, some points go through the relaxation of an edge, which will be optimized). There are n points and m edges, We find the shortest path from point S to other points. We can get the shortest path as long as we relax each edge n-1 times.

The success of each relaxation means that we have found the shortest path of s to this point. We can know that we open the dis array to INF, dis[s]=0. When the first iterative relaxation (i.e. enumerating each edge for relaxation operation) can relax successfully (i.e. dis [v] > dis [u] + W), only dis[s] with edge head s, Then, the first relaxation operation can only let us relax the points that do not need to be transferred in the past (and this edge has been defaulted to the current shortest edge). The second relaxation operation, that is, try to relax all edges again. We can find that only the points that have been relaxed before can participate in this relaxation operation, that is, affect the points around it, You can start from the point that has not been relaxed, because the dis array is still infinite, it is impossible to add edge weight to make the edge small; In other words, after relaxing all the edges in the first round, we get the shortest path length of s point "passing through only one edge" to the other vertices. In the second round, after relaxing all the edges, we get the current shortest path length from the s point "through up to two edges" to the other vertices. If you do K rounds, you will get the current shortest path length of vertex 1 "passing through k edges at most" to other vertices.

Then we can know that if the nth relaxation operation is carried out, it is the current shortest path for the vertex "to experience up to n edges" to reach the other points, but can we really succeed in the nth relaxation? (in the case of no negative weight circle), it must not be, because the shortest path between any two points contains at most n-1 edges (draw it and you will know that n vertices have at least n-1 edges, which just connect into a line. Where do you add more edges?)

Core code:

for(int i=0;i<n;i++){
    
    for(int j=1;j<=m;j++){
        
        if(dis[v]>dis[u]+w)
            
            dis[v]=dis[u]+w;
    }
}

It can be found that in fact, I may have obtained all the shortest paths before relaxing n-1 times! All n-1 is only a maximum, so how to judge whether the next round of relaxation is needed? Let'S assume that when we perform the k-1 relaxation operation, what we do is the current shortest path for the vertex of point S to "experience k-1 edges at most" to reach other points, and we have previously obtained that only the points experiencing relaxation can perform this relaxation operation. Assuming that this relaxation operation is unsuccessful, in fact, our dis array has obtained all the shortest paths (do you think it makes sense? Every time we relax, we traverse m edges, so the shortest path we reach to the current point when we relax k-1 times, i.e. "experience k-1 edges at most!" then the shortest path for the vertex to reach other points when we relax k-2 times, i.e. "experience k-2 edges at most!" is already the shortest!) , when we perform the k-th relaxation operation, it is impossible to change the value of the dis array, because there is no relaxation success point in the previous round. What we do in this round is just the operation of the previous round (here is another small example: if we have 9 points and 8 edges, all of which point from 1 to 2 ~ 9, in fact, we have found all the shortest circuits in the first relaxation operation, so the remaining 8 cycles are wasted) (the main premise is that there is no negative weight circle)

The reason is: when the k-th operation is not relaxed successfully, we have obtained the shortest path at the K-1st operation. As a general reason, each relaxation operation must be successful. If the k-th relaxation is not relaxed successfully, the dis array is already the shortest path, so we can exit the loop

int flag;
for(int i=0;i<n;i++){

        flag=1;
    
    for(int j=1;j<=m;j++){
        
        if(dis[v]>dis[u]+w){
            
            flag=0;
         
            dis[v]=dis[u]+w;
            
        }
    }
    if(flag) break;//If the flag remains unchanged, it means that the relaxation fails, and the loop can be pushed out
}

We can also find a rule from the analysis. Only the relaxed points can undergo the next relaxation operation, that is, affect the points around them. We can also optimize here, because we traverse m edges every time, including the useless points that have been successfully relaxed. Then why do we not only relax the adjacent edges of those points that have been successfully relaxed? That is, construct Create a queue and include the relaxation success points into the queue. Use the queue to maintain these relaxation success points. For example, we have a point of Q relaxation success, and we include this point into the queue. After this round of relaxation operation, when we carry out the next relaxation operation, we only carry out the relaxation operation on the edge of point Q, that is (if (dis[v] > dis[u] + w)), in which dis[u] Is the distance from the vertex to Q, w is the edge weight, and dis[v] is the location of the edge of Q; in this way, we greatly reduce the enumeration.

Original:

int flag;
for(int i=0;i<n;i++){

        flag=1;
    
    for(int j=1;j<=m;j++){//That's what changed here
        
        if(dis[v]>dis[u]+w){
            
            flag=0;
         
            dis[v]=dis[u]+w;
            
        }
    }
    if(flag) break;//If the flag remains unchanged, it means that the relaxation fails, and the loop can be pushed out
}

Optimization:

int book[MAX],e[MAX][MAX];//Here is the stored graph. When the data is small, use the adjacency matrix
int flag;
queue<int>q;
while(!Q.empty()){
    int cur=Q.top();
    Q.pop();
    book[v]=0;//This array prevents a point from entering multiple times for judgment
    for(int i=1;i<n;i++){//Still, the upper limit is n-1 times
        flag=1;
        if(dis[i]>dis[cur]+e[cur][i]){
            dis[i]=dis[cur]+e[cur][i];
            if(!book[v]){
                Q.push(v);
                book[v]=1;
            }
        }
        
    }
}

We can see that the above example uses the adjacency matrix to store the graph, which is obviously a waste of space for sparse graphs, and it will explode when the data is large. Therefore, we all use the chained forward star (i.e. static adjacency table) to store the graph as mentioned in dij before traversing:

int book[MAX],head[MAX];
int flag;
queue<int>q;
struct edge
{
    int go;
    int nex;
    int val;

}e[MAX];

void addadge(int u,int v,int w)//Chain forward star
{
    e[++cnt].go=v;
    e[cnt].val=w;
    e[cnt].nex=head[u];//Note u the number of the previous side and store the next one below
    head[u]=cnt;//Note the number of the current edge of the u point
}

while(!Q.empty()){
    int cur=Q.top();
    Q.pop();
    book[v]=0;//This array prevents a point from entering multiple times for judgment
    for(int i=head[cur];i;i=e[i].nex){
        flag=1;
        int v=e[i].go;
        if(dis[v]>dis[cur]+e[i].val){
            dis[v]=dis[cur]+e[i].val;
            if(!book[v]){
                Q.push(v);
                book[v]=1;
            }
        }
        
    }
}

In fact, the spfa algorithm we often hear is the bellman Ford algorithm after queue optimization. Its queue operation greatly optimizes the time complexity of the original algorithm.

Summary: floyd algorithm is a violent enumeration, which can be done directly without brain when there is little data; the main idea of dijkstra algorithm is to select a point closest to the source point each time to relax its edge until all nodes are selected. At present, we do not know how to solve the negative edge problem; Bellman Ford algorithm mainly considers the edge out operation, that is, all edges are used for nodes in each round Relax to obtain the current shortest path of each round obtained by "the vertex passes through m edges at most". This method can solve the graph problem with negative weight: the reasons are as follows:

dijkstra algorithm assumes that the point is the shortest distance after each relaxation operation on the outgoing edge of the nearest point. If there is a negative edge in the back, it may affect the judgment of the shortest path in the front, that is, it may deny the shortest path in the front, and the correctness of the algorithm cannot be guaranteed. Bellman Ford algorithm is an operation on the edge, that is, let the vertex experience a path The shortest path is obtained by going through all edges at most. The existence of negative edge will not affect the correctness of its front, because the shortest path of each relaxation is the current shortest path.

Topics: Algorithm Graph Theory