P3935 Calculating
题面
若 分解质因数为 ,则记 。现在,请求出 的值。
由于答案可能过大,请将其对 取模。
思路
首先,十分显然的是, 就是 的因数个数。所以,我们要求的就是从 到 每个数的因数个数之和。
直接算时时间复杂度为 显然超时,考虑优化。首先,我们需要一个前缀和的思想,即先求出 与 ,那么前者减后者就是答案。
于是现在难度在于如果求 。根据套路,我们可以算贡献;即,对于 这个数,它对答案的贡献就是1至n中k的倍数的个数,就是 。
所以, ;同理有 。
时间复杂度 ,很好了吧~但是,看到 后,我们又心透凉了。
现在,难点仍然在于求 。
这个式子是不可能用 的代价计算的。那么,我们能怎么办呢?找找规律试试看~
设
,则
哎哎? 中每两数之差越来越小,后半段完全一样,中前部分也有几个相同的( )?
是不是,我们就可以通过分块,每个块都是相同的数来做这题呢?
答案是可以的。对于每一个块,我们已经知道了左端点 ,那么右端点 就是满足 的最大的 。
把这可爱的式子化一下:
。
好啦!给定左端点,我们可以在 的代价内求出右端点。显然,一个块对答案的贡献就是 。
所以时间复杂度为 块的数量 。而块的数量不会超过 ,所以时间复杂度就是 。
这就是整除分块~
综上所述,时间复杂度就是
。并不会超时,但是容易溢出(因此我提交了
发才过),请瞪 第
行。
上代码~
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int mod=998244353;
int L,R;
inline int cal(int n)
{
if (n==0) return 0;
int ans=0,r;
for (register int l=1;l<=n;l++)
{
r=n/(n/l);
ans=(ans+((((r-l+1)%mod)*((n/l)%mod))%mod))%mod;
l=r;
}
return ans;
}
signed main()
{
cin>>L>>R;
cout<<((cal(R)-cal(L-1))%mod+mod)%mod<<endl;
return 0;
}
P2261 [CQOI2007]余数求和
题面
求 。
思路
啥话不说,直接把这个式子给搞一搞~
于是,我们只需要运用整除分块就可以求出这可爱的式子啦?时间复杂度 。
问题 : 如何用整除分块呢?这是 ,后面还有一个 呢 QAQ
回答 : 对于每一个同样使得 的块,我们发现这个 都是相同的。那么,我们就把 给拎出来,用**乘法分配律得到该块的贡献就是 ,即 。
时间复杂度仍然是 。
问题
: 我卡死+RE了QWQ
答案
: 本小蒟蒻也遇到了这个问题~因此调试了至少一小时(菜死了)。
首先,分析您 的原因,显然是因为除数为 导致的。那么,我们该如何控制呢?读入 和 的时候,如果 比 打,那么就把 改成 ,但是会影响答案。即,原 这部分被少算了,所以最终答案要加上它。
上代码~
#include <bits/stdc++.h>
#define int long long
using namespace std;
int n,r,k,ans=0;
inline int get_sum(int ll,int rr)
{
return ((rr-ll+1)*(ll+rr))/2;
}
signed main()
{
cin>>n>>k;
ans=n*k;
for (register int l=1,r;l<=n;l=r+1)
{
if (k<l) r=n;
else r=min(k/(k/l),n);
ans=ans-get_sum(l,r)*(k/l);
}
cout<<ans<<endl;
return 0;
}
P1613(押:NOIP普及组T4)
题面
定义一次跑路为跑长度 为任意自然数 。现在,给定一个联通有向图,且任意边的长度均为1;请求出从 号节点到 号节点需要的跑路次数的最小值。
思路
首先,这里直接否认一种思维(本蒟蒻曾经想错的):
跑出原图从
到
号节点的最短路径
,然后看一看
最少能够分为多少个
的次方数相加,其次数就是答案。
这种思路错误,是因为最短路不一定是最优解。即,样例中 到 的最短距离是 而不是 ,但是 只需要跑路 次,而 却需要跑路 次。
同时,警告我们勿忽略自环。
显然这是一道图上倍增优化 +Floyd的好题。
状态设计 表示,从 到 号节点,跑路 次是否可以。如果 ,那么我们从 到 只需要使用一次跑路就能到达(跑 步即可)。
状态转移十分经典(肯定能看懂,博主太懒):
for (int step=1;step<=60;step++)
{
for (int k=1;k<=n;k++)
{
for (int i=1;i<=n;i++)
{
for (int j=1;j<=n;j++)
{
if (dp[i][k][step-1]==1&&dp[k][j][step-1]==1)
{
dp[i][j][step]=1;
GA[i][j]=1;
}
}
}
}
}
所以,我们再次建图。即,对于能够直接通过一次跑路互相到达的节点连一条有向边,其长度为 。注意对于没有连边的两点之间的最短路径长度暂时设为inf。最后,跑一边代价为 的弗雷德,即可得到从 到 号节点最少需要跑几次路。
时间复杂度: ,其中 为最优解路径长度。
代码
#include <bits/stdc++.h>
#define int long long
#define inf 2000000007ll//无穷大
using namespace std;
int n,m;
int dp[105][105][105],GA[105][105];
signed main()
{
cin>>n>>m;
for (int i=1;i<=n;i++)
{
for (int j=1;j<=n;j++)
{
for (int k=1;k<=60;k++) dp[i][j][k]=inf;
GA[i][j]=inf;//预处理
}
}
for (int i=1;i<=m;i++)
{
int u,v;
cin>>u>>v;
dp[u][v][0]=GA[u][v]=1;//加边并标记
}
for (int step=1;step<=60;step++)
{
for (int k=1;k<=n;k++)
{
for (int i=1;i<=n;i++)
{
for (int j=1;j<=n;j++)
{
if (dp[i][k][step-1]==1&&dp[k][j][step-1]==1)
{
dp[i][j][step]=1;
GA[i][j]=1;
}//倍增dp
}
}
}
}
for (int k=1;k<=n;k++)
{
for (int i=1;i<=n;i++)
{
for (int j=1;j<=n;j++)
{
if (GA[i][j]>GA[i][k]+GA[k][j]) GA[i][j]=GA[i][k]+GA[k][j];//Floyd
}
}
}
cout<<GA[1][n]<<endl;
return 0;
}
P2629(押:NOIP普及组T3/T4)
题面
给定一个环,请求出存在多少个这样的 ,使得从 开始顺时针遍历这个环时,无论何时,此次遍历的数之和不小于 。
思路
首先,第一个套路: 破环成链。即,我们开大小为 的数组。
然后,正式开始思考。首先,外层循环枚举 绝不能少,所以我们希望可以优化内层循环,即快速地判断 到 这个区间是否满足要求。显然,贪心地发现,我们只需要找到 ,使得从 的总和最小,然后直接判断这个总和是否小于0;如果是,则该区间不满足条件,否则满足。
那么,怎么在区间中找到最小的
呢?我们可以预处理出前缀和,在
到
中前缀和最小的值就是所求的。同时,对于不带修的区间最值查询,我们可以使用简单粗暴的线段树或一点不熟的RMQ,从而对于每次在
代价下找到
,然后用上面所说的方法判断区间是否满足要求即可。
综上所述,时间复杂度为 。
强制使用快读或 且去掉强迫症般的 ,并且建议用线段树的同学多使用位运算,否则卡不过去。
上代码~
#include <bits/stdc++.h>
using namespace std;
const int maxlen=2000000;
int n,cnt=0;
int a[2*maxlen+5],pre[2*maxlen+5],tree[maxlen*4+5];
inline void pushup(int rt)
{
tree[rt]=min(tree[2*rt],tree[2*rt+1]);
}
inline void build_tree(int rt,int l,int r)
{
if (l==r)
{
tree[rt]=pre[l];
return;
}
int mid=(l+r)>>1;
build_tree(rt<<1,l,mid);
build_tree(rt<<1|1,mid+1,r);
pushup(rt);
}
inline int query(int nl,int nr,int rt,int l,int r)
{
int ans=1e15+7,mid=(l+r)>>1;
if (nl<=l&&r<=nr) return tree[rt];
if (nl<=mid) ans=min(ans,query(nl,nr,rt<<1,l,mid));
if (nr>mid) ans=min(ans,query(nl,nr,rt<<1|1,mid+1,r));
return ans;
}
inline int read()
{
int s=0,w=1;
char ch=getchar();
while (ch<'0'||ch>'9')
{
if (ch=='-') w=-w;
ch=getchar();
}
while (ch>='0'&&ch<='9')
{
s=(s<<1)+(s<<3)+(ch^'0');
ch=getchar();
}
return s*w;
}
signed main()
{
cin>>n;
for (int i=1;i<=n;i++) a[i]=read();
for (int i=n+1;i<=2*n;i++) a[i]=a[i-n];
for (int i=1;i<=2*n;i++) pre[i]=pre[i-1]+a[i];
build_tree(1,1,2*n);
for (register int i=1;i<=n;++i)
{
int l=i,r=i+n-1;
int minv=query(l,r,1,1,2*n);
if (minv-pre[i-1]>=0) cnt++;
}
cout<<cnt<<endl;
return 0;
}//线段树版本
另外,本题也可以用 时间复杂度更优的滑动窗口(单调队列)做法,思路基本一样。
P6583(押: NOIP2020提高组D2T1)
Solution
显然,若 为十进制有限小数,那么约分后 一定有且仅有质因数 和 。
换句话说,若 中, 若是十进制有限小数,那么 是不含任何质因子 或 的数(约分时不能约掉质因数 或 ,否则就会像 这样,约掉了 结果分子成为小数,从而误判该分数不满足要求),留下的是有且仅有质因子 和 的数 ,还有正整数 。
于是,我们要数一数存在多少个满足上述要求的 。枚举 时,显然 有 种取值。假设 表示 中不含有质因子 或 的数的个数,则 有 种取值。
即答案为 。
当 超过 的时候, 怎么快速求得呢?预处理出所有有且仅有质因子 和 的数,可以通过枚举 中的 与 ,并将所有这样的 存入一个数组 来找到。然后,对于每次询问 ,我们可以从反面思考,找到小于等于 的数中仅含有质因子 和 的数的个数,再用 减去这个值;显然前者可以快速地通过 _ 求出(找到在所有有且仅有质因子 和 的数中第一个不大于 的数的位置即可)。求 的时间复杂度是 ,可以认为是 。
于是,我们得到了时间复杂度 ,空间复杂度 的解法。但是我们仍然不满意,尝试进一步优化。
回到公式,发现
它似乎可以用整除分块优化,但是由于有限制( 不含质因子 或 ),我们显然不能直接无脑用整除分块。
这时,我放弃了整除分块,但是……
定义函数 ,答案就是 。
此时,由于 的取值成为了等差数列,我们可以普通地整除分块,但是在算 时,每个块中 对答案贡献的次数不是 中所有的数,而是 中 的倍数。
此时时间复杂度为 。
引用一下《算法竞赛进阶指南》上的一句经典话: “虽然很多问题的时间复杂度是有下界的,但从某种程度上说,算法的设计、优化是永无止境的。”
本题作为整除分块的好题,启示我们在棘手的整除分块中用容斥的思想轻松解决,给了我们极大帮助!
Code
#include <bits/stdc++.h>
#define int long long//记得开long long
using namespace std;
int n,ans=0,l=1,r,len=0;
int a[10005];
int quick_power(int x,int y)//快速幂优化预处理
{
int res=1;
for (;y;y>>=1,x=(x*x))
{
if (y&1) res=(res*x);
}
return res;
}
bool cmp(int x,int y)
{
return x>y;
}
inline void init()//预处理出所有有且仅有质因数2和5的数
{
for (int i=0;i<=40;i++)
{
int pos=quick_power(2,i);
if (pos>n) break;
for (int j=0;j<=20;j++)
{
a[++len]=pos;
pos*=5;
if (pos>n) break;
}
}
sort(a+1,a+len+1,cmp);
}
inline int f(int k)
{
return len-(lower_bound(a+1,a+len+1,k,greater<int>())-a)+1;
}
inline int cal2(int ll,int rr,int mul)//计算区间内有多少个mul的倍数
{
int L,R;
if (ll%mul==0) L=ll;
else L=ll+(mul-ll%mul);
R=(rr/mul)*mul;
return ((R-L)/mul)+1;
}
inline int cal(int l,int r)
{
return cal2(l,r,1)-cal2(l,r,2)-cal2(l,r,5)+cal2(l,r,10);
}
signed main()
{
cin>>n;
init();
for (l=1;l<=n;l++)
{
r=n/(n/l);
ans+=f(n/l)*(n/l)*cal(l,r);
l=r;
}//整除分块
cout<<ans<<endl;
return 0;
}