数位DP

更新日志 2025/01/05:开工。

2025/04/07:之前写了和没写一样,重构。


概念

一种按数位进行DP的方式。一般情况下,我们从高位到低位DP。

问题

通常解决计数问题,给出一个数据范围,找范围内满足要求的数个数。这个要求与数位有关。

解决

我们解决在 \([0,x]\) 前缀区间内满足要求的数,然后左右端点一减即可得到答案。

转化为前缀问题后,实现就会简单很多,因为你无需考虑下界。

常见形式

我们考虑每一位进行枚举,这时候我们就遇到了一个问题:枚举的上界在哪里?

我们要找的数必须要 \(\le x\),如果之前的几位都与 \(x\) 一模一样,那么这一位就不能超出 \(x\) 的当前位。否则,\([0,9]\) 之内随便取。因此我们要记录当前是否要抵着上界。

因此,我们的搜索函数必须的两个参数是当前所在位数以及是否抵着上界。除此之外,就是具体的要求有关的参数,比如前导零、上一位数等等。

关于优化,我们考虑记忆化,当前不抵着上界且其他参数相同时答案一定。

例题

windy 数

我们需要的额外参数为:前面是否全为零(要求没有前导零,因此这一位没有额外限制)以及上一位数是什么(满足额外限制)。

详见代码注释。

int num[11];//开个数组存数,方便查询当前位
int f[11][2][10];
int dfs(int now,int lim,int zer,int lst){
	if(!now)return 1;//搜到一个答案直接返回。
	if(!lim&&~f[now][zer][lst])return f[now][zer][lst];//记忆化
	int res=0;
	rep(i,0,lim?num[now]:9)//看是不是抵着开
		if(zer||abs(i-lst)>=2)res+=dfs(now-1,lim&&i==num[now],zer&&!i,i);
		//之前抵着开这一位还抵着上限那么下一位也要抵着开,前面全为0这一位也为0那么下一位还是前导零
	if(!lim)f[now][zer][lst]=res;
	return res;
}

int getans(int x){
	int len=0;
	while(x)num[++len]=x%10,x/=10;
	return dfs(len,1,1,0);//最高位开始,第一位数抵着,前面是前导零忽视限制,上一位可以随便传
}

int main(){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	memset(f,-1,sizeof(f));
	int a,b;cin>>a>>b;
	cout<<getans(b)-getans(a-1)<<"\n";
	return 0;
}

同类分布

额外传参当前各位数之和与产生的数。

这时候发现产生的数特别大,无法储存。那么考虑提前取模各位数之和即可。

但各位数之和在变化,所以我们考虑枚举各位数之和。

code
int num[19];
ll f[19+1][9*19+1][9*19];
ll dgdp(int now,int lim,int sum,int nnm,int mod){
	if(!now)return sum==mod&&!nnm;
	if(!lim&&~f[now][sum][nnm])return f[now][sum][nnm];
	ll res=0;
	rep(i,0,lim?num[now]:9)res+=dgdp(now-1,lim&&i==num[now],sum+i,(nnm*10%mod+i)%mod,mod);
	if(!lim)f[now][sum][nnm]=res;
	return res;
}

ll getans(ll x){
	int len=0;
	while(x)num[++len]=x%10,x/=10;
	ll res=0;
	rep(i,1,9*19){
		memset(f,-1,sizeof(f));
		res+=dgdp(len,1,0,0,i);
	}
	return res;
}

int main(){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	ll a,b;cin>>a>>b;
	cout<<getans(b)-getans(a-1);
	return 0;
}

杠杆数

类似于上一道题,我们枚举支点。

额外储存当前力矩和和支点即可,当和小于零时无法存入数组,不想两倍空间的话,先加后减,则负数必然无解。

每个数只会有一个支点,但 \(0\) 会被判多次,故而最后应当减去 \(0\) 多次贡献。

code
ll num[19+1];
ll f[19+1][19*9*19+1][19+1];
ll dgdp(int now,int lim,int sum,int mid){
	if(!now)return sum==0;
	if(sum<0)return 0;
	if(!lim&&~f[now][sum][mid])return f[now][sum][mid];
	ll res=0;
	rep(i,0,lim?num[now]:9)res+=dgdp(now-1,lim&&num[now]==i,sum+i*(now-mid),mid);
	if(!lim)f[now][sum][mid]=res;
	return res;
}

ll getans(ll x){
	int len=0;
	while(x)num[++len]=x%10,x/=10;
	ll res=0;
	rep(i,1,len)res+=dgdp(len,1,0,i);
	return res-len+1;
}

int main(){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	memset(f,-1,sizeof(f));
	ll x,y;cin>>x>>y;
	cout<<getans(y)-getans(x-1);
	return 0;
}

Odometer S

很多细节的一道题吧。

同样的,我们考虑枚举 \(\ge \frac{len}{2}\) 的那个数是什么。

值得注意的是,这个 \(len\) 并不是上界的长度,而是这个数的实际长度,所以我们要处理前导零。

然后我们发现,可能同时存在两个数 \(\ge \frac{len}{2}\),所以容斥一下,枚举这两个数即可。

可以优化的一点是,上述情况发生时那个数长度为偶,且只含有那两个数,所以我们可以在 DP 时只转移那两个数的情况和前导零

此外记得特殊处理一下 \(0\),当然有的写法可能不用处理。

我的代码里并没有判断数的长度奇偶性。

code
int num[19+1];
ll f[19+1][2][19+1+19][10];
ll dgdp(int now,int lim,int zer,int ned,int std){
	if(!now)return ned<=0;
	if(!lim&&~f[now][zer][ned+19][std])return f[now][zer][ned+19][std];
	ll res=0;
	rep(i,0,lim?num[now]:9)res+=dgdp(now-1,lim&&i==num[now],zer&&!i,((zer&&i)?(now+1>>1):ned)-(i==std&&!(zer&&!i)),std);
	if(!lim)f[now][zer][ned+19][std]=res;
	return res;
}

ll f2[19+1][2][19+1+19][19+1+19][10][10];
ll dgdp2(int now,int lim,int zer,int ned1,int std1,int ned2,int std2){
	if(!now)return ned1<=0&&ned2<=0;
	if(!lim&&~f2[now][zer][ned1+19][ned2+19][std1][std2])return f2[now][zer][ned1+19][ned2+19][std1][std2];
	ll res=0;
	rep(i,0,lim?num[now]:9)if(i==std1||i==std2||!i)res+=dgdp2(now-1,lim&&i==num[now],zer&&!i,((zer&&i)?(now+1>>1):ned1)-(i==std1&&!(zer&&!i)),std1,((zer&&i)?(now+1>>1):ned2)-(i==std2&&!(zer&&!i)),std2);
	if(!lim)f2[now][zer][ned1+19][ned2+19][std1][std2]=res;
	return res;
}

ll getans(ll x){
	int len=0;
	while(x)num[++len]=x%10,x/=10;
	ll res=1;
	rep(i,0,9)res+=dgdp(len,1,1,1,i);
	rep(i,0,9)rep(j,i+1,9)res-=dgdp2(len,1,1,1,i,1,j);
	return res;
}

int main(){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	memset(f,-1,sizeof(f));
	memset(f2,-1,sizeof(f2));
	ll x,y;cin>>x>>y;
	cout<<getans(y)-getans(x-1);
	return 0;
}
posted @ 2025-01-05 19:46  LastKismet  阅读(14)  评论(2)    收藏  举报