我爱数学!我爱数学!我爱数学!
从前有一位老师说,如果你想学好一个学科,只需要每天早上醒来,大喊三遍你爱它。
所以说,数学到底是什么呢?
一路走来,她不断变换着样子。从前,她作为一门学科,伴随我们走完完整的学生时代。现在,她化为算法竞赛中的一个板块,藏匿在一道道题目描述中,等待着我们的造访。
我常认为,在算法竞赛的旅程中,数学是最艰难的试炼,但同时也是最具魅力的存在。如果说其他算法在对题目进行手术,那么数学则是在与题目静静交谈。
她只是温和地坐在那里,倾听规则流动的声音。通过那些看似杂乱无章的算式,去触碰题目最柔软的内心。
题面
小 sun 最近对计数问题产生了兴趣,现在他有一个问题想请你帮忙解决。
给定一个长度为 n 的整数序列,该序列满足以下条件:
- 序列是单调不增的;
- 每个元素都是不超过
1000的正整数; - 在存储过程中,部分数字遗失了,用
0表示。
请你根据以上条件,计算有多少种不同的原始序列可能满足要求。
答案需要对 1000000007 取模。
思路
题意很明确,意思是我们需要向空缺的位置填上数字,使得序列在全局上单调不增,求总方案数。
不难发现这个问题在不连续的0之间是独立的,所以我们可以求得每段连续0的方案数,最后使用乘法原理相乘。
整理0区间
那么第一步就是整理出所有连续0的区间,这里使用双指针实现;
vector<pair<int, int>> yuna;
int st=0; a[0]=1000; a[n+1]=1;
for(int i=1; i<=n; i++) cin>>a[i];
for(int i=1; i<=n+1; i++) {
if(a[i]) yuna.push_back({st+1, i-1}), st=i;
}
计算每段方案数
为了表述方便,对于每一段区间,我们把允许填入数字的范围记作 w,每段需要填充的长度记作 h:
int w=a[l-1]-a[r+1]+1, h=r-l+1;
我的第一种想法
我们有 w 种可以填入的数字,那么我们可以选择其中的 i 种填入,而余下的空位使用重复的值填充。
我们可以先使用组合数算出在不同的 i 时有多少种可能的组合,再使用隔板法确定当分块数为 i 时可能的分隔方案数。对于每一个 i,二者相乘即为总方案数。
for(int i=1; i<=h; i++) {
//间隔为h-1,在分块数为i时,需要的隔板数为i-1
cnt=(cnt+C(h-1, i-1)*C(w, i)%M)%M;
}
ans=ans*cnt%M;
AC后的思考
细细品味之后,我觉得我的解法还是有些许丑陋了。为什么要讨论 i 的情况呢,我的心路历程是,我只会求单调减的方案数()。我不确定大家高中是不是都有学单调不升的求法,但我确实没有学过。
当题目的条件为单调递减时,我们从可取的数字中任意取出一种组合,组合中的数组都一定互异。换句话说,排列方法与组合种类一一对应,那么每个区间的方案数即为 。
对于选出的 w 个元素可以重复,取出组合数的意义不再能表示方案数,这样我就要像刚刚那样枚举 i 再求和了。
有没有什么更优雅的求法?
既然没法从每次取出的 h 个数的角度考虑,我们可以尝试从 w 的角度出发。
既然一个数字既可以没出现过,也可以出现很多次,那么我们可以考虑每个数字出现的次数。这时候你突然想起 h 的意义就可以是每个数字出现次数之和,这个时候问题就转化为了简单不定方程解的个数的求解(太好了高中学过这个()):
那么问题就变成了数量上的隔板法:
ans=ans*C(w+h-1, h)%M;
优雅!(意满离)
虽然两种解法的最坏复杂度是一致的,但是我想,去思考不同的解法这件事本身应该是有意义的。
至于组合数
如果你没有学过组合数板子,这里可以简单介绍一下。
我们知道,组合数的公式为:
在这个公式中,阶乘我们可以 地预处理出来:
Fac[0]=1;
for(int i=1; i<=N-1; i++) {
Fac[i]=Fac[i-1]*i;
}
然后代入组合数公式:
int C(int a, int b) {
if(a<b) return 0;
return Fac[a]/Fac[b]/Fac[a-b];
}
如果题目不要求对一个大数取模,那么到这里也就结束了。
然而,在模意义除法操作是不具有封闭性的。也就是说,在普通运算中 并不等于
。这个时候我们就要引入乘法逆元。
乘法逆元
概念:模逆元
如果我们能为 找到一个数
,使得(注意是模意义下)一个数乘以
的结果恰好等于其除以
,我们就称
为
的模逆元。
那么组合数公式将会被转化成这样:
int C(int a, int b) {
if(a<b) return 0;
return Fac[a]*inv(Fac[b])%M*inv(Fac[a-b])%M;
}
引入的乘法逆元的概念,除法就可以像加法与乘法一样参与模运算了。
求解方法:费马小定理
关于 ,这里使用费马小定理求解,你可以简单地理解为这是一个数学公式,记住就好了:
换句话说, 在模
意义下的逆元就等于
的
次幂,我们可以使用快速幂求解:
int inv(int a) {
return qpow(a, p-2);
}
要注意的点是,费马小定理求解逆元仅适用于模数为素数时。不过对于此类对结果取模的题目而言,提供的模数一般均为素数(998244353, 1e9+7)。
附上代码
#include<bits/stdc++.h>
#include <climits>
using namespace std;
#define int long long
const int N=1000005;
const int M=1e9+7;
int n, a[N], Fac[N];
vector<pair<int, int>> yuna;
int qpow(int a, int b) {
int ret=1;
while(b) {
if(b&1) ret=ret*a%M;
a=a*a%M; b>>=1;
}
return ret;
}
int inv(int x) {
return qpow(x, M-2);
}
int C(int a, int b) {
if(a<b) return 0;
return Fac[a]*inv(Fac[b])%M*inv(Fac[a-b])%M;
}
void solve() {
cin>>n; int st=0; a[0]=1000; a[n+1]=1;
for(int i=1; i<=n; i++) cin>>a[i];
for(int i=1; i<=n+1; i++) {
if(a[i]) yuna.push_back({st+1, i-1}), st=i;
}
int ans=1;
for(auto &[l, r]: yuna) {
if(l>r) continue;
int w=a[l-1]-a[r+1]+1, h=r-l+1;
ans=ans*C(w+h-1, h)%M;
}
cout<<ans;
}
signed main() {
ios::sync_with_stdio(0); cin.tie(0);
Fac[0]=1;
for(int i=1; i<=N-1; i++) {
Fac[i]=Fac[i-1]*i%M;
}
solve();
// int T; cin>>T; while(T--) solve();
return 0;
}
写在最后
这份题解写的比较详细,希望可以帮助到大家。特别是正在算法竞赛门槛前徘徊的同学,希望能为你CP旅途中的一盏小灯。
意满,离。

京公网安备 11010502036488号