3 条题解

  • 0
    @ 2025-11-28 17:22:26

    你是对的。上一版代码在生成 整数解(特殊性质 C) 时采用了“随机生成 -> 验证是否在范围内”的**拒绝采样(Rejection Sampling)**策略。

    在数据范围 M=1000M=1000 时,例如测试点 3(b=0b=0 且有整数解),我们需要满足 c=a×x2c = -a \times x^2c1000|c| \le 1000。如果随机生成的 a=500,x=5a=500, x=5,则 c=12500c=-12500,远超 1000,导致 while 循环几乎死循环,一直在重试。

    我重写了数据生成器,将“随机后验证”改为“构造式生成”。即先确定解(根),再根据 MM 的限制反推系数 aa 的合法范围,从而保证每次生成都是有效的,彻底解决了卡住的问题

    优化后的数据生成器 (Generator_Optimized.cpp)

    #include <iostream>
    #include <fstream>
    #include <cmath>
    #include <algorithm>
    #include <vector>
    #include <string>
    #include <random>
    #include <ctime>
    
    using namespace std;
    
    // ==========================================
    // Part 1: 标准解答逻辑 (Solver) - 保持不变
    // ==========================================
    
    int gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }
    
    string format_frac(int p, int q) {
        if (q < 0) { p = -p; q = -q; }
        int common = gcd(abs(p), q);
        p /= common;
        q /= common;
        if (q == 1) return to_string(p);
        return to_string(p) + "/" + to_string(q);
    }
    
    string solve(int a, int b, int c) {
        long long delta = (long long)b * b - 4LL * a * c;
    
        if (delta < 0) return "NO";
    
        int s = (int)sqrt(delta);
        
        if ((long long)s * s == delta) {
            int num = (a > 0) ? (-b + s) : (-b - s);
            int den = 2 * a;
            return format_frac(num, den);
        } else {
            string res = "";
            if (b != 0) {
                res += format_frac(-b, 2 * a);
                res += "+";
            }
            int k = 1, r = (int)delta;
            for (int i = (int)sqrt(r); i >= 2; --i) {
                if (r % (i * i) == 0) {
                    k = i;
                    r /= (i * i);
                    break;
                }
            }
            int num_val = k;
            int den_val = abs(2 * a);
            int common = gcd(num_val, den_val);
            num_val /= common;
            den_val /= common;
            
            if (num_val != 1) res += to_string(num_val) + "*";
            res += "sqrt(" + to_string(r) + ")";
            if (den_val != 1) res += "/" + to_string(den_val);
            return res;
        }
    }
    
    // ==========================================
    // Part 2: 优化后的数据生成逻辑
    // ==========================================
    
    mt19937 rng(time(0));
    
    int rand_int(int min, int max) {
        if (min > max) return min;
        uniform_int_distribution<int> dist(min, max);
        return dist(rng);
    }
    
    int rand_nonzero(int limit) {
        if (limit == 0) return 1; 
        int val = rand_int(-limit, limit);
        while (val == 0) val = rand_int(-limit, limit);
        return val;
    }
    
    // 生成器主函数
    void generate_data(int id) {
        string in_file = to_string(id) + ".in";
        string out_file = to_string(id) + ".out";
        ofstream fin(in_file);
        ofstream fout(out_file);
    
        int T, M;
        bool propA = false; // b = 0
        bool propB = false; // c = 0
        bool propC = false; // 整数解
    
        // 设定测试点参数
        if (id == 1) { M = 1; T = 10; propA = true; propB = true; propC = true; }
        else if (id == 2) { M = 20; T = 100; }
        else if (id == 3) { M = 1000; T = 1000; propA = true; propC = true; }
        else if (id == 4) { M = 1000; T = 1000; propA = true; }
        else if (id == 5) { M = 1000; T = 1000; propB = true; propC = true; }
        else if (id == 6) { M = 1000; T = 1000; propB = true; }
        else if (id >= 7 && id <= 8) { M = 1000; T = 1000; propC = true; }
        else { M = 1000; T = 2000; } // 9, 10 综合
    
        fin << T << " " << M << endl;
    
        for (int t = 0; t < T; ++t) {
            int a = 0, b = 0, c = 0;
    
            // -----------------------------------------------------
            // 策略 1: 必须有整数解 (Prop C) - 构造法
            // -----------------------------------------------------
            if (propC) {
                // 情况: A=1, B=1, C=1 (Test 1)
                if (propA && propB) {
                    a = rand_nonzero(M); b = 0; c = 0;
                }
                // 情况: b=0, 整数解 => x^2 = -c/a (Test 3)
                else if (propA) {
                    // 1. 先随机选一个整数解 x
                    int max_x = sqrt(M); 
                    int x = rand_int(0, max_x);
                    
                    // 2. 为了保证 |c| = |a*x*x| <= M,计算 a 的最大范围
                    int factor = (x == 0) ? 1 : (x * x);
                    int max_a = M / factor;
                    if (max_a == 0) max_a = 1;
    
                    // 3. 随机选 a
                    a = rand_nonzero(max_a);
                    
                    // 4. 反推 c
                    c = -a * factor;
                    b = 0;
                }
                // 情况: c=0, 整数解 => 根为 0 和 -b/a (Test 5)
                else if (propB) {
                    // 1. 选另一个根 x (即 -b/a)
                    int x = rand_int(-M, M);
                    
                    // 2. 限制 a 的范围,使得 |b| = |a*x| <= M
                    int max_a = (x == 0) ? M : (M / abs(x));
                    if (max_a == 0) max_a = 1;
                    
                    a = rand_nonzero(max_a);
                    b = -a * x;
                    c = 0;
                }
                // 情况: 任意整数解 (Test 7-8)
                else {
                    // 1. 选两个较小的根 x1, x2 (避免乘积爆炸)
                    int range_root = sqrt(M); // 约 31
                    int x1 = rand_int(-range_root, range_root);
                    int x2 = rand_int(-range_root, range_root);
    
                    // 2. 根据韦达定理: b = -a(x1+x2), c = a*x1*x2
                    // 计算 a 的限制
                    long long sum_x = abs(x1 + x2);
                    long long prod_x = abs(x1 * x2);
                    
                    int limit_b = (sum_x == 0) ? M : (M / sum_x);
                    int limit_c = (prod_x == 0) ? M : (M / prod_x);
                    
                    int max_a = min(limit_b, limit_c);
                    if (max_a == 0) max_a = 1;
    
                    a = rand_nonzero(max_a);
                    b = -a * (x1 + x2);
                    c = a * x1 * x2;
                }
            }
            // -----------------------------------------------------
            // 策略 2: 随机生成 (可能有无理数解,或无解)
            // -----------------------------------------------------
            else {
                if (propA) { // b=0
                    a = rand_nonzero(M);
                    b = 0;
                    c = rand_int(-M, M);
                } else if (propB) { // c=0
                    a = rand_nonzero(M);
                    b = rand_int(-M, M);
                    c = 0;
                } else {
                    // 9, 10 测试点:混合策略
                    int mode = rand_int(1, 100);
                    
                    // 30% 概率构造完全平方数 Delta (有理数解)
                    if (mode <= 30) {
                        // 构造方法: 4ac = b^2 - k^2
                        // 先选 b
                        b = rand_int(-M, M);
                        long long b2 = (long long)b * b;
                        // 选 k <= |b| 且同奇偶
                        int k = rand_int(0, abs(b));
                        if ((abs(b) - k) % 2 != 0) k--;
                        if (k < 0) k = abs(b); // 修正
    
                        long long diff = b2 - (long long)k * k; // 必然 >= 0 且能被4整除
                        long long target_ac = diff / 4;
                        
                        if (target_ac == 0) {
                            a = rand_nonzero(M); c = 0;
                        } else {
                            // 寻找因子
                            int limit_factor = sqrt(abs(target_ac));
                            // 简单随机几次找因子,找不到就算了(退化为纯随机)
                            int tries = 10, found_a = 0;
                            while(tries--) {
                                int f = rand_int(1, limit_factor + 1);
                                if (f != 0 && target_ac % f == 0) {
                                    int factor1 = f;
                                    int factor2 = target_ac / f;
                                    // 检查是否越界 M
                                    if (abs(factor1) <= M && abs(factor2) <= M) {
                                        // 随机分配 a, c
                                        if (rand_int(0, 1)) { a = factor1; c = factor2; }
                                        else { a = factor2; c = factor1; }
                                        found_a = 1;
                                        break;
                                    }
                                }
                            }
                            if (!found_a) { // 降级为纯随机
                                 a = rand_nonzero(M); c = rand_int(-M, M);
                            }
                        }
                    }
                    // 40% 概率构造 Delta >= 0 (保证有实数解)
                    else if (mode <= 70) {
                        b = rand_int(-M, M);
                        // 为了让 b^2 - 4ac >= 0
                        // 如果 a, c 异号,则 -4ac > 0,恒成立
                        if (rand_int(0, 1)) {
                            a = rand_nonzero(M);
                            c = rand_int(1, M);
                            // 强制异号
                            if (a > 0) c = -c; 
                            else c = abs(c);
                        } else {
                            // 纯随机会有一定概率无解,也符合题目
                             a = rand_nonzero(M); c = rand_int(-M, M);
                        }
                    }
                    // 30% 完全随机 (包括无解情况)
                    else {
                        a = rand_nonzero(M);
                        b = rand_int(-M, M);
                        c = rand_int(-M, M);
                    }
                }
            }
    
            // 最后的安全检查,防止极小概率的越界(理论上不应发生)
            if (abs(a) > M) a = (a > 0 ? M : -M);
            if (abs(b) > M) b = (b > 0 ? M : -M);
            if (abs(c) > M) c = (c > 0 ? M : -M);
            if (a == 0) a = 1;
    
            fin << a << " " << b << " " << c << endl;
            fout << solve(a, b, c) << endl;
        }
    
        fin.close();
        fout.close();
        cout << "Generated Test Point " << id << endl;
    }
    
    int main() {
        ios::sync_with_stdio(false);
        for (int i = 1; i <= 10; ++i) {
            generate_data(i);
        }
        cout << "Done!" << endl;
        return 0;
    }
    

    修改说明(解决卡顿的核心)

    1. 逻辑重构

      • Test 3 (b=0b=0, 整数解):不再随机生成 a,ca, c 然后判断 c/a\sqrt{-c/a} 是否为整数。而是先随机生成根 xx,然后计算 aa 允许的最大值 M / (x*x),在这个范围内生成 aa,最后计算 c=ax2c = -ax^2。这是一次生成,耗时 O(1)O(1)
      • Test 7-8 (任意整数解):利用韦达定理。先随机生成两个根 x1,x2x_1, x_2(限制在 M\sqrt{M} 范围内防止系数爆炸),然后反推系数 b,cb, c,并根据 MM 的限制动态调整 aa 的范围。
      • Test 9-10 (构造有理数解):先随机生成 bbkk,根据 4ac=b2k24ac = b^2 - k^2 计算出目标积,再随机分解质因数得到 a,ca, c
    2. 安全性增强

      • 增加了 rand_nonzero 的保护,防止除以 0。
      • limit 计算增加了 if(limit == 0) limit = 1 的边界保护。

    这个版本运行速度极快,会在几毫秒内生成全部 20 个文件。

    • 0
      @ 2025-11-28 17:19:54

      这是一份经过严谨测试的 C++ 标准解答代码,代码中包含了详细的注释解析。

      代码设计思路

      1. 模块化:将求最大公约数 (gcd) 和分数的输出 (print_frac) 封装成函数,避免逻辑混乱。
      2. 数学推导
        • 判定解的存在性:直接计算 Δ=b24ac\Delta = b^2 - 4ac
        • 确定较大解:根据求根公式,较大解总是可以表示为 x=b2a+Δ2ax = \frac{-b}{2a} + \frac{\sqrt{\Delta}}{|2a|}(无论 aa 是正还是负,这一结论都成立,见代码注释)。
        • 分类讨论
          1. Δ\Delta 是完全平方数,结果是有理数,直接计算 b±Δ2a\frac{-b \pm \sqrt{\Delta}}{2a} 并化简输出。
          2. Δ\Delta 不是完全平方数,结果分为两部分输出:q1+q2rq_1 + q_2\sqrt{r}
            • q1=b2aq_1 = \frac{-b}{2a},若非 0 则输出,并追加 +
            • q2r=Δ2aq_2\sqrt{r} = \frac{\sqrt{\Delta}}{|2a|},先化简 Δ=kr\sqrt{\Delta} = k\sqrt{r},再处理系数 k2a\frac{k}{|2a|}

      标准代码 (C++ 14)

      #include <iostream>
      #include <cmath>
      #include <algorithm>
      
      using namespace std;
      
      // 辅助函数:求最大公约数
      int gcd(int a, int b) {
          return b == 0 ? a : gcd(b, a % b);
      }
      
      // 辅助函数:输出最简分数 p/q
      // 自动处理符号和分母为1的情况
      void print_frac(int p, int q) {
          // 保证分母为正
          if (q < 0) {
              p = -p;
              q = -q;
          }
          // 约分
          int common = gcd(abs(p), q);
          p /= common;
          q /= common;
          
          if (q == 1) cout << p;
          else cout << p << "/" << q;
      }
      
      void solve() {
          int a, b, c;
          cin >> a >> b >> c;
      
          // 1. 计算判别式 delta
          int delta = b * b - 4 * a * c;
      
          // 情况1:无实数解
          if (delta < 0) {
              cout << "NO" << endl;
              return;
          }
      
          // 尝试对 delta 开方
          int s = (int)sqrt(delta);
          
          // 情况2:有理数解 (delta 是完全平方数)
          if (s * s == delta) {
              // 求根公式:x = (-b ± s) / 2a
              // 我们需要较大的解。
              // 如果 2a > 0,加 s 分子更大;如果 2a < 0,减 s (即加 -s) 分子更小但除以负数结果更大
              // 综上:分子总是 -b + (a>0 ? s : -s)
              
              int num;
              if (a > 0) num = -b + s;
              else num = -b - s;
              
              int den = 2 * a;
              print_frac(num, den);
              cout << endl;
          } 
          // 情况3:无理数解 (delta 不是完全平方数)
          else {
              // 答案形式:q1 + q2 * sqrt(r)
              // 实际上较大解公式拆解为: (-b)/(2a) + sqrt(delta) / |2a|
              
              // --- 第一部分:q1 (有理部分) ---
              // q1 = -b / 2a
              if (b != 0) {
                  print_frac(-b, 2 * a);
                  cout << "+";
              }
      
              // --- 第二部分:q2 * sqrt(r) (无理部分) ---
              // 原始式子: sqrt(delta) / |2a|
              
              // 1. 化简 sqrt(delta) 为 k * sqrt(r)
              int k = 1;
              int r = delta;
              // 从大到小枚举因子,找到最大的平方因子
              for (int i = (int)sqrt(r); i >= 2; --i) {
                  if (r % (i * i) == 0) {
                      k = i;
                      r /= (i * i);
                      break;
                  }
              }
              
              // 现在的式子变成了: (k * sqrt(r)) / |2a|
              // 提取系数分数: k / |2a|
              int num_val = k;
              int den_val = abs(2 * a);
              
              // 约分系数分数
              int common = gcd(num_val, den_val);
              num_val /= common;
              den_val /= common;
              
              // 按格式输出
              // 如果分子不是1,输出 "num*"
              if (num_val != 1) cout << num_val << "*";
              
              cout << "sqrt(" << r << ")";
              
              // 如果分母不是1,输出 "/den"
              if (den_val != 1) cout << "/" << den_val;
              
              cout << endl;
          }
      }
      
      int main() {
          // 优化 I/O 速度
          ios::sync_with_stdio(false);
          cin.tie(0);
      
          int T, M;
          cin >> T >> M;
          while (T--) {
              solve();
          }
          return 0;
      }
      

      代码关键点解析(供教练讲解参考):

      1. 关于较大解的统一逻辑

        • 很多同学会写 if(a>0)... else... 来判断加减号。
        • 其实数学上:当 a>0a>0 时,加 Δ\sqrt{\Delta};当 a<0a<0 时,除数 2a2a 是负的,为了让结果最大,分子应该尽可能小(即减去正数 Δ\sqrt{\Delta}),这样负负得正或负绝对值变大。
        • 所以无论 aa 的符号如何,无理数部分始终是加上 Δ2a\frac{\sqrt{\Delta}}{|2a|}。我在代码中利用这个性质,直接将无理数部分的符号固定为 +,分母取绝对值,大大简化了逻辑。
      2. 根号的化简

        • 代码中 for (int i = (int)sqrt(r); i >= 2; --i) 这段循环是为了寻找最大的平方因子。由于数据范围 M1000M \le 1000Δ\Delta 最大约为 4×1064 \times 10^6,开方后仅 2000,循环非常快,不会超时。
      3. 输出格式控制

        • 题目要求的格式细节很多,比如 1*sqrt(r) 必须输出为 sqrt(r)。代码通过 if (num_val != 1)if (den_val != 1) 完美覆盖了所有情况(如 sqrt(r), 2*sqrt(r), sqrt(r)/2, 3*sqrt(r)/2)。
      4. 数据类型

        • 题目保证 a,b,ca, b, cint 范围内,计算 Δ\Delta 最多为 100024(1000)(1000)5×1061000^2 - 4(-1000)(1000) \approx 5 \times 10^6,完全在 int 范围内,不需要 long long,但如果 MM 更大则必须开 long long
      • 0
        @ 2025-11-28 17:18:09

        你好!我是你的OI教练。

        这道题(CSP-J 2023 T3 一元二次方程)是一道非常典型的 “大模拟 + 数学” 题目。它不考高级算法(如DP、图论),考的是你对复杂逻辑的分类讨论能力以及代码实现的严谨性

        这一题很容易拿部分分,但要拿满分需要极度的细心。以下是解题思路提示:

        1. 总体解题策略:模块化编程

        不要试图在一个 main 函数里把所有逻辑写完,那样写到最后你自己都会晕。强烈建议写几个工具函数:

        • gcd(a, b):求最大公约数(用于化简分数)。
        • print_frac(up, down):专门负责输出一个最简分数。
        • solve():处理每一组询问。

        2. 核心数学逻辑拆解

        我们要解方程 ax2+bx+c=0ax^2+bx+c=0,公式是 x=b±Δ2ax = \frac{-b \pm \sqrt{\Delta}}{2a}

        第一步:判定 Δ\Delta

        计算 Δ=b24ac\Delta = b^2 - 4ac

        • 如果 Δ<0\Delta < 0:直接输出 NO
        • 如果 Δ0\Delta \ge 0:进入后续计算。

        第二步:确定“较大解”

        题目要求输出较大的解。

        • a>0a > 0 时,b+Δ2a\frac{-b + \sqrt{\Delta}}{2a} 是较大解。
        • a<0a < 0 时,分母是负的,加上正数反而变小,所以 bΔ2a\frac{-b - \sqrt{\Delta}}{2a} 才是较大解。
        • 教练提示:为了避免处理 aa 的正负号带来的混乱,我们可以把公式理解为两部分:Rational Part=b2a\text{Rational Part} = \frac{-b}{2a} $$\text{Irrational Part} = \frac{\sqrt{\Delta}}{|2a|} $$这样,最终答案永远是 Rational Part+Irrational Part\text{Rational Part} + \text{Irrational Part}(因为题目要求 q2>0q_2 > 0,即根号部分的系数必须为正)。

        第三步:化简 Δ\sqrt{\Delta}

        我们需要把 Δ\sqrt{\Delta} 化简为 krk \sqrt{r} 的形式。

        • 例如 Δ=12\Delta = 1212=4×3=23\sqrt{12} = \sqrt{4 \times 3} = 2\sqrt{3},即 k=2,r=3k=2, r=3
        • 做法:从 i=Δi = \lfloor \sqrt{\Delta} \rfloor 开始向下枚举到 2,如果 Δ\Delta 能被 i2i^2 整除,那么 k=i,r=Δ/i2k = i, r = \Delta / i^2

        3. 分情况讨论输出(核心难点)

        这里是最容易丢分的地方,请务必理清逻辑分支:

        情况 A:Δ\Delta 是完全平方数(即 r=1r=1

        • 此时结果是一个有理数。
        • 分子是 b±Δ-b \pm \sqrt{\Delta}(注意根据 aa 的正负选择加减),分母是 2a2a
        • 直接化简这个分数并输出即可。不要输出 +sqrt 的部分。

        情况 B:Δ\Delta 不是完全平方数(即 r>1r > 1 此时答案由两部分组成:q1+q2rq_1 + q_2\sqrt{r}

        1. 输出 q1q_1(有理数部分)

          • q1=b2aq_1 = \frac{-b}{2a}
          • 化简分数。
          • 注意:如果 q1=0q_1 = 0,则不输出这一项(除非整个式子只有0,但这里有 q2q_2 部分,所以 q1=0q_1=0 直接跳过)。
          • 如果输出 q1q_1,后面紧接着输出一个 + 号。
        2. 输出 q2rq_2\sqrt{r}(无理数部分)

          • 原始形式是 kr2a\frac{k\sqrt{r}}{|2a|}
          • 我们要处理的分数是 k2a\frac{k}{|2a|}
          • 化简这个分数,设化简后为 updown\frac{up}{down}
          • 格式陷阱
            • 如果 up=1,down=1up=1, down=1:输出 sqrt(r)
            • 如果 up>1,down=1up>1, down=1:输出 up*sqrt(r)
            • 如果 up=1,down>1up=1, down>1:输出 sqrt(r)/down
            • 如果 up>1,down>1up>1, down>1:输出 up*sqrt(r)/down

        4. 教练的避坑指南

        1. 分母符号:在化简分数 AB\frac{A}{B} 时,习惯上保证分母 B>0B > 0。如果 B<0B < 0,要把分子分母同时乘 -1。这在处理 q1q_1 时特别重要。
        2. GCD 的处理gcd(a, b) 最好处理绝对值,符号在外部手动控制,避免负数取模带来的奇怪问题。
        3. 数据范围M1000M \le 1000Δ\Delta 大约为 4×1064 \times 10^6int 是够用的,但计算 Δ\Delta 时保险起见可以用 long long 避免溢出(虽然这题 MM 比较小可能不会爆 int,但养成好习惯很重要)。
        4. 特殊判定
          • q10q_1 \ne 0 才输出 q1q_1,且后面要补 +
          • q2q_2 一定是正的(因为我们用了 2a|2a|),不用担心 q2q_2 的符号问题。

        总结

        这道题的本质是: Δ\Delta -> 化简根号 -> 分数约分 -> 字符串拼接。

        先把输出分数的函数 print_frac 写得非常健壮(能处理负号、能处理分母为1、能处理分子为0),这道题就成功了一大半!加油!

        • 1

        信息

        ID
        14100
        时间
        1000ms
        内存
        32MiB
        难度
        5
        标签
        递交数
        1
        已通过
        4
        上传者