大意:
n<=1e50
思路:
如果我们去掉一个0,可以看成它变成了前导0,所以转化一下题意,就是求一个可重数集的康托展开,这个数集的大小是10
对于普通的康托展开,处理的是全排列(非重集)到自然数的双射
我们的思路是:
假设有n位
对于每一位i,确定后面有多少个数字比它小,记为x,那么这一位的贡献就是x*(n-i)!,意义其实很好理解,就是枚举把后面某一个比较小的数字提上来会有多少种情况,每一个的情况数自然就是一个全排列,因为这是非重集。
回到这题,我们的思路其实是差不多的,我们也得算一个可重集的全排列,
令a[i]表示数字i的出现次数,n为出现过的数字之和
则这个可重集的全排列就是
但是这样会爆ll,我们还得转化一下:
先放0,方案数是C(n,a[0]),然后放1,方案数就是C(n-a[0],a[1]),以此类推
得到:
也很好验证,手模一下就有了
这样我们只需要算一些简单的组合数就可以了
得到这个之后,我们就可以倒序遍历来得到康托展开了
具体来说,当前在第i位,值为val,之前枚举得到的数字出现情况为a[],然后我们遍历取代这一位的数字x,x属于0~val-1,此时a[x]先-1,相应的,val放入后面的序列中,所以对应的方案数就是一个可重集的全排列。这样求和即可。最后再把a[x]加回来
细节见代码
code
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'
#define low(x) x&(-x)
const ll N=1e5+10;
ll c[60][60];
string s;
ll a[20];
void init()
{
for(int i=0;i<=50;++i) c[i][0]=1;
for(int i=1;i<=50;++i)
{
for(int j=1;j<=i;++j)
{
c[i][j]=c[i-1][j]+c[i-1][j-1];
}
}
}
ll f()
{
ll ans=1;
ll num=0;
for(int i=0;i<=9;++i) num+=a[i];
ll pre=0;
for(int i=0;i<=9;++i)
{
ans=ans*c[num-pre][a[i]];
pre+=a[i];
}
return ans;
}
void solve()
{
init();
cin>>s;
ll len=s.size();
s=" "+s;
ll ans=0;
for(int i=len;i;--i)
{
ll val=s[i]-'0';
a[val]++;
for(int j=0;j<val;++j)
{
a[j]--;
ans+=f();
a[j]++;
}
}
cout<<ans<<endl;
}
int main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
// ll t;cin>>t;while(t--)
solve();
return 0;
}
不过这题我是在数位dp的题单里面找到的,数位dp的写法就等到期末考结束之后再补吧(挖坑不填预警)~
顺便有空把康托展开和逆康托展开的板子存一下