【整除分块】【前缀和】【DP】D. Up the Strip(简单版 + 正常版)

124 阅读2分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

整除分块

给出一道例题,已知n, 求i=1nni\sum_{i=1}^{n} \lfloor\frac{n}{i} \rfloor

这就是整除分块的基本例题

画一个表格找一下怎么计算,以n = 15为例

i123456789101112131415
15i\lfloor \frac{15}{i}\rfloor1575332211111111

证明:

假设分块的左端点为l,要求分块的右端点r.

设分块的值为k,对于区间[l,r][l,r]的每个数满足k=ni=nlk=\lfloor \frac{n}{i}\rfloor=\lfloor \frac{n} {l}\rfloor, 即kinki \leq n,需要找到最大的i使其成立

可得r=nk=nnlr = \lfloor \frac{n}{k}\rfloor=\lfloor \frac{n}{ \frac{n}{l}}\rfloor

计算的相关代码如下:

每次计算出相同值的左右端点[l,r][l, r] ,那么相同值的个数就为rl+1r-l+1

int res = 0;
for(int l = 1, r; l <= n; l = r + 1)
{
    r = n / (n / l);
    res += n / l * (r - l + 1);
}

题目

简单版

链接:

codeforces.com/problemset/…


状态表示:

f[i]f[i]: i变为1的种类数

状态转移:

f[i]=j=1i1f[j]+j=2if[ij]f[i] = \sum_{j=1}^{i-1}f[j] + \sum_{j=2}^{i}f[ \lfloor \frac{i}{j}\rfloor ]

前一部分是考虑减法的方程,后一部分是考虑除法的方程

  • 减法:可以使用前缀和进行优化

  • 除法:ij\lfloor \frac{i}{j}\rfloor 考虑使用整除分块

复杂度为O(nn)O(n \sqrt n)

#include<bits/stdc++.h>
using namespace std;

using ll = long long;


void solve()
{
	int n, m;
	cin >> n >> m;
	
	vector<ll> f(n + 1, 0), s(n + 1, 0);
	f[1] = 1;
	s[1] = 1;
	
	for(int i = 2; i <= n; i++)
	{
		f[i] = (f[i] + s[i - 1]) % m;
		
		for(int l = 2, r; l <= i; l = r + 1)
		{
			r = i / (i / l);
			int cnt = r - l + 1;
			
			ll x = f[i / l] * cnt % m;
			f[i] = (f[i] + x) % m;
		}

		s[i] = s[i - 1]	+ f[i];
		s[i] %= m;
	}
	cout << f[n] % m << "\n";
	
}

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	
	int t;
//	cin >> t;
	t = 1;
	while(t--) 
		solve();
	return 0;
}
 

正常版

codeforces.com/contest/156…


这道题n的限制变大,整除分块无法过去

反过来考虑,从小到大进行考虑

状态表示:

f[i]f[i] i变到n的方案数

状态转移:

f[i]=j=i+1nf[j]+j=2jink=ijmin(ij+j1,n)f[k]f[i] = \sum_{j=i+1}^{n}f[j] + \sum_{j=2}^{j*i\leq n} \sum_{k=i*j}^{min(i*j+j-1,n)}f[k]

状态转移公式可能看着很难懂,下面进行解释:

  • 前一部分:通过加法变到j的方案和,i可以变到[i+1,n][i+1,n]的任意一个

    • 计算方法:可以通过后缀和进行计算
  • 后一部分:通过乘法变到k的方案数,因为题目是除法,我们反过来就变成了乘法,除法进行考虑,我们找一下原始的一个区间,区间中的每一个值可以通过除法变到同一个值

    • 若除2,区间[2i,2i+1][2*i, 2*i+1]中的数可以变到i, 计算次数为n/2n/2

    • 若除3,区间[3i,3i+2][3*i, 3*i+2]中的数可以变到i,计算次数为n/3n/3

    • 若除4,区间[4i,4i+3][4*i, 4*i+3]中的数可以变到i,计算次数为n/4n/4

    • ...

    • 若除j,区间[ji,ji+j1][j*i, j*i+j-1]中的数可以变到i,计算次数为n/jn/j

    • 统计方法:后缀和:s[ij]s[min(ij+j,n+1)]s[i * j] - s[min(i * j + j, n + 1)]

总的计算次数就是n/2+n/3+n/4+...+n/n=n(1/2+1/3+...+1/n)n/2+n/3+n/4+...+n/n=n(1/2+1/3+...+1/n) 后面的是调和级数,复杂度为log(n)log(n),故总的复杂度为O(nlog(n))O(nlog(n))


#include<bits/stdc++.h>
using namespace std;

using ll = long long;


void solve()
{
	int n, m;
	cin >> n >> m;
	
	vector<ll> f(n + 2, 0), s(n + 2, 0);

	f[n] = 1;
	s[n] = 1;
	
	for(int i = n - 1; i >= 1; i--)
	{
		f[i] = (f[i] + s[i + 1]) % m;
		
		for(int j = 2; j * i <= n; j++)
		{
			// [i * j, i * j + j - 1]
			f[i] = (f[i] + s[i * j] - s[min(i * j + j, n + 1)]) % m;
			f[i] = (f[i] + m) % m;
		}
		s[i] = (s[i + 1] + f[i]) % m;
	}
	
	cout << f[1] << "\n";
}

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	
	int t;
//	cin >> t;
	t = 1;
	while(t--) 
		solve();
	return 0;
}