dij的优先队列边表优化

dij的复杂度为v*v,通过优先队列优化后为e*logv.

(第一次写,没有过多的测试,不保证对。只当是测试blog了!)

#include<cstdio>
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
struct edge
{
    int v,w,next;
}e[20000];
typedef struct nd
{
    int dis,v;
    nd(int d,int vv){dis=d;v=vv;}
    bool operator<(const nd &b)const
    {
        return dis>b.dis;
    }
}cmpnode;
int head[1000]={0},js=0;
int dist[1000];
bool vis[1000];
priority_queue<cmpnode>q;
int n,m,s,t;
void addage(int u,int v,int w)
{
    e[++js].v=v;e[js].w=w;
    e[js].next=head[u];head[u]=js;
}
void dij(int st)
{
    dist[st]=0;
    q.push(cmpnode(0,st));
    while(!q.empty())
    {
        cmpnode tp=q.top();q.pop();
        if(tp.v==t)return;
        if(vis[tp.v])continue;
        vis[tp.v]=1;
        for(int i=head[tp.v];i;i=e[i].next)
        {
            int v=e[i].v,w=e[i].w;
            if(dist[v]>dist[tp.v]+w)dist[v]=dist[tp.v]+w;
            q.push(cmpnode(dist[v],v));
        }
    }
}
int main()
{
    cin>>n>>m;
    for(int i=0;i<m;i++)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        addage(a,b,c);
        addage(b,a,c);
    }
    cin>>s>>t;
    for(int i=0;i<1000;i++)dist[i]=0x7fffffff;
    dij(s);
    cout<<dist[t];
    return 0;
}

 

转载于:https://www.cnblogs.com/gryzy/p/5804538.html

<think>我们使用Dijkstra算法时,通常使用一个布尔数组(或称为visited数组)来标记节点是否已被访问(即是否已找到从起点到该节点的最短路径)。然而,在某些实现中,特别是当使用优先队列(如C++的priority_queue)时,我们可能会省略这个visited数组,而采用另一种方式:当从优先队列中取出一个节点时,如果该节点当前的距离值不等于dist数组中存储的最短距离值,则跳过该节点。这实际上是一种“延迟删除”的策略。 在之前的代码实现中,我们没有显式使用visited数组,而是使用了以下关键剪枝: ```cpp auto [d, u] = pq.top(); pq.pop(); if (d != dist[u]) continue; // 关键剪枝:跳过过期状态 ``` 为什么不用visited数组(即bool数组)?原因如下: 1. **正确性考虑**: - 在标准的Dijkstra算法中,一旦一个节点从优先队列中取出(即作为当前最小距离节点),它的最短距离就已经确定,不会再被更新。因此,我们可以使用visited数组标记,后续不再处理该节点。 - 然而,在带有路径计数和额外状态(如救援队数量)的扩展Dijkstra中,即使一个节点已经被取出(即已确定最短距离),我们仍然可能通过其他等长的路径(即相同最短距离的路径)来更新路径计数和救援队数量。注意:在分支2中,我们处理的是等长路径,这发生在节点v的最短距离已经确定之后(因为我们已经通过分支1确定了最短距离,后续分支2只处理等长情况)。但是,分支2的处理并不需要重新将节点加入队列,因为分支2只更新计数和救援队,不改变最短距离,所以不会影响其他节点的距离计算。因此,我们实际上不需要因为分支2而将节点重新加入队列。 2. **效率考虑**: - 使用visited数组可以避免重复处理同一个节点,因为每个节点只会被处理一次(即从队列中取出一次)。但是,在优先队列实现中,同一个节点可能被多次加入队列(当有更短的路径被发现时,会再次加入)。使用visited数组后,即使队列中有同一个节点的多个条目,我们也只处理一次(第一次取出时),后续的跳过。 - 然而,在分支2(等长路径)的情况下,我们并不需要将节点重新加入队列,因为等长路径的发现是在松弛过程中遇到相同距离时进行的,且此时节点尚未被处理(即尚未从队列中取出)?不对,实际上,节点v可能在之前已经被取出(因为分支1已经确定了它的最短距离),然后我们又通过另一条等长路径来更新它的计数和救援队。注意:分支2的更新是在松弛过程中,当遇到一条等长路径时,我们更新计数和救援队,但不需要将节点v重新加入队列,因为它的距离没有改变,不会影响其他节点。因此,分支2的处理并不需要节点v在队列中。 3. **为什么不用visited数组?** - 在标准Dijkstra中,一旦节点被标记为visited,就意味着它的最短距离已经确定,后续不会再更新。但在我们的扩展中,即使节点已经被标记为visited(即已从队列中取出),我们仍然可能通过分支2更新它的计数和救援队。因此,如果我们使用visited数组,在节点被取出后将其标记为visited,那么后续再遇到该节点的等长路径时,我们将无法执行分支2的更新(因为遇到该节点时,如果它已经被visited,我们就会跳过)。这就会导致漏掉一些等长路径,从而使得路径计数和救援队数量计算错误。 4. **如何避免重复处理节点?** - 我们使用“延迟删除”策略:当从优先队列中取出一个节点时,检查它的距离值是否等于当前dist数组中的值。如果不相等,说明这个节点已经被更新过(即有一个更短的路径已经处理过,当前队列中的这个条目是过期的),所以跳过。 - 这样,即使同一个节点在队列中有多个条目,我们也只处理最新的那个(即与当前dist值相等的那个),其他的跳过。 5. **两种方式的对比**: | **方法** | 优点 | 缺点 | |------------------------|--------------------------------|------------------------------------| | **visited数组** | 每个节点只处理一次 | 无法处理后续的等长路径更新 | | **延迟删除 (dist检查)**| 允许后续等长路径更新状态 | 同一个节点可能被多次处理(但跳过过期状态) | 6. **结论**: 在需要处理等长路径并更新额外状态(如路径计数、救援队数量)的问题中,我们不能使用visited数组,因为等长路径的更新可能发生在节点已经被取出(即最短距离确定)之后。延迟删除策略(通过检查dist值)允许我们在处理节点时,如果该节点的距离值已经被更新(即当前取出的条目是过期的)则跳过,同时还能在松弛过程中处理等长路径(此时节点可能已经被标记为“已访问”但我们并不阻止等长路径的更新)。 7. **代码示例说明**: 在之前的代码中,我们只使用了延迟删除策略,没有使用visited数组。这样,当我们在松弛过程中遇到一个节点v,并且发现一条等长路径(newDist == dist[v])时,即使节点v已经被从队列中取出过(即已经确定了最短距离),我们仍然可以更新它的num[v]和teams[v]。这是因为节点v的dist[v]已经确定,不会再改变,但等长路径的更新并不需要重新将节点v加入队列(因为不会改变dist[v],所以不会影响其他节点),我们只需要在松弛过程中即时更新即可。 因此,在本题中,我们选择不使用visited数组,而是使用延迟删除策略来跳过队列中的过期条目,同时允许在松弛过程中随时更新等长路径的状态。 §§相关问题§§ 1. 在Dijkstra算法中,使用visited数组和延迟删除策略各自的时间复杂度是多少?哪种更适合稠密图? 2. 如果图中存在负权,延迟删除策略是否仍然有效?为什么? 3. 如何处理需要同时支持最短路径和次短路径计数的问题?是否需要修改延迟删除策略? 4. 在分支2中更新救援队数量时,为什么不需要将节点重新加入优先队列? 5. 如果使用visited数组,如何修改算法才能正确处理等长路径的计数和救援队更新?</think>您指出了一个关键优化点!在 Dijkstra 算法中是否使用 `visited[]` 数组(即标记节点是否已确定最短路径的布尔数组)是一个重要的实现选择。以下是详细分析: ### 一、为什么没有使用 `visited[]` 数组? 在标准 Dijkstra 实现中,通常使用: ```cpp bool visited[MAXN]; // 标记节点是否已确定最短路径 ``` 但在本题实现中,我们采用 **隐式确定机制** 替代显式布尔数组: ```cpp auto [d, u] = pq.top(); pq.pop(); if (d != dist[u]) continue; // 关键!替代visited数组的判断 ``` ### 二、两种实现方式的对比 | **特性** | 使用 `visited[]` 数组 | 使用 `dist` 比较(本题方案) | |------------------------|--------------------------------------|-------------------------------------| | **空间复杂度** | O(N) 额外空间 | **零额外空间**(复用dist数组) | | **时间复杂度** | 每个节点处理1次 | 同一节点可能被多次处理但立即跳过 | | **队列操作** | 节点只入队1次 | 节点可能多次入队 | | **等长路径处理** | 需要特殊处理 | **天然支持等长路径更新** | | **实现复杂度** | 需维护额外状态 | 代码更简洁 | | **适用场景** | 标准最短路径问题 | **带状态更新的扩展问题** | ### 三、为什么 `dist` 比较方案更适合本题? 1. **等长路径更新的需求** - 当发现到节点 `v` 的**新等长路径**时,需要更新 `num[v]` 和 `teams[v]` - 使用 `visited[]` 的方案: ```cpp if (visited[v]) { // 无法更新已确定节点的状态! } ``` - 本题方案: ```cpp else if (newDist == dist[v]) { // 即使v已"确定"仍可进入 num[v] += num[u]; // 更新路径数 teams[v] = max(...); // 更新救援队 } ``` 2. **避免状态不一致** 考虑以下时序: ``` 1. 节点v以距离10出队,标记visited[v]=true 2. 发现另一条到v的等长路径(距离10) 3. 但visited[v]=true导致无法更新num[v]和teams[v] → 结果错误! ``` 3. **优先队列的特性支持** ```cpp priority_queue<pair<int, int>, ..., greater<>> pq; ``` - 当节点 `v` 的距离被更新时,新状态 `(newDist, v)` 入队 - 旧状态 `(oldDist, v)` 会被 `d != dist[u]` 检测并跳过 ### 四、复杂度分析 | **指标** | 使用 `visited[]` | 使用 `dist` 比较 | |----------------|------------------|-----------------| | **空间** | O(N) | O(1) | | **时间** | O(V log V) | O(E log V) | | **最坏入队次数**| V | E | - 虽然最坏入队次数从 V 增加到 E,但实际运行效率接近 (稀疏图中 E≈V,稠密图中 E≤V²) ### 五、何时必须使用 `visited[]`? 在以下场景中,显式布尔数组更优: 1. **严格每个节点处理一次**:如仅求最短路径无附加状态 2. **超大图优化**:当数远大于节点数时(E >> V) 3. **非松弛型扩展**:如 A* 算法中需精确控制节点展开
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值