在acwing上学习算法的一点思考和总结
Dijkstra求最短路
思考1:dijkstra不能解决负权边是因为一旦当st[ j ]变为true后,这个值就不能被修改了,而如果有负权边连接到这个点的话,这个点的最短距离理论上应该被更新,但由于已经被标记为true所以不会再更新。注:正权回环不会有影响,因为正权边只会让距离变大。
思考2:堆优化中对冗余的处理,首先解释以下冗余的来源,代码中的for循环会把所有满足dist[j] > distance + w[i]的点都加进去,所以在更新最短路时,可能会将同一个点加到堆中。因此我们在拿到堆顶元素时,我们要判断它是否已经被更新过了,若是,则跳过下面的代码。
dijkstra通过遍历点来找最短路,并且只适用于边是正权的题。其本质是贪心算法,每次去找点的时候,都是找到与原点距离最短的点,然后去更新距离数组。
一般来说稀疏图用邻接表存,稠密图(边多)用邻接矩阵存。
朴素的dijkstra算法的时间复杂度是O(n^2)
堆优化后的时间复杂度是O(mlogn),用小根堆来维护堆顶的最小值。
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 510, M = 10010;
int g[N][N];
int dis[N];
bool st[N];
int m,n;
int dij()
{
memset(dis, 0x3f, sizeof dis);
dis[1] = 0;
for(int i = 0; i < n-1; i ++) //将所有点遍历完
{
int t = -1;
for(int j = 1; j <= n; j ++)
if(!st[j] && (t == -1 || dis[t] > dis[j])) //寻找当前距离原点最短的点,遍历n次
t = j;
for(int j = 1; j <= n; j ++) //更新该点到原点的最短距离
dis[j] = min(dis[j], dis[t] + g[t][j] );
st[t] = true; //标记该点已被更新完
}
for(int i = 1; i <= n; i ++) cout<<dis[i]<<endl;
if(dis[n] > 0x3f) return -1;
return dis[n];
}
int main()
{
cin>>n>>m;
memset(g, 0x3f, sizeof g);
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
g[a][b] = min(g[a][b], c); //处理重边,保留重边中最小的边
}
cout<<dij();
}
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 1e6 + 10;
int dis[N];
int e[N], ne[N], h[N], w[N], idx;
int n,m;
bool st[N];
void add(int a,int b, int c)
{
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx ++;
}
int dij()
{
memset(dis, 0x3f, sizeof dis );
dis[1] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap;
heap.push({0,1});
while(heap.size())
{
auto t = heap.top();
heap.pop();
int point = t.second, dist = t.first;
if(st[point]) continue;
else st[point] = true;
for(int i = h[point]; i != -1; i = ne[i] )
{
int j = e[i];
if( dis[j] > dis[point] + w[i])
{
dis[j] = dis[point] + w[i];
heap.push({dis[j], j});
}
}
}
return dis[n];
}
int main()
{
memset(dis, 0x3f, sizeof dis);
cin>>n>>m;
memset(h, -1, sizeof h);
for(int i = 0; i < m; i ++)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
int t = dij();
if(t >= 0x3f3f3f3f) cout<<-1<<endl;
else cout<<t<<endl;
}
bellman-ford
bellman-ford算法用来解决负权边且有边数限制的题。
具体实现方式就是每次遍历只会更新一个点走一次后距离原点的最短路。所以在下一次更新时需要用到上一次的dis存的数据,相当于是把遍历的最短路的过程分成多步进行。
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 510, M = 10010;
struct Edege
{
int a,b,c;
}edges[M];
int dis[N];
int last[N];
int n,m,k;
void bell()
{
memset(dis, 0x3f, sizeof dis);
dis[1] = 0;
for(int i = 0; i < k; i ++)
{
memcpy(last, dis, sizeof dis);
for(int j = 0; j < m; j ++)
{
auto e = edges[j];
dis[e.b] = min(dis[e.b], last[e.a] + e.c );
}
}
}
int main()
{
scanf("%d%d%d", &n, &m, &k);
for(int i = 0; i < m; i ++)
{
int a,b,c;
scanf("%d%d%d", &a, &b, &c);
edges[i] = {a, b, c};
}
bell();
if (dis[n] > 0x3f3f3f3f / 2) puts("impossible");
else printf("%d\n", dis[n]);
}
spfa
遍历所有连接在一起的边,并把新出现的边加入到队列,用队列来维护
被淘汰的点之后又会被加入队列是因为此时他的最短距离又被更新了,那么自然和他相连的节点距离也会更新,所以需要把他重新加入队列之中。
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int N = 100010;
int n,m;
int e[N], ne[N], h[N], w[N], idx;
int dis[N];
bool st[N];
void add(int a, int b, int c )
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
int spfa()
{
memset(dis, 0x3f, sizeof dis);
dis[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;
while(q.size())
{
int t = q.front();
q.pop();
st[t] = false;
for(int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if( dis[j] > dis[t] + w[i])
{
dis[j] = dis[t] + w[i];
if(!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
return dis[n];
}
int main()
{
cin>>n>>m;
memset(h, -1, sizeof h);
for(int i = 0; i < m; i ++)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
int t = spfa();
if(t == 0x3f3f3f3f) cout<<"impossible"<<endl;
else cout<<t<<endl;
}