P5285 [十二省联考2019]骗分过样例

骗分过样例

根据数据猜测题目内容并完成输出,打表是不太可能的。

真正的传统题在这里(doge

那就一个一个分析,这其实是一道 数论大杂烩 + 乱搞精神 的题目。

前置芝士

这题的黑科技较多,请耐心食用。

快速乘

区别于 \(O(\log n)\) 的龟速乘。

一般两个 long long 级别的数相乘用的是龟速乘,大概长这样

LL Mul(LL a, LL b, LL p){
    LL sum = 0;
    for(; b; b >>= 1){
        if(b & 1) sum = (sum + a) % p;
        a = (a + a) % p;
    }
    return sum;
}

但是它让你的代码无端多了个 \(O(\log n)\),看上去很不爽,于是有了黑科技 \(O(1)\) 快速乘。

LL Mul(LL a, LL b, LL p){
	a %= p, b %= p;
	LL c = (long double) a * b / p;
	c = a * b - c * p;
	if(c < 0) return c + p;
	if(c >= p) return c - p;
	return c;
}

本质上利用了 \(a\ {\rm mod}\ b=a-b\times \lfloor \frac{a}{b}\rfloor\) 的原理。

Miller_Rabin

理论 \(O(n^{1/4})\) 的素数判定算法。

利用费马小定理(若 \(p\) 为素数,那么 \(a^{p-1}\equiv 1\pmod p\))和二次探测定理,有大概率不会判定错误。

具体了解自行百度,这里为了卡常,只用了前两个质数,但是正确性没有受到影响。

void Pre(){
	memset(vis, false, sizeof(vis));
	for(int i = 2; i <= N - 10; i ++){
		if(!vis[i]) prm[++ tot] = i;
		for(int j = 1; j <= tot && 1LL * prm[j] * i <= N; j ++){
			vis[prm[j] * i] = true;
			if(i % prm[j] == 0) break;
		}
	}
}

bool Miller_Rabin(LL x){
	if(x < 3) return x == 2;
	if(x <= N) return !vis[x];
	for(int i = 1; i <= 30; i ++)
		if(x % prm[i] == 0) return false;
	LL p[3] = {0, 2, 3};
	LL t = x - 1, k = 0;
	while(!(t & 1)) k ++, t >>= 1;
	for(int i = 1; i <= 2; i ++){
		if(x == p[i]) return true;
		LL a = Pow(p[i], t, x), nxt;
		for(int j = 1; j <= k; j ++){
			nxt = Mul(a, a, x);
			if(nxt == 1 && a != 1 && a != x - 1) return false;
			a = nxt;
		}
		if(a != 1) return false;
	}
	return true;
}

原根相关

对于质数 \(p\),若存在正整数 \(a\),满足 \(a^{0\sim p-1}\mod p\) 的值都不相同,称 \(a\)\(p\) 的原根。

显然,原根的判定即为除了 \(a^{p-1}\equiv 1\pmod p\) 之外没有其它指数使之成立。

不难得到若存在 \(a^{x}\equiv 1\pmod p,x<p-1\),那么 \(x|p-1\)

难道需要枚举 \(a\) 的所有约数吗,这样的时间复杂度过高。

但是不难发现,若 \(a^{x}\equiv 1\pmod p\) 那么 \(a^{x\times t}\equiv 1\pmod p\),所以只需要用 \(p-1/\) 它的每一个质因子来判断即可。

void Div(int x){
	for(int i = 2; i * i <= x; i ++) if(x % i == 0){
		p[++ cnt] = i;
		while(x % i == 0) x /= i;
	}
	if(x) p[++ cnt] = x;
}

bool G(int x){
	for(int i = 1; i <= cnt; i ++)
		if(Pow(x, (P - 1) / p[i], P) == 1) return false;
	return true;
}

单次判断时间复杂度为 \(O(\log^2 n)\)

还有就是若 \(x\) 为原根,那么 \(y=x^a\) 为原根当且仅当 \((y,\varphi(p))=1\)

于是我们可以枚举 \(\varphi(p)\) 的因子,然后枚举倍数,筛掉其它的非原根数,时间复杂度为 \(O(n\log \log n)\)

这也证明了原根的个数为 \(\varphi(\varphi(p))\) 个。

1_998244353

肉眼观察输出得到 \(ans=19^x\),模数显然是标题中的 998244353

于是一个快速幂即可。

但是发现数据 \(3\) 的大小达到了 \(10^{40}\) 级别,那么利用欧拉定理,边输入边对 998344352 取模即可。

当然你也可以写高精,但是写高精是不可能的,这辈子都不可能的

LL Mul(LL a, LL b, LL p){
	a %= p, b %= p;
	LL c = (long double) a * b / p;
	c = a * b - c * p;
	if(c < 0) return c + p;
	if(c >= p) return c - p;
	return c;
}

LL Get_Num(LL p){
	string num; cin >> num;
	int len = num.length();
	LL sum = 0;
	for(int i = 0; i < len; i ++)
		sum = (Mul(sum, 10, p) + (num[i] - '0')) % p;
	return sum;
}

LL Pow(LL a, LL b, LL p){
	LL sum = 1;
	for(; b; b >>= 1){
		if(b & 1) sum = Mul(sum, a, p);
		a = Mul(a, a, p);
	}
	return sum;
}

void Work1(LL MOD){
	int n = read();
	while(n --){
		LL a = Get_Num(MOD - 1);
		printf("%lld\n", Pow(19, a, MOD));
	}
}

if(opt == "1_998244353") Work1(998244353);

1?

不知道模数了,但是可以暴力枚举 \(1\sim 10^7\) 中的每个数,然后根据前 \(15\) 个结果判断就差不多了。

判断的代码:

#include<cstdio>
#include<string>
#include<cstring>
#include<algorithm>
#include<iostream>
using namespace std;

typedef long long LL;
LL ans[] = {0, 1, 19, 361, 642666, 986870, 310269, 992342, 384794, 134675, 177504, 1002776, 649864, 664505, 128746, 56645};
string num[20];

LL Get_Num(string num, LL p){
	int len = num.length();
	LL sum = 0;
	for(int i = 0; i < len; i ++)
		sum = (sum * 10LL + num[i] - '0') % p;
	return sum;
}

LL Mul(LL a, LL b, LL p){
	LL sum = 0;
	for(; b; b >>= 1){
		if(b & 1) sum = (sum + a) % p;
		a = (a + a) % p;
	}
	return sum;
}

LL Pow(LL a, LL b, LL p){
	LL sum = 1;
	for(; b; b >>= 1){
		if(b & 1) sum = Mul(sum, a, p);
		a = Mul(a, a, p);
	}
	return sum;
}

bool chck(LL MOD){
	for(int i = 1; i <= 15; i ++)
		if(Pow(19, Get_Num(num[i], MOD - 1), MOD) != ans[i]) return false;
	return true;
}

int main(){
	num[1] = "0";
	num[2] = "1";
	num[3] = "2";
	num[4] = "627811703016764290815178977207148434322";
	num[5] = "856773959631992699884816425292659199878";
	num[6] = "9252516556604991340257983180339089701407";
	num[7] = "489253720019916666717422858681089087632";
	num[8] = "361492456259178260319627241577661012313";
	num[9] = "6873285858347382093454667858252046752166";
	num[10] = "3148173111755199960582784172788830450730";
	num[11] = "503142180766309744162636115563208116498";
	num[12] = "198994173692720779018490094767574486547";
	num[13] = "9512507902619525009686627736024447779464";
	num[14] = "7352719788670584612661569096966195045732";
	num[15] = "5392078508855533698663328274209427052165";

	for(LL MOD = 2; MOD <= 10000000; MOD ++)
		if(chck(MOD)) printf("%lld\n", MOD);
	return 0;
}

得到结果:1145141。于是:

else if(opt == "1?") Work1(1145141);

1?+

不难发现模数变的很大,无法穷举判断了。

但是我们可以在数据中找到三元组:\((x,y,z)\) 使得 \(y-x=z\)

那么有模数 \(p\)\(ans_x\times ans_z - ans_y\) 的约数,然后就是枚举大数约数的问题了。

需要用到 pollard_rho 算法,或者有很多神奇方法(比如在 NOI Linux 上调用 factor 指令)都可以。

得到模数为 5211600617818708273。于是:

else if(opt == "1?+") Work1(5211600617818708273);

1wa_998244353

一个好好的题目被搞 wa 掉了,原因很简单是因为整型溢出。因为溢出也不太好搞到规律,或许可以近似认为是随机数。

那么根据生日悖论,在很小的范围内就有很大概率可以找到循环节,代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<map>
using namespace std;

int MOD = 998244353;
map<int, int> vis;

int main(){
	int x = 1;
	for(int i = 1; ; i ++){
		if(vis[x = x * 19 % MOD]){
			printf("%d %d\n", vis[x], i - vis[x]);
			return 0;
		}
		else vis[x] = i;
	}
	return 0;
}

结果为 55245 45699

于是直接上代码就是了:

void Work2(){
	int n = read(), MOD = 998244353;
	LL A = 55245, B = 45699;
	ans[0] = 1;
	for(int i = 1; i <= A + B; i ++)
		ans[i] = ans[i - 1] * 19 % MOD;
	while(n --){
		LL x; scanf("%lld", &x);
		printf("%d\n", ans[x < A ? x : (x - A) % B + A]);
	}
}

else if(opt == "1wa_998244353") Work2();

2p

显然是判断一个区间内的素数,那么直接 Miller_Rabin 即可。

void Pre(){
	memset(vis, false, sizeof(vis));
	for(int i = 2; i <= N - 10; i ++){
		if(!vis[i]) prm[++ tot] = i;
		for(int j = 1; j <= tot && 1LL * prm[j] * i <= N; j ++){
			vis[prm[j] * i] = true;
			if(i % prm[j] == 0) break;
		}
	}
}

bool Miller_Rabin(LL x){
	if(x < 3) return x == 2;
	if(x <= N) return !vis[x];
	for(int i = 1; i <= 30; i ++)
		if(x % prm[i] == 0) return false;
	LL p[3] = {0, 2, 3};
	LL t = x - 1, k = 0;
	while(!(t & 1)) k ++, t >>= 1;
	for(int i = 1; i <= 2; i ++){
		if(x == p[i]) return true;
		LL a = Pow(p[i], t, x), nxt;
		for(int j = 1; j <= k; j ++){
			nxt = Mul(a, a, x);
			if(nxt == 1 && a != 1 && a != x - 1) return false;
			a = nxt;
		}
		if(a != 1) return false;
	}
	return true;
}

void Work3(){
	int n = read();
	while(n --){
		LL l, r; scanf("%lld %lld", &l, &r);
		for(LL i = l; i <= r; i ++)
			if(Miller_Rabin(i)) putchar('p'); else putchar('.');
		puts("");
	}
}

2u

莫比乌斯函数。

分别达到了 \(10^6,10^{12},10^{18}\) 级别,当然区间长度都比较小。

第一种直接线性筛,第二种可以优先筛出 \([1,\sqrt R]\),然后用这个筛 \([L,R]\) 即可。

对于三,先还是用 \([1,10^7]\) 来筛一遍,然后剩余的大部分数字都降到了 \(10^{7}\) 级别之下,但难免意外。

那么就简单讨论一下,对于剩余数字 \(a\)

  1. \(a\) 为质数(用 Miller_Rabin 判断,或者根据 \(a\leq 10^{14}\) 量级直接得到 \(a\) 一定是一个大质数),\(\mu(i)=-\mu(i)\)
  2. \(a\) 为完全平方数,\(\mu(i)=0\)
  3. \(a\) 为两个大数的乘积,\(\mu(i)=\mu(i)\)

于是就筛完了。

int u[N]; LL c[N];

void Work4(){
	int n = read();
	while(n --){
		LL l, r; scanf("%lld %lld", &l, &r);
		for(LL i = 1; i <= r - l + 1; i ++)
			u[i] = 1, c[i] = i + l - 1;
		for(LL i = 1; i <= tot; i ++){
			LL x = prm[i];
			for(LL p = x * ((l - 1) / x + 1) - l + 1; p <= r - l + 1; p += x){
				if(c[p] % (x * x) == 0) u[p] = 0;
				else u[p] = -u[p], c[p] /= x;
			}
		}
		for(LL i = 1; i <= r - l + 1; i ++){
			LL x = c[i];
			if(u[i] && x > 1){
				LL y = sqrt(x);
				if(x == y * y) u[i] = 0;
				else if(x <= 1e14 || Miller_Rabin(x)) u[i] = -u[i];
			}
			if(u[i]) putchar(u[i] == 1 ? '+' : '-');
			else putchar('0');
		}
		puts("");
	}
}

else if(opt == "2u") Work4();

2g

原根,利用前置芝士里的内容不难得到代码。

值得一提的是当模数比较大的时候,区间范围就比较小,而区间范围达到 \(10^6\) 的时候,模数又比较小。

这启发我们用两种不同的方法,区间范围较小的时候直接暴力枚举 + \(O(\log^2 n)\) 判断,否则利用因子倍数来预处理筛原根。

int cnt, P, p[N], pos[N << 1];

void Div(int x){
	for(int i = 2; i * i <= x; i ++) if(x % i == 0){
		p[++ cnt] = i;
		while(x % i == 0) x /= i;
	}
	if(x) p[++ cnt] = x;
}

bool G(int x){
	for(int i = 1; i <= cnt; i ++)
		if(Pow(x, (P - 1) / p[i], P) == 1) return false;
	return true;
}

void Work5(){
	int n = read();
	while(n --){
		cnt = 0; int l, r;
		l = read(), r = read();
		if(l == 233333333) P = 1515343657; else P = read();
		Div(P - 1);
		if(r - l + 1 <= 1e6){
			for(int i = l; i <= r; i ++)
				if(G(i)) putchar('g'); else putchar('.');
		}
		else{
			memset(vis, false, sizeof(vis));
			for(int i = 1; i <= cnt; i ++)
				for(int j = 1; j <= (P - 1) / p[i]; j ++)
					vis[p[i] * j] = true;
			int x = 2;
			for(; !G(x); x ++);
			for(int i = 1, num = x; i < P; num = 1LL * num * x % P, i ++)
				pos[num] = i;
			for(int i = l; i <= r; i ++)
				if(vis[pos[i]]) putchar('.'); else putchar('g');
		}
		puts("");
	}
}

else Work5();

代码总结

于是这道题就结束了,总体来讲题目形式还是很新颖的,而且很好的考验了选手的初等数论能力(和乱搞能力)

个人有非常良好的做题体验感,感觉挺有趣的。

AC Code:

#include<cstdio>
#include<string>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<cmath>
using namespace std;

typedef long long LL;
const int N = 10000010;
int tot, ans[200010], prm[N];
bool vis[N + 10];

int read(){
	int x = 0, f = 1; char c = getchar();
	while(c < '0' || c > '9') f = (c == '-') ? -1 : 1, c = getchar();
	while(c >= '0' && c <= '9') x = x * 10 + c - 48, c = getchar();
	return x * f;
}

void Pre(){
	memset(vis, false, sizeof(vis));
	for(int i = 2; i <= N - 10; i ++){
		if(!vis[i]) prm[++ tot] = i;
		for(int j = 1; j <= tot && 1LL * prm[j] * i <= N; j ++){
			vis[prm[j] * i] = true;
			if(i % prm[j] == 0) break;
		}
	}
}

LL Mul(LL a, LL b, LL p){
	a %= p, b %= p;
	LL c = (long double) a * b / p;
	c = a * b - c * p;
	if(c < 0) return c + p;
	if(c >= p) return c - p;
	return c;
}

LL Get_Num(LL p){
	string num; cin >> num;
	int len = num.length();
	LL sum = 0;
	for(int i = 0; i < len; i ++)
		sum = (Mul(sum, 10, p) + (num[i] - '0')) % p;
	return sum;
}

LL Pow(LL a, LL b, LL p){
	LL sum = 1;
	for(; b; b >>= 1){
		if(b & 1) sum = Mul(sum, a, p);
		a = Mul(a, a, p);
	}
	return sum;
}

void Work1(LL MOD){
	int n = read();
	while(n --){
		LL a = Get_Num(MOD - 1);
		printf("%lld\n", Pow(19, a, MOD));
	}
}

void Work2(){
	int n = read(), MOD = 998244353;
	LL A = 55245, B = 45699;
	ans[0] = 1;
	for(int i = 1; i <= A + B; i ++)
		ans[i] = ans[i - 1] * 19 % MOD;
	while(n --){
		LL x; scanf("%lld", &x);
		printf("%d\n", ans[x < A ? x : (x - A) % B + A]);
	}
}

bool Miller_Rabin(LL x){
	if(x < 3) return x == 2;
	if(x <= N) return !vis[x];
	for(int i = 1; i <= 30; i ++)
		if(x % prm[i] == 0) return false;
	LL p[3] = {0, 2, 3};
	LL t = x - 1, k = 0;
	while(!(t & 1)) k ++, t >>= 1;
	for(int i = 1; i <= 2; i ++){
		if(x == p[i]) return true;
		LL a = Pow(p[i], t, x), nxt;
		for(int j = 1; j <= k; j ++){
			nxt = Mul(a, a, x);
			if(nxt == 1 && a != 1 && a != x - 1) return false;
			a = nxt;
		}
		if(a != 1) return false;
	}
	return true;
}

void Work3(){
	int n = read();
	while(n --){
		LL l, r; scanf("%lld %lld", &l, &r);
		for(LL i = l; i <= r; i ++)
			if(Miller_Rabin(i)) putchar('p'); else putchar('.');
		puts("");
	}
}

int u[N]; LL c[N];

void Work4(){
	int n = read();
	while(n --){
		LL l, r; scanf("%lld %lld", &l, &r);
		for(LL i = 1; i <= r - l + 1; i ++)
			u[i] = 1, c[i] = i + l - 1;
		for(LL i = 1; i <= tot; i ++){
			LL x = prm[i];
			for(LL p = x * ((l - 1) / x + 1) - l + 1; p <= r - l + 1; p += x){
				if(c[p] % (x * x) == 0) u[p] = 0;
				else u[p] = -u[p], c[p] /= x;
			}
		}
		for(LL i = 1; i <= r - l + 1; i ++){
			LL x = c[i];
			if(u[i] && x > 1){
				LL y = sqrt(x);
				if(x == y * y) u[i] = 0;
				else if(x <= 1e14 || Miller_Rabin(x)) u[i] = -u[i];
			}
			if(u[i]) putchar(u[i] == 1 ? '+' : '-');
			else putchar('0');
		}
		puts("");
	}
}

int cnt, P, p[N], pos[N << 1];

void Div(int x){
	for(int i = 2; i * i <= x; i ++) if(x % i == 0){
		p[++ cnt] = i;
		while(x % i == 0) x /= i;
	}
	if(x) p[++ cnt] = x;
}

bool G(int x){
	for(int i = 1; i <= cnt; i ++)
		if(Pow(x, (P - 1) / p[i], P) == 1) return false;
	return true;
}

void Work5(){
	int n = read();
	while(n --){
		cnt = 0; int l, r;
		l = read(), r = read();
		if(l == 233333333) P = 1515343657; else P = read();
		Div(P - 1);
		if(r - l + 1 <= 1e6){
			for(int i = l; i <= r; i ++)
				if(G(i)) putchar('g'); else putchar('.');
		}
		else{
			memset(vis, false, sizeof(vis));
			for(int i = 1; i <= cnt; i ++)
				for(int j = 1; j <= (P - 1) / p[i]; j ++)
					vis[p[i] * j] = true;
			int x = 2;
			for(; !G(x); x ++);
			for(int i = 1, num = x; i < P; num = 1LL * num * x % P, i ++)
				pos[num] = i;
			for(int i = l; i <= r; i ++)
				if(vis[pos[i]]) putchar('.'); else putchar('g');
		}
		puts("");
	}
}

int main(){
	Pre();
	string opt; cin >> opt;
	if(opt == "1_998244353") Work1(998244353);
	else if(opt == "1?") Work1(1145141);
	else if(opt == "1?+") Work1(5211600617818708273);
	else if(opt == "1wa_998244353") Work2();
	else if(opt == "2p") Work3();
	else if(opt == "2u") Work4();
	else Work5();
	return 0;
}
posted @ 2021-07-05 21:54  LPF'sBlog  阅读(63)  评论(0编辑  收藏  举报