系数大到百位还要求根:在单精度范围内用秦九韶取模判定,把 1e6 个候选点的精确求值降为逐点 O(n) 的模运算。
OJ: roj
题目 ID: 3623
难度:提高
标签:秦九韶算法同余哈希霍纳法则NOIP
创建: 2026-10-02 11:07
更新: 2026-10-10 11:30
形式化题目
给定
的整数
样例:
正解
思路
朴素做法与它的瓶颈。 最直接的想法是:枚举
核心观察:只判"是不是 0",可以在模意义下判。 设一个素数
必然成立(
秦九韶算法本身也是本题的正式考点。对
求值就变成"从最高次系数开始,反复做 乘
| 步骤 | 读到的系数 | 结果( |
|
|---|---|---|---|
| 初始 | — | 0 | |
| 1 | 1 | ||
| 2 | 1 | ||
| 3 | 4 |
模
筛子必须配复验。 模
C++ 里没有大整数,"筛 + 双模数复验"就是终态;Python 天生有任意精度整数,复验反而成了免费的正确性保险。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 11:30
* update_at: 2026-10-10 11:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int BASE = 1000000000;
struct BigInt {
vector<int> d;
int sign;
BigInt(ll x = 0) { sign = 1; if (x < 0) { sign = -1; x = -x; } if (x == 0) sign = 0; while (x) { d.push_back(x % BASE); x /= BASE; } }
};
void trim(BigInt &a) { while (!a.d.empty() && a.d.back() == 0) a.d.pop_back(); if (a.d.empty()) a.sign = 0; }
BigInt read_big(string s) { BigInt a; a.d.clear(); a.sign = 1; int pos = 0; if (s[0] == '-') { a.sign = -1; pos = 1; } for (int i = s.size(); i > pos; i -= 9) { int l = max(pos, i - 9); int x = 0; for (int j = l; j < i; j++) x = x * 10 + s[j] - '0'; a.d.push_back(x); } trim(a); return a; }
int cmp_abs(const BigInt &a, const BigInt &b) { if (a.d.size() != b.d.size()) return a.d.size() < b.d.size() ? -1 : 1; for (int i = (int)a.d.size() - 1; i >= 0; i--) if (a.d[i] != b.d[i]) return a.d[i] < b.d[i] ? -1 : 1; return 0; }
BigInt add_abs(const BigInt &a, const BigInt &b) { BigInt c; c.sign = 1; int n = max(a.d.size(), b.d.size()); c.d.assign(n, 0); ll carry = 0; for (int i = 0; i < n; i++) { ll v = carry; if (i < (int)a.d.size()) v += a.d[i]; if (i < (int)b.d.size()) v += b.d[i]; c.d[i] = v % BASE; carry = v / BASE; } if (carry) c.d.push_back(carry); trim(c); return c; }
BigInt sub_abs(const BigInt &a, const BigInt &b) { BigInt c; c.sign = 1; c.d.assign(a.d.size(), 0); ll borrow = 0; for (int i = 0; i < (int)a.d.size(); i++) { ll v = (ll)a.d[i] - borrow - (i < (int)b.d.size() ? b.d[i] : 0); if (v < 0) { v += BASE; borrow = 1; } else borrow = 0; c.d[i] = v; } trim(c); return c; }
BigInt add_big(BigInt a, BigInt b) { if (a.sign == 0) return b; if (b.sign == 0) return a; if (a.sign == b.sign) { BigInt c = add_abs(a, b); c.sign = a.sign; return c; } int cmp = cmp_abs(a, b); if (cmp == 0) return BigInt(0); if (cmp > 0) { BigInt c = sub_abs(a, b); c.sign = a.sign; return c; } BigInt c = sub_abs(b, a); c.sign = b.sign; return c; }
BigInt mul_int(BigInt a, ll m) { if (a.sign == 0 || m == 0) return BigInt(0); if (m < 0) { a.sign = -a.sign; m = -m; } BigInt c; c.sign = a.sign; c.d.assign(a.d.size(), 0); ll carry = 0; for (int i = 0; i < (int)a.d.size(); i++) { __int128 v = (__int128)a.d[i] * m + carry; c.d[i] = (ll)(v % BASE); carry = (ll)(v / BASE); } while (carry) { c.d.push_back(carry % BASE); carry /= BASE; } trim(c); return c; }
BigInt div_int(BigInt a, ll m) { BigInt c; c.sign = a.sign; c.d.assign(a.d.size(), 0); ll rem = 0; for (int i = (int)a.d.size() - 1; i >= 0; i--) { __int128 cur = (__int128)rem * BASE + a.d[i]; c.d[i] = (ll)(cur / m); rem = (ll)(cur % m); } trim(c); return c; }
bool less_big(const BigInt &a, const BigInt &b) { if (a.sign != b.sign) return a.sign < b.sign; if (a.sign == 0) return false; int cmp = cmp_abs(a, b); return a.sign > 0 ? cmp < 0 : cmp > 0; }
bool is_zero(const BigInt &a) { return a.sign == 0; }
ostream& operator<<(ostream &os, const BigInt &a) { if (a.sign == 0) { os << 0; return os; } if (a.sign < 0) os << '-'; int n = a.d.size(); os << a.d[n - 1]; for (int i = n - 2; i >= 0; i--) os << setw(9) << setfill('0') << a.d[i]; return os; }
const ll P=2147483647LL;
ll str_mod(string s){int i=0,neg=0; if(s[0]=='-'){neg=1;i=1;} ll v=0; for(;i<(int)s.size();i++) v=((__int128)v*10+s[i]-'0')%P; if(neg && v) v=P-v; return v;}
ll mod_horner(vector<ll>&rev,ll x){ll v=0; for(int i=0;i<(int)rev.size();i++) v=((__int128)v*x+rev[i])%P; return v;}
BigInt horner(vector<BigInt>&c,int x){BigInt v(0); for(int i=(int)c.size()-1;i>=0;i--) v=add_big(mul_int(v,x), c[i]); return v;}
int main(){ios::sync_with_stdio(false);cin.tie(nullptr); int n,m; if(!(cin>>n>>m)) return 0; vector<BigInt> coeff(n+1); vector<ll> rev(n+1); for(int i=0;i<=n;i++){string s;cin>>s; coeff[i]=read_big(s); rev[n-i]=str_mod(s);} vector<int> roots; for(int x=1;x<=m;x++) if(mod_horner(rev,x)==0 && is_zero(horner(coeff,x))) roots.push_back(x); cout<<roots.size()<<'\n'; for(int i=0;i<(int)roots.size();i++) cout<<roots[i]<<'\n'; return 0;}#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-02 11:10
# update_at: 2026-10-02 11:17
import sys
sys.set_int_max_str_digits(20000) # 系数可达上万位十进制,放开 int() 的转换位数限制
P = 2147483647 # 梅森素数 2^31-1:期望假阳性 O(m/p) ≈ 0,幸存者再精确复验
def mod_horner(coeffs_rev_mod: list[int], x: int) -> int:
"""模意义下秦九韶:传入倒序(a_n 在前)的系数,反复乘 x 加下一项,随时取模。"""
v = 0
for c in coeffs_rev_mod:
v = (v * x + c) % P
return v
def horner(coeffs: list[int], x: int) -> int:
"""大整数精确秦九韶:Python 原生大整数直接算,不怕系数上百位。"""
v = 0
for c in reversed(coeffs):
v = v * x + c
return v
def solve() -> None:
it = map(int, sys.stdin.buffer.read().split())
n, m = next(it), next(it)
coeffs = [next(it) for _ in range(n + 1)]
# 第一遍:模 P 筛掉绝大多数非根(C++ 正解的本体就是这一步)。
# Horner 要从最高次算起:把负系数先归一到 [0, P),再整体反转成 a_n 在前。
coeffs_rev_mod = [c % P for c in reversed(coeffs)]
candidates = [x for x in range(1, m + 1) if mod_horner(coeffs_rev_mod, x) == 0]
# 第二遍:幸存者用原系数精确复验,保证零误判。
roots = [x for x in candidates if horner(coeffs, x) == 0]
print(len(roots))
print('\n'.join(map(str, roots)))
if __name__ == "__main__":
solve()复杂度
- 时间:模筛
次小整数乘加( 、 时约 次机器运算,正是 C++ 正解的量级);候选根 个,精确复验 次大数位运算,可忽略。Python 常数大,同一算法在官方 1s 限时内可能超时,但复杂度与 C++ 正解同阶。 - 空间:
存系数(模意义一份、原值一份),与 无关。
总结
本题是 NOIP2014 提高组经典题,考点是秦九韶算法 + 同余判定的组合:数据范围攻击的是"精确求值",于是把"值是否为 0"这个判定整体搬进模 v = v * x + c 一行就是嵌套形式的逐层展开;二是"模意义判定 = 单向筛",真根必中、假根可能漏进来,所以筛子后面必须补一道复验(双模数或这里的大整数精确复验),否则等于把正确性押在数据不卡上;三是
图示解析
筛与验的两段流水线
flowchart LR
A["全部候选 x = 1 … m<br/>(m ≤ 10^6)"] --> B["模 p 秦九韶筛<br/>每点 O(n) 次小整数乘加"]
B -->|"模值 ≠ 0<br/>绝不是根"| C["直接淘汰 ≈ m 个"]
B -->|"模值 = 0<br/>幸存者 ≤ n 个"| D["大整数精确复验<br/>原系数 Horner"]
D -->|"值 ≠ 0"| E["淘汰假根"]
D -->|"值 = 0"| F["确认真根,按序输出"]
图里体现的是"漏斗"结构:左边的模筛负责把