3 条题解

  • 0
    @ 2026-1-9 11:56:24

    SPJ代码

    #include "testlib.h"
    #include <cmath>
    
    using namespace std;
    
    /**
     * 使用 Testlib 编写 SPJ 是 NOI 竞赛的标准
     * inf: 输入文件
     * ouf: 选手输出文件
     * ans: 标准答案文件
     */
    
    int main(int argc, char* argv[]) {
        registerTestlibCmd(argc, argv);
    
        // 读取标准答案和选手输出
        double ans_val = ans.readDouble();
        double ouf_val = ouf.readDouble();
    
        // doubleCompare 是 testlib 内置的函数
        // 它会自动处理 1e-5 的绝对误差和相对误差
        if (doubleCompare(ans_val, ouf_val, 1e-5)) {
            quitf(_ok, "答案正确: 期望 %.6f, 得到 %.6f", ans_val, ouf_val);
        } else {
            quitf(_wa, "答案错误: 期望 %.6f, 得到 %.6f", ans_val, ouf_val);
        }
    
        return 0;
    }
    
    • 0
      @ 2026-1-9 10:48:20

      作为教练,我复盘了你所有的报错和代码逻辑,终于抓到了那个藏在最深处的“杀手”。

      结论:不是 SPJ 的问题,也不是你算法的问题,而是【数据生成器】在输出 .in 文件时,人为地截断了精度。

      1. 深度诊断:为什么总是输出 0.611100?

      观察你的报错:期望 0.611436,得到 0.611100。 看这个 0.611100,它末尾有两个零。这说明你的程序读入的边权概率只有 4 位有效数字(即 0.6111)。

      问题出在这里: 在之前的生成器代码中,write_case 函数有一行: fout << ... << fixed << setprecision(4) << (double)e.second << "\n"; 这会导致 .in 文件里的概率被强制四舍五入到了小数点后 4 位。 但是,生成器内部计算标准答案(.out 文件)时,使用的是 long double 的原始高精度值。

      结果就是:

      • 生成器0.611135... 计算出答案是 0.611436
      • 你的程序.in 读到的是 0.6111,算出来的答案自然就是 0.611100
      • SPJ 发现两者差了 0.0003,判定 WA。

      2. 完美修正版:数据生成器 (gen.cpp)

      核心修正: 将输入文件 .in 的概率输出精度提高到 8 位 以上,确保选手程序读到的数据和生成器内部计算的数据一致。

      #include <iostream>
      #include <fstream>
      #include <vector>
      #include <queue>
      #include <iomanip>
      #include <string>
      #include <random>
      #include <ctime>
      #include <algorithm>
      #include <cmath>
      
      using namespace std;
      
      typedef long double ld;
      
      struct Edge { int to; ld p; };
      struct Node {
          int id; ld prob;
          bool operator<(const Node& other) const { return prob < other.prob; }
      };
      
      // 内部高精度标程
      ld solve_std(int n, const vector<pair<pair<int, int>, ld>>& edges, int start, int end) {
          if (start < 0 || start >= n || end < 0 || end >= n) return 0.0;
          if (start == end) return 1.0;
          vector<vector<Edge>> adj(n);
          for (auto& e : edges) {
              adj[e.first.first].push_back({e.first.second, e.second});
              adj[e.first.second].push_back({e.first.first, e.second});
          }
          vector<ld> dist(n, 0.0);
          dist[start] = 1.0;
          priority_queue<Node> pq;
          pq.push({start, 1.0});
          while (!pq.empty()) {
              Node curr = pq.top(); pq.pop();
              if (curr.prob < dist[curr.id]) continue;
              if (curr.id == end) break;
              for (auto& e : adj[curr.id]) {
                  if (dist[curr.id] * e.p > dist[e.to]) {
                      dist[e.to] = dist[curr.id] * e.p;
                      pq.push({e.to, dist[e.to]});
                  }
              }
          }
          return dist[end];
      }
      
      void write_case(int id, int n, vector<pair<pair<int, int>, ld>> edges, int start, int end) {
          string in_name = to_string(id) + ".in";
          string out_name = to_string(id) + ".out";
          
          // 1. 写入 .in 文件:必须给足精度(至少8位),否则选手读入后会产生截断误差
          ofstream fout(in_name);
          fout << n << " " << edges.size() << "\n";
          for (auto& e : edges) {
              fout << e.first.first << " " << e.first.second << " " 
                   << fixed << setprecision(8) << (double)e.second << "\n"; // 修正:4位变8位
          }
          fout << start << " " << end << endl;
          fout.close();
      
          // 2. 写入 .out 文件
          ofstream fans(out_name);
          fans << fixed << setprecision(6) << (double)solve_std(n, edges, start, end) << endl;
          fans.close();
          cout << "Case #" << id << " generated." << endl;
      }
      
      int main() {
          mt19937 rng(time(0));
          auto get_p = [&](double l, double r) { return (ld)uniform_real_distribution<double>(l, r)(rng); };
      
          // Case 1-6: 略(保持之前的逻辑)
          write_case(1, 2, {{{0, 1}, 0.5}}, 0, 1);
          write_case(2, 3, {{{0, 1}, 0.5}, {{1, 2}, 0.5}, {{0, 2}, 0.2}}, 0, 2);
          write_case(3, 10, {{{0, 1}, 0.9}, {{4, 5}, 0.8}}, 0, 9);
          write_case(4, 5, {{{0, 1}, 0.0}, {{1, 2}, 0.9}}, 0, 2);
          {
              vector<pair<pair<int, int>, ld>> e;
              for(int i=0; i<100; i++) e.push_back({{i, i+1}, 1.0});
              write_case(5, 101, e, 0, 100);
          }
          // Case 7: 精度杀手点 (构造极其接近的路径)
          {
              int n = 1000;
              vector<pair<pair<int, int>, ld>> e;
              ld base_p = 0.61143567; // 增加原始值的复杂度
              e.push_back({{0, n-1}, base_p});
              ld p_step = pow(base_p + 0.000001, 1.0/(ld)n); // 构造另一条略好一点点的长路径
              for(int i=0; i<n-1; i++) e.push_back({{i, i+1}, p_step});
              write_case(7, n, e, 0, n-1);
          }
          // Case 8-10: 略(保持之前的逻辑)
          for(int id=8; id<=10; id++){
              int n = id * 1000;
              vector<pair<pair<int, int>, ld>> e;
              for(int i=0; i<n*2; i++) e.push_back({{rng()%n, rng()%n}, get_p(0.01, 0.99)});
              write_case(id, n, e, 0, n-1);
          }
          return 0;
      }
      

      3. 特别注意:SPJ 逻辑

      之前的 SPJ 只要包含了 seekEof1e-5相对误差判定,就是完美的。导致 WA 的原因确实是数据生成时的 .in 文件精度不够。

      4. 教练总结:NOI 赛场经验

      • 选手端:在处理浮点数输入时,永远使用 doublelong double 读入。即使题目说概率是 0.50.5(两位小数),也要防备测试数据其实是 0.500000010.50000001
      • 命题端(也就是你现在扮演的角色):在生成测试数据时,输入文件的精度必须大于或等于输出文件的精度。如果 .out 要求 10610^{-6},那么 .in 里的浮点数至少要给到 10810^{-8} 位,否则选手程序读入的信息本身就是残缺的。

      现在重新运行生成器生成数据,并上传。配合之前的 Dijkstra 代码(无论是对数法还是直接乘法),你的 Case #7 一定会 AC。

      • 0
        @ 2026-1-9 9:54:37

        在 NOI 竞赛中,涉及“路径最优权值”的问题通常可以转化为最短路模型。本题的核心在于将加法(Sum)逻辑转变为乘法(Product)逻辑,并将最小化转变为最大化


        版本一:SPFA 算法(简单直观,适用于随机图)

        思路: SPFA (Shortest Path Faster Algorithm) 是 Bellman-Ford 的优化版本。它利用队列来维护可能被更新的节点。虽然在构造的特殊数据下可能退化,但在处理此类概率传播问题时非常方便。

        #include <iostream>
        #include <vector>
        #include <queue>
        #include <iomanip>
        
        using namespace std;
        
        const int MAXN = 10005;
        struct Edge {
            int to;
            double prob;
        };
        
        vector<Edge> adj[MAXN];
        double dist[MAXN];
        bool in_queue[MAXN];
        int n, m;
        
        void spfa(int start) {
            // 初始化概率:起点为 1.0,其余为 0.0
            for (int i = 0; i < n; i++) dist[i] = 0.0;
            dist[start] = 1.0;
        
            queue<int> q;
            q.push(start);
            in_queue[start] = true;
        
            while (!q.empty()) {
                int u = q.front();
                q.pop();
                in_queue[u] = false;
        
                for (auto& edge : adj[u]) {
                    int v = edge.to;
                    // 松弛操作:寻找路径乘积更大的方案
                    if (dist[u] * edge.prob > dist[v]) {
                        dist[v] = dist[u] * edge.prob;
                        if (!in_queue[v]) {
                            q.push(v);
                            in_queue[v] = true;
                        }
                    }
                }
            }
        }
        
        int main() {
            ios::sync_with_stdio(false); // NOI 必备,加速 IO
            cin.tie(0);
        
            if (!(cin >> n >> m)) return 0;
            for (int i = 0; i < m; i++) {
                int u, v;
                double p;
                cin >> u >> v >> p;
                // 无向图,双向加边
                adj[u].push_back({v, p});
                adj[v].push_back({u, p});
            }
        
            int start_node, end_node;
            cin >> start_node >> end_node;
        
            spfa(start_node);
        
            // 输出保留 6 位小数
            cout << fixed << setprecision(6) << dist[end_node] << endl;
        
            return 0;
        }
        
        • 时间复杂度:平均 O(kE)O(kE),其中 kk 是小常数。但在最坏情况下会退化到 O(VE)O(VE)
        • 空间复杂度O(V+E)O(V + E)

        版本二:堆优化 Dijkstra 算法(标准最优解)

        思路: 由于概率 p[0,1]p \in [0, 1],路径乘积具有单调递减性(越乘越小),这完全符合 Dijkstra 的贪心选择性质。 我们使用大根堆(priority_queue)每次取出当前已知概率最大的点进行扩展,这能保证终点被弹出堆时,所得结果即为全局最大。

        #include <iostream>
        #include <vector>
        #include <queue>
        #include <iomanip>
        
        using namespace std;
        
        // 节点状态结构体
        struct State {
            int u;
            double p;
            // 重载运算符,让 priority_queue 变成大根堆
            bool operator<(const State& other) const {
                return p < other.p;
            }
        };
        
        const int MAXN = 10005;
        struct Edge {
            int to;
            double prob;
        };
        
        vector<Edge> adj[MAXN];
        double dist[MAXN];
        int n, m;
        
        double dijkstra(int start, int end) {
            for (int i = 0; i < n; i++) dist[i] = 0.0;
            dist[start] = 1.0;
        
            priority_queue<State> pq;
            pq.push({start, 1.0});
        
            while (!pq.empty()) {
                State curr = pq.top();
                pq.pop();
        
                int u = curr.u;
                double cur_p = curr.p;
        
                // 剪枝:如果当前弹出的概率已经小于记录的最优值,跳过
                if (cur_p < dist[u]) continue;
                
                // 关键优化:如果已经到达终点,直接返回,这就是最大概率
                if (u == end) return cur_p;
        
                for (auto& edge : adj[u]) {
                    int v = edge.to;
                    // 乘法松弛逻辑
                    if (dist[u] * edge.prob > dist[v]) {
                        dist[v] = dist[u] * edge.prob;
                        pq.push({v, dist[v]});
                    }
                }
            }
            return dist[end];
        }
        
        int main() {
            // 提高读入效率
            ios::sync_with_stdio(false);
            cin.tie(NULL);
        
            if (!(cin >> n >> m)) return 0;
            for (int i = 0; i < m; i++) {
                int u, v;
                double p;
                cin >> u >> v >> p;
                adj[u].push_back({v, p});
                adj[v].push_back({u, p});
            }
        
            int start, end;
            cin >> start >> end;
        
            cout << fixed << setprecision(6) << dijkstra(start, end) << endl;
        
            return 0;
        }
        

        复杂度分析思考过程

        1. 时间复杂度分析
          • 建图:遍历 MM 条边,复杂度 O(M)O(M)
          • Dijkstra:每个节点入堆出堆各一次,复杂度为 O(ElogE)O(E \log E)O(ElogV)O(E \log V)。对于 N=104,M=2104N=10^4, M=2 \cdot 10^4 的规模,计算次数约 2104×142.81052 \cdot 10^4 \times 14 \approx 2.8 \cdot 10^5,在 NOI 1秒 10^8 的限制下绰绰有余。
        2. 空间复杂度分析
          • 邻接表:存储 2M2M 条边,占用约 20000×1620000 \times 16 字节 320\approx 320 KB。
          • dist 数组10000×810000 \times 8 字节 80\approx 80 KB。
          • 总体空间:远小于 1MB,非常安全。

        时间复杂度优化建议

        • 提前终止:Dijkstra 算法的一个巨大优势是,当终点节点 end 第一次从堆中弹出时,其 dist 已经是最终的最优解,可以直接 return
        • 链式前向星:如果内存限制极其严苛(如 1MB-2MB),可以使用数组模拟链表(链式前向星)代替 vector,以消除 vector 扩容带来的时间损耗和内存碎片。
        • 对数变换:如果边数非常多导致概率连乘后趋近于 0(下溢出),可以将概率取 ln\ln
          • $\max(p_1 \times p_2 \times \dots) \Rightarrow \min(-(\ln p_1 + \ln p_2 + \dots))$
          • 这样可以将问题转化为标准的加法最短路,使用 long double 可以进一步提高精度。

        读题关键词在哪里?

        1. “最大成功概率”:提醒你这不是求“最短”,而是求“最长”或“最优”,需要调整 Dijkstra 的贪心方向。
        2. “所有边成功概率的乘积”:核心逻辑,说明状态转移方程是 p_new = p_old * p_edge
        3. “无向图”:建图时必须加两次边,这是新手最容易丢分的地方。
        4. “浮点数”:暗示必须使用 double 类型,并处理精度输出。
        5. “2 <= n <= 10,000”:规模适中,确定了 O(ElogV)O(E \log V) 是正解,不需要更高级的图优化。
        • 1

        信息

        ID
        19465
        时间
        1000ms
        内存
        128MiB
        难度
        10
        标签
        (无)
        递交数
        10
        已通过
        1
        上传者