CF213B Numbers
题目描述
给定一个正整数 N N N 和长度为 10 10 10 的数组 A 0 , A 1 , A 2 , … , A 9 A_0,A_1,A_2,\dots,A_9 A0,A1,A2,…,A9,请求满足以下条件的数的个数:
- 长度不超过 N N N
- 数字 i i i 的出现次数至少为 A i A_i Ai 次
- 无前导 0 0 0
数据范围
1 ≤ n ≤ 100 1 \leq n \leq 100 1≤n≤100
0 ≤ a [ i ] ≤ 100 0 \leq a[i] \leq 100 0≤a[i]≤100
思路
题目要求计算构造一个长度不超过 n n n 、无前导零、规定数字个数的数的方法个数,对于这一题目,可以尝试使用 DP 求解。
设 f [ i ] [ j ] f[i][j] f[i][j] 表示使用 1 , 2 , … , j 1,2,\ldots,j 1,2,…,j 可以构成多少个 i i i 位的满足条件的数。
现在考虑如何进行转移,先枚举 i , j i,j i,j 表示当前的状态,再枚举 k k k ,表示向数中添加 k k k 位 j j j 。
对于这 k k k 位 j j j ,可以放在原数中任意两数位之间,也可以放在数尾,除 0 0 0 外,也都可以放在数的开头。
由此可以想到使用排列组合,对于这种有多个物品需要放入多个空中的问题,可以使用插板法。
-
对于 j ≠ 0 j \neq 0 j=0 的情况:
举一个例子(以下例子中 i = 4 , k = 6 i=4,k=6 i=4,k=6)
◯ 1 ◯ 2 ◯ 3 ◯ 4 ◯ 4 个数位有 5 个可以放置的空 \bigcirc 1 \bigcirc 2 \bigcirc 3 \bigcirc 4 \bigcirc\\ 4 个数位有 5 个可以放置的空 ◯1◯2◯3◯4◯4个数位有5个可以放置的空
对于已经放好的 i i i 个数位,我们有 i + 1 i+1 i+1 个空可以放入 j j j 。
1 ∣ 2 ∣ 3 ∣ 4 ∣ 5 6 6 个 j 插入 4 个隔板就可以分成 5 组 1~|~2~|~3~|~4~|~5~~6\\ 6 个 j 插入 4 个隔板就可以分成 5 组 1 ∣ 2 ∣ 3 ∣ 4 ∣ 5 66个j插入4个隔板就可以分成5组
对于要放入的 k k k 个 j j j ,我们需要插入 i + 1 − 1 = i i+1-1=i i+1−1=i 个隔板,将其分割为 i + 1 i+1 i+1 组。
由于插板法分割的一组数至少有一个数字,但原数中的两个数位间却可以不放入 j j j 。
为解决这个问题,可以先加入 i + 1 i+1 i+1 个 j j j ,使插板后的每一组先分到一个 j j j ,再从每一组中拿出一个 j j j ,这样分到一个 j j j 的组就会变为没有分到,而插板的方案个数并不改变。
1 2 3 ∣ 4 5 6 7 ∣ 8 9 ∣ 10 ∣ 11 先将 6 个 j 补为 11 个,再插入 4 个隔板 11 个 j 中有 10 个可以插入隔板的空 1~~2~~3~|~4~~5~~6~~7~|~8~~9~|~10~|~11\\ 先将 6 个 j 补为 11 个,再插入 4 个隔板\\ 11 个 j 中有 10 个可以插入隔板的空 1 2 3 ∣ 4 5 6 7 ∣ 8 9 ∣ 10 ∣ 11先将6个j补为11个,再插入4个隔板11个j中有10个可以插入隔板的空
所以我们最终我们要在 k + i + 1 k+i+1 k+i+1 个 j j j ,也就是 k + i k+i k+i 个空中插入 i i i 个隔板,可以用组合数来表示,方案数为 ( k + i i ) {k+i \choose i} (ik+i) 种。
-
对于 j = 0 j = 0 j=0 的情况:
1 ◯ 2 ◯ 3 ◯ 4 ◯ 4 个数位有 4 个可以放入 j 的空 1 \bigcirc 2 \bigcirc 3 \bigcirc 4 \bigcirc\\ 4 个数位有 4 个可以放入 j 的空 1◯2◯3◯4◯4个数位有4个可以放入j的空
原理与 j ≠ 0 j \neq 0 j=0 的情况相同,只不过我们现在只有 i i i 个空可以放置 j j j 。
也就是说,我们只需要插入 i − 1 i-1 i−1 个隔板,将要放入的 k k k 个 j j j 分割为 i i i 组。
插板时需先加入 i i i 个 j j j ,共 k + i − 1 k+i-1 k+i−1 个空中插入 i − 1 i-1 i−1 个隔板,故方案数为 ( k + i − 1 i − 1 ) {k+i-1 \choose i-1} (i−1k+i−1) 种。
知道了添加一种数字可行的方案数就可以写出 DP 转移方程了。
-
对于 j ≠ 0 j \neq 0 j=0 的情况:
∀ k ∈ [ a [ j ] , n − i ] f [ i + k ] [ j ] = f [ i ] [ j − 1 ] ⋅ ( k + i i ) \forall k \in [a[j],n-i]\\ f[i+k][j] = f[i][j-1] \cdot {k+i \choose i} ∀k∈[a[j],n−i]f[i+k][j]=f[i][j−1]⋅(ik+i)
-
对于 j = 0 j = 0 j=0 的情况:
∀ k ∈ [ a [ 0 ] , n − i ] f [ i + k ] [ 10 ] = f [ i ] [ 9 ] ⋅ ( k + i − 1 i − 1 ) \forall k \in [a[0],n-i]\\ f[i+k][10] = f[i][9] \cdot {k+i-1 \choose i-1} ∀k∈[a[0],n−i]f[i+k][10]=f[i][9]⋅(i−1k+i−1)
解释/说明
数字转移的顺序为 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 0 1,2,3,4,5,6,7,8,9,0 1,2,3,4,5,6,7,8,9,0 。必须最后转移 0 0 0 ,因为 0 0 0 是有特殊处理的,只有在其余数字全部放完后才能确定放入的 0 0 0 不会成为前导零。
0 0 0 是最后一个转移,并且 1 1 1 转移的时候已经占用了下标 0 0 0 ,因此, 0 0 0 的下标是 10 10 10 。
组合数需要使用杨辉三角预处理出来。
记得开 long long ,DP数组与组合数相乘时会爆 int 。
详细了解插板法可以看这里。
代码
#include<bits/stdc++.h>
#define ll long long//需要开long long
using namespace std;
const ll mod=1e9+7;
int n,a[20];
ll f[110][20],c[110][110];
int main()
{
scanf("%d",&n);
for(int i=0;i<=9;i++)
scanf("%d",&a[i]);
c[0][0]=1;
for(int i=1;i<=n;i++)//杨辉三角预处理组合数
{
c[i][0]=c[i][i]=1;
for(int j=1;j<i;j++)
c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
}
f[0][0]=1;//什么都不放,1种方案
for(int i=0;i<=n;i++)//现在放到第i位(从i+1位开始放)
{
for(int j=1;j<=9;j++)//放入j
for(int k=a[j];k<=n-i;k++)//至少放a[j]个,长度不超过n,最多放n-i个
f[i+k][j]=(f[i+k][j]+f[i][j-1]*c[k+i][i])%mod;//j!=0时的转移方程
for(int k=a[0];k<=n-i;k++)
f[i+k][10]=(f[i+k][10]+f[i][9]*c[k+i-1][i-1])%mod;//j=0时的转移方程
}
ll ans=0;
for(int i=1;i<=n;i++)
ans=(ans+f[i][10])%mod;
printf("%lld\n",ans);
return 0 ;
}