按固定元素是否出现过来统计不同元素个数之和,把每个长度的贡献化成两段等比数列求和。
OJ: luogu
题目 ID: P7355
难度:普及/提高-
标签:数学计数推导快速幂思维
日期: 2026-06-20 15:17
题意
题面虽然写成“抽奖 + 体验卡 + 永久道具”,但形式化之后,问题其实是:
- 枚举所有长度在
[0,m]之间的整数序列; - 每个位置都可以填
1..n中的一个数; - 一个序列的权值等于
不同元素个数 + 1; - 求所有这些序列权值之和,对
10^9+7取模。
所以这题本质是一个数学计数题。
思路
先看一个最直接的暴力程序:
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const i64 MOD = 1000000007LL;
const int MAXM = 25;
int T;
int n, m;
int seq_arr[MAXM];
bool used[MAXM];
i64 ans;
// 暴力枚举所有长度为 len 的序列。
void dfs(int pos, int len) {
if (pos > len) {
int distinct_cnt = 0;
memset(used, 0, sizeof(used));
for (int i = 1; i <= len; i++) {
if (!used[seq_arr[i]]) {
used[seq_arr[i]] = true;
distinct_cnt++;
}
}
ans += distinct_cnt + 1;
ans %= MOD;
return;
}
for (int x = 1; x <= n; x++) {
seq_arr[pos] = x;
dfs(pos + 1, len);
}
}
i64 solve_one() {
ans = 0;
for (int len = 0; len <= m; len++) {
dfs(1, len);
}
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> T;
while (T--) {
cin >> n >> m;
cout << solve_one() << '\n';
}
return 0;
}暴力版做法很朴素:
- 枚举长度
0..m; - 对每个长度枚举所有序列;
- 统计这个序列里出现了多少种数;
- 把
不同元素个数 + 1累加到答案里。
它只能处理很小的数据,但特别适合帮助理解题意,也适合拿来对拍。
固定一个长度来想
设现在只考虑长度恰好为 k 的所有序列。
先看权值里的那个 +1:
- 长度为
k的序列总数是n^k; - 每个序列都会贡献一个常数
1; - 所以这部分总贡献是
n^k。
怎么统计不同元素个数之和
直接数“每个序列里有多少种不同元素”不太方便。
更好的办法是按元素拆贡献。
固定某个元素 x:
- 如果一个序列里出现过
x,那么它就会给“不同元素个数”贡献1; - 如果没出现过
x,贡献就是0。
所以元素 x 的总贡献,等于:
- “至少出现过一次
x的序列个数”
这个数量可以用补集计数:
- 全部序列数:
n^k - 完全不含
x的序列数:(n-1)^k
因此至少出现一次 x 的序列数是:
n^k - (n-1)^k
一共有 n 个元素,所以所有序列的“不同元素个数之和”就是:
n * (n^k - (n-1)^k)
得到固定长度的总贡献
把两部分合起来:
长度为 k 的总贡献 = n^k + n * (n^k - (n-1)^k)
整理可得:
= (n+1) * n^k - n * (n-1)^k
从单个长度推广到全部长度
题目要求长度从 0 到 m 全部都算,所以答案是:
ans = sum((n+1) * n^k - n * (n-1)^k), k=0..m
拆开就是:
(n+1) * (1 + n + n^2 + ... + n^m)- n * (1 + (n-1) + (n-1)^2 + ... + (n-1)^m)
这两部分都是等比数列。
设公比为 r,则:
1 + r + r^2 + ... + r^m = (r^(m+1)-1)/(r-1)
因为 m 很大,所以幂要用快速幂。
一个必须注意的边界
如果 r ≡ 1 (mod 10^9+7),那么分母 r-1 在模意义下等于 0,没有逆元。
这时不能再套上面的除法公式,而应该直接返回:
m + 1
代码里把这个情况放进 geometric_sum() 单独处理即可。
代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const i64 MOD = 1000000007LL;
int T;
i64 n, m;
// 计算 base^exp mod MOD。
i64 quick_pow(i64 base, i64 exp) {
i64 ans = 1;
base %= MOD;
while (exp > 0) {
if (exp & 1LL) {
ans = ans * base % MOD;
}
base = base * base % MOD;
exp >>= 1LL;
}
return ans;
}
// 计算 1 + ratio + ratio^2 + ... + ratio^m。
// 这里要特别处理 ratio ≡ 1 (mod MOD) 的情况,此时分母没有逆元。
i64 geometric_sum(i64 ratio, i64 m) {
ratio %= MOD;
if (ratio < 0) {
ratio += MOD;
}
if (ratio == 1) {
return (m + 1) % MOD;
}
i64 up = (quick_pow(ratio, m + 1) - 1 + MOD) % MOD;
i64 down = (ratio - 1 + MOD) % MOD;
i64 inv_down = quick_pow(down, MOD - 2);
return up * inv_down % MOD;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> T;
while (T--) {
cin >> n >> m;
// 对固定长度 k:
// 1. 序列总数是 n^k;
// 2. 不同元素个数之和,可以按“每种道具是否出现过”来统计。
// 固定一种道具,它出现在序列中的方案数是 n^k - (n-1)^k。
// 一共 n 种道具,所以不同元素个数总和是
// n * (n^k - (n-1)^k)。
//
// 因而长度为 k 的所有序列的权值和为:
// n^k + n * (n^k - (n-1)^k)
// = (n+1) * n^k - n * (n-1)^k
//
// 再把 k = 0..m 全部加起来,就是两段等比数列。
i64 sum_pow_n = geometric_sum(n, m);
i64 sum_pow_n_minus_1 = geometric_sum(n - 1, m);
i64 ans = (n + 1) % MOD * sum_pow_n % MOD;
ans = (ans - (n % MOD) * sum_pow_n_minus_1 % MOD + MOD) % MOD;
cout << ans << '\n';
}
return 0;
}复杂度
- 时间复杂度:
- 空间复杂度:
总结
这题关键不是抽奖背景,而是把形式化题面真正读懂:
- 固定长度后,先拆出常数
1的贡献; - “不同元素个数之和”改成按元素统计是否出现过;
- 对固定元素用补集计数;
- 最后得到两段等比数列,用快速幂求和。
一旦完成这一步拆分,整题就只剩下一个很短的数学公式了。