数位DP
数位DP
与数位有关的 DP.
一般来说,无脑记忆化搜索就完事了.
直接递推不仅细节繁琐,而且还不好想.
记忆化搜索的时候要记录:
1. 当前数位.
2. 是否顶着上界.
3. 是否有前导零.
4. 计数器(为了满足题目中的条件)
luoguP4317 花神的数论题
非常经典的数位 DP.
注意如果想要采取快速幂取模的话在 DP 的时候要开 $\mathrm{long }$ $\mathrm{long}$.
不然得用欧拉定理什么的.
记忆化搜索部分:
// 从高位向低位开始 dp ll dfs(int cur,int d,int one,int tot) { if(cur == 0) { return (one == tot); } if(f[cur][d][one][tot] != -1) return f[cur][d][one][tot]; int lim = d ? a[cur] : 1; f[cur][d][one][tot] = 0 ; for(int i = 0 ; i <= lim ; ++ i) { f[cur][d][one][tot] += dfs(cur - 1, d && (i == lim), one + (i == 1), tot); } return f[cur][d][one][tot]; }
完整代码:
#include <cstdio> #include <vector> #include <cstring> #include <algorithm> #define N 60 #define ll long long #define pb push_back #define setIO(s) freopen(s".in","r",stdin) using namespace std; const int mod=10000007; ll f[N][2][N][N], ans[N]; int a[N], cnt; int qpow(int x,ll y) { int tmp=1; for(;y;y>>=1,x=(ll)x*x%mod) { if(y&1) tmp=(ll)tmp*x%mod; } return tmp; } // 从高位向低位开始 dp ll dfs(int cur,int d,int one,int tot) { if(cur == 0) { return (one == tot); } if(f[cur][d][one][tot] != -1) return f[cur][d][one][tot]; int lim = d ? a[cur] : 1; f[cur][d][one][tot] = 0 ; for(int i = 0 ; i <= lim ; ++ i) { f[cur][d][one][tot] += dfs(cur - 1, d && (i == lim), one + (i == 1), tot); } return f[cur][d][one][tot]; } int main() { // setIO("input"); memset(f, -1, sizeof(f)); ll n; scanf("%lld",&n); while(n) { a[++cnt] = n & 1; n >>= 1; } for(int i=1;i<=50;++i) { ans[i] = dfs(cnt, 1, 0, i); } int re = 1; for(int i=1;i<=50;++i) { re = (ll)re * qpow(i, ans[i]) % mod; } printf("%d\n",re); return 0; }
luoguP2657 [SCOI2009] windy 数
同样也是十分经典的数位 DP.
不过在这道题中就要记录是否有前导零.
因为如果有前导零的话第一位填入什么数字是没有限制的.
要注意每次算的时候要将数组什么的清空.
#include <cstdio> #include <vector> #include <cstring> #include <algorithm> #define N 12 #define ll long long #define pb push_back #define setIO(s) freopen(s".in","r",stdin) using namespace std; int f[N][N][2][2], a[N], cnt; // 从高位向低位. int dfs(int cur, int num, int d, int l) { if(cur == 0) { return 1; } if(f[cur][num][d][l] != -1) { return f[cur][num][d][l]; } int lim = d ? a[cur] : 9; int re = 0; for(int i = 0 ; i <= lim ; ++ i) { if(!l) { // 无前导 0. // 即前面至少有一个数不是 0. if(abs(i - num) >= 2) re += dfs(cur - 1, i, d && (i == lim), l); } else { // 含有前导 0. re += dfs(cur - 1, i, d && (i == lim), l && (i == 0)); } } return f[cur][num][d][l] = re; } int solve(int x) { cnt = 0 ; memset(f, -1, sizeof(f)); while(x) { a[++cnt] = x % 10; x /= 10; } // 处理完毕 a. // 1: 含有前导 0. return dfs(cnt, 0, 1, 1); } int main() { // setIO("input"); int L, R; scanf("%d%d",&L,&R); printf("%d\n",solve(R) - solve(L - 1)); return 0; }
luoguP2602 [ZJOI2010]数字计数
令 $\mathrm{f(i,d,l,num)}$ 表示位数,是否顶上界,是否有前导0,统计数字.
令 $\mathrm{g(i,d,l)}$ 表示位数,是否顶上界,是否有前导 0 的方案数.
我们要求 $\mathrm{f}$, 但在统计的时候要用到 $\mathrm{g}$.
先记忆化搜索出所有的 $\mathrm{g}$, 再来统计 $\mathrm{f}$ 即可.
#include <cstdio> #include <cstring> #include <vector> #include <algorithm> #define pb push_back #define ll long long #define N 20 #define setIO(s) freopen(s".in","r",stdin) using namespace std; // f 数组: // g 数组:方案数. ll f[N][2][2][10], g[N][2][2]; int a[N], cnt; // 计算 g 数组,即多少个数字满足这个条件. // 即 cur 位,是否顶上界,是否有前导 0. ll dfs1(int cur,int d,int l) { if(cur == 0) { return g[cur][d][l] = 1; } if(g[cur][d][l] != -1) { return g[cur][d][l]; } g[cur][d][l] = 0; ll re = 0; int lim = d ? a[cur] : 9; for(int i = 0 ; i <= lim ; ++ i) { re += dfs1(cur - 1, d && (i == lim), l && (i == 0)); } return g[cur][d][l] = re; } ll dfs2(int cur,int d,int l,int num) { if(cur == 0) { return 0; } if(f[cur][d][l][num] != -1 ) return f[cur][d][l][num]; int lim = d ? a[cur] : 9; f[cur][d][l][num] = 0; ll re = 0; for(int i = 0 ; i <= lim ; ++ i) { ll det; if(l == 0) { // 无前导 0. det = dfs2(cur-1, d&&(i==lim),0,num) + (i==num)*g[cur-1][d&&(i==lim)][0]; } else { if(num) { det = dfs2(cur-1,d&&(i==lim),l&&(i==0),num)+(i==num)*g[cur-1][d&&(i==lim)][l&&(i==0)]; } else { det = dfs2(cur-1,d&&(i==lim),l&&(i==0),num); } } re += det; } return f[cur][d][l][num] = re; } ll solve(ll x, int num) { memset(f, -1, sizeof(f)); memset(g, -1, sizeof(g)); cnt = 0; while(x) { a[++cnt] = x % 10; x /= 10; } // 初始:有前导 0,且有上界. dfs1(cnt, 1, 1); return dfs2(cnt,1,1,num); } int main() { // setIO("input"); ll L, R; scanf("%lld%lld",&L,&R); for(int i=0;i<=9;++i) { printf("%lld ", solve(R, i) - solve(L - 1, i)); } return 0; }
luoguP4124 [CQOI2016]手机号码
细节较前两道题多了一点.
由于题目中规定了 $\mathrm{11}$ 位数,所以可以先枚举最高位.
这样就无需考虑前导 $0$ 的问题了.
剩下的就是大力进行数位 $\mathrm{DP}$ 了.
#include <cstdio> #include <cstring> #include <vector> #include <algorithm> #define N 13 #define pb push_back #define ll long long #define setIO(s) freopen(s".in","r",stdin) using namespace std; int cnt, a[N]; /* j: cur + 1 位. k: cur + 2 位. d: 是否顶着上界. sta: 4/8 的状态. o: 是否有 3 个相邻的状态. cur : 当前还没有装呢. */ ll f[N][N][N][2][N][2]; ll dfs(int cur,int j,int k,int d,int sta,int o) { if(cur == 0) { return (o == 1); } if(f[cur][j][k][d][sta][o] != -1) { return f[cur][j][k][d][sta][o]; } f[cur][j][k][d][sta][o] = 0; ll re = 0; int lim = d ? a[cur] : 9; // 向下一个放置 nex. for(int nex = 0; nex <= lim ; ++ nex) { if(sta == 4 && nex == 8) continue; if(sta == 8 && nex == 4) continue; int pp = 0; if(sta == 4 || nex == 4) pp = 4; if(sta == 8 || nex == 8) pp = 8; re += dfs(cur - 1, nex, j, d && (nex == lim), pp, o || (j == k && k == nex)); } return f[cur][j][k][d][sta][o] = re; } ll calc(ll x) { cnt = 0 ; while(x) { a[++cnt] = x % 10; x /= 10; } ll re = 0ll; for(int i = 1; i <= a[cnt] ; ++ i) { int j = 0; if(i == 4) j = 4; if(i == 8) j = 8; memset(f, -1, sizeof(f)); re += dfs(10, i, 0, (i == a[cnt]), j, 0); } return re; } int main() { // setIO("input"); ll L, R; scanf("%lld%lld",&L,&R); if(L == (ll)1e10) { printf("%lld\n",calc(R)); } else { printf("%lld\n",calc(R) - calc(L - 1)); } return 0; }
Round Numbers 圆环数
来源:bzoj1662. [Usaco2006 Nov]Round Numbers 圆环数
和前面的题一样,用记忆化搜索解决.
数位 DP 的题用记忆化搜索不仅简单,而且非常好理解.
#include <cstdio> #include <cstring> #include <algorithm> #include <vector> #define N 35 #define ll long long #define pb push_back #define setIO(s) freopen(s".in","r",stdin) using namespace std; int a[N], cnt, f[N][2][N][N][2]; /* cur: 当前位. d: 是否顶着上界. zero: 0 的个数. one: 1 的个数 l : 是否有前导 0. */ int dfs(int cur, int d, int zero, int one, int l) { if(cur == 0) { return zero >= one; } if(f[cur][d][zero][one][l] != -1) { return f[cur][d][zero][one][l]; } f[cur][d][zero][one][l] = 0; int re = 0; int lim = d ? a[cur] : 1; for(int i = 0; i <= lim ; ++ i) { if(l == 0) { // 并没有前导 0. re += dfs(cur - 1, d && (i == lim), zero + (i == 0), one + (i == 1), 0); } else { re += dfs(cur - 1, d && (i == lim), zero, one + (i == 1), l && (i == 0)); } } return f[cur][d][zero][one][l] = re; } int solve(int x) { cnt = 0; while(x) { a[++ cnt] = x & 1; x >>= 1; } memset(f, -1, sizeof(f)); return dfs(cnt, 1, 0, 0, 1); } int main() { // setIO("input"); int L, R; scanf("%d%d",&L,&R); //printf("%d\n", solve(R)); printf("%d\n", solve(R) - solve(L - 1)); return 0; }