数位DP
嗯,作为一类比较自成一派的DP还是要学一下。
其实不是很难,主要通过记忆化搜索实现。
用例题来讲解吧。
第一道数位DP/洛谷三萌成就达成!
洛谷P3413 萌数
大意:求[l, r]中存在长度至少为2的回文子串的数的个数,对1e9+7取模。
r <= 10^1000
首先可以发现,回文串长度为2或3即可。
然后我们就设计状态:f[k][last][ll][0/1][0/1]表示后k位中,第k + 1位是last,第k + 2位是ll,k位之前是否已出现回文串,k位之前是否出现过非0数(不是全是前导0),满足条件的数的个数。
枚举k位放什么即可。顺便更新一波状态。
注意有limit的时候记忆化无用,要往后算。
1 #include2 #include 3 #include 4 5 const int N = 1010, MO = 1000000007; 6 7 int f[N][11][11][2][3], num[N]; 8 9 int DFS(int k, int last, int ll, int sta, int first, bool limit) {10 if(k == 0) {11 return sta;12 }13 if(!limit && f[k][last][ll][sta][first] != -1) {14 return f[k][last][ll][sta][first];15 }16 int ans = 0;17 int large = limit ? num[k] : 9;18 for(int i = 0; i <= large; i++) {19 if(sta) {20 ans += DFS(k - 1, i, last, sta, first, limit && (i == num[k]));21 ans %= MO;22 }23 else {24 int temp = 0;25 if(first == 1) {26 temp = (i == last);27 }28 else if(first == 2) {29 temp = (i == last) || (i == ll);30 }31 int second = first;32 if(i || first) {33 second = std::min(2, second + 1);34 }35 ans += DFS(k - 1, i, last, temp, second, limit && (i == num[k]));36 ans %= MO;37 }38 }39 if(!limit) {40 f[k][last][ll][sta][first] = ans;41 }42 //printf("%d %d %d %d %d %d = %d \n", k, last, ll, sta, first, limit, ans);43 return ans;44 }45 46 char str[N];47 48 int main() {49 memset(f, -1, sizeof(f));50 51 scanf("%s", str);52 int n = strlen(str);53 for(int i = n - 1; i >= 0; i--) {54 num[n - i] = str[i] - '0';55 }56 int temp = 0;57 if(n > 1 || num[1]) {58 num[1]--;59 int t = 1;60 while(num[t] < 0) {61 num[t] += 10;62 num[t + 1]--;63 t++;64 }65 if(!num[n]) {66 n--;67 }68 temp = DFS(n, 10, 10, 0, 0, 1);69 }70 71 scanf("%s", str);72 n = strlen(str);73 for(int i = n - 1; i >= 0; i--) {74 num[n - i] = str[i] - '0';75 }76 77 int ans = DFS(n, 10, 10, 0, 0, 1) - temp;78 ans = (ans % MO + MO) % MO;79 printf("%d", ans);80 return 0;81 }
洛谷P2657 windy数
大意:求[l, r]之中每两个相邻数码之差大于1的数的个数。
这还比前一道题水一点......
f[k][last][0/1]表示后k位,第k + 1位是last,k位之前是否全是0的满足条件的数的个数。
然后就过了。
1 #include2 #include 3 #include 4 5 const int N = 20; 6 7 int f[N][10][2], num[N]; 8 9 int DFS(int k, int last, int sta, bool limit) {10 if(k == 0) {11 return 1;12 }13 if(!limit && f[k][last][sta]) {14 return f[k][last][sta];15 }16 17 int ans = 0;18 int large = limit ? num[k] : 9;19 for(int i = 0; i <= large; i++) {20 if(sta) {21 if(i <= last + 1 && i >= last - 1) {22 continue;23 }24 ans += DFS(k - 1, i, sta, limit && (i == num[k]));25 }26 else {27 ans += DFS(k - 1, i, i > 0, limit && (i == num[k]));28 }29 }30 31 if(!limit) {32 f[k][last][sta] = ans;33 }34 //printf("%d %d %d %d %d \n", k, last, sta, limit, ans);35 return ans;36 }37 38 inline int solve(int x) {39 if(!x) {40 return 1;41 }42 int n = 0;43 while(x) {44 num[++n] = x % 10;45 x /= 10;46 }47 return DFS(n, 0, 0, 1);48 }49 50 int main() {51 52 int a, b;53 scanf("%d%d", &a, &b);54 printf("%d", solve(b) - solve(a - 1));55 56 return 0;57 }
注意有一个细节:这两题(大部分)中,sta状态(就是之前是否全是0)其实可以只在DFS函数中传,而不在f数组中多开一维。
原理我还没想清楚呢......闲的没事还是加上好了,避免出锅。
洛谷P2518 计数
比较特立独行的一道题.....
大意:给你若干个数码,总和<=50,让你求比给定的某一个全排列字典序小的全排列数。
保证答案在long long内。
就相当于给定每个数个数的数位DP。
先开个桶。可以发现,按照上面的套路的话,!limit的时候可以直接用公式。
然后就直接过了......我的map其实没啥用,就比暴力快了0.1ms........
至于统计过程中爆long long...大家各显神通吧。我是分解质因数。也可以拆成若干个组合数相乘,避免除法。
1 #include2 #include
洛谷P2602 数字计数
大意:求在[l, r]之中每个数码出现了几次。
首先简化它,对每个数码分别做。
这里的状态是f[k][0/1]表示在后k位是否中含有数码aim的个数。aim是枚举的。
然后我们发现,如果我们当前选的数码恰好是aim,那么额外加上后面的总个数就行了。
1 #include2 #include 3 4 typedef long long LL; 5 const int N = 20; 6 7 LL f[N][2], pw[N], s; 8 int num[N], aim; 9 10 LL DFS(int k, int sta, bool limit) {11 if(k == 0) {12 return sta == 0 && aim == 0;13 }14 if(!limit && f[k][sta] != -1) {15 return f[k][sta];16 }17 LL ans = 0;18 int large = limit ? num[k] : 9;19 for(int i = 0; i <= large; i++) {20 if(sta) {21 ans += DFS(k - 1, sta, limit && (i == num[k]));22 }23 else {24 ans += DFS(k - 1, i > 0, limit && (i == num[k]));25 }26 if(i == aim && (sta || i)) {27 if(limit && i == num[k]) {28 ans += s % pw[k - 1] + 1;29 }30 else {31 ans += pw[k - 1];32 }33 }34 }35 if(!limit) {36 f[k][sta] = ans;37 }38 return ans;39 }40 41 inline LL solve(LL x) {42 if(!x) {43 return !aim;44 }45 int n = 0;46 s = x;47 while(x) {48 num[++n] = x % 10;49 x /= 10;50 }51 return DFS(n, 0, 1);52 }53 54 int main() {55 LL a, b;56 scanf("%lld%lld", &a, &b);57 pw[0] = 1;58 for(int i = 1; i <= 15; i++) {59 pw[i] = pw[i - 1] * 10;60 }61 62 for(int i = 0; i <= 9; i++) {63 aim = i;64 memset(f, -1, sizeof(f));65 printf("%lld ", solve(b) - solve(a - 1));66 }67 return 0;68 }
参考资料:红太阳的博客。
总结一下:基本都是套路......做的都是板题.......
哇!他们考的都好难啊.....要命哇...
:一个比较独特的数位DP。