2023第14届蓝桥杯大赛软件赛省赛C/C++大学A组第10题题解:翻转硬币

目录

问题描述:

方法一:直接模拟(40%)

方法二:整除分块、狄利克雷卷积、莫比乌斯反演、线性筛、杜教筛、容斥原理、前缀和


问题描述:

        给定 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)的前缀和递推求法,s(n)=1-\sum_{i=2}^{n} s(floor(n/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;
}

        正赛遇到这样的题目,还是直接快速写出第一种方法稳稳拿分比较好,满分做法如果之前不了解相关知识真的很难做。

  • 31
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值