原题大意 给出一个大数$N$其形式为:
其中 $c_i\in{0,1,\cdots,9} \ , \ 1 \le l_i \le 10^9 \, \ 1 \le k \le 10^5$
求 $\lfloor \frac{N}{M} \rfloor \pmod {10007} $
思路 我们考虑将向下取整符号去掉,
对于 $\text{ans}:=\lfloor \frac{n}{m} \rfloor \pmod {M} $ 可以化成:
我们记 $ \text{inv}(m) $ 为 $m$ 在模 $M$ 意义下的逆元 ,并令:
又由 $(*)$ 式可知:
这里 $G(l_i,M)$ 表示 $c_i\cdot(10^0+10^1+\cdots+10^{l_i-1})\pmod M$ 可以使用几何级数的递推公式求出:
同理:
然后将 $r_m$ 和 $r_M$ 改成递推的形式(从高位到低位),就可以在 $O(k)$ 的时间复杂度内求出答案。
代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 const ll M=10007 ;ll qpow (ll a, ll b, ll m) { ll res=1 ; a%=m; while (b) { if (b&1 )res=res*a%m; a=a*a%m; b>>=1 ; } return res; } ll inv (ll x) { return qpow (x,M-2 ,M); } ll sum (ll l, ll m) { if (l==0 )return 0 ; if (l==1 )return 1 ; if (~l&1 )return sum (l>>1 , m)*(qpow (10 , l>>1 , m)+1 )%m; return (sum (l-1 ,m)*10 +1 )%m; } int main () { ios::sync_with_stdio (false ); cin.tie (0 ); cout.tie (0 ); ll k,m; cin >> k >> m; vector<pll> A (k) ; for (int i=k-1 ; i>=0 ; i--) cin >> A[i].first >> A[i].second; ll a=0 , rm=0 , p=1 , pm=1 ; for (auto [c,l]:A) { ll ca=c*sum (l,M)%M; a=(a+ca*p)%M; p=p*qpow (10 ,l,M)%M; ll cm=c*sum (l,m)%m; rm=(rm+cm*pm)%m; pm=pm*qpow (10 ,l,m)%m; } ll ans=(a-(rm%M)+M)%M*inv (m)%M; cout<<ans; return 0 ; }
扩展 M不是质数 如果M不是质数,但 $gcd(m,M)≠1$ 那么逆元 $ \text{inv}(m) $ 可以用扩展欧几里得算法求出; 但 $gcd(m,M)≠1$ 时,逆元根本不存在,这是 $(**)$ 式可以变形成:
一个关于q的线性同余方程组,可以先提取公因子 $g=gcd(m,M)$ 归一化,
此时 $gcd(\frac{m}{g} , \frac{M}{g})=1$ ,就可以用扩展欧几里得算法求出特解,
在加上 $k$ 个 $\frac{M}{g}$ ,就可以得到通解:
但是 $k$ 是由 $n$ 唯一确定的,只通过 $r_m \ , \ r_M $ 而不计算 n/m 时是无法直接确定k的,应该换个思路。
显然这里 $gcd(n,M)>1$ 对于周期为 $M$ 的 $r_M$ 以及周期为 $m$ 的 $r_m$ 来说,将周期扩大到其最小公倍数 $mM$ 。 这样当n加上m时 $\lfloor n/m \rfloor$ 就增加1,继续增加到 $nM$ 后此时商就变成了M,在模M意义下又回到了0,当然是符合向下取整后取余的。 具体地,我们设n对 $mM$ 的商为Q余数为R,那么有:
我们要算的是 $\lfloor \frac{n}{m} \rfloor \pmod M$。将 $n$ 代入,得:
这样完美避开了逆元和k无法确定的问题。
m和l变大 如果 $1 \le m,l \le 10^{18}$ ,最暴力的方法就是直接使用大数模拟完成,例如以下程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include <bits/stdc++.h> using namespace std;using i128 = __int128;using ll = long long ;i128 mul (i128 a, i128 b, i128 m) { i128 res=0 ; while (b) { if (b&1 ) res=(res+a)%m; a=(a+a)%m; b>>=1 ; } return res; } pair<i128,i128> calc (i128 l,i128 m) { if (l==0 ) return {1 %m,0 }; if (l==1 ) return {10 %m,1 %m}; auto [p,s]=calc (l>>1 ,m); i128 p2=mul (p,p,m), s2=(mul (s,p,m)+s)%m; if (~l&1 ) return {p2,s2}; else return {mul (p2,10 ,m),(mul (s2,10 ,m)+1 )%m}; } int main () { ios::sync_with_stdio (false ); cin.tie (0 ); int k;ll m,M; cin >> k >> m >> M; i128 mM=(i128)m*M, cur=0 ; for (int i=0 ;i<k;i++) { ll c,l; cin >> c >> l; auto [p,g]=calc (l,mM); i128 add=mul ((i128)c,g,mM); cur=(mul (cur,p,mM)+add)%mM; } i128 ans=(cur/m)%M; cout<<(ll)ans; }
但是很不幸,由于防止过程中乘法溢出,这里使用了模数乘法,导致最后的时间复杂度变成 $O(k \log l \cdot \log (m\cdot M))$ ,显然会T。
如果预处理 $10^i$ 模 $m\cdot M$ 的各项/和,但由于范围过大防止溢出,还是只能通过模数乘法来计算,时间复杂度仍然不变。
拆模 引理 :
今有一长度为 $l$ 的十进制数 $n$ ,
那么其 $\lfloor n/m \rfloor \mod M$ 的结果可以表示为:
我们记数字的前i位(高i位)为 $ni:=\sum {j=l-i-1}^{l-1}d_j10^j$ ,那么有:
令 $n0=d {l-1}$ 不难发现有递推公式:
这样就可以递推处理商了,而且很好地避免了逆元等情况。
为了处理乘法范围过大,同时复杂度又不能接受,由上面的引理我们可以想到维护 $q\ ,\ r$ 模M,通过之前的递推公式,我们知道从高位到低位读入时有:
那么其商和余数的递推公式可以写成:
这里使用预处理 $10^i$ 分别模M和m的个项和合,然后把长度 $l$ 用二进制拆成若干个 $2^i$ 的和,这样就可以在 $O(k \log l)$ 的时间复杂度内求出答案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #include <bits/stdc++.h> using namespace std;using i128 = __int128;using ll = long long ;const int LOG=60 ;vector<ll> p10m (LOG) , p10M (LOG) ;vector<ll> s10m (LOG) , s10M (LOG) ;void init (ll m, ll M) { p10m[0 ]=10 %m,p10M[0 ]=10 %M; s10m[0 ]=1 %m,s10M[0 ]=1 %M; for (int i=1 ;i<LOG;i++) { p10m[i]=(i128)p10m[i-1 ]*p10m[i-1 ]%m; p10M[i]=(i128)p10M[i-1 ]*p10M[i-1 ]%M; s10m[i]=((i128)s10m[i-1 ]*p10m[i-1 ]+s10m[i-1 ])%m; s10M[i]=((i128)s10M[i-1 ]*p10M[i-1 ]+s10M[i-1 ])%M; } } pair<i128,i128> calc (i128 l,i128 mod, const vector<ll>& p10, const vector<ll>& s10) { i128 p=1 ,s=0 ; for (int i=0 ;l;i++,l>>=1 ) if (l&1 ) { p=p*p10[i]%mod; s=(s*p10[i]+s10[i])%mod; } return {p,s}; } int main () { ios::sync_with_stdio (false ); cin.tie (0 ); int k;ll m,M; cin >> k >> m >> M; init (m,M); i128 r=0 , q=0 ; for (int i=0 ;i<k;i++) { ll c,l; cin >> c >> l; auto [p,s]=calc (l,m,p10m,s10m); auto [P,S]=calc (l,M,p10M,s10M); i128 t=(i128)p*r+(i128)c*s; r=t%m; q=((i128)P*q+t/m) % M; } cout << (ll)q; }
但是这样写WA了,原因是对于 $q{i+1}$ 的向下取整的分子应该是除了 $r_i$ 剩下都不应该取模,以上程序计算 $10^{l_i} r_i+c_i\sum {j=0}^{l_i-1}10^j$ 时,对于10的次幂以及和都做了取模处理,这显然是错误的。
如果使用模数为 $mM$ ,同样地,中间过程计算的积肯定会溢出,此时又用懒乘法,导致时间复杂度又变回去了。
下面给出正确至少可以ac的代码 虽然是pypy版本 ,直接完整地算出其分子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import sysdef sol (): it=iter (map (int , sys.stdin.read().split())) try : K=next (it); m=next (it); M=next (it) except StopIteration: return mM = m * M P, S = [10 %mM], [1 %mM] for i in range (62 ): S.append((S[i]*P[i]+S[i])%mM) P.append((P[i]*P[i])%mM) r, q = 0 , 0 for _ in range (K): c, l = next (it), next (it) p, s, i = 1 , 0 , 0 l = l while l: if l&1 : s=(s*P[i]+S[i])%mM p=(p*P[i])%mM l>>=1 i+=1 t = r*p+c*s q = (q*(p%M)+(t%mM//m))%M r = t%m sys.stdout.write(str (q)) if __name__ == "__main__" : sol()
得益于pypy的JIT,这个代码既可以简洁地保持高精度,又可以在2.5s内跑完。
拆彻底