数位DP

数位dp

我从就只用记忆化写数位dp, 随便写写, 因为某天突然不会写了

首先数位dp一般会把 \([l,r]\) 拆成 \([1,r]\)\([1,l-1]\) , 因为不管你问同一个数什么问题, 它的答案一定都是一样的, 我们之后就不提这个事了

怎么dp? 我不会, 我不会, 我不会

但是搜索简单, 我们写记忆化(致敬我有关变量在外边的错误记忆化)

我们那一道题做例子


P4999 烦人的数学作业

题意: [l,r] 每个数的数位和

正常的爆搜, 一位一位填数谁都会, 我们考虑如何把两个一模一样的结构给挖出来, 也就是我们需要记录什么

首先是解决问题必须的, 当前填到哪一位, 数位到现在加到多少

其次我们考虑对于一个情况, 怎么保证搜索树中一个情况的子树完全相同

我们在处理 [1,114514] 时, 假设我们在添第四位, 如果第三为添的是 \(4\) , 我们在第四位便不可以添超过 \(5\) 的数字, 若没有则可以从 \(0\) 填到 \(9\) , 我们需要记录当前位置有没有限制, 这个是所有数位dp都需要的

关于前导零, 这个问题求的是和, 所以不用考虑, 但做题的时候记得研究会不会造成不同

这样我们就在搜索树中, 一模一样的子树我们只需要计算一次, 大大加速了我们的算法

具体到代码中看一看吧


\(dfs\) 函数

int dfs(int pos, bool limit, int sum){
    if(!pos) return sum;//如果填完就返回当前值
    if(!limit&&dp[pos][sum]) return dp[pos][sum]; //若有搜索过一模一样的返回搜过的值
    int up=(limit?a[pos]:9), res=0;//确定枚举填数的上限
    for(int i=0; i<=up; ++i){
        res=(res+dfs(pos-1,i==up&&limit, sum+i))%mod;//直接继续, limit和sum继承当前
    }
    if(!limit) dp[pos][sum]=res%mod;//保存子树
    return res%mod;//返回
}

其实我们同样可以将我们的 \(limit\) 记作 \(dp\) 中的一维, 只是记录没什么大用罢了


之后放一下整体代码

#include <bits/stdc++.h>
#define int long long
using namespace std;
int dp[20][10000];
const int mod=1e9+7;
int a[20], len;
int dfs(int pos, bool limit, int sum){
    if(!pos) return sum;
    if(!limit&&dp[pos][sum]) return dp[pos][sum];
    int up=(limit?a[pos]:9), res=0;
    for(int i=0; i<=up; ++i){
        res=(res+dfs(pos-1,i==up&&limit, sum+i))%mod;
    }
    if(!limit) dp[pos][sum]=res%mod;
    return res%mod;
}
int solve(int x){
    len=0;
    while(x>0){//分解数字
        a[++len]=x%10;
        x/=10;
    }
    return dfs(len,true,0)%mod;
}
signed main(){
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T; cin>>T;
    while(T--){
        int l, r; cin>>l>>r;
        cout<<(solve(r)-solve(l-1)+mod)%mod<<'\n';
    }
    return 0;
}

P2602 数字计数

给定两个正整数 ab,求在 [a,b] 中的所有整数中,每个数码(digit)各出现了多少次。

这个需要考虑前导零了, 填到 00_ 的时候如果填 \(0\) 总不能记一个贡献吧

其实记搜写这个都大差不差

\(dfs\) 函数

int dfs(int pos, bool limit, bool lead, int sum){
	if(!pos) return sum;
	if(!limit&&!lead&&dp[pos][sum][digit!=0]!=-1) return dp[pos][sum][digit!=0];//0与其他不同,受到前导零的影响
	int res=0, up=limit?a[pos]:9;
	for(int i=0; i<=up; ++i){
		int tmp=sum+(i==digit);
		if(lead&&digit==0&&i==0) tmp=0; //就是刚刚提到的情况
		res+=dfs(pos-1,limit&&i==up,lead&&i==0,tmp);
	}
	if(!limit&&!lead) dp[pos][sum][digit!=0]=res;
	return res;
}

想必也容易看懂


直接放代码↓

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int MN=100;
int dp[MN][MN*MN][2];
int a[MN], len, digit;
int dfs(int pos, bool limit, bool lead, int sum){
	if(!pos) return sum;
	if(!limit&&!lead&&dp[pos][sum][digit!=0]!=-1) return dp[pos][sum][digit!=0];
	int res=0, up=limit?a[pos]:9;
	for(int i=0; i<=up; ++i){
		int tmp=sum+(i==digit);
		if(lead&&digit==0&&i==0) tmp=0;
		res+=dfs(pos-1,limit&&i==up,lead&&i==0,tmp);
	}
	if(!limit&&!lead) dp[pos][sum][digit!=0]=res;
	return res;
}
int cnt[10];
void solve(int x, int opt){
	len=0; while(x){
		a[++len]=x%10; x/=10;
	}
	for(int i=0; i<=9; ++i){
		digit=i; int res=dfs(len,1,1,0);
		cnt[i]+=opt*res;
	}
}
int l, r;
signed main(){
	memset(dp,-1,sizeof(dp));
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); cin>>l>>r;
	solve(r,1); solve(l-1,-1);
	for(int i=0; i<=9; ++i) cout<<cnt[i]<<" ";

	return 0;
}

用记忆化写数位dp, 我们可以直接套用这个模式走

dfs函数:

  • 填完返回贡献
  • 返回相同情况
  • 确定枚举上限
  • 进行下一步搜索
  • 记录当前情况的值

所以这个没什么需要讲的, 真正搞明白一个, 其他的就很好懂了

下边放点题, 没啥好说的, 就说说dfs的细节, 然后给代码了


题目

P6218 # [USACO06NOV] Round Numbers S

如果一个正整数的二进制表示中,0 的数目不小于 1 的数目,那么它就被称为「圆数」。

例如,9 的二进制表示为 1001,其中有 2021。因此,9 是一个「圆数」。

请你计算,区间 [l,r] 中有多少个「圆数」。

我们按照二进制拆数, 肯定需要考虑前导零, 记录 \(0\)\(1\) 的数量, 直接套板子

代码↓

#include <bits/stdc++.h>
using namespace std;
const int MN=1e2+114;
int dp[MN][MN][MN], a[MN];
int dfs(int pos, bool limit, bool lead, int zero, int one){
    if(!pos) return zero>=one;
    if(!lead&&!limit&&dp[pos][zero][one]) return dp[pos][zero][one];
    int res=0, up=limit?a[pos]:1;
    for(int i=0; i<=up; ++i){
        if(lead&&i==0)
            res+=dfs(pos-1,limit&&i==up,1,zero,one);
        else
            res+=dfs(pos-1,limit&&i==up,0,zero+(i==0),one+(i==1));
    }
    if(!lead&&!limit) dp[pos][zero][one]=res;
    return res;
}
int solve(int x){
    int len=0;
    while(x){
        a[++len]=x%2;
        x/=2;
    }
    return dfs(len,1,1,0,0);
}
int main(){
    int l, r;
    cin>>l>>r;
    cout<<solve(r)-solve(l-1);
    return 0;
}

P2657 Windy数

不含前导零且相邻两个数字之差至少为 2 的正整数被称为 windy 数。windy 想知道,在 ab 之间,包括 ab ,总共有多少个 windy 数?

每一次填数合法仅与上一个数有关, 记录上一个数字, 考虑前导零, 套板子

代码↓

#include <iostream>
#define int long long
using namespace std;
int A, B; const int MN=40;
int a[MN];
int dp[MN][MN];
int dfs(int pos, bool limit, bool lead, int last){
	if(!pos) return 1;
	if(!limit&&!lead&&dp[pos][last]) return dp[pos][last];
	int up=limit?a[pos]:9, res=0;
	for(int i=0; i<=up; ++i){
		if(lead&&i==0) res+=dfs(pos-1,limit&&i==up, 1, 0);
		else if(lead||abs(last-i)>=2) res+=dfs(pos-1,limit&&i==up, 0, i);
	}if(!limit&&!lead) dp[pos][last]=res;
	return res;
}
int solve(int x){
	int len=0;
	while(x){
		a[++len]=x%10; x/=10;
	}
	return dfs(len,1,1,0);
}
signed main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); cin>>A>>B;
	cout<<solve(B)-solve(A-1); return 0;
}
posted @ 2025-10-05 10:38  BaiBaiShaFeng  阅读(11)  评论(0)    收藏  举报
Sakana Widget右下角定位