题解:洛谷 P17017 堆石子
【题目来源】
【题目描述】
有 \(m\) 堆石子,编号为 \(1, 2, \cdots, m\),其石子数量分别记为 \(a_1, a_2, \cdots, a_m\)。
现在要求第 \(1\) 堆石子恰有 \(n\) 个(即 \(a_1 = n\)),并且此后每堆石子的数量严格小于前一堆,即 \(a_i < a_{i-1}\) (\(2 \le i \le m\))。此外,每堆至少需要有一个石子,即 \(a_i \ge 1\) (\(1 \le i \le m\))。
在总石子数量不设限制的情况下,给定 \(m \ge 2, n \ge 1\),有多少个满足要求的石子堆放方案?
两个方案不同,当且仅当,两个方案中至少有一堆石子数量不同。
如果不存在满足要求的方案,输出 \(0\)。由于方案数可能很大,请输出方案数对 \(10^9 + 7\) 取模后的结果。
【输入】
输入一行两个正整数 \(m\) 和 \(n\)。
【输出】
输出一个整数,表示总方案数对 \(10^9 + 7\) 取模后的结果。
【输入样例】
3 5
【输出样例】
6
【核心思想】
-
问题分析:\(m\) 堆石子,第 \(1\) 堆恰有 \(n\) 个,之后每堆严格小于前一堆,每堆至少 \(1\) 个。求方案数对 \(10^9+7\) 取模。本质是组合计数问题,将严格递减序列转化为从 \([1, n-1]\) 中选 \(m-1\) 个不同数的组合问题。
-
算法选择:
- 组合数公式:\(a_1 = n\) 固定,\(a_2, \ldots, a_m\) 需满足 \(n > a_2 > a_3 > \cdots > a_m \geq 1\),即从 \([1, n-1]\) 中选 \(m-1\) 个不同数,选法唯一确定递减序列,方案数为 \(C(n-1, m-1)\)
- 费马小定理求逆元:模数为质数 \(10^9+7\),用快速幂求阶乘逆元
-
关键步骤:
- 特判:若 \(n < m\),输出 \(0\)(无法形成长度 \(m\) 的严格递减正整数序列)
- 分子计算:\(up = (n-1)(n-2)\cdots(n-m+1) = \prod_{i=1}^{m-1}(n-i)\)
- 分母计算:\(down = (m-1)! = \prod_{i=2}^{m-1}i\)
- 逆元与组合数:\(ans = up \times qmi(down, mod-2) \bmod mod = C(n-1, m-1)\)
-
时间/空间复杂度:
- 时间复杂度:\(O(m + \log mod)\),分子分母各 \(O(m)\),快速幂 \(O(\log mod)\)
- 空间复杂度:\(O(1)\),仅使用常数变量
-
排列组合的核心思想:
- 严格递减与组合的一一对应:从 \([1, n-1]\) 中任选 \(m-1\) 个数,按从大到小排列即唯一确定一个合法序列,故方案数 = 组合数
- 最小序列约束:长度 \(m\) 的严格递减正整数序列的最小值为 \(m, m-1, \ldots, 1\),故需 \(n \geq m\)
- 模质数逆元:利用费马小定理 \(a^{p-2} \equiv a^{-1} \pmod{p}\),通过快速幂 \(O(\log p)\) 求逆元
- 分子分母分离计算:不计算完整阶乘,直接计算分子 \((n-1)(n-2)\cdots(n-m+1)\) 和分母 \((m-1)!\),避免大数阶乘的中间溢出风险
- 适用于"严格递减序列计数 / 组合数直接计算"类问题
【算法标签】
普及 #排列组合
【代码详解】
#include <bits/stdc++.h>
using namespace std;
#define int long long // 将int宏定义为long long,防止阶乘和快速幂计算溢出
const int mod = 1e9 + 7; // 定义模数mod为1e9+7
int n, m; // m为堆数,n为第1堆的石子数
// 快速幂算法:计算a的b次方对mod取模的结果
int qmi(int a, int b)
{
int res = 1; // res存储结果,初始化为1
a %= mod; // 先将底数a对mod取模,防止溢出
while (b) // 当指数b大于0时继续循环
{
if (b & 1) res = res * a % mod; // 如果b的最低位为1,将res乘上当前的a,并对mod取模
a = a * a % mod; // a自乘(平方),准备处理下一位,对mod取模
b >>= 1; // b右移一位(相当于b除以2)
}
return res; // 返回a^b % mod的结果
}
signed main() // 使用signed main是因为#define int long long后,main返回值类型需要显式声明
{
cin >> m >> n; // 读入堆数m和第1堆石子数n
if (n < m) // 如果n < m,无法构造满足条件的严格递减序列
{
cout << 0 << endl; // 输出0表示不存在方案
return 0; // 直接结束程序
}
// 组合数推导:
// 设b_i = a_i - (m-i),则b_i >= 0,且b_1 >= b_2 >= ... >= b_m >= 0
// 再设c_i = b_i + 1,转化为隔板法问题
// 最终方案数等价于从n-1个位置中选m-1个位置,即C(n-1, m-1)
// C(n-1, m-1) = (n-1)(n-2)...(n-m+1) / (m-1)!
// 分子:m-1项,从n-1递减到n-m+1,即(n-1)(n-2)...(n-m+1)
// 等价于(n-1)! / (n-m)!
// 本代码写法:从i=1到m-1,累乘(n-i),即(n-1)(n-2)...(n-m+1)
int up = 1; // up为组合数的分子部分
for (int i=1; i<=m-1; i++) // 循环计算分子的m-1项乘积:(n-1) * (n-2) * ... * (n-m+1)
up = up * (n-i) % mod; // 累乘每一项(n-i),并对mod取模
int down = 1; // down为组合数的分母部分,即(m-1)!
for (int i=2; i<=m-1; i++) // 循环计算(m-1)的阶乘
down = down * i % mod; // 累乘2到m-1,并对mod取模
// 答案 = 分子 * 分母逆元 % mod
// 根据费马小定理:a^(p-1) ≡ 1 (mod p),所以a的逆元为a^(p-2) mod p
int ans = up * qmi(down, mod - 2) % mod; // 计算组合数C(n-1, m-1)对mod取模
cout << ans << endl; // 输出方案数对1e9+7取模的结果
return 0;
}
【运行结果】
3 5
6
【算法真传】
浙公网安备 33010602011771号