[BZOJ2425][HAOI2010]计数(组合数学)

首先,统计出数字串中 0,1,...,9 出现的次数 cnt[0],cnt[1],...,cnt[9]
先考虑用其中的所有非零数字能构成多少个允许含前导 0 len位数。
可以想到,现在 len 位中选出 cnt[1] 位设为 1 ,然后在剩下的lencnt[1]位中选出 cnt[2] 位设为 2 ,再在剩下的lencnt[1]cnt[2]位中选出 cnt[3] 位设为 3 …,即结果为:
9i=1Ccnt[i]leni1j=1cnt[j]
而对于不含前导 0 的,就只要枚举最高位,就可以用上面的式子了(有些小细节,具体见代码实现)。
代码实现如下(lowe(len,0/1)表示构成 len 位数,第二个参数为 0 表示可以含前导0,为 1 表示不能含前导0):

ll lowe(int len, bool first) {
    int i, j; ll ans = 1; if (!first) {
        for (i = 1; i <= 9; len -= cnt[i++])
            ans *= C[len][cnt[i]];
        return ans;
    }
    ans = 0; for (i = 1; i <= 9; i++) {
        cnt[i]--; ans += lowe(len - 1, 0);
        cnt[i]++;
    }
    return ans;
}

继续考虑求位数相等且比原数字串小的数字串个数。
考虑最高位的取值,分两部分计算:
1、最高位的取值比原数字串的最高位小。这样后面位的值不管取多少,都是不会大于等于原数字串的。这时候可以用上面的方法求得。
2、最高位的取值等于原数字串的最高位。这时候就要求去掉最高位之后比原数字串小。但可以发现,这是和原问题相同的一个子问题,可以递归处理(在标程中使用的是非递归)。
注意判断前导 0
组合数可以使用公式Cmn=Cmn1+Cm1n1预处理。
代码:

#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 1005, R = 12;
int n, num[N], tot, cnt[R];
char s[N]; ll C[N][N], Ans;
void init() {
    int i, j; for (i = 0; i <= 1000; i++) C[i][0] = 1;
    for (i = 1; i <= 1000; i++) for (j = 1; j <= i; j++)
        C[i][j] = C[i - 1][j] + C[i - 1][j - 1];
}
ll lowe(int len, bool first) {
    int i, j; ll ans = 1; if (!first) {
        for (i = 1; i <= 9; len -= cnt[i++])
            ans *= C[len][cnt[i]];
        return ans;
    }
    ans = 0; for (i = 1; i <= 9; i++) {
        cnt[i]--; ans += lowe(len - 1, 0);
        cnt[i]++;
    }
    return ans;
}
void solve() {
    int i, j; for (i = 1; i <= n; i++) {
        for (j = (i == 1); j < num[i]; j++) {
            if (!cnt[j]) continue;
            cnt[j]--; Ans += lowe(n - i, 0);
            cnt[j]++;
        }
        cnt[num[i]]--;
    }
}
int main() {
    int i; scanf("%s", s + 1); n = strlen(s + 1);
    for (i = 1; i <= n; i++) {
        cnt[num[i] = s[i] - '0']++;
        if (num[i]) tot++;
    }
    init(); for (i = tot; i < n; i++) Ans += lowe(i, 1);
    cout << (solve(), Ans) << endl;
    return 0;
}
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值