【已整理】一道非常重要的基础数论题,很多题目的优化都与本题有关
lightoj1245

设bound=sqrt(n),则所有的n/i中,大于等于bound的数如果出现了,则只会出现一次。小于等于bound的数如果出现了,则会出现大于等于1次。注意等于bound在这里两边都可以算进去,所以对于本题,如果利用此规律来计算,会当bound==n/bound时,会重复计算,需要特判减去bound
直接上代码吧
#include <bits/stdc++.h> using namespace std; typedef long long int ll; int main(){ int t; ll n; scanf("%d",&t); for(int cas=1;cas<=t;cas++){ scanf("%lld",&n); ll res=0; ll bound=(ll)sqrt(n); for(ll i=1;i<=bound;i++){ res+=n/i; res+=i*(n/i-n/(i+1)); } if(n/bound==bound) res-=bound; printf("Case %d: %lld\n",cas,res); } return 0; }
这里有个很重要的结论
n/i==b,则b表示的是所有满足n/k==i的k中最大的
所以枚举i时,商为i的个数即为(n/i-n/(i+1)),再乘上i即可
而大于等于bound的数都只会出现一次,所以直接加上n/i
本题还有一个更好懂的写法,但是比较慢,不过也可以AC
#include <bits/stdc++.h> using namespace std; typedef long long int ll; int main(){ int t; ll n; scanf("%d",&t); for(int cas=1;cas<=t;cas++){ scanf("%lld",&n); ll curdiv,curend,res=0; for(ll i=1;i<=n;i=curend+1){ curdiv=n/i; curend=n/curdiv; res+=(curend-i+1)*curdiv; } printf("Case %d: %lld\n",cas,res); } return 0; }
curdiv表示当前的商,curend表示商为curdiv时,i的最大值
本题如果暴力算,就是O(n)的,当前的算法的时间复杂度取决于n/i的个数。n/i的个数我们可以近似的用n的因子数去估计,即最大不超过2*sqrt(n),所以这是一个根号级别的优化。

此后如果碰到枚举n/i的题,就可以考虑这个根号n的分块优化
系数可能是需要求个前缀和,也可能是直接提出来
容斥原理7.hdu6051 TrickGCD

题意非常简单,直接看上面的截图吧,基于最经典的gcd容斥问题修改得来。如果直接套用经典方法,很快可以得到一个n方的算法。
枚举gcd,从大到小递推进行隐式容斥。计算每个GCD i的时候,dp[i]=π(a[j]/i),根据题目的数据量,这样n方的算法过不了。
然后我就觉得肯定不是这么做的,其实是可以优化的。
除以i,想下取整,也就是说以i为循环节的自然数,想下取整除以i都是得到相同的结果,我们利用这一良好的性质进行优化。先记录一下每个数出现了多少次,然后求这个统计数组的前缀和。然后用n*调和级数的复杂度去计算dp[i]即可。
在学习了n/i的重要性质后,我再来理解一下本题
首先我能写出n方的算法
#include <bits/stdc++.h> using namespace std; typedef long long int ll; const int MAXN=100005; const ll mod=1e9+7; int a[MAXN]; ll cnt[MAXN]; ll powerOfTwo[MAXN]; ll dp[MAXN]; int main(){ int t,n; scanf("%d",&t); powerOfTwo[0]=1; for(ll i=1;i<MAXN;i++) powerOfTwo[i]=(powerOfTwo[i-1]*2)%mod; for(int cas=1;cas<=t;cas++){ memset(cnt,0,sizeof(cnt)); memset(dp,0,sizeof(dp)); scanf("%d",&n); int maxnum=0; for(int i=0;i<n;i++){ scanf("%d",a+i); maxnum=max(maxnum,a[i]); } ll res=0; for(int i=maxnum;i>=2;i--){ dp[i]=1; for(int j=0;j<n;j++) dp[i]=(dp[i]*(a[j]/i))%mod; for(int j=i+i;j<=maxnum;j+=i) dp[i]=(dp[i]-dp[j]+mod)%mod; res=(res+dp[i])%mod; } printf("Case #%d: %lld\n",cas,res); } return 0; }
for(int j=0;j<n;j++) dp[i]=(dp[i]*(a[j]/i))%mod;
这一层循环怎么优化呢?出现了/i运算。但是分子是a[j]。怎么办呢?我们把a数组放到一个桶里,记录每个数出现的次数,那么有一段连续区间的数/i的取值是相同的,我们处理出前缀和,就可以计算出有多少个这样的数,然后用快速幂算下贡献乘上去就行了。
这个和上面所说的/i优化有些不同,上面的/i,i是从1连续增加的,但是这里i不变,分子变化,也有相同的结论
今天CF的D题又要用到这个优化,愣是没想起来。。。事不过三啊
http://codeforces.com/contest/851/problem/D
题意:n个数,每次删除一个数的代价是y,每次给一个数加1的代价是x(这个我好像说反了,主要是下面的推导是这么设的变量,读入的时候两个反着读即可),问你最少需要多少代价,使得所有数的最大公因数大于1
枚举因数,对于每一个因数,可以得到下列公式
,a取遍这n个数
这里出现了a/d这样的东西,这就是本文所讲的优化所能应用的情况。
枚举因数,对于一个因数d,k*d+1~(k+1)*d这个区间所有的数除以d的结果都是一样的,利用这一特征进行优化
考察这个式子,x都是一样的,我们可以不管,枚举了因数d后,每种情况的d也是一样的,也可以不管
要实现上述优化,我们枚举d的倍数k(从0直至上限),则k*d+1~(k+1)*d之间所有数的
的值都是(k+1)*d,那么我们用一个桶记录每个数出现了多少次,然后求前缀和,就可以O(1)的求出k*d+1~(k+1)*d这个区间内的数的贡献是(k+1)*d*N,N是这个区间内的数的个数。
而-a这一部分,意思就是k*d+1~(k+1)*d这个区间内的数的和,这个我们也可以通过维护前缀和求出来。
现在还有一个棘手的问题,因为还要考虑y的影响,如果第二项太大,我们是不要第二线而是要y的。但是很好,第二项在k*d+(k+1)*d这个区间内有良好的单调性。
设这个区间内的一个数为k*d+g,1<=g<=d。那么第二项就是((k+1)*d-(k*d+g))*x=(d-g)*x
当(d-g)*x<=y时,我们才可以用上述优化,可以算出
,对于每个因数,我们都可以算出g,按照g将每个k定下的区间分成两部分,前一部分取第一项,后一部分取第二项。
上代码
#include <cstdio> #include <cstring> #include <algorithm> #include <cmath> #include <iostream> #include <cstdlib> const int MAXN=2000005; using namespace std; typedef long long int ll; const ll INF=1e18+9LL; int a[MAXN]; ll cnt[MAXN]; int main(){ /* freopen("in.txt","w",stdout); cout<<"500000 1000000000 1000000000"<<endl; for(int i=0;i<500000;i++) cout<<"1 "; cout<<endl; */ //freopen("in.txt","r",stdin); int n,y,x; int p; scanf("%d%d%d",&n,&y,&x); memset(a,0,sizeof(a)); memset(cnt,0,sizeof(cnt)); for(int i=0;i<n;i++){ scanf("%d",&p); a[p]++; cnt[p]+=p; } for(int i=1;i<MAXN;i++){ a[i]+=a[i-1]; cnt[i]+=cnt[i-1]; } int rat=y/x; ll res=INF; for(int d=2;d<=1000000;d++){//枚举因数 ll tt=0; for(int j=0;j<=1000000;j+=d){ int g=max(d-rat,1); tt+=1LL*(a[j+d]-a[j+g-1])*(j+d)*x; tt-=1LL*(cnt[j+d]-cnt[j+g-1])*x; tt+=1LL*(a[j+g-1]-a[j])*y; } res=min(res,tt); } printf("%I64d\n",res); }

浙公网安备 33010602011771号