CF908 G. New Year and Original Order 思维较高的数位dp

传送门:CF

前题提要:此题是我CF账号上第一道*2800的题,故写篇题解记录一下

n < = 1 0 700 n<=10^{700} n<=10700,问1到n中每个数在各数位排序后得到的数的和

观察到n达到了 1 0 700 10^{700} 10700并且求的是区间满足数的贡献,所以不难想到应该使用数位dp来求解.但是该如何设计状态呢?

先随便考虑一个排序后的数字,例如 23799 23799 23799,考虑这个数字的贡献,直接考虑是不好考虑的,所以我们将其拆分.对于 23799 23799 23799来说,我们可以将其拆成
11111 11111 1111 111 111 111 111 11 11 \begin{align} 11111 \\ 11111 \\ 1111 \\111\\111\\111\\111\\11\\11 \end{align} 111111111111111111111111111111
简单来说就是将每一位数字都拆成几个1的形式,因为其是按照竖列形式拆分的,所以正确性是显然的.所以 23799 23799 23799的贡献我们就可以拆分成几个包含k个1的数的相加.我们再来观察这些数该怎么求出来呢.

我们会发现对于第 i i i行的1的个数就等于原数中大于等于 i i i的数的个数.就比如上面的那一个数字的第 4 4 4行,我们发现一共有三个大于4的数,所以第四行1的个数为3. f [ x ] f[x] f[x]为一个数中大于x的数字的个数,那么对于一个排好序的数来说,这个数的贡献就是 ∑ i = 1 9 11...111 ( f [ i ] 个 1 ) \sum_{i=1}^{9}11...111(f[i]个1) i=1911...111(f[i]1)
然后我们又会发现上述的贡献式子对于这个数有没有排序是没有任何影响的,只跟于这个数包含的每一位的数字有关,所以对于任意一个数来说,我们只要看每一位的数即可.

这个时候我们就发现跟数位dp有点关系了.我们可以考虑枚举每一个数字 i i i,然后再来枚举一个数字中恰好有 j j j i i i的情况,然后对于每一种情况,我们使用数位dp来算出这种情况的个数.此时的dp式子的定义又有一点东西需要考虑了.

对于一般的数位dp的套路,我们一般都会定义 d p [ p o s ] [ n u m ] [ s u m ] dp[pos][num][sum] dp[pos][num][sum]为前 p o s pos pos位填好了,且前 p o s pos pos位填的 n u m num num的总个数为 s u m sum sum,之后所有位置随便填最后满足总个数 s u m sum sum等于上述我们枚举的 j j j的数字个数.但是此时我们会发现这种定义是不行的,因为对于每一个 j j j来说,我们最后比较的情况是不一样的,但是上述dp定义并没有考虑这一点.如果硬要使用上述dp定义的话,我们需要在每一次枚举 j j j的时候重新清空一下dp数组.这样的话我们的复杂度就会爆炸了,所以我们得想一个更为优秀的dp方程.

考虑换一种方式来想,我们定义 d p [ p o s ] [ n u m ] [ n e e d ] dp[pos][num][need] dp[pos][num][need]为前 p o s pos pos位填好了,且前 p o s pos pos位填的 n u m num num的总个数距离我们的目标 j j j还差 n e e d need need个数字,之后所有位置随便填最后满足上述情况的数字个数.诶,此时我们就会发现对于每一个 j j j来说,我们就不会有冲突的情况了.因为对于前 p o s pos pos为填了相同的 s u m sum sum,但是此时我们的 n e e d need need是不一样的,但是当我们的 n e e d need need是相同时,显然我们的dp是可以通用的,所以我们并不会出现上述直接返回错误答案的情况.

需要注意need<0的情况,此时不特判数组下标会越界


下面是具体的代码部分:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define root 1,n,1
#define ls rt<<1
#define rs rt<<1|1
#define lson l,mid,rt<<1
#define rson mid+1,r,rt<<1|1
inline ll read() {
	ll x=0,w=1;char ch=getchar();
	for(;ch>'9'||ch<'0';ch=getchar()) if(ch=='-') w=-1;
	for(;ch>='0'&&ch<='9';ch=getchar()) x=x*10+ch-'0';
	return x*w;
}
#define maxn 1000000
#define int long long
const int mod=1e9+7;
const double eps=1e-8;
#define	int_INF 0x3f3f3f3f
#define ll_INF 0x3f3f3f3f3f3f3f3f
int a[maxn];int dp[705][11][705];
int dfs(int pos,int num,int need,int iflead0,int limit) {
	if(pos==0) return need==0; 
	if(!iflead0&&!limit&&dp[pos][num][need]!=-1) 
		return dp[pos][num][need];
	int len=limit?a[pos]:9;
	int ans=0;
	for(int i=0;i<=len;i++) {
		if(iflead0&&i==0) {
			ans+=dfs(pos-1,num,need,1,limit&&i==len);ans%=mod;
		}
		else {
			if(need-(i==num)<0) continue;
			ans+=dfs(pos-1,num,need-(i>=num),0,limit&&i==len);ans%=mod;
		}
	}
	if(!limit&&!iflead0) dp[pos][num][need]=ans;
	return ans;
}
int solve(string x) {
	int pos=0;
	for(int i=0;i<x.length();i++) {
		a[++pos]=x[i]-'0';
	}
	reverse(a+1,a+pos+1);
	int ans=0;
	for(int i=1;i<=9;i++) {
		int now=0;
		for(int j=1;j<=pos;j++) {//枚举恰好有j个数字大于i的方案数
			now=(now*10ll+1ll)%mod;
			ans+=now*dfs(pos,i,j,1,1)%mod;ans%=mod;
		}
	}
	return ans;
}
signed main() {
	string n;cin>>n;
	memset(dp,-1,sizeof dp);
	cout<<solve(n)<<endl;
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值