POJ2635同余模定理与千进制
基础知识点:同余模定理
所谓的同余,顾名思义,就是两个(或多个)数对同一个数做取模运算所得结果相同。如 a = 11 与 b = 6 对 n = 5 同余,用符号表示就是 a mod n = b mod n = 1 或者a%n = b%n = 1。
同余模定理包含很多子定理,常用的有以下两个:
(a+b)%n = (a%n+b%n)%n = (a%n+b)%n = (a+b%n)%n;
(a*b)%n = (a%n*b%n)%n;
先看十进制下情况(十进制指最小单元a/b/c/d均为小于十的数字):
a%n = A
(ab)%n = (b+10*a)%n = (b%n+10*a%n)%n = (b%n+10*A)%n = B
//ab表示一个两位数,以此类推
(abc)%n = (c+10*(ab))%n = (c%n+10*(ab)%n)%n = (c%n+10*B)%n = C
(abcd)%n = (d+10*(abc)%n)%n = (d%n+10*(abc)%n)%n = (d%n+10*C)%n
另外由于a/b/c/d都较小,为减少求模次数,由(a%n+b)%n = (a+b)%n
(ab)%n = (b+10*A)%n = B
(abc)%n = (c+10*B)%n = C
(abcd)%n = (d+10*C)%n
由此我们可以得到对一个无法用整数类型储存的大数从最高位开始逐位取模计算的方法:
#include <cstdio>
const int maxn = 1005;
char num[maxn];
int main() {
int n;
while (~scanf("%s%d", num, &n)) {
//与scanf("%s%d", num, &n) != EOF作用相同
int ans = 0;
for (int i = 0; num[i]; i++)
ans = ((ans * 10 + (num[i] - '0'))% n;
printf("%d\n", ans);
}
return 0;
}
防止TLE要点一:改用千进制,减少求模次数
如果你曾经在OJ上试验过,你会发现把a[1000]改为a[10]并不能减少耗时,反之也不会增加耗时。此处原理类似,对两位数取模与对四位数取模每次运算耗时几乎相同,而减少求模次数后整体耗时将大大减少(千进制下求模次数是十进制的三分之一)。
防止TLE要点二:改进素数生成方法
博主最开始采用的是最朴素的素数打表方法:
int cnt;
int plist[maxn];
bool isPrime(int n) {
for (int i = 2; i * i <= n; i++)
if (n % i == 0)
return false;
return true;
}
void getPrime(int n) {
for (int i = 2; i < n; i++)
if (isPrime(i))
plist[cnt++] = i;
}
时间复杂度O(n3/2)
结果当然是TLE
为此我们需要改用时间复杂度O(n)的欧拉生成法.
AC代码(844ms):
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 1000010;
char a[101];
int ka[35], prime[maxn];
int n, cnt;
int getPrime(int n) {
for (int i = 2; i <= n; i++) {
if (!prime[i])
prime[++*prime] = i;//*prime用来计数,prime[1]=2
for (int j = 1; j <= *prime && i * prime[j] <= n; j++) {
prime[prime[j] * i] = 1;
if (i % prime[j] == 0)
break;
}
}
return *prime;
}
int cal(char c, int n = 1) { return (c - '0') * n; }
int main() {
getPrime(maxn);//要生成比1000000大的素数
while (scanf("%s%d", a, &n) && n) {
memset(ka, 0, sizeof(ka));
cnt = 0;
for (int i = strlen(a) - 1; i >= 2; i -= 3) {
ka[cnt++] = cal(a[i - 2], 100) + cal(a[i - 1], 10) + cal(a[i]);
}
switch (strlen(a) % 3) {
case 1:
ka[cnt++] = cal(a[0]);
break;
case 2:
ka[cnt++] = cal(a[0], 10) + cal(a[1]);
break;
}
int ans, res;
bool bad = false;
for (int j = 1; prime[j] < n && !bad; j++) { //(*)
int k = prime[j];
ans = 0;
for (int i = cnt - 1; i >= 0; i--)
ans = (ans * 1000 + ka[i]) % k;
if (ans == 0) {
bad = true;
res = k;
}
}
if (!bad)
printf("GOOD\n");
else
printf("BAD %d\n", res);
}
return 0;
}
其中需要注意的一点是素数表里要有比1000000大的素数,否则n=1000000时(*)处循环不能及时终止会造成数组越界访问。
更慢的的万进制
最后,博主突发奇想,如果采用万进制,会不会更快呢?
AC代码(1297ms):
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 1000010;
char a[101];
int wa[26]; //wa这个名字只是巧合
int n, cnt;
int prime[maxn];
int getPrime(int n) {
for (int i = 2; i <= n; i++) {
if (!prime[i])
prime[++*prime] = i;
for (int j = 1; j <= *prime && i * prime[j] <= n; j++) {
prime[prime[j] * i] = 1;
if (i % prime[j] == 0)
break;
}
}
return *prime;
}
int cal(char c, int n = 1) { return (c - '0') * n; }
int main() {
getPrime(maxn);
while (scanf("%s%d", a, &n) && n) {
memset(wa, 0, sizeof(wa));
cnt = 0;
for (int i = strlen(a) - 1; i >= 3; i -= 4) {
wa[cnt++] = cal(a[i - 3], 1000) + cal(a[i - 2], 100) +
cal(a[i - 1], 10) + cal(a[i]);
}
switch (strlen(a) % 4) {
case 1:
wa[cnt++] = cal(a[0]);
break;
case 2:
wa[cnt++] = cal(a[0], 10) + cal(a[1]);
break;
case 3:
wa[cnt++] = cal(a[0], 100) + cal(a[1], 10) + cal(a[2]);
break;
}
long long ans; //int会溢出
int res;
bool bad = false;
for (int j = 1; prime[j] < n && !bad; j++) {
int k = prime[j];
ans = 0;
for (int i = cnt - 1; i >= 0; i--)
ans = (ans * 10000 + wa[i]) % k;
if (ans == 0) {
bad = true;
res = k;
}
}
if (!bad)
printf("GOOD\n");
else
printf("BAD %d\n", res);
}
return 0;
}
结果却令人失望,为什么更慢了呢?其实这也不难理解,对五位数求模的单次耗时相比对四位数显著增加。这样看,千进制大概是一个较好的平衡点。