课件-2

P3935 Calculating

题面

x x 分解质因数为 Σ i = 1 k c i p i Σ_{i=1}^k {c_i}^{p_i} ,则记 f ( x ) = i = 1 k ( p i + 1 ) f(x)=∏_{i=1}^k (p_i+1) 。现在,请求出 i = l r f ( i ) \sum_{i=l}^r f(i) 的值。

由于答案可能过大,请将其对 998244353 998244353 取模。

思路

首先,十分显然的是, f ( x ) f(x) 就是 x x 的因数个数。所以,我们要求的就是从 l l r r 每个数的因数个数之和

直接算时时间复杂度为 O ( n l o g n ) O(nlogn) 显然超时,考虑优化。首先,我们需要一个前缀和的思想,即先求出 i = 1 r f ( i ) \sum_{i=1}^r f(i) i = 1 l 1 f ( i ) \sum_{i=1}^{l-1} f(i) ,那么前者减后者就是答案。

于是现在难度在于如果求 i = 1 n f ( i ) \sum_{i=1}^n f(i) 。根据套路,我们可以算贡献;即,对于 k k 这个数,它对答案的贡献就是1至n中k的倍数的个数,就是 [ n / k ] [n/k]

所以, i = 1 r f ( i ) = k = 1 r [ n k ] \sum_{i=1}^r f(i)=\sum_{k=1}^r [\frac n k] ;同理有 i = 1 l 1 f ( i ) = k = 1 l 1 [ l 1 k ] \sum_{i=1}^{l-1} f(i)=\sum_{k=1}^{l-1} [\frac {l-1} k]

时间复杂度 O ( n ) O(n) ,很好了吧~但是,看到 l 1 0 14 , r 1.6 × 1 0 14 l≤10^{14},r≤1.6×10^{14} 后,我们又心透凉了。


现在,难点仍然在于求 i = 1 n [ n i ] \sum_{i=1}^n [\frac n i]

这个式子是不可能用 O ( 1 ) O(1) 的代价计算的。那么,我们能怎么办呢?找找规律试试看~

n = 10 n=10 ,则
10 ÷ 1 = 10 10÷1=10
10 ÷ 2 = 5 10÷2=5
10 ÷ 3 = 3 10÷3=3
10 ÷ 4 = 2 10÷4=2
10 ÷ 5 = 2 10÷5=2
10 ÷ 6 = 1 10÷6=1
10 ÷ 7 = 1 10÷7=1
10 ÷ 8 = 1 10÷8=1
10 ÷ 9 = 1 10÷9=1
10 ÷ 10 = 1 10÷10=1

哎哎? 10    3   2   2   1   1   1   1   1 10\ \ 3\ 2\ 2\ 1\ 1\ 1\ 1\ 1 中每两数之差越来越小,后半段完全一样,中前部分也有几个相同的( 2   2 2\ 2 )?

是不是,我们就可以通过分块,每个块都是相同的数来做这题呢?

答案是可以的。对于每一个块,我们已经知道了左端点 l l ,那么右端点 r r 就是满足 [ n l ] = [ n r ] [\frac n l]=[\frac n r] 的最大的 r r

[ n l ] = [ n r ] [\frac n l]=[\frac n r]

把这可爱的式子化一下:
r = [ n n l ] r=[\frac n {\frac n l}]

好啦!给定左端点,我们可以在 O ( 1 ) O(1) 的代价内求出右端点。显然,一个块对答案的贡献就是 ( r l + 1 ) × [ n l ] (r-l+1)×[\frac n l]

所以时间复杂度为 O ( O( 块的数量 ) ) 。而块的数量不会超过 2 × n 2×\sqrt n ,所以时间复杂度就是 O ( n ) O(\sqrt n)

这就是整除分块~


综上所述,时间复杂度就是 O ( l + r ) O(\sqrt l+\sqrt r) 。并不会超时,但是容易溢出(因此我提交了 7 7 发才过),请 16 16 行。

上代码~

#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]余数求和

题面

i = 1 n k   m o d   i \sum_{i=1}^n k\ mod\ i

思路

啥话不说,直接把这个式子给搞一搞~

i = 1 n k   m o d   i \sum_{i=1}^n k\ mod\ i
= i = 1 n k [ k / i ] × i =\sum_{i=1}^n k-[k/i]×i
= k n Σ i = 1 n [ k / i ] × i =kn-Σ_{i=1}^n [k/i]×i

于是,我们只需要运用整除分块就可以求出这可爱的式子啦?时间复杂度 O ( n ) O(\sqrt n)


问题 1 1 : 如何用整除分块呢?这是 Σ i = 1 n [ k / i ] × i Σ_{i=1}^n [k/i]×i ,后面还有一个 i i 呢 QAQ

回答 1 1 : 对于每一个同样使得 [ k / l ] = [ k / r ] [k/l]=[k/r] 的块,我们发现这个 [ k / t ] [k/t] ( l t r ) (l≤t≤r) 都是相同的。那么,我们就把 [ k / t ] [k/t] 拎出来,用**乘法分配律得到该块的贡献就是 [ k / l ] × Σ j = l r [k/l]×Σ_{j=l}^r ,即 [ k / l ] ( l + r ) ( r l + 1 ) 2 [k/l]\frac {(l+r)(r-l+1)} 2

时间复杂度仍然是 O ( n ) O(\sqrt n)

问题 2 2 : 我卡死+RE了QWQ
答案 2 2 : 本小蒟蒻也遇到了这个问题~因此调试了至少一小时(菜死了)。

首先,分析您 R E RE 的原因,显然是因为除数为 0 0 导致的。那么,我们该如何控制呢?读入 n n k k 的时候,如果 n n k k 打,那么就把 n n 改成 k k ,但是会影响答案。即,原 i = k + 1 n k   m o d   i = ( n k ) k \sum_{i=k+1}^n k\ mod\ i=(n-k)k 这部分被少算了,所以最终答案要加上它。


上代码~

#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)

题面

定义一次跑路为跑长度 2 k ( k 2^k(k 为任意自然数 ) ) 。现在,给定一个联通有向图,且任意边的长度均为1;请求出从 1 1 号节点到 n n 号节点需要的跑路次数的最小值。

思路


首先,这里直接否认一种思维(本蒟蒻曾经想错的):
跑出原图从 1 1 n n 号节点的最短路径 d d ,然后看一看 d d 最少能够分为多少个 2 2 的次方数相加,其次数就是答案。

这种思路错误,是因为最短路不一定是最优解。即,样例中 1 1 n n 的最短距离是 3 3 而不是 4 4 ,但是 4 4 只需要跑路 1 1 次,而 3 3 却需要跑路 2 2 次。

同时,警告我们勿忽略自环


显然这是一道图上倍增优化 d p dp +Floyd的好题。

状态设计 d p i , j , k dp_{i,j,k} 表示,从 i i j j 号节点,跑路 2 k 2^k 次是否可以。如果 d p i , j , k = 1 dp_{i,j,k}=1 ,那么我们从 i i j j 只需要使用一次跑路就能到达(跑 2 k 2^k 步即可)。

状态转移十分经典(肯定能看懂,博主太懒):

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;
					}
				}
			}
		}
	}

所以,我们再次建图。即,对于能够直接通过一次跑路互相到达的节点连一条有向边,其长度为 1 1 。注意对于没有连边的两点之间的最短路径长度暂时设为inf。最后,跑一边代价为 O ( n 3 ) O(n^3) 的弗雷德,即可得到从 1 1 n n 号节点最少需要跑几次路。

时间复杂度: O ( n 3 l o g 2 m ) O(n^3log_2m) ,其中 m m 为最优解路径长度。

代码

#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)

题面

给定一个环,请求出存在多少个这样的 k k ,使得从 k k 开始顺时针遍历这个环时,无论何时,此次遍历的数之和不小于 0 0

思路

首先,第一个套路: 破环成链。即,我们开大小为 2 n 2n 的数组。

然后,正式开始思考。首先,外层循环枚举 k k 绝不能少,所以我们希望可以优化内层循环,即快速地判断 k k k + n 1 k+n-1 这个区间是否满足要求。显然,贪心地发现,我们只需要找到 i i ,使得从 k i k—i 的总和最小,然后直接判断这个总和是否小于0;如果是,则该区间不满足条件,否则满足。

那么,怎么在区间中找到最小的 j = k i p r e j \sum_{j=k}^i pre_j 呢?我们可以预处理出前缀和,在 k k k + n 1 k+n-1 中前缀和最小的值就是所求的。同时,对于不带修的区间最值查询,我们可以使用简单粗暴的线段树或一点不熟的RMQ,从而对于每次在 O ( l o g 2 n ) O(log_2n) 代价下找到 i i ,然后用上面所说的方法判断区间是否满足要求即可。

综上所述,时间复杂度为 O ( n l o g 2 n ) O(nlog_2n)

强制使用快读或 s c a n f scanf 且去掉强迫症般的 l o n g long l o n g long ,并且建议用线段树的同学多使用位运算,否则卡不过去。

上代码~

#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;
}//线段树版本

另外,本题也可以用 O ( n ) O(n) 时间复杂度更优的滑动窗口(单调队列)做法,思路基本一样。

P6583(押: NOIP2020提高组D2T1)

Solution

显然,若 x y \frac x y 为十进制有限小数,那么约分后 y y 一定有且仅有质因数 2 2 5 5

换句话说,若 a c b c \frac {ac} {bc} 中, a c b c \frac {ac} {bc} 若是十进制有限小数,那么 c c 不含任何质因子 2 2 5 5 的数(约分时不能约掉质因数 2 2 5 5 ,否则就会像 12 15 \frac {12} {15} 这样,约掉了 15 15 结果分子成为小数,从而误判该分数不满足要求),留下的是有且仅有质因子 2 2 5 5 的数 b b ,还有正整数 a a

于是,我们要数一数存在多少个满足上述要求的 a c b c \frac {ac} {bc} 。枚举 c c 时,显然 a a [ n c ] [\frac n c ] 种取值。假设 f ( x ) f(x) 表示 1 x 1-x 不含有质因子 2 2 5 5 的数的个数,则 b b f ( [ n c ] ) f([\frac n c]) 种取值。

即答案为 c f ( [ n c ] ) [ n c ] \sum_c f([\frac n c]) [\frac n c]


x x 超过 1 0 8 10^8 的时候, f ( x ) f(x) 怎么快速求得呢?预处理出所有有且仅有质因子 2 2 5 5 的数,可以通过枚举 2 i × 5 j 2^i×5^j 中的 i i j j ,并将所有这样的 2 i × 5 i 2^i×5^i 存入一个数组 t m p tmp 来找到。然后,对于每次询问 f ( x ) f(x) ,我们可以从反面思考,找到小于等于 x x 的数中仅含有质因子 2 2 5 5 的数的个数,再用 x x 减去这个值;显然前者可以快速地通过 l o w e r lower _ b o u n d bound 求出(找到在所有有且仅有质因子 2 2 5 5 的数中第一个不大于 x x 的数的位置即可)。求 f f 的时间复杂度是 O ( l o g 2 ( l o g 2 n l o g 5 n ) ) O(log_2({log_2nlog_5n})) ,可以认为是 O ( 1 ) O(1)

于是,我们得到了时间复杂度 O ( n ) O(n) 空间复杂度 T ( 1 ) T(1) 的解法。但是我们仍然不满意,尝试进一步优化。

回到公式,发现
c f ( [ n c ] ) [ n c ] \sum_c f([\frac n c]) [\frac n c]

它似乎可以用整除分块优化,但是由于有限制( c c 不含质因子 2 2 5 5 ),我们显然不能直接无脑用整除分块。

这时,我放弃了整除分块,但是……

定义函数 F ( x ) = x c f ( [ n c ] ) [ n c ] F(x)=\sum_{x|c} f([\frac n c]) [\frac n c] ,答案就是 F ( 1 ) F ( 2 ) F ( 5 ) + F ( 10 ) F(1)-F(2)-F(5)+F(10)

此时,由于 c c 的取值成为了等差数列,我们可以普通地整除分块,但是在算 F ( x ) F(x) 时,每个块中 f ( [ n c ] ) [ n c ] f([\frac n c]) [\frac n c] 对答案贡献的次数不是 [ l , r ] [l,r] 中所有的数,而是 [ l , r ] [l,r] x x 的倍数。

此时时间复杂度为 O ( n ) O(\sqrt n)

引用一下《算法竞赛进阶指南》上的一句经典话: “虽然很多问题的时间复杂度是有下界的,但从某种程度上说,算法的设计、优化是永无止境的。”

本题作为整除分块的好题,启示我们在棘手的整除分块中用容斥的思想轻松解决,给了我们极大帮助!

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;
}

猜你喜欢

转载自blog.csdn.net/Cherrt/article/details/106867227