[SDOI2014] 数表
题目描述
有一张 \(n\times m\) 的数表,其第 \(i\) 行第 \(j\) 列(\(1\le i\le n\),\(1\le j\le m\))的数值为能同时整除 \(i\) 和 \(j\) 的所有自然数之和。给定 \(a\),计算数表中不大于 \(a\) 的数之和。
输入格式
输入包含多组数据。
输入的第一行一个整数 \(Q\) 表示测试点内的数据组数。
接下来 \(Q\) 行,每行三个整数 \(n\),\(m\),\(a\)(\(|a|\le 10^9\))描述一组数据。
输出格式
对每组数据,输出一行一个整数,表示答案模 \(2^{31}\) 的值。
样例 #1
样例输入 #1
2
4 4 3
10 10 5
样例输出 #1
20
148
提示
数据范围及约定
对于全部数据,\(1\le n,m\le 10^5\),\(1\le Q\le 2\times 10^4\)。
\(a_{i,j} = \displaystyle\sum\sigma(gcd(i,j))\)
\(ans=\displaystyle\sum_{i=1}^{n}\displaystyle\sum_{j=1}^{m}(a[i][j])*[a[i][j]\leq a]\)
先不考虑a的限制,把式子写出来。
对于单次询问,答案为\(\displaystyle\sum_{i=1}^n\displaystyle\sum_{j=1}^m\displaystyle\sum_{d|gcd(i,j)}d\)
把最后一重循环提前,直接枚举约数\(d\),可以得到\(\displaystyle\sum_{d=1}^{min(n,m)}\sigma(d)\displaystyle\sum_{i=1}^{\lfloor\frac n d\rfloor}\displaystyle\sum_{j=1}^{\lfloor\frac m d\rfloor}[gcd(i,j)==1]\)
后面的部分就是典型的反演。\(\displaystyle\sum_{d=1}^{min(n,m)}\sigma(d)\displaystyle\sum_{i=1}^{\lfloor\frac n d\rfloor}\displaystyle\sum_{j=1}^{\lfloor\frac m d\rfloor}\epsilon(gcd(i,j))\)然后\(\epsilon(n)=\displaystyle\sum_{d|n}\mu(d)\)带入得到\(\displaystyle\sum_{d=1}^{min(n,m)}\sigma(d)\displaystyle\sum_{i=1}^{\lfloor\frac n d\rfloor}\displaystyle\sum_{j=1}^{\lfloor\frac m d\rfloor}\displaystyle\sum_{d|i,d|j}\mu(d)\)
然后又是经典的枚举因子\(d\),\(\displaystyle\sum_{d=1}^{min(n,m)}\sigma(d)\displaystyle\sum_{e=1}^{min(\lfloor\frac n d\rfloor,\lfloor\frac n d\rfloor)}\mu(e)\lfloor\frac n {de}\rfloor\lfloor\frac m {de}\rfloor\)
根据之前的题目,整除分块的部分最好放到前面。枚举\(f=d\times e\)
\(\displaystyle\sum_{f=1}^{min(n,m)}\lfloor\frac n {f}\rfloor\lfloor\frac m {f}\rfloor \displaystyle\sum_{d|f}\sigma(d)\mu(\frac f d)\)后面这部分能够预处理。如果没有\(a\)的限制的话,这题就到此为止了。(我咋感觉到此为止也算是有点难度)
考虑一下带上\(a\)的限制会产生什么变化。可以注意到其实\(a\)限制的是函数\(\sigma\),对于后面部分的循环,我们只计算\(\sigma(d)\geq a\)的贡献。
于是考虑函数\(g(f)=\displaystyle\sum_{d|f}\sigma(d)\mu(\frac f d)\),只需要对每个\(a\)得到相应的函数\(g_a(f)\)就可以计算答案。于是考虑维护\(g(f)\),将询问按照\(a\)排序,\(n,m\)的大小对于答案无关紧要,重要的是当\(a\)变化时如何快速维护好对应的\(g_a(f)\)。考虑当\(a\)变大时,\(g_a(f)\)的变化,可以发现有一些\(\sigma(d)\)原本不对\(g\)产生贡献,经过变化后会产生贡献。
先预处理\(\sigma\)函数,然后顺着对每个询问,\(a_i\)到\(a_{i+1}\),需要枚举出所有的\(a_i<\sigma(j)\leq{a_{i+1}}\),于是很明显需要把\(\sigma(j)\)按照大小排序。每个原本不计算的\(\sigma(j)\)在需要计算后会对\(g(1*j),g(2*j)\)这些倍数位置的\(g_a(f)\)都产生影响,每一个\(1\leq j\leq n\)都需要枚举倍数,总体复杂度\(n\ ln\ n\),然后看看维护的\(g_a(f)\)的修改查询需求,单点修改和区间查询,所以直接树状数组就可以。
数论分块复杂度\(O(\sqrt n)\),共计\(q\)次,每次需要查询的\(O(log\ n)\)所以总体是\(O(q\sqrt n\ log\ n+n\ln n\ log\ n)\)
其实思路不好出,因为不知道如果不先处理\(a\)的限制的话后面是否能够补救。如果这个思路能够具有一定的普适性的话两种可能,一种是这里\(a\)的限制具有一定的特殊性还有一种就是\(a\)的限制先考虑就是没法做。其实应该是两种都有。\(a\)的限制其实说到底就是对于\(\sigma\)函数的大小的限制,而事实上从开始就能够非常明显的发现函数\(\sigma\)会出现在答案中。而莫反的答案形式基本上都是整除分块在前,数论函数在后。既然\(a\)的变化只影响了\(\sigma\)的贡献,那么只需要和这题的做法一样修改后面的部分即可,毕竟本来莫反的数论函数的部分就是预处理得到的。
那其实能够总结得到一个结论,对于与统计的结果相关的数论函数的部分的限制可以考虑对于限制进行动态维护。这个是一个思路。主要是第一眼看到这个\(a\)的限制这题我是完全没有思路,一个很重要的原因就是想着先处理\(a\)的限制而不是先不考虑这个限制。毕竟如果不考虑\(a\)的限制的话我其实做的就不是这题了。。但是现在根据上面的分析其实能够得到对于数论函数的限制大概率可以通过反演将其移动至整除分块后面的部分,这样就能够预处理和动态维护了。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline ll read()
{
char c=getchar();ll a=0,b=1;
for(;c<'0'||c>'9';c=getchar())if(c=='-')b=-1;
for(;c>='0'&&c<='9';c=getchar())a=a*10+c-'0';
return a*b;
}
const ll N=1e5;
ll tr[N+1],prime[N+1],cnt,mu[N+1],g[N+1],vis[N+1],Q,ans[20001];
struct D
{
ll id,v;
}sigma[N+1];
struct query
{
ll n,m,a,id;
}q[20001];
inline ll lowbit(ll x)
{
return x&(-x);
}
inline void add(ll i,ll x)
{
while(i<=N)
{
tr[i]+=x;
i+=lowbit(i);
}
}
inline ll ask(ll i)
{
ll ans=0;
while(i>0)
{
ans+=tr[i];
i-=lowbit(i);
}
return ans;
}
bool mycmp1(D a,D b)
{
return a.v<b.v;
}
void init()
{
mu[1]=1;
for(ll i=2;i<=N;i++)
{
if(!vis[i])prime[++cnt]=i,mu[i]=-1;
for(ll j=1;j<=cnt&&i*prime[j]<=N;j++)
{
vis[i*prime[j]]=1;
if(i%prime[j]==0)
{
mu[i*prime[j]]=0;
break;
}
mu[i*prime[j]]=mu[i]*mu[prime[j]];
}
}
for(ll i=1;i<=N;i++)
{
sigma[i].id=i;
for(ll j=i;j<=N;j+=i)
{
sigma[j].v+=i;
}
}
sort(sigma+1,sigma+1+N,mycmp1);
}
bool mycmp2(query a,query b)
{
return a.a<b.a;
}
bool mycmp3(query a,query b)
{
return a.id<b.id;
}
void solve(ll x)
{
ll n=q[x].n,m=q[x].m;
for(ll l=1,r=0;l<=min(n,m);l=r+1)
{
r=min(n/(n/l),m/(m/l));
ans[q[x].id]+=(n/l)*(m/l)*(ask(r)-ask(l-1));
}
}
int main()
{
init();
Q=read();
for(ll i=1;i<=Q;i++)
{
q[i].n=read(),q[i].m=read(),q[i].a=read();
q[i].id=i;
}
sort(q+1,q+1+Q,mycmp2);
ll now=1;
for(ll i=1;i<=Q;i++)
{
while(now<=N&&sigma[now].v<=q[i].a)
{
for(ll j=1;j*sigma[now].id<=N;j++)
{
add(j*sigma[now].id,mu[j]*sigma[now].v);
// cout<<"?"<<endl;
}
now++;
}
ans[q[i].id]=0;
solve(i);
}
sort(q+1,q+1+Q,mycmp3);
for(ll i=1;i<=Q;i++)
{
cout<<ans[i]%(1LL<<31)<<endl;
}
return 0;
}
最近写代码运气这么好的,都一遍过的。
应该是这些莫反的题目没有什么代码细节导致的吧