「NOIP Record」#15 数论题目选讲

Hankson的趣味题

从质因子的角度考虑。

\(a,b,c,d\) 都分解了,对于一个质因子 \(p_i\),题目给出的条件等价于 \[ \min \Big( e_{p_i} (x) ,e_{p_i} (a)\Big) =e_{p_i}(c) \]

\[ \max \Big( e_{p_i}(x),e_{p_i}(b) \Big) = e_{p_i}(d) \]

讨论一下就能得到 \(e_{p_i}(x)\) 的取值范围,或者报告无解。

CF1114C Trailing Loves (or L'oeufs?)

\(k\) 分解为 \(\prod_{i=1}^m p_i^{e_i}\),设 \(E_i\)\(p_i\)\(N\) 中的幂次。

\(N\)\(k\) 进制下后导 \(0\) 的个数就是 \(\min\Big(\Big\lfloor \frac{E_i}{e_i} \Big\rfloor\Big)\)

对于每个 \(p_i\)\(n!\) 中质因子 \(p_i\) 的个数为 \[ \sum_{k=1 \wedge p_i^k \le n} \Big\lfloor \frac{n}{p_i^k} \Big\rfloor \] 解释一下上式。

\(n!\) 中包含质因子 \(p_i\) 的指数,等价于 \([1,n]\) 中每个数中 \(p_i\) 的指数之和。

指数至少为 \(1\) 的数有 \(\Big\lfloor \frac{n}{p_i} \Big\rfloor\) 个,这里只统计第一个。指数至少为 \(2\) 的有 \(\Big\lfloor \frac{n}{p_i^2} \Big\rfloor\) 个,这里只统计第二个,以此类推。

luogu1445 樱花

\[ \begin{aligned} \frac{1}{x} + \frac{1}{y} &= \frac{1}{n!} \\ \frac{x+y}{xy} &=\frac{1}{n!} \\ xn! + yn! &= xy \\ xy - xn! -yn! + (n!)^2 &= (n!)^2 \\ (x-n!)(y-n!) &= (n!)^2 \end{aligned} \]

\(x\)\(y\) 一一对应。

用上面的办法求出 \((n!)^2\) 的质因子指数,再求 \(\sigma_0\Big((n!)^2\Big)\) 即可。

\(\texttt{Bonus:}\) 求出所有 \((x+y)\) 的和。

求出 \(\sigma_1\Big((n!)^2\Big)\) 即可。

luogu1069 [NOIP2009 普及组] 细胞分裂

先把 \(m_1\) 分解了,对于 \(m_1\) 的一个质因数 \(p_j\),如果 \(s_i\) 中不存在 \(p_j\) 则无解。

否则,设 \(p_j\)\(s_i\) 中的指数为 \(E_j\),则 \(x\) 至少为 \(\Big\lceil \frac{e_j \times m_2}{E_j} \Big\rceil\)

取最大值即可。

CF1325E Ehab's REAL Number Theory Problem

每个数的约数个数不超过 \(7\),也就是每个数最多有 \(2\) 个质因数。

首先我们判一下完全平方数。然后我们就可以不考虑次数为偶数的质因子,所以所有数一共可以分成两类。

  1. 单个质数 \(p\)
  2. 两个质数 \(p,q\) 的乘积 \(pq\)

用这些数的乘积得到完全平方数,那么每一种质数应该都出现偶数次。

考虑经典的图论建模问题。我们把出现过的质数当作点,能表示为 \(pq\) 的点,看作 \(p\)\(q\) 之间连一条无向边。另外还要额外建立一个点 \(1\),对于单个质数 \(p\),在 \(1\)\(p\) 之间连一条无向边。

这样,任何合法解都是图中的一个环。问题转化为求这张图的最小环。

由于边权都为 \(1\),我们直接使用 \(\text{BFS}\) 找环。

\(\text{BFS}\) 树上的每一条返祖边都对应着一个环。枚举起点,如果这个点在环里,那么找到的第一条返祖边就是它所在的最小环,并且两个端点到它的距离再加 \(1\) 就是环长。

如果一个点不在环里,那么如果有环,以它为起点搜到的环一定比答案更大;否则以它为起点一定不能搜到环。所以这样做不会有问题。

然而复杂度约是 \(O\Big(n \frac{n}{\ln n}\Big)\),无法通过。

注意到每条边至少有一个点小于等于 \(\sqrt{\max \{a_i\}}\),只枚举这部分点作为起点即可。复杂度 \(O\Big(n \sqrt{\max \{a_i\}}\Big)\)

CF582A GCD Table

一个重要的性质是 \(\gcd(a,b) \le \min(a,b)\)

表中最大的数、次大的数一定都是原序列中最大和次大的数,但是其他的就不一定了。

可以这样做。找到表中最大的数 \(k\),它一定是序列元素。然后删掉 \(k\),在序列中加入 \(k\),然后扫一遍序列中的其他元素 \(j\),在表中删掉两个 \(\gcd(k,j)\)

使用std::set可以做到 \(O(n^2 \log_2 n^2)\)

CF1344A Hilbert's Hotel

注意到原本位置距离为 \(tn, t \in \mathbb{Z}\) 的点,在移动后仍然会距离 \(tn\),所以就可以只考虑 \([0,n-1]\) 的点。

然后 check 每个 \((i+a_i) \bmod n\) 是否唯一即可。

注意要把 \(i+a_i\) 可能小于 \(0\)

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define uint unsigned long long
#define PII pair<int,int>
#define MP make_pair
#define fi first
#define se second
#define pb push_back
#define eb emplace_back
#define SET(a,b) memset(a,b,sizeof(a))
#define CPY(a,b) memcpy(a,b,sizeof(b))
#define rep(i,j,k) for(int i=(j);i<=(k);++i)
#define per(i,j,k) for(int i=(j);i>=(k);--i)
int read() {
	int a=0, f=1; char c=getchar();
	while(!isdigit(c)) {
		if(c=='-') f=-1;
		c=getchar();
	}
	while(isdigit(c)) a=a*10+c-'0', c=getchar();
	return a*f;
}
const int N=2e5+5;
int T, n, a[N];
unordered_map<int,int> p;
int r(int x) {
	return ((x+a[x])%n+n)%n;
} 
void solve() {
	
	n=read();
	p.clear();
	rep(i,0,n-1) a[i]=read(), ++p[r(i)];
	int fg=0;
	rep(i,0,n-1) {
		if(p[r(i)]>1) { fg=1; break; }
	}
	if(fg) puts("NO");
	else puts("YES"); 
}
signed main() {
	T=read();
	while(T--) solve();
	return 0;
}

CF1342C Yet Another Counting Problem

容易看出这个是有循环节的,周期为 \(\operatorname{lcm}(a,b)\)

然后 \(a,b\) 都很小,可以直接暴力处理每个周期内的信息,最后即可 \(O(1)\) 回答询问。

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define uint unsigned long long
#define PII pair<int,int>
#define MP make_pair
#define fi first
#define se second
#define pb push_back
#define eb emplace_back
#define SET(a,b) memset(a,b,sizeof(a))
#define CPY(a,b) memcpy(a,b,sizeof(b))
#define rep(i,j,k) for(int i=(j);i<=(k);++i)
#define per(i,j,k) for(int i=(j);i>=(k);--i)
int read() {
	int a=0, f=1; char c=getchar();
	while(!isdigit(c)) {
		if(c=='-') f=-1;
		c=getchar();
	}
	while(isdigit(c)) a=a*10+c-'0', c=getchar();
	return a*f;
}
int T, a, b, q, c, d;
int s[50000];
int calc(int x) {
	int t=x/c;
	return t*s[c]+s[x-t*c];
}
int lcm(int x,int y) { return x/__gcd(x,y)*y; }
void solve() {
	a=read(), b=read(), q=read();
	c=lcm(a,b);
	rep(i,1,c) s[i]=s[i-1]+(i%a%b!=i%b%a);
	while(q--) {
		int l=read(), r=read();
		printf("%lld ",calc(r)-calc(l-1));
	}
	puts("");
}
signed main() {
	T=read();
	while(T--) solve();
	return 0;
}

luogu1477 [NOI2008] 假面舞会

link

CF980D Perfect Groups

该上那个经典套路了。。

乘积为完全平方数具有传递性,因此可以轻易划分成若干唯一确定的不相交集合。

题目要求最小化分组的数量,那么就要保证每个集合都是极大的。

枚举起点,每考虑一个数都贪心把它加入它所对应的集合,没有就新开一个。

应该注意的是, \(0\) 可以放到任何一个集合中,我们钦定所有 \(0\) 都放到第一个集合里。所以,只有以下两种情况才需要新开集合。

  1. 序列中没有元素。
  2. \(a_i \neq 0\)\(a_i\) 所在集合还没有元素并且当前序列中不是全 \(0\)
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define SET(a,b) memset(a,b,sizeof(a))
#define rep(i,j,k) for(int i=(j);i<=(k);++i)
#define per(i,j,k) for(int i=(j);i>=(k);--i)
int read() {
	int a=0, f=1; char c=getchar();
	while(!isdigit(c)) {
		if(c=='-') f=-1;
		c=getchar();
	}
	while(isdigit(c)) a=a*10+c-'0', c=getchar();
	return a*f;
}
const int N=5005;
int n, k, a[N], l[N], ans[N];
int squ(int x,int y) {
	int t=(int)sqrt(x*y);
	return x*y==t*t;
}
signed main() {
	n=read();
	rep(i,1,n) a[i]=read();
	rep(i,1,n) {
		per(j,i-1,1) if(a[i]*a[j]>0&&squ(a[i],a[j])) { l[i]=j; break; }
	}
	rep(i,1,n) {
		int k=0, zero=0;
		rep(j,i,n) {
			if(i==j||(a[j]!=0&&zero&&l[j]<i)) ++k;
			++ans[k];
			zero|=a[j]!=0;
		}
	}
	rep(i,1,n) printf("%lld ",ans[i]);
	puts("");
}

CF354C Vasya and Beautiful Arrays

考虑直接枚举答案。容易发现一个答案 \(d\) 是整个序列的公约数,当且仅当 \({\forall} i \in [1,n], a_i \bmod d \le k\)

然而貌似无法优化了。

注意到答案不会超过 \(mn = \min_{i=1}^n \{a_i\}\)

如果 \(mn \le k+1\),那么 \(a_i \bmod mn \le k\)

如果 \(mn> k+1\),那么令答案为 \(k+1\),显然都可以满足。因此答案区间为 \([k+1,mn]\)

考虑如果一个 \(d\) 能成为答案,每个 \(a_i\) 一定都能写成 \(tk+r\),其中 \(r \in[0,k]\)

注意到值域不大,可以开一个桶,在桶上做前缀和,然后枚举 \(t\),对每个区间求和,最后检查是否等于 \(n\) 即可。

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define uint unsigned long long
#define PII pair<int,int>
#define MP make_pair
#define fi first
#define se second
#define pb push_back
#define eb emplace_back
#define SET(a,b) memset(a,b,sizeof(a))
#define CPY(a,b) memcpy(a,b,sizeof(b))
#define rep(i,j,k) for(int i=(j);i<=(k);++i)
#define per(i,j,k) for(int i=(j);i>=(k);--i)
int read() {
	int a=0, f=1; char c=getchar();
	while(!isdigit(c)) {
		if(c=='-') f=-1;
		c=getchar();
	}
	while(isdigit(c)) a=a*10+c-'0', c=getchar();
	return a*f;
}
const int N=3e5+5, M=1e6+5, lim=1e6;
int n, k, mx, mn=1e9, a[N], c[M];
signed main() {
	n=read(), k=read();
	rep(i,1,n) {
		a[i]=read();
		mx=max(mx,a[i]);
		mn=min(mn,a[i]);
		++c[a[i]];
	}
	if(mn<=k+1) { printf("%lld\n",mn); return 0; }
	for(int i=1;i<=min(mx+k,lim);++i) c[i]+=c[i-1];
	per(d,mn,k+1) {
		int cnt=0;
		for(int i=1;i<=mx/d;++i) {
			cnt+=c[min(i*d+k,lim)]-c[i*d-1];
		}
		if(cnt==n) { printf("%lld\n",d); return 0; }
	}	
	return 0;
}

CF1114F Please, another Queries on Array?

不要忘了欧拉函数最原始的式子 \[ \varphi(n) = n \prod_{i=1}^m \frac{1-p_i}{p_i} \] 这个式子的优点在于只和 \(n\) 以及它的质因子有关,并且是个积式。

也就是说,对于一个区间 \([l,r]\),我们只需要知道区间积以及区间出现过的质因子集合即可。

而值域小得令人发指,并且对于乘法操作,能增加的质因子也不超过 \(300\)。也就是说可能出现的质因子只有 \(62\) 个,正好能用long long状压。

用线段树维护区间乘积和区间质因子集合即可。

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define uint unsigned long long
#define PII pair<int,int>
#define MP make_pair
#define fi first
#define se second
#define pb push_back
#define eb emplace_back
#define SET(a,b) memset(a,b,sizeof(a))
#define CPY(a,b) memcpy(a,b,sizeof(b))
#define rep(i,j,k) for(int i=(j);i<=(k);++i)
#define per(i,j,k) for(int i=(j);i>=(k);--i)
int read() {
	int a=0, f=1; char c=getchar();
	while(!isdigit(c)) {
		if(c=='-') f=-1;
		c=getchar();
	}
	while(isdigit(c)) a=a*10+c-'0', c=getchar();
	return a*f;
}
const int N=4e5+5, mod=1e9+7;
int n, q, a[N];
int cnt, v[N];
int p[N], inv[N], rev[N];
int t[N<<2], tag[N<<2], s[N<<2], stag[N<<2];
void init() {
	inv[0]=inv[1]=1;
	for(int i=2;i<=300;++i) {
		inv[i]=(mod-mod/i)*inv[mod%i]%mod;
		if(!v[i]) {
			p[cnt]=i, rev[cnt]=(i-1)*inv[i]%mod, ++cnt;
			for(int j=i*i;j<=300;j+=i) v[j]=1;
		}
	}
}
char ss[14];
int fp(int a,int b) {
	int c=1;
	for(;b;a=a*a%mod,b>>=1) if(b&1) c=c*a%mod;
	return c;
}
void pushup(int x) {
	t[x]=t[x<<1]*t[x<<1|1]%mod;
	s[x]=s[x<<1]|s[x<<1|1];
}
void maketag(int x,int len,int d,int s0) {
	(t[x]*=fp(d,len))%=mod;
	(tag[x]*=d)%=mod;
	s[x]|=s0;
	stag[x]|=s0;
}
void pushdown(int x,int l,int r) {
	if(tag[x]>1||stag[x]) {
		int mid=(l+r)>>1;
		maketag(x<<1,mid-l+1,tag[x],stag[x]);
		maketag(x<<1|1,r-mid,tag[x],stag[x]);
		tag[x]=1, stag[x]=0;
	}
}
void build(int x=1,int l=1,int r=n) {
	tag[x]=t[x]=1;
	if(l==r)  {
		t[x]=a[l];
		for(int i=0;i<cnt;++i) if(a[l]%p[i]==0) s[x]|=1ll<<i;
		return;
	}
	int mid=(l+r)>>1;
	build(x<<1,l,mid);
	build(x<<1|1,mid+1,r);
	pushup(x);
}
void upd(int L,int R,int d,int s0,int x=1,int l=1,int r=n) {
	if(L<=l&&r<=R) { maketag(x,r-l+1,d,s0); return; }
	pushdown(x,l,r);
	int mid=(l+r)>>1;
	if(L<=mid) upd(L,R,d,s0,x<<1,l,mid);
	if(R>mid) upd(L,R,d,s0,x<<1|1,mid+1,r);
	pushup(x);
}
PII query(int L,int R,int x=1,int l=1,int r=n) {
	if(L<=l&&r<=R) {
		return MP(t[x],s[x]);
	}
	pushdown(x,l,r);
	int mid=(l+r)>>1;
	PII res; res.fi=1, res.se=0;
	if(L<=mid) {
		PII tmp=query(L,R,x<<1,l,mid);
		(res.fi*=tmp.fi)%=mod, res.se|=tmp.se;
	}
	if(R>mid) {
		PII tmp=query(L,R,x<<1|1,mid+1,r);
		(res.fi*=tmp.fi)%=mod, res.se|=tmp.se;
	}
	return res;
} 
signed main() {
	n=read(), q=read();
	rep(i,1,n) a[i]=read();
	init();
	build();
	while(q--) {
		scanf("%s",ss);
		if(ss[0]=='M') {
			int l=read(), r=read(), x=read();
			int s0=0;
			for(int i=0;i<cnt;++i) if(x%p[i]==0) s0|=1ll<<i;
			upd(l,r,x,s0);
		} else {
			int l=read(), r=read();
			PII a=query(l,r);
			int ans=a.fi, S=a.se;
			for(int i=0;i<cnt;++i) if((S>>i)&1) (ans*=rev[i])%=mod;
			printf("%lld\n",ans);
		}
	}
	return 0;
}

求区间中与给定数互质的数的个数

最后我们关注一个小问题。

给定 \(n,L,R\),求 \([L,R]\) 中与 \(n\) 互质的数的个数。

怎么做?

转化医学,求 \([L,R]\) 中有多少个数和 \(n\) 含有相同质因子,先差分成 \([1,L-1]\)\([1,R]\)

设当前区间为 \([1,R]\)。我们把 \(n\) 分解了,暴力搜索所有质因数的组合方式,设其为 \(m\),那么就有 \(\Big\lfloor \frac{R}{m} \Big\rfloor\) 个数含有这个质因子集合。根据熟悉的集合容斥,容易知道集合 \(S\) 的容斥系数就是 \((-1)^{|S|}\)

设质因子个数为 \(\omega(n)\),那么复杂度就是 \(O(\sqrt{n} + 2^{\omega(n)})\)

通过提前筛质数能做到 \(O(\log_2 n + 2^{\omega(n)})\),实际上第一项常数较小,第二项小于 \(O(n)\)

CF1750D Count GCD

有解一定要有 \(a_i \mid a_{i-1}\)

考虑 \(\gcd_{j=1}^{i-1} \{b_j\} = a_{i-1}\)\(\gcd_{j=1}^{i} \{b_j\} = a_{i}\),也就是说 \(b_i\) 不能有 \(\frac{a_{i-1}}{a_i}\) 的任何质因子。

由于 \(a_i \mid b_i\),所以 \(\frac{b_i}{a_i} \in [1,\lfloor \frac{m}{a_i} \rfloor]\)

求区间中与 \(\frac{a_{i-1}}{a_i}\) 互质的数的个数即可。

单次求解的复杂度已经到了 \(O(2^{\omega(m)})\) 了,而 \(\omega(m)\) 的上界大概是 \(10\)

注意到那个很重要的条件 \(a_i \mid a_{i-1}\),也就是这玩意是 \(\log\) 级别递减的,所以直接暴力做就行。

// LUOGU_RID: 121128333
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define uint unsigned long long
#define PII pair<int,int>
#define MP make_pair
#define fi first
#define se second
#define pb push_back
#define eb emplace_back
#define SET(a,b) memset(a,b,sizeof(a))
#define CPY(a,b) memcpy(a,b,sizeof(b))
#define rep(i,j,k) for(int i=(j);i<=(k);++i)
#define per(i,j,k) for(int i=(j);i>=(k);--i)
int read() {
	int a=0, f=1; char c=getchar();
	while(!isdigit(c)) {
		if(c=='-') f=-1;
		c=getchar();
	}
	while(isdigit(c)) a=a*10+c-'0', c=getchar();
	return a*f;
}
const int N=2e5+5, mod=998244353;
int T, n, m, a[N];
vector<int> p;
map<PII,int> mp;
void divide(int x) {
	p.clear();
	for(int i=2;i*i<=x;++i) if(x%i==0) {
		p.pb(i);
		while(x%i==0) x/=i;
	}
	if(x>1) p.pb(x);
}
int dfs(int i,int j,int k,int lim) {
	if(i==p.size()) {
		if(j>0) {
			if(j&1) return lim/k;
			else return -lim/k;
		} else return 0;
	}
	return dfs(i+1,j,k,lim)+dfs(i+1,j+1,k*p[i],lim);
}
int calc(int lim,int r) {
	PII t={lim,r};
	if(mp.count(t)) return mp[t];
	divide(r);
	return mp[t]=lim-dfs(0,0,1,lim);
}
void solve() {
	n=read(), m=read();
	int fg=0;
	rep(i,1,n) {
		a[i]=read();
		if(i>1&&a[i-1]%a[i]) fg=1;
	}
	if(fg) {
		puts("0");
		return;
	}
	int ans=1;
	rep(i,2,n) {
		(ans*=calc(m/a[i],a[i-1]/a[i]))%=mod;
	}
	printf("%lld\n",ans);
}
signed main() {
	T=read();
	while(T--) solve();	
	return 0;
}

「NOIP Record」#15 数论题目选讲
https://yozora0908.github.io/2023/noip-record-15/
作者
yozora0908
发布于
2023年8月24日
许可协议