可重元素集的康托展开&数位dp [HAOI2010]计数

大意:

n<=1e50

思路:
如果我们去掉一个0,可以看成它变成了前导0,所以转化一下题意,就是求一个可重数集的康托展开,这个数集的大小是10

对于普通的康托展开,处理的是全排列(非重集)到自然数的双射

我们的思路是:

假设有n位

对于每一位i,确定后面有多少个数字比它小,记为x,那么这一位的贡献就是x*(n-i)!,意义其实很好理解,就是枚举把后面某一个比较小的数字提上来会有多少种情况,每一个的情况数自然就是一个全排列,因为这是非重集。

回到这题,我们的思路其实是差不多的,我们也得算一个可重集的全排列,

令a[i]表示数字i的出现次数,n为出现过的数字之和

则这个可重集的全排列就是\frac{n!}{a[0]!*a[1]!*...*a[9]!}

但是这样会爆ll,我们还得转化一下:

先放0,方案数是C(n,a[0]),然后放1,方案数就是C(n-a[0],a[1]),以此类推

得到:

\frac{n!}{a[0]!*a[1]!*...*a[k]!}=\binom{n}{a[0]}*\binom{n-a[0]}{a[1]}*\binom{n-a[0]-a[1]}{a[2]}*...*\binom{n-\sum_{i=0}^{k-1}a[i]}{a[k]}

也很好验证,手模一下就有了

这样我们只需要算一些简单的组合数就可以了


得到这个之后,我们就可以倒序遍历来得到康托展开了

具体来说,当前在第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的写法就等到期末考结束之后再补吧(挖坑不填预警)~

顺便有空把康托展开和逆康托展开的板子存一下

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值