【例题1】黑暗城堡(信息学奥赛一本通- P1486)

【题目描述】

知道黑暗城堡有 N 个房间,M 条可以制造的双向通道,以及每条通道的长度。

城堡是树形的并且满足下面的条件:

设 Di为如果所有的通道都被修建,第 i 号房间与第 1 号房间的最短路径长度;

而 Si 为实际修建的树形城堡中第 i 号房间与第 1 号房间的路径长度;

要求对于所有整数 i(1≤i≤N),有 Si=Di 成立。

你想知道有多少种不同的城堡修建方案。当然,你只需要输出答案对 2^31−1 取模之后的结果就行了。

【输入】

第一行为两个由空格隔开的整数 N,M;

第二行到第 M+1 行为 3 个由空格隔开的整数 x,y,l:表示 x 号房间与 y 号房间之间的通道长度为 l。

【输出】

一个整数:不同的城堡修建方案数对 2^31−1 取模之后的结果。

【输入样例】

复制代码
4 6
1 2 1
1 3 2
1 4 3
2 3 1
2 4 2
3 4 1

【输出样例】

复制代码
6

【提示】

样例说明

一共有 4 个房间,6 条道路,其中 1 号和 2 号,1 号和 3 号,1 号和 4 号,2 号和 3 号,2 号和 4 号,3 号和 4 号房间之间的通道长度分别为 1,2,3,1,2,1。

而不同的城堡修建方案数对 2^31−1 取模之后的结果为 6。

数据范围:

对于全部数据,1≤N≤1000,1≤M≤N*(N−1)/2,1≤l≤200。

今天我们要剖析的是图论中一个进阶模型------最短路径生成树(Shortest Path Spanning Tree, SPST)。

这道题完美地将"单源最短路算法"与"组合数学计数"糅合在了一起。只要你看破了它的数学本质,代码写起来会行云流水!

1. 题意转换

用一句话剥离题目的背景故事:

给定一张N个点、M条边的带权无向图,求以 1 号节点为根,且树上任意节点到根的距离都等于原图最短距离的"生成树"的构建方案总数。

2. 思考过程与解题思路

第一直觉的暴力解法是什么?

初学者看到"树形的城堡"和"方案数",第一直觉可能是用 DFS 暴力搜索出所有的生成树,然后再用求出这棵树上每个点到根的距离去和原图验证。但根据 Cayley 公式,完全图的生成树数量高达 。在 的数据规模下,枚举生成树的时间复杂度会绝对的超时。

考点锁定:

  • 关键字 :"" (树上距离必须等于原图理论最短路)是极其强烈的暗示!这指明了我们必须先求出单源最短路。

  • 关键字 :"树形"。一棵包含所有节点的树,意味着每个节点(除了根节点 1)必须有且仅有一个直接父亲。

如何一步步推导到正解?

  1. 既然树上距离要等于最短路,第一步毫无悬念:以 1 号点为起点,跑一遍标准的 Dijkstra,求出到达每个节点v的真实最短距离。

  2. 在最终的生成树里,如果节点u有资格当节点v的父亲(通道长度为l),它必须完美满足:。物理意义就是:水只能沿着最短路的斜坡,从高处(距离近的点)流向低处(距离远的点)。

  3. 统计原图中有多少个邻居能成为v的"合法父亲"。因为各个节点的选择是互相独立的,根据高中的乘法原理,把所有节点的合法父亲数量乘起来,就是最终生成树的方案数。

3. 算法设计与样例推演

核心逻辑与数据结构:

  • 链式前向星:应对密集图,提供最极致的内存和常数表现。

  • Dijkstra(小根堆优化):处理无负权边的单源最短路。

  • 乘法原理统计:遍历全图边,累乘合法入边。

极简数据手玩推演:

带入题目样例:4 个房间。

  1. 跑完 Dijkstra 后得出最短路:。

  2. 寻找各点的合法父亲(满足):

    • 对于房间 2:只有 1 号房间满足。合法父亲 1 个。

    • 对于房间 3:1 号房间满足;2 号房间满足。合法父亲 2 个。

    • 对于房间 4:1 号、2 号、3 号房间加对应边长后均满足。合法父亲 3 个。

  3. 方案数相乘:。与样例输出完全一致。

4. 时空复杂度分析

  • 时间复杂度 :。其中M为原图边数。Dijkstra 堆优化的复杂度为。最后遍历一遍所有的边寻找"合法父亲",复杂度为O(M)。在 时,总运算量在千万级别,C++ 在 1 秒内轻松满分跑完。

  • 空间复杂度 :O(N + M)。链式前向星只需三个长度为的一维数组(应对无向图双向存边),内存开销约为十几 MB,绝对安全。

5. 坑点与易错总结

  1. 链式前向星数组越界(极易TLE/RE) :本题 ,极限情况下完全图M高达近 50 万。由于是双向存边,边数数组必须开到1,000,000以上。如果开小了,溢出的指针会被篡改,导致死循环从而TLE。

  2. 位运算优先级(极易 WA) :题目要求对取模。在 C++ 中,减号 - 的优先级高于左移 << !如果写成 1LL << 31 - 1,会先算减法变成 1LL << 30。必须严格加括号写成 ((1LL<<31)-1)。

  3. 大坑:Dijkstra 忘记 pop :在使用 priority_queue 时,取出 q.top() 后必须紧接着立刻 q.pop(),绝不能被后续的 if(vis) continue; 跳过,否则堆顶元素不清除会导致无限死循环。

6. 代码进阶与优化剖析

审视下方的代码,这是一份非常成熟、完全达到省选金牌标准的代码。它不仅避开了所有的坑点,更包含了值得初学者学习的 2 个高阶编码习惯:

  • 高阶习惯 1:大巧不工的模块化解耦

    作者并没有试图把"找父亲"的逻辑强行揉碎在 Dijkstra 的松弛操作中,而是极其干净利落地分成两步:先纯粹地跑完最短路,然后再单独开一个循环遍历边集寻找。这种"将复杂问题解耦"的思维,让代码的可读性和正确率极高。

  • 高阶习惯 2:极其稳健的常数控制

    使用了静态数组模拟链表(链式前向星)来存图,彻底避免了 vector 动态扩容带来的常数开销;同时利用小根堆重载 < 运算符,代码精简,运行常数被压榨到了极致。

7. 完整代码

cpp 复制代码
//最短路径生成树

#include <iostream>
#include <cstring>//memset
#include <queue>
using namespace std;
int n,m;
int h[1010];
//无向图边数500000 双向存边开到1000010 防止越界
int vtex[1000010];
int nxt[1000010];
int wt[1000010];
int idx;
int d[2020];//求出1号点到每个点的理论最短路径长度d
int vis[2020];
long long sum[2020];//统计每个节点有合法入边


struct node{
    int id;//节点编号
    int dis;//该节点到起点的距离
    //最大堆重载为最小堆
    friend bool operator <(node a,node b){
        return a.dis>b.dis;
    }
}no[2020];

priority_queue<node> q;

//链式前向星存图
void addedge(int u,int v,int l){
    vtex[idx]=v;
    wt[idx]=l;
    nxt[idx]=h[u];
    h[u]=idx++;
}

//求单源最短路径 x为起点
void dijkstra(int x){
    d[x]=0;//起点到自己距离为0
    node tmp;
    tmp.id=x;
    tmp.dis=0;
    q.push(tmp);//起点入队
    while(!q.empty()){
        node tmp;
        tmp=q.top();//访问队首节点
        q.pop();//该节点出队
        int nid=tmp.id;//队首节点编号
        //如果该节点已经点亮继续访问下一个队首节点
        if(vis[nid]==1) continue;
        
        //否则就点亮该节点
        vis[nid]=1;
        int p=h[nid];
        //访问当前节点nid的所有邻接点v
        while(p!=-1){
            int v=vtex[p];
            //如果v到1号点的距离大于经过nid到起点的距离 
            if(d[v]>d[nid]+wt[p]){
                //就更新v到1号点最短距离 
                d[v]=d[nid]+wt[p];
                //并把v入队
                q.push({v,d[v]});
            }
            p=nxt[p];//更新指针 去下一个nid的邻接点
        }
    }

}


int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n>>m;
    //初始化头指针数组为空
    memset(h,-1,sizeof(h));

    //链式前向星存图
    for(int i=1;i<=m;i++){
        int x,y,l;
        cin>>x>>y>>l;
        //无向图双向存边
        addedge(x,y,l);
        addedge(y,x,l);
    }
    //初始化所有节点到1号点的距离为无穷
    memset(d,0x3f,sizeof(d));
    //接下来求出1号点到每个点的理论最短路径长度d
    dijkstra(1);

    //题目告诉我们最后的城堡是树形的,所以最后的城堡相邻点之间一定只有一条边相连
    //最终也只有n-1条边 每个点v只会有一个父亲u
    //我们需要找到对于每个点v,有多少条入边(sum[v])可以让他满足d[v]=d[u]+l即可
    //最后把每个点满足条件的边数累乘就好了(sum[2]*sum[3]*......*sum[v])

    //接下来遍历所有点 
    for(int i=2;i<=n;i++){
        //访问i点的所有边 有多少条入边(sum[i])可以让他满足d[i]=d[v]+wt[p]
        int p=h[i];
        while(p!=-1){
            //假设v是i的父节点
            int v=vtex[p];
            //看能否满足d[i]=d[v]+wt[p] 如果能满足代表v确实可以是i的父亲
            if(d[i]==d[v]+wt[p]) sum[i]++;
            //如果不满足d[i]=d[v]+wt[p] 如果能满足代表v不可以是i的父亲
            p=nxt[p];
        }
    }
    long long sum2=1;//存储最后结果
    for(int i=2;i<=n;i++){
        sum2=(sum2*sum[i])%((long long)(1ll<<31)-1);
    }
    cout<<sum2;
    return 0;
}
相关推荐
hetao17338371 小时前
2026-10-20 hetao1733837 的刷题记录
c++·算法
碧海银沙音频科技研究院3 小时前
基于ATS308X的无线语音交互对话功能实现
人工智能·嵌入式硬件·深度学习·算法
尹玉玺3 小时前
从原子残片到天道公理:以德谟克利特思想全貌,重新定界西方唯物认知的历史层级与边界
人工智能·算法·架构
青少儿编程课堂3 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:差分数组与区间修改实战解析
c++·python·算法·bfs·信息学竞赛
重生之小比特3 小时前
【算法拾光】二分查找|二分查找 + 再排序数组中查找元素的第一个和最后一个位置
算法·leetcode·职场和发展
零基础1234 小时前
基于模型的强化学习与不基于模型的强化学习:核心区别与实战选择
python·算法
罗西的思考15 小时前
【Agentic RL】Dressage Staleness 控制深度分析
人工智能·算法·机器学习
quaer15 小时前
弗洛伊德算法--判圈法寻找环的入口O(n) O(1)
数据结构·算法