题解:洛谷 P17017 堆石子

【题目来源】

洛谷:P17017 [GESP202606 八级] 堆石子

【题目描述】

\(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

【核心思想】

  1. 问题分析\(m\) 堆石子,第 \(1\) 堆恰有 \(n\) 个,之后每堆严格小于前一堆,每堆至少 \(1\) 个。求方案数对 \(10^9+7\) 取模。本质是组合计数问题,将严格递减序列转化为从 \([1, n-1]\) 中选 \(m-1\) 个不同数的组合问题。

  2. 算法选择

    • 组合数公式\(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\),用快速幂求阶乘逆元
  3. 关键步骤

    • 特判:若 \(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)\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(m + \log mod)\),分子分母各 \(O(m)\),快速幂 \(O(\log mod)\)
    • 空间复杂度:\(O(1)\),仅使用常数变量
  5. 排列组合的核心思想

    • 严格递减与组合的一一对应:从 \([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

【算法真传】

从一道 GESP 真题出发:聊聊组合计数与严格递减序列的一一对应

posted @ 2026-08-15 08:12  团爸讲算法  阅读(14)  评论(0)    收藏  举报