目录
方法二:整除分块、狄利克雷卷积、莫比乌斯反演、线性筛、杜教筛、容斥原理、前缀和
问题描述:
给定 n 个按顺序摆好的硬币,一开始只有第 1 个硬币朝下,其他硬币均朝上。你每次操作可以选择任何一个整数 i 并将所有满足 j mod i = 0 的位置 j 的硬币翻转。求最少需要多少次操作可以让所有硬币都朝上。
输入格式:
输入一行包含一个整数 n 。
输出格式:
输出一行包含一个整数表示最少需要的操作次数。
评测用例规模与约定:
对于 30% 的评测用例,n ≤ 5 × 10^6 ;
对于 70% 的评测用例,n ≤ 10^9 ;
对于所有评测用例,1 ≤ n ≤ 10^18 。
方法一:直接模拟(40%)
直接按照题目描述做。从1到n判断每个硬币,如果朝上则跳过,朝下则翻转,并将它的所有倍数号硬币都翻转。从1到n判断的话,如果硬币i朝下,要使它最终朝上则必须翻转,因为后面的数字j比它大,小的数字i不可能整除比它大的数字,此时不翻的话后面也不能再翻i了。这样能保证次数最少。
#include <iostream>
using namespace std;
int a[50000000];//0朝上 1朝下
int main()
{
int n,ans=0;
cin>>n;
a[1]=1;//第1个硬币朝下
for(int i=1;i<=n;i++)
{
if(a[i]==0) continue;//硬币i朝上 跳过
ans++;//硬币i朝下 翻转1次
for(int j=i;j<=n;j+=i)
a[j]=a[j]^1;//硬币i的所有倍数j都翻转
}
cout<<ans;
return 0;
}
想出这种做法和写出代码都是很简单的,能拿到将近一半的分数,正赛遇到这种情况我肯定就交这份代码了。
方法二:整除分块、狄利克雷卷积、莫比乌斯反演、线性筛、杜教筛、容斥原理、前缀和
看到官方答案的那一刻,各种高深的数学名词,非常简短的描述,我是一头雾水的完全看不懂在说什么。在网上学习了很久各个数论概念之后,勉强理解了这道题的满分做法,下面尽量不提数学名词稍微解释一下本题答案。(本人萌新,如有错误感谢大佬指正)。
首先,定义一个函数f(i),表示第i枚硬币是否翻转,1表示翻,0表示不翻。根据题意,f(i)等于i的所有不等于i的因数j的f(j)的和除以2的余数,也就是i前面的因数如果翻了奇数次则i此时朝下需要再翻一次。
再定义一个函数g(i),表示i的所有因数j的f(j)的和。乍一看g(i)好像就等于f(i),其实与f(i)相等的是i的所有不等于i的因数j的f(j)的和,再加上f(i)才等于g(i)。显然g(i)只有当i为1的时候等于1,其余情况都为0。因此g(i)可以表示所有硬币的最终情况,只有第一枚硬币原来朝下被翻,其余硬币原来朝上都没有被翻(翻了偶数次)。
而在数论中,恰好有一个函数就是g(i),那就是单位函数,当n为1时等于1否则为0。刚才定义g(i)表示所有i的因数j的f(j)的和,可以计算出f(i)就是莫比乌斯函数u(i)(通过莫比乌斯反演计算)。当n=1时,u(i)=1;当n有至少一个平方因数时,u(i)=0;其余情况,也就是n的所有质因数都只出现了1次,u(i)=(-1)^k,k为质因数的个数。
由于定义f(i)只有1和0两个值,相当于f(i)等于u(i)对2取余。
最终要求的总翻转次数,也就是u(i)=1的个数等于u(i)从1到n的和。那么怎么求u(i)以及u(i)的和呢?要求u(i),可以使用线性筛法,从2遍历到n,如果i未被标记过,说明i是质数,将i存入质数数组,质数的u(i)=1。将i与之前存储的所有质数相乘,得到的数字标记为合数。如果i是某一质数的倍数,则此乘积包含平方因子,u(i*prime[j])=0。否则u(i*prime[j])=-u(i),因为多了一个质因数导致质因数的个数奇偶性改变。
这样求得总数的方法还是超时的,并且当数据范围达到10^18时也不能开如此规模的数组存储u(i)的值。为了更进一步,通过杜教筛的方法可以推导出一个公式,表达u(i)的前缀和递推求法,,floor(n/i)表示整除的商。
这样求前缀和的方法最后还是超时的。为了更进一步,对u(i)进行分析,当i包含平方因子时u(i)=0。那么可以从2枚举到根号n,计算出包含i^2因子的数字的个数也就是0的个数,等于n/(i^2)。可如果直接全加起来的话有的数字被加了多次,比如36的因数包含2的平方、3的平方、6的平方,应当去掉重复计算的部分。这里很像组合中的容斥问题,就是有10个人喜欢数学6个人喜欢英语,2个人既喜欢数学又喜欢英语,问一共有几个人。
容斥定理的广义定义为设 U 中元素有 n 种不同的属性,而第 i 种属性称为 pi,拥有属性pi的元素构成集合si,那么所有集合交集的基数,等于所有奇数个属性构成的集合的交集的基数总和,减去所有偶数个属性构成的集合交集的基数总和。带入到本题中,属性pi就是包含质因数i,包含偶数个质因数就是偶数个属性构成的交集,包含奇数个质因数就是奇数个属性构成的交集。那么在计算0的个数的时候,如果i的质因数有奇数个就加上n/(i^2),否则减去n/(i^2),质因数的个数可以通过u(i)的正负判断。最后计算出0的总数,再用n减去,就得到了1的总数即翻转次数。也就是ans=n-Σ质因数个数为奇数的i*n/(i^2)+Σ质因数个数为偶数的i*n/(i^2)。正好,第一项n等于u(1)*n/(1^2),后两项等于Σu(i)*n/(i^2),答案就可以统一成一个式子ans=Σu(i)*n/(i^2)。
计算这个式子还可以进一步优化,因为n/(i^2)这个整除值可能会重复计算很多遍,采取整除分块的方法可以进一步优化。就是遍历到l时,从l到r的n/(i^2)的值都是一样的,这个范围的右界r=sqrt(n/(n/(i^2))),那么就只需要计算u(i)从l到r的前缀和差值乘以这个相同的n/(i^2),并将l指向r+1即可。
上面杜教筛求递推公式求前缀和的部分也要用到整除分块优化。
#include<bits/stdc++.h>
using namespace std;
const int N=1e7;
long long ans=0,n;
bool vis[N];
vector<int> prime;//存储质数的容器
int f_mu[N],sum_mu[N];//莫比乌斯函数的值,莫比乌斯函数的前缀和
unordered_map<int,int> sum_mu_long;//莫比乌斯函数的前缀和,当n超过数组的存储范围时用map存储
void findprime()//寻找质数,主要是计算莫比乌斯函数的值和前缀和
{
f_mu[1]=1;
sum_mu[1]=1;//设定u(1)的初始值和前缀和
for(int i=2;i<=n&&i<N;i++)
{
if(vis[i]==0)//未被筛过
{
prime.push_back(i);//存入质数
f_mu[i]=-1;//质数的莫比乌斯函数为1
}
for(int j=0;j<prime.size()&&i*prime[j]<=n&&i*prime[j]<N;j++)//遍历质数
{
vis[i*prime[j]]=1;//标记为合数
if(i%prime[j]==0)
{
f_mu[i*prime[j]]=0;//包含平方因子
break;
}
f_mu[i*prime[j]]=-f_mu[i];//不包含平方因子,质因子个数奇偶性改变
}
sum_mu[i]=f_mu[i]+sum_mu[i-1];//前缀和递推
}
}
int SumMu(int x)//求莫比乌斯函数的前缀和
{
if(x<N) return sum_mu[x];
if(sum_mu_long.count(x)) return sum_mu_long[x];
int sum=1;
for(long long l=2,r;l<=x;l=r+1)
{
r=x/(x/l);//整除分块
sum-=SumMu(x/l)*(r-l+1);//杜教筛的递推公式
}
return sum_mu_long[x]=sum;
}
int main()
{
cin>>n;
findprime();
for(long long l=1,r;l*l<=n;l=r+1)
{
r=sqrt(n/(n/(l*l)));//整除分块
ans+=(SumMu(r)-SumMu(l-1))*(n/(l*l));
}
cout<<ans;
return 0;
}
正赛遇到这样的题目,还是直接快速写出第一种方法稳稳拿分比较好,满分做法如果之前不了解相关知识真的很难做。