浅谈整数分拆
Starrykiller · · 个人记录
例题。(无序分拆数)
将
n 拆分成若干正整数(可重复)之和,不区分顺序,求方案数。
做法 1(搜索)
直接暴力枚举分拆即可,期望通过数据为
做法 2(DP)
令
朴素地,有:
运用经典的滚动数组优化,可以做到
做法 3(Another DP)
令
转移考虑:给这
也就是:
时间复杂度
正确性证明:如下图。
这张图中,格子的数量表示被表示的数,例如这里表示的就是
10 ;每一行的列数表示分拆,例如这里表示的就是10=5+4+1 ,注意每一行的列数要求递减。这张图被称为杨图(Young diagram)或 Ferrers 图。
做法 2 可以视为按行 DP,而做法 3 可以视为按列 DP。由于每一行的列数是递减的,所以唯一性显然。
习题:解释
做法 4(根号分治)
例题。(NOI Online #1 跑步)求正整数
n (n\le 10^5 )的无序分拆数。模数给定。
前两种做法时间复杂度均为
注意到一个事实:在
具体地,令
这样两个 DP 都在
做法 5(GF)
令
运用经典的
做法 6(五边形数定理)
略。
整数分拆往往可以用在一些困难的题目中。
例题。(「AHOI 2022」山河重整)
求有多少个
S\subseteq [n] 满足存在T\subseteq S ,使得n=\sum_{i\in T}i 。1\le n\le 5\times 10^5 ,对给定模数(不一定是素数)取模。
寻找充要条件。 令
必要性是显然的。充分性留作习题。
知道了这一点后,容易设计一个
正难则反,考虑容斥。考虑一个不合法的方案应该怎么不重不漏地被计数。
由充要条件,可以知道,必然存在一个
这启示我们设计状态
令
为了方便理解,我们具体地讲解 DP 实现。
还是这张图。我们仍然要按列 DP。显然列长度最长是
由于每个数都不相同,所以显然每一列的长度相差至多为
具体来看代码实现。我们从大到小枚举每一列的长度
我们以
然后我们强制选择
然后做完全背包之后就是这个样子的:
然后强制加入
不难发现这样确实不重不漏地计数了。
这里给出另一种理解:由于拆分出的数的数量不大于
\sqrt n ,将 「计数\Theta(\sqrt n) 个不同数字和为n 的方案数」变为「对i\in [1,n] ,考虑有多少个数\ge i 」。(by CXY07)。注意观察这张图:以拆分
9=4+3+2 为例,这意味着,图中列数\ge 4 的行数为1 ,列数\ge 3 的行数为2 ,列数\ge 2 的行数为3 。观察图中的每一列。从左往右看,行数为
3 的列数为\textcolor{cyan}{2} (这意味着,有3 行长度大于\textcolor{cyan}{2} ),长度为2 的列数为\textcolor{cyan}{1} (这意味着,有2 行长度大于3=\textcolor{cyan}{2+1} ),长度为1 的列数为\textcolor{cyan}{1} (这意味着,有1 行长度大于4=\textcolor{cyan}{2+1+1} )。我们得到结论:杨表中,行数
{}\ge x 的列数y ,意义为选择了x 个\ge y 的数。
所以代码长成这样:
for (int i=n; i; --i) if (i*(i+1)/2<=n) {
for (int j=n; j>=i; --j) g[j]=g[j-i]; // 强制选择
g[i]++; // 加入自成一列的方案数
for (int j=i; j<=n; ++j) g[j]+=g[j-i];
}
其中,g[j]++ 是
容易发现时间复杂度为
当然我们还有 GF 解释(参考 一叶知秋 的题解):我们本质上就是要求出
考虑一个杨图,高为
我们就是在从大到小枚举
- 先把原多项式乘上
z^h ——就是里面的z^{h(h+1)/2}=z^0z^1z^2\cdots z^h 项。 - 加入新的一项
z^0 。 - 乘上
1/(1-z^h) 。
不难发现这是对的。
紧接着我们可以知道:
有一个很神仙的观察:
还是考虑杨图。从大到小枚举
枚举到
然后对于每个
这个东西的生成函数就是
\displaystyle \prod_{i\ge j+2}(1+z^i)=\sum_{1\le h}z^{h(h+1)/2}z^{\textcolor{cyan}{(j+1)}h}\prod_{1\le i\le h}\frac{1}{1-z^i} ,可以用类似的手法证明。于是就得到f(i)=g(i)-[z^i]\sum_{j}f(j)z^j\sum_{1\le h}z^{h(h+1)/2}z^{\textcolor{cyan}{(j+1)}h}\prod_{1\le i\le h}\frac{1}{1-z^i} 然后枚举
h ,再枚举外层的j ,照猫画虎转移就可以了。实现里面为什么是
f(j)\to z^{(j+2)h+j} :因为之前乘以了z^{h} 。下面的代码也是可以通过的。for (int i=n; i; --i) if (i*(i+1)/2<=n) { for (int j=0; j<=n; ++j) if (j+i*(j+1)<=n) g[j+i*(j+1)]+=f[j]; for (int j=n; j>=i; --j) g[j]=g[j-i]; for (int j=i; j<=n; ++j) g[j]+=g[j-i]; }
由于转移
使用倍增转移,时间复杂度
例题。(青春有悔)
有
n 个随机变量,X_i 等概率随机,且独立地取[0,a_i] 中的整数。所有数字均不大于 $10^5$。