[XYD20241118] NOIP 模拟赛
[XYD20241118] NOIP 模拟赛
Super Math Round
因子之和
Description
定义 \(\sigma(x)\) 表示 \(x\) 的所有因数之和。
即 \(\sigma(x)=\sum_{d\mid x} d\) 。
现在有 \(Q\) 个询问,每次查询区间 \([l,r]\) 内有多少个整数,满足 \(\sigma(x)\ge 2x\) 。
对于 \(30\%\) 的数据,\(1\le n\le 100,1\le l,r\le 1000\) 。
对于 \(100\%\) 的数据,\(1\le n\le 2*10^5,1\le l,r\le 10^6\) 。
Solution
比较好想,直接线筛出 \(\sigma(x)\) ,然后前缀和做差。
官方题解提供了一种比较慢的做法,它是枚举每一个 \(d\) ,然后让 \(d\) 去给它的倍数的 \(\sigma()\) 做贡献,复杂度是 \(O(N \log N)\) 的。
过于简单,不放代码。
小 w 与数字游戏
Description
给定一个包含 \(n\) 个非负整数的序列 \(s\) 。
每次可以选择两个数 \(s_i,s_j\ (i\neq j)\) ,在序列中删去它们,并且将下列三个数之一加入序列:
- \(s_i+s_j\)
- \(|s_i-s_j|\)
- \(s_i*s_j\)
问,进行 \(n-1\) 次操作之后,序列中剩下来的一个数最小是多少。
多组测试数据
对于 \(30\%\) 的数据,\(n\le 7\) 。
对于 \(50\%\) 的数据,\(n\le 10\) 。
对于 \(100\%\) 的数据,\(T\le 5,n\le 3\times 10^5,0\le s_i\le 30\) 。
Solution
\(30\%\) 做法
直接暴力,维护一个序列,每次枚举把哪两个数删掉以及换成什么
Final Solution
首先最后答案肯定是非负整数,然后大概想一想就会发现似乎很多情况下答案都可以做到 \(0\) 。
具体地说,如果初始序列中包含 \(0\) 或者有某个数出现了两次及以上,那么就可以构造出一个方案使得最后答案为 \(0\) 。前者是显然的,对于后者,只需要将两个相同的数相减就可以得到一个 \(0\) 。
然后发现 \(s_i\) 的范围很小,根据抽屉原理,在 \(n>30\) 的情况下,要么序列里面有 \(0\) ,要么就会有至少一个数出现了两次及以上,那么这个时候答案就是 \(0\) 。
于是现在我们只需要解决 \(n\le 30\) 的问题。
但是似乎并没有一个 \(n\le 30\) 的能过做法,所以我们需要继续缩小 \(n\) 的范围。
然后就尝试构造一个尽可能长的序列,使得里面的任何一个元素都不能够通过别的元素之间做加减乘(减法是取绝对值的减法)操作得到。我这边构造出来最长的长度是 \(5\) 。也就是说只有 \(n\le 5\) 的情况,才可能出现答案不为 \(0\) 的情况。(这么说其实不是很严谨,但是确实如此)
但是官方题解说什么根据斐波那契数列,对于(本题背景下)所有长度大于 \(7\) 的序列 \(s\) ,里面一定可以找到三个数 \(a,b,c\) ,使得 \(a+b-c=0\) 。于是可以得到 \(n\le 7\) 的情况下才可能出现答案不为 \(0\) 的情况。但反正都差不多。
于是原问题就被归约到了 \(n\le 7\) (或者 \(n\le 5\) )的问题,所以我们可以直接使用前面 \(30\%\) 的做法。
树论
Description
给定一张 \(n\) 个节点的完全图,点的编号 \(1\sim n\) ,点 \(i\) 和点 \(j\) 之间有一条权值为 \(\frac{f(i,j)+f(j,i)}{2}\) 的无向边。
其中:
求这张图的最小生成树。
答案四舍五入至整数。
测试点编号 | $n\le $ |
---|---|
\(1\sim 2\) | 100 |
\(3\sim 5\) | 300 |
\(6\sim 7\) | 5000 |
\(8\sim 9\) | 10000 |
\(10\) | 3000000 |
对于所有数据,满足 \(2\le n \le 3\times 10^6\) 。
如果只答对了最小生成树的边权和,也可以获得该测试点 \(40\%\) 的分数。
Solution
\(50\%\) 做法
Kruskal
\(O(N^2 \log N)\)
据说有着巨大常数,无法通过第三档分,反正我赛时只过了前两档。
\(70\%\) 做法
Prim
\(O(N^2)\)
Final Solution
推式子时间到!
但是先通过打表猜测 \(\DeclareMathOperator{\lcm}{lcm}f(i,j)=\frac{ij}{\gcd(i,j)^2}=\frac{\lcm(i,j)}{\gcd(i,j)}\)
然后考虑归纳,\(i\bmod j=0\) 时显然,
然后是两个引理
对于两个点 \(i,j\ \ (i<j)\) ,若 \(i\nmid j\) 或者 \(\frac{j}{i}\) 不是质数,那么边 \((i,j)\) 一定不会出现在最小生成树中。
\(\forall 1<i\le n\) ,边 \((i,\frac{i}{mn_i})\) 在最小生成树中,其中 \(mn_i\) 为 \(i\) 的最小质因子。
显然这些边不会构成环,所以这些就是 MST 全部的边。
(这东西我不会证,所以搬了题解证明。好了,我们先不管它)
然后线筛求出 \(mn_i\) 就做完了。
至于这题能否在推不出引理的情况下,通过 Boruvka 做出,还不知道。
Proof
引理 1
考虑模拟 Kruskal 的过程,从小到大枚举 \(x\) ,加入权值为 \(x\) 的边。
对于 \(i,j\ \ (i<j)\) ,若 \(i\nmid j\) 或 \(\frac{j}{i}\) 不为质数,则一定可以找到一个长为 \(k\) 的序列 \(c\) 满足:
- \(c_1=i,c_k=j\) 。
- \(\forall 1\le w<k,c_w\mid c_{w+1}\ \text{或}\ c_{w+1}\mid c_w\) 。
- \(\forall 1\le w<k, \frac{\lcm(c_w,c_{w+1})}{\gcd(c_w,c_w+1)}<\frac{\lcm(i,j)}{\gcd(i,j)}\) 。
如果存在这样一个序列,则意味着 \(i,j\) 在边 \((i,j)\) 加入之前已经连通了。
考虑构造这样的序列 \(c\) 。先将 \(i,j\) 分解质因子。初始令 \(x=i\) ,然后将 \(i\) 多出的质因子除掉,并加入 \(j\) 多出的质因子,使得最后 \(x=j\) ,将这个过程中的 \(x\) 排列出来得到序列 \(c\) 。
引理 2
继续考虑模拟 Kruskal 。从小到大枚举质数 \(p\) ,将 \(x\) 与 \(xp\) 相连。
显然,刚枚举到质数 \(p=mn_i\) 时,\(i\) 与 \(\frac{i}{mn_i}\) 一定不连通,故将所有满足 \(mn_i=p\) 的 \((i,\frac{i}{p})\) 全部连起来。
基础数论练习题
Description
定义 \(d(x)\) 表示 \(x\) 的约数个数,即 \(d(x)=\sum_{i\mid x}1\) 。
给定 \(n,m\) ,考虑所有长度为 \(n\) ,值域为 \([1,m]\) 的由正整数构成的序列 \(a\) ,定义其权值为 \(d(\prod_{i=1}^n a_i)\) ,求所有的 \(m^n\) 个序列的权值之和对 \(p\) 取模的结果。
有多次问询,每次询问给出一个 \(n\) ,求上述问题的答案。但是对于所有问询,其 \(m\) 是相同的。
测试点编号 | \(T\le\) | \(n\le\) | $m\le $ |
---|---|---|---|
\(1,2\) | \(1\) | \(7\) | \(10\) |
\(3\sim 6\) | \(10\) | \(10^{18}\) | \(16\) |
\(7 \sim 10\) | \(10^5\) | \(10^{18}\) | \(60\) |
\(11\sim 14\) | \(10^5\) | \(10^{18}\) | \(100\) |
\(15\sim 20\) | \(10^5\) | \(10^{18}\) | \(10^3\) |
对于 \(100\%\) 的数据,\(1\le T\le 10^5,1\le n \le 10^{18},1\le m\le 1000,10^8 \le p\le 10^9+9\) 。不保证 \(p\) 为质数。
Solution
\(10\%\) 做法
暴搜
\(30\%\) 做法
首先对于约数个数函数,有这样一个观察: \(d(xy)=\sum_{i\mid x}\sum_{j\mid y}[\gcd(i,j)=1]\)
然后这个性质可以推广到多个数的情况:
然后 \(m\) 非常的小,所以我们可以考虑枚举序列 \(b\) 然后计算每一个合法的序列 \(b\) 对应到多少序列 \(a\) ,因为序列 \(b\) 上面有更强的约束,所以这个比起枚举序列 \(a\) 应该是好做的。
考虑枚举 \(S\subseteq \{2,3,\dots,m\}\) ,表示 \(S\) 中的数在序列 \(b\) 中出现过。由于序列 \(b\) 自身的约束,所以序列 \(b\) 中所有大于 \(1\) 的数只能够出现一次,其他的位置就只能都填 \(1\) 。对于序列 \(b\) 中一个不为 \(1\) 的数 \(x\) ,这个位置对应的序列 \(a\) 中的数可以是任何 \(x\) 的倍数,所以提供一个 \(\lfloor \frac{m}{x} \rfloor\) 的贡献;而对于 \(1\) 则是给出 \(m\) 的贡献。再加上定序的贡献,总贡献为:
然后 \(|S|\) 的上界是 \(\pi(m)\) (其中 \(pi(m)\) 表示 \([1,m]\) 以内的质数个数)
所以预处理一些东西之后,复杂度可以做到 \(O(2^m+T(\pi(m)+\log n))\)
Final Solution
考虑优化 \(30\%\) 做法的计算 \(S\) 的贡献那一部分
设 \(T(x)\) 表示 \(x\) 的质因子集合,按照顺序加入 \([2,m]\)
考虑 dp :
\(f_{i,S}\) 表示现在序列里面有 \(i\) 个不为 \(1\) 的数,并且质因子出现的集合为 \(S\) 的总贡献
当 \(T(x)\&S=0\) 时,有转移 \(f_{i+1,S|T(x)}\gets f_{i,S}\times \lfloor \frac{m}{x}\rfloor\)
直接写的复杂度是 \(O(m\pi(m)2^{\pi(m)})\)
大概可以拿到 50 分,稍微优化一下可以拿到 70 分
然后注意到复杂度瓶颈在 \(S\) 的状态数上面。而且你注意到对于一个数,它的质因子里面最多只会出现一个数 \(>\sqrt m\) ,所以其实我们没有必要把 \(>\sqrt m\) 的质因子纳入到状态里面,我们只要对于每个 \(x\) ,算出它的最大的质因子 \(mx_x\) ,然后把所有的 \(x\) 按照 \(mx_x\) 升序排列并依次加入,就可以不需要额外记录 \(>\sqrt m\) 的质因子了。
复杂度降到 \(O(m\pi(m)2^{\pi(\sqrt m)})\)
Code
Code for \(30\%\) Solution
namespace subtask1{
ll ans=0;
bool vst[10];
ll fact[20];
ll pb[20];
void precompute(){
fact[0]=1;
for(int i=1;i<=6;i++)fact[i]=fact[i-1]*((n-i+1)%mod)%mod;
pb[6]=qpow(m,n-6);
for(int i=5;i>=0;i--)pb[i]=pb[i+1]*m%mod;
}
void solve(){
int nPtn=(1<<(m-1));
ans=0;
for(int ptn=0;ptn<nPtn;ptn++){
ll mul=1;bool tg=1;int cnt=0;
for(int i=1;i<=6;i++)vst[i]=0;
for(int i=2;i<=m;i++)if((ptn>>(i-2))&1){
mul*=m/i;cnt++;
mul%=mod;
if(i%2==0){
if(vst[1]){tg=0;break;}
vst[1]=1;
}
if(i%3==0){
if(vst[2]){tg=0;break;}
vst[2]=1;
}
if(i%5==0){
if(vst[3]){tg=0;break;}
vst[3]=1;
}
if(i%7==0){
if(vst[4]){tg=0;break;}
vst[4]=1;
}
if(i%11==0){
if(vst[5]){tg=0;break;}
vst[5]=1;
}
if(i%13==0){
if(vst[6]){tg=0;break;}
vst[6]=1;
}
}
if(cnt>n)continue;
if(tg){
// cout<<"ptn: "<<ptn<<" mul: "<<mul<<"\n";
for(int i=0;i<cnt;i++)mul=mul*((n-i)%mod)%mod;
mul=mul*qpow(m,n-cnt)%mod;
ans=(ans+mul)%mod;
}
}
cout<<ans<<"\n";
}
}
Code for Final Solution
namespace subtaskf{
const int smallPr[11]={2,3,5,7,11,13,17,19,23,29,31};
int mxp[1005];
const int R=1e3;
vector<int> pr;int nP;
bool isnPrime[1005];
int ord[1005];
ll t[1005];
vector<int> toAdd[180];
int nTier=0;
ll f[180][(1<<12)+5];ll sum[180];
bool cmp(int x,int y){return mxp[x]<mxp[y];}
ll fact[1005];
void precompute(){
for(int i=2;i<=R;i++){
if(!isnPrime[i]){
pr.emplace_back(i);
nP++;
}
for(int j=0;j<nP&&1ll*pr[j]*i<=R;j++){
isnPrime[i*pr[j]]=1;
if(i%pr[j]==0)break;
}
}
for(int i=2;i<=R;i++){
for(int j=nP-1;j>=0;j--)if(i%pr[j]==0){
mxp[i]=pr[j];
break;
}
for(int j=0;j<11;j++)if(i%smallPr[j]==0)t[i]|=(1<<j);
}
for(int i=2;i<=m;i++)ord[i]=i;
sort(ord+2,ord+1+m,cmp);
int lst=-1;
for(int i=2;i<=m;i++){
int x=ord[i];
if(mxp[x]!=lst){
++nTier;
toAdd[nTier].emplace_back(x);
lst=mxp[x];
}else toAdd[nTier].emplace_back(x);
}
// cout<<"check t: "<<t[37]<<" "<<t[41]<<"\n";
// cout<<"nTier: "<<nTier<<"\n";
// for(int i=1;i<=nTier;i++,cout<<"\n"){
// cout<<"mxp: "<<mxp[toAdd[i][0]]<<"\n";
// for(auto x:toAdd[i])cout<<x<<" ";
// }
// for(int i=2;i<=m;i++)cout<<t[i]<<" ";cout<<"\n";
int nPtn=(1<<11);
for(int i=1;i<=nTier;i++)
for(int ptn=0;ptn<nPtn;ptn++)f[i][ptn]=0;
f[0][0]=1;
for(int i=1;i<=nTier;i++)
for(int j=i-1;j>=0;j--)
for(int ptn=0;ptn<nPtn;ptn++){
// f[j+1][ptn]+=f[j][ptn];
for(auto x:toAdd[i]){
if((ptn&t[x])==0){
f[j+1][ptn|t[x]]+=(m/x)*f[j][ptn]%mod;
f[j+1][ptn|t[x]]%=mod;
}
}
}
for(int i=0;i<=nTier;i++)
for(int ptn=0;ptn<nPtn;ptn++)
sum[i]=(sum[i]+f[i][ptn])%mod;
// for(int i=0;i<=nTier;i++,cout<<"\n")
// for(int ptn=0;ptn<nPtn;ptn++)cout<<f[i][ptn]<<" ";
}
ll pm[1005];
void solve(){
fact[0]=1;
for(int i=1;i<=min(n,1000ll);i++)fact[i]=(ll)((n%mod-i+1)%mod+mod)%mod*fact[i-1]%mod;
// int nPtn=(1<<11);
ll lim=min(n,nTier*1ll);
pm[lim]=qpow(m,n-lim);
for(ll i=lim-1;i>=0;i--)pm[i]=pm[i+1]*m%mod;
ll ans=0;
/*
for(int i=0;i<=min(n,1ll*nTier);i++)
for(int ptn=0;ptn<nPtn;ptn++)if(f[i][ptn]){
ll mul=f[i][ptn];
// if(mul)cout<<"ptn: "<<ptn<<" ISI: "<<i<<" mul: "<<mul<<"\n";
mul=mul*fact[i]%mod;
mul=mul*qpow(m,n-i)%mod;
ans=(ans+mul)%mod;
// cout<<"add: "<<mul<<"\n";
}*/
for(int i=0;i<=min(n,1ll*nTier);i++)if(sum[i]){
ll mul=sum[i];
mul=mul*fact[i]%mod;
mul=mul*pm[i]%mod;
ans=(ans+mul)%mod;
}
cout<<ans<<"\n";
// for(int i=0;i<=nTier;i++,cout<<"\n")
// for(int ptn=0;ptn<(1<<4);ptn++)cout<<f[i][ptn]<<" ";
}
}