数位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;
}

浙公网安备 33010602011771号