如果我们将每个数a分解质因式:a=∏∞i=1pkii,那么任意一个数a都可以看作一个无穷维的向量(k1,k2,...),其中第i维的系数表示从小到大第i个质数在a中的指数。这样的话ab就可以看作是向量的数乘,所以如果有ab11=ab22,就必然有a1,a2共线。
所以我们可以统一处理所有共线的向量,这部分向量会有一个基向量a,即每一个向量都可以写成它的若干倍,即gcd(k1,k2,...,)=1。注意到0向量会是一个特例,不过还好,这道题的数据范围(a≥2)保证了0向量不在待求矩阵中;不过b≥2这个约束其实是没有意义,≥1也并无妨。。
所以就相当于是有一个乘法矩阵,第i行第j列是i*j,求[l1,r1]∗[l2,r2]这个矩形中的不重复元素数量。[l1,r1]是a的指数范围,所以是O(logan)的。
V1
2≤n,m,a,b≤100
既然数据范围这么小,根据上面的分析,就可以直接把矩形中所有元素拎出来,排序/set去重O(nmlog2n),或者直接用一个布尔数组判重O(nm)。
或者也可以不分析,直接按照题意取log/分解质因子/hash判重。
V2
2≤n,m,a,b≤5∗105
注意到随着a的增大,l1,r1不升。
所以我们可以从小到大处理每个a,就相当于是需要支持插入、删除一个范围在O(nlogn)的元素,求当前有多少个不同的数。只需要记录下每个元素的出现次数,当一个元素的数量由0->1,就把当前的答案+1;当它由1->0,就把当前的答案-1。然后当l1,r1改变,只需要扫一下变化的这一行就可以了,时间复杂度O(nlogn)。
或者我们可以这样,每当l1,r1改变时,都用一个bool数组暴力统计一下答案,这样时间复杂度看起来似乎是O(nlog2n)的;但是其实l1,r1会很快地衰减,所以也不会花费很大的时间。
当然我们也可以容斥(为什么我一看见不重复就想容斥。。。),假如说在[l1,r1]中有序地选了a1,a2,...,akk个数,那么它们会贡献(−1)k−1(⌈l2lcm(a1,a2,...,ak)ak⌉,⌊r2lcm(a1,a2,...,ak)a1⌋),所以直接按这个式子dfs,时间复杂度O(∑ni=22login)≈3∗106。
V3
2≤n,m≤5∗1015,2≤a,b≤1015
注意到实际上时间复杂度远远不会及此,因为我们可以对它进行一些行之有效的剪枝。
当底数大于等于max(n+a−1−−−−−−−−√+1,l)时,必然会贡献m,而这部分是可以通过从中减去底数小于max(n+a−1−−−−−−−−√+1,l)而计算出来的,所以我们只需要枚举到O(n√)级别。
一个很显然的剪枝是如果⌈l2lcm(a1,a2,...,ak)ak⌉>⌊r2lcm(a1,a2,...,ak)a1⌋,那么答案就必然会是0.
另一个关键的剪枝是如果在还没有dfs到的部分存在一个数是当前的lcm的因子,那么会意味着它不会对lcm造成影响,选它和不选它会对应着互为相反数的答案,所以此时答案必然为0,便可以剪枝。
注意到答案的计算只与最小的数、最大的数和lcm有关,所以我们可以先枚举最小的数和最大的数,然后记下搜索的结果,这样在再次枚举的时候就不需要重复搜索了。
如果当前的l1,r1没变的话,我们也没必要再次搜索,直接用上次的答案即可。
如果当前的l1,r1已经等于n+a−1−−−−−−−−√的l1,r1的话,再进行一些冗余的判断就没有用处了,反而会带来很大的常数,可以直接迅速地处理完这一部分(O(n13∼n12))。(这个剪枝让我快了1s)
在xjb剪了很久之后,终于a掉了!
ac后看到别人的代码,惊讶的发现大家似乎都写的爆搜?那他们怎么跑的那么快!
然后就又学习了一些别人代码的姿势:
算lcm的时候因为是一个很大的数与一个很小的数求gcd,所以其实是可以预处理gcd以做到O(1)的。(加上这个剪枝让我又快了1s)
容斥可以不记符号,而是把减法写在中间的计算里。
中间没有改变的状态是没有必要dfs下去的,这样只会使搜索树更大,让一个合法的状态消耗很多剪枝的判断。
代码:
#include<stdio.h>
#include<iostream>
using namespace std;
#include<algorithm>
#include<cmath>
#include<ctime>
#include<cstring>
#include<bitset>
typedef long long LL;
const int N=8e7+5;
bool vst[N];
int l1,r1;
LL l2,r2;
const int Mod=1e9+7;
const int Log=50+5;
int gcd[Log][Log];
int dfs(int x,LL lcm,LL l,LL r,int first,int end)
{
if(l>r)return 0;
for(int i=x;i<end;++i)
if(lcm%i==0)
return 0;
int ans=(r-l+1)%Mod;
LL nextlcm;
for(int i=x;i<end;++i)
{
nextlcm=i/gcd[i][lcm%i]*lcm;
(ans-=dfs(i+1,nextlcm,(l2+nextlcm/end-1)/(nextlcm/end),r2/(nextlcm/first),first,end))%=Mod;
}
return ans;
}
int f[Log][Log];
inline int query(int l,int r)
{
if(!~f[l][r])
if(l==r)f[l][r]=(r2-l2+1)%Mod;
else
{
printf("f[%d,%d]=",l,r);
int lcm=l*r/gcd[r][l];
f[l][r]=(Mod-dfs(l+1,lcm,(l2+lcm/r-1)/(lcm/r),r2/(lcm/l),l,r))%Mod;
printf("=%d\n",f[l][r]);
}
return f[l][r];
}
int main()
{
freopen("51nod1026.in","r",stdin);
LL l,r;
cin>>r2>>r>>l>>l2;
r+=l-1,r2+=l2-1;
//printf("2:[%lld,%lld]\n",l2,r2);
int sqroot=sqrt(r);
LL cnt1=r-max((LL)sqroot,l-1);
int now=0,ans=0;
memset(f,-1,sizeof(f));
for(int i=Log;i--;)gcd[i][0]=i;
for(int i=1;i<Log;++i)
for(int j=i;j;--j)
gcd[i][j]=gcd[j][i%j];
for(int i=2,prel,prer;i<=sqroot;++i)
if(!vst[i])
{
prel=l1,prer=r1;
l1=0;
{
LL j=i;
int k=1;
for(;j<=r/i;j*=i,++k)
{
if(j<=sqroot)vst[j]=1;
else cnt1-=j>=l;
if(j>=l&&!l1)l1=k;
}
if(j>=l)
{
--cnt1;
if(!l1)l1=k;
}
r1=k;
}
if(l1)
{
if(l1!=prel||r1!=prer)
{
//printf("----%d:[%d,%d]----\n",i,l1,r1);
now=0;
for(int k=l1;k<=r1;++k)
for(int j=k;j<=r1;++j)
(now+=query(k,j))%=Mod;
//printf("%lld\n",now);
//printf("%.5f\n",(double)clock()/CLOCKS_PER_SEC);
if(sqroot>=l&&l1==1&&r1==2)
{
(ans+=now)%=Mod;
for(;i<=sqroot;++i)
if(!vst[i])
--cnt1,(ans+=now)%=Mod;
break;
}
}
}
else now=0;
(ans+=now)%=Mod;
}
//printf("%.5f\n",(double)clock()/CLOCKS_PER_SEC);
cout<<((ans+cnt1%Mod*((r2-l2+1)%Mod))%Mod+Mod)%Mod<<endl;
}
总结:
①判重:可以排序O(logn),但是其实如果元素值域允许的话,可以直接用bool判重。
②可以通过取log将指数上的数放下来,以减小数的大小。
③容斥时可以不记符号,把减法写在中间的计算里!
④没有改变的状态是没有必要dfs下去的,这样虽然会更好写,但它也会让搜索树多一个叉。