堆石子
给定石子堆数 $m$ 和第一堆的石子数 $n$,要求后续每堆石子数都严格单调递减且每堆至少有 1 个石子,求满足条件的石子堆放方案数对 $10^9 + 7$ 取模后的结果。

有 堆石子,编号为 ,其石子数量分别记为 。
现在要求第 堆石子恰有 个(即 ),并且此后每堆石子的数量严格小于前一堆,即 ()。此外,每堆至少需要有一个石子,即 ()。
在总石子数量不设限制的情况下,给定 ,有多少个满足要求的石子堆放方案?
两个方案不同,当且仅当,两个方案中至少有一堆石子数量不同。
如果不存在满足要求的方案,输出 。由于方案数可能很大,请输出方案数对 取模后的结果。
输入格式
输入一行两个正整数 和 。
输出格式
输出一个整数,表示总方案数对 取模后的结果。
输入样例 1
3 5输出样例 1
6样例解释 1
有 ,,,, 和 共计 种方案。
数据范围
数据点编号 数据范围 特殊性质 无 无
题目要求构造一个长度为 的严格递减正整数序列:
其中首项固定为 。
我们可以引入差分思想。令每两堆之间的差值为 :
- (因为 ,所以 )
- ()
- ()
- 最后一堆: (因为 ,所以 )
根据这些定义,我们可以把每一项反过来用 表示出来。特别是首项 :
题目已知 ,所以问题等价于:
求满足 ,且满足每一个 的正整数解的个数。
这是一个非常经典的组合数学模型:将 个相同的物品分配给 个不同的组(这里是 到 ),要求每组至少分到一个。
我们可以使用隔板法来求解:
- 把 个石子排成一排,它们之间一共有 个空隙。
- 我们需要在这些空隙中插入 个隔板,将石子分成 份。
- 每一种插板的方案,都唯一对应了一组满足条件的 序列。
因此,总方案数就是从 个空隙中选出 个位置放隔板的组合数:
在直接计算组合数之前,需要注意以下几种情况:
-
无法构造的情况:
由于每一堆石子都必须比后一堆至少多 个,且最后一堆至少为 ,那么最小的合法序列是 。
此时首项 至少要为 。也就是说,如果 ,方案数直接为
0(此时 在数学上也为 0)。 -
的情况:
虽然题目中说了 ,但如果从公式来看,,也是合理的。
由于 很大(),我们不能预处理阶乘。但注意到组合数的展开式为:
分子和分母都只有 项。因为 ,我们完全可以在 的时间复杂度内计算出结果:
- 分子:从 开始往下乘 项,边乘边对 取模。
- 分母:计算 。
- 除法变乘法:使用费马小定理求分母的模逆元。因为 是质数,分母 的逆元就是 ,可以通过快速幂在 内求出。
#include <iostream>
using namespace std;
const int MOD = 1e9 + 7;
// 快速幂计算逆元long long quick_pow(long long base, long long exp) { long long res = 1; base %= MOD; while (exp > 0) { if (exp % 2 == 1) res = (res * base) % MOD; base = (base * base) % MOD; exp /= 2; } return res;}
int main() { long long m, n; if (!(cin >> m >> n)) return 0;
// 如果 n < m,无法凑出严格递减且最后一堆 >= 1 的方案 if (n < m) { cout << 0 << endl; return 0; }
// 计算组合数 C(n-1, m-1) // 分子: (n-1)*(n-2)*...*(n-m+1) // 分母: (m-1)! long long numerator = 1; long long denominator = 1; long long k = m - 1; // 需要乘的项数
for (int i = 0; i < k; i++) { numerator = (numerator * ((n - 1 - i) % MOD)) % MOD; denominator = (denominator * (i + 1)) % MOD; }
// 结果 = 分子 * 分母的逆元 long long ans = (numerator * quick_pow(denominator, MOD - 2)) % MOD;
cout << ans << endl; return 0;}- 时间复杂度:,主要消耗在循环计算分子和分母上,快速幂只需要 次运算,完全能在一秒内跑完 的数据。
- 空间复杂度:,只需要常数级别的变量空间。