数位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 数字计数
给定两个正整数 a 和 b,求在 [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,其中有 2 个 0 与 2 个 1。因此,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);elseres+=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 想知道,在 a 和 b 之间,包括 a 和 b ,总共有多少个 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;
}