浅谈数位DP

时间:2022-06-21 05:15:53

在了解数位dp之前,先来看一个问题:

例1.求a~b中不包含49的数的个数. 0 < a、b < 2*10^9

注意到n的数据范围非常大,暴力求解是不可能的,考虑dp,如果直接记录下数字,数组会开不起,该怎么办呢?要用到数位dp.

数位dp一般应用于:

求出在给定区间[A,B]内,符合条件P(i)的数i的个数.

条件P(i)一般与数的大小无关,而与 数的组成 有关.

这样,我们就要考虑一些特殊的记录方法来做这道题.一般来说,要保存给定数的每个位置的数.然后要记录的状态为当前操作数的位数,剩下的就是根据题目的需要来记录.可以发现,数位dp的题做法一般都差不多,只是定义状态的不同罢了.

下面开始针对例题进行分析:

我们要求[a,b]不包含49的数的个数,可以想到利用前缀和来做,具体来说,就是[a,b] = [0,b] - [0,a),(")"是不包括a),我们先求出给定a,b的每个位置的数,保存在数组s中,例如a = 109,那么a[1] = 9,a[2] = 0,a[3] = 1.然后开始dp,我们可以选择记忆化搜索或者是递推,前一种相对于第二种而言简单和较为容易理解一些,所以我们选择记忆化搜索.那么需要记录些什么呢?首先长度是一定要记录的,然后记录当前的数位是否为4,这样就便于在记忆化搜索中得到答案.

然后进行记忆化搜索,记录上一位是否为4和枚举这一位,如果没有限制的话很好办,直接枚举就可以了,但是这样可能会超空间,因此我们每次都必须要判断是否有最大的限制,这里不是很好说,看代码更容易理解:

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm> using namespace std; int a, b, shu[], dp[][]; int dfs(int len, bool if4, bool shangxian)
{
if (len == )
return ;
if (!shangxian && dp[len][if4]) //为什么要返回呢?可以画图理解当我们搜到3XXX时,程序运行到1XXX时就已经把3XXX之后的搜索完了,记忆化也是这个用意.
return dp[len][if4];
int cnt = , maxx = (shangxian ? shu[len] : );
for (int i = ; i <= maxx; i++)
{
if (if4 && i == )
continue;
cnt += dfs(len - , i == , shangxian && i == maxx); //只有之前有限制现在的达到了上限才能构成限制
}
return shangxian ? cnt : dp[len][if4] = cnt; //如果有限制,那么就不能记忆化,否则记忆的是个错误的数.
} int solve(int x)
{
memset(shu, , sizeof(shu));
int k = ;
while (x)
{
shu[++k] = x % ; //保存a,b的数
x /= ;
}
return dfs(k, false, true);
} int main()
{
scanf("%d%d", &a, &b);
printf("%d\n", solve(b) - solve(a - )); //while (1);
return ;
}

再来看一道题:例题2.求a~b中不包含62和4的数的个数. 0 < a、b < 2*10^9

分析:和上一题一样,只需要再判断一下4是否出现和上一位是否为6即可.

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm> using namespace std; int a, b,shu[],dp[][]; int dfs(int len, bool if6, bool shangxian)
{
if (len == )
return ;
if (!shangxian && dp[len][if6])
return dp[len][if6];
int cnt = , maxx = (shangxian ? shu[len] : );
for (int i = ; i <= maxx; i++)
{
if (i == || if6 && i == )
continue;
cnt += dfs(len - , i == , shangxian && i == maxx);
}
return shangxian ? cnt : dp[len][if6] = cnt;
} int solve(int x)
{
memset(shu, , sizeof(shu));
int k = ;
while (x)
{
shu[++k] = x % ;
x /= ;
}
return dfs(k, false, true);
} int main()
{
scanf("%d%d", &a, &b);
printf("%d\n", solve(b) - solve(a - )); return ;
}

例题3:bzoj1026 windy数

对于这道题,我写了一个较为详细的题解:传送门

例题4:找出1~n范围内含有13并且能被13整除的数字的个数.

分析:和例1相比多了一个整除,怎么处理呢?其实只需要在记忆化搜索中增加一个参数mod即可,利用(a * b) % mod = (a % mod) * (b % mod)和(a + b) % mod = (a % mod) + (b % mod)来计算.比如说73 % 10 = ((7 % 10) * 10 + 3) % 10,但要注意本题是要找含有13的数,那么在处理的时候就要进行分类讨论.

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm> using namespace std; int n, shu[], dp[][][]; int dfs(int len, int mod, int zhuangtai, bool shangxian)
{
if (len == )
return mod == && zhuangtai == ;
if (!shangxian && dp[len][mod][zhuangtai])
return dp[len][mod][zhuangtai];
int cnt = , maxx = (shangxian ? shu[len] : );
for (int i = ; i <= maxx; i++)
{
int tz = zhuangtai;
if (zhuangtai != && i != )
tz = ;
if (zhuangtai == && i == )
tz = ;
if (i == && zhuangtai != )
tz = ;
cnt += dfs(len - , (mod * 10 + i) % 13, tz, shangxian && i == maxx);
}
if (!shangxian)
dp[len][mod][zhuangtai] = cnt;
return cnt;
} int main()
{
while (~scanf("%d", &n))
{
memset(shu, , sizeof(shu));
memset(dp, , sizeof(dp));
int k = ;
while (n)
{
shu[++k] = n % ;
n /= ;
}
printf("%d\n", dfs(k, , , ));
} return ;
}

例题5:找出区间内平衡数的个数,所谓的平衡数,就是以这个数字的某一位为支点,另外两边的数字大小乘以力矩之和相等,即为平衡数。

分析:对于这道题,如果一个数中每个数位到支点的距离*这个数位的和为0,那么这个数为平衡数.这样我们定义状态就要考虑力矩和和支点.支点可以在dfs前枚举得到,力矩和可以在处理每个数位的时候得到.但是这个算法是有缺陷的,例如0000,000000也会被统计,我们只需要减去给定范围0全是0的数的个数即可.这里可以进行一个小小的优化,如果力矩和已经为负数,说明已经处理到了支点左边,接着处理下去绝对会小于0,那么回溯即可.

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm> using namespace std; int t;
long long dp[][][];
long long l, r;
int shu[]; long long dfs(int len, int zhidian, int liju, bool shangxian)
{
if (len == )
return liju == ;
if (liju < )
return ;
if (!shangxian && dp[len][zhidian][liju])
return dp[len][zhidian][liju];
long long cnt = ;
int maxx = (shangxian ? shu[len] : );
for (int i = ; i <= maxx; i++)
{
int temp = liju;
temp += (len - zhidian) * i;
cnt += dfs(len - , zhidian, temp, shangxian && i == maxx);
}
if (!shangxian)
dp[len][zhidian][liju] = cnt;
return cnt;
} long long solve(long long x)
{
int k = ;
while (x)
{
shu[++k] = x % ;
x /= ;
}
long long ans = ;
for (int i = ; i <= k; i++)
ans += dfs(k, i, , );
return ans - (k - );
} int main()
{
scanf("%d", &t);
memset(dp, , sizeof(dp));
while (t--)
{
scanf("%lld%lld", &l, &r);
printf("%lld\n", solve(r) - solve(l - ));
} //while (1);
return ;
}

例题6:

Round Numbers
Time Limit: 2000MS   Memory Limit: 65536K
Total Submissions: 14628   Accepted: 5878

Description

The cows, as you know, have no fingers or thumbs and thus are unable to play Scissors, Paper, Stone' (also known as 'Rock, Paper, Scissors', 'Ro, Sham, Bo', and a host of other names) in order to make arbitrary decisions such as who gets to be milked first. They can't even flip a coin because it's so hard to toss using hooves.

They have thus resorted to "round number" matching. The first cow picks an integer less than two billion. The second cow does the same. If the numbers are both "round numbers", the first cow wins,
otherwise the second cow wins.

A positive integer N is said to be a "round number" if the binary representation of N has as many or more zeroes than it has ones. For example, the integer 9, when written in binary form, is 1001. 1001 has two zeroes and two ones; thus, 9 is a round number. The integer 26 is 11010 in binary; since it has two zeroes and three ones, it is not a round number.

Obviously, it takes cows a while to convert numbers to binary, so the winner takes a while to determine. Bessie wants to cheat and thinks she can do that if she knows how many "round numbers" are in a given range.

Help her by writing a program that tells how many round numbers appear in the inclusive range given by the input (1 ≤ Start < Finish ≤ 2,000,000,000).

Input

Line 1: Two space-separated integers, respectively Start and Finish.

Output

Line 1: A single integer that is the count of round numbers in the inclusive range Start..Finish

Sample Input

2 12

Sample Output

6

Source

题目大意:求区间[a,b]中的数转化为二进制后0比1多的数的个数.
分析:典型的数位dp题,先在二进制上做dp,最后转化到十进制上.求出[0,b]和[0,a-1]的答案,相减就可以了.
      一个坑点:二进制数必须要存在,也就是说必须要有一个1开头.
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm> using namespace std; typedef long long ll; ll a, b, num[], cnt, f[][][]; ll dfs(ll len, ll num1, ll num0, bool limit,bool can)
{
if (len == )
{
if (num0 >= num1)
return ;
return ;
}
if (!limit && f[len][num1][num0])
return f[len][num1][num0];
ll cntt = , shangxian = (limit ? num[len] : );
for (int i = ; i <= shangxian; i++)
{
if (i == )
{
if (can)
cntt += dfs(len - , num1, num0 + , (limit && i == shangxian), can || i == );
else
cntt += dfs(len - , num1, num0, (limit && i == shangxian), can || i == );
}
if (i == )
cntt += dfs(len - , num1 + , num0, (limit && i == shangxian),can || i == );
}
if (!limit)
return f[len][num1][num0] = cntt;
else
return cntt;
} ll solve(ll x)
{
memset(num, , sizeof(num));
cnt = ;
while (x)
{
num[++cnt] = x % ;
x /= ;
}
return dfs(cnt, , , true,false);
} int main()
{
scanf("%lld%lld", &a, &b);
printf("%lld\n", solve(b) - solve(a - ));
return ;
}

例题7(一道noip模拟赛题):

浅谈数位DP

分析:显然是一道数位dp题,不过需要一些奇怪的姿势.常规的数位dp能统计出一个区间内满足条件的数的个数,可是我们要求第k个,怎么办呢?转化为经典的二分问题,我们二分当前数的大小,看它是第几大的,就可以了.

显然数位dp套上模板,再用上kmp的next数组就可以了,传递4个参数:还剩下多少位没有匹配,匹配了多少位,是否达到上限和是否匹配成功,到最后判断一下就可以了.

学到了一种很强的思想:如果能求出i是第几个数,要求出第k个数就可以二分i的值.

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm> using namespace std; long long L, R, K, f[][][];
int m, nextt[], a[];
char X[]; void init()
{
nextt[] = ;
nextt[] = ;
for (int i = ; i < m; i++)
{
int j = nextt[i];
while (j && X[i] != X[j])
j = nextt[j];
nextt[i + ] = X[i] == X[j] ? j + : ;
}
} long long dfs(int len, int w, bool limit, bool flag)
{
if (len == )
return flag;
if (!limit && f[len][w][flag] != -)
return f[len][w][flag];
int maxn = limit ? a[len] : ;
long long cnt = ;
for (int i = ; i <= maxn; i++)
{
int t = w;
while (t && X[t] - '' != i)
t = nextt[t];
if (X[t] - '' == i)
t++;
cnt += dfs(len - , t, limit && (i == a[len]), flag || (t == m));
}
return limit ? cnt : f[len][w][flag] = cnt;
} long long query(long long u)
{
int cnt = ;
while (u)
{
a[++cnt] = u % ;
u /= ;
}
memset(f, -, sizeof(f));
return dfs(cnt, , , );
} int main()
{
scanf("%lld %lld %s %lld", &L, &R, X, &K);
m = strlen(X);
init();
if (query(R) < K + query(L - ))
{
printf("Hey,wake up!\n");
return ;
}
long long t = query(L - ),ans = L;
long long l = L, r = R;
while (l < r)
{
long long mid = (l + r) >> ;
if (query(mid) - t >= K)
{
r = mid;
ans = mid;
}
else
l = mid + ;
}
printf("%lld\n", r); return ;
}

例题8:

XHXJ's LIS

Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)
Total Submission(s): 3649    Accepted Submission(s): 1521

Problem Description
#define xhxj (Xin Hang senior sister(学姐)) 
If you do not know xhxj, then carefully reading the entire description is very important.
As the strongest fighting force in UESTC, xhxj grew up in Jintang, a border town of Chengdu.
Like many god cattles, xhxj has a legendary life: 
2010.04, had not yet begun to learn the algorithm, xhxj won the second prize in the university contest. And in this fall, xhxj got one gold medal and one silver medal of regional contest. In the next year's summer, xhxj was invited to Beijing to attend the astar onsite. A few months later, xhxj got two gold medals and was also qualified for world's final. However, xhxj was defeated by zhymaoiing in the competition that determined who would go to the world's final(there is only one team for every university to send to the world's final) .Now, xhxj is much more stronger than ever,and she will go to the dreaming country to compete in TCO final.
As you see, xhxj always keeps a short hair(reasons unknown), so she looks like a boy( I will not tell you she is actually a lovely girl), wearing yellow T-shirt. When she is not talking, her round face feels very lovely, attracting others to touch her face gently。Unlike God Luo's, another UESTC god cattle who has cool and noble charm, xhxj is quite approachable, lively, clever. On the other hand,xhxj is very sensitive to the beautiful properties, "this problem has a very good properties",she always said that after ACing a very hard problem. She often helps in finding solutions, even though she is not good at the problems of that type.
Xhxj loves many games such as,Dota, ocg, mahjong, Starcraft 2, Diablo 3.etc,if you can beat her in any game above, you will get her admire and become a god cattle. She is very concerned with her younger schoolfellows, if she saw someone on a DOTA platform, she would say: "Why do not you go to improve your programming skill". When she receives sincere compliments from others, she would say modestly: "Please don’t flatter at me.(Please don't black)."As she will graduate after no more than one year, xhxj also wants to fall in love. However, the man in her dreams has not yet appeared, so she now prefers girls.
Another hobby of xhxj is yy(speculation) some magical problems to discover the special properties. For example, when she see a number, she would think whether the digits of a number are strictly increasing. If you consider the number as a string and can get a longest strictly increasing subsequence the length of which is equal to k, the power of this number is k.. It is very simple to determine a single number’s power, but is it also easy to solve this problem with the numbers within an interval? xhxj has a little tired,she want a god cattle to help her solve this problem,the problem is: Determine how many numbers have the power value k in [L,R] in O(1)time.
For the first one to solve this problem,xhxj will upgrade 20 favorability rate。
Input
First a integer T(T<=10000),then T lines follow, every line has three positive integer L,R,K.(
0<L<=R<263-1 and 1<=K<=10).
Output
For each query, print "Case #t: ans" in a line, in which t is the number of the test case starting from 1 and ans is the answer.
Sample Input
1 123 321 2
Sample Output
Case #1: 139
Author
peterae86@UESTC03
Source
题目大意:一个数k的LIS称作这个数每个数位上的数字的LIS,例如1324的LIS为3. 求[L,R]中有多少个数的LIS = k.
分析:很显然,这是道数位dp题,但是要怎么记录状态呢?
   考虑LIS问题的O(nlogn)做法,每次插入一个数,找第一个大于这个数的数,弹出去,再插入这个数.最后还有多少个数,LIS就是多少。组成LIS的数字只有0~9,所以可以用一个二进制数S来表示状态,如果S的第i位为1,就说明i这个数存在.那么f[i][j]表示处理到第i位,状态为j的个数,这样处理就可以了.
   T比较大,每次初始化f数组的话会超时,能不能不清空呢?考虑到LIS的长度最大就是10,在状态后面再加一维f[i][j][k]表示处理到第i位,状态为j,并且LIS长度为k的个数,这样就不需要每次都初始化f了.
   细节:要排除前导0.
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm> using namespace std; typedef long long ll;
int T,cas;
ll L,R,K,f[][( << ) + ][],num[]; ll calc(ll S,ll &k,ll x)
{
int cur = -;
for (ll i = x; i <= ; i++)
if (S & ( << i))
{
cur = i;
break;
}
if (cur == -)
{
k++;
S |= ( << x);
}
else
{
S ^= ( << cur);
S |= ( << x);
}
return S;
} ll dfs(ll len,ll S,ll k,bool limit,bool flag)
{
if (!len)
return k == K;
if (!limit && f[len][S][K] != -)
return f[len][S][K];
ll maxx = (limit ? num[len] : ),cnt = ;
for (ll i = ; i <= maxx; i++)
{
ll tempk = k,tempS;
if (!i && flag)
tempS = ;
else
tempS = calc(S,tempk,i);
cnt += dfs(len - ,tempS,tempk,i == maxx && limit,flag && !i);
}
if (!limit)
return f[len][S][K] = cnt;
return cnt;
} ll solve(ll x)
{
ll len = ;
while (x)
{
num[++len] = x % ;
x /= ;
}
return dfs(len,,,,);
} int main()
{
memset(f,-,sizeof(f));
scanf("%d",&T);
while (T--)
{
scanf("%lld%lld%lld",&L,&R,&K);
printf("Case #%d: %lld\n",++cas,solve(R) - solve(L - ));
} return ;
}

经过对以上例题的探讨与研究,自然也就不难得到数位dp模板(...根据实际情况来填):

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm> using namespace std; int t;
long long dp[][][];
long long l, r;
int shu[]; long long dfs(int len,..., bool shangxian)
{
if (len == )
return ...;
if (!shangxian && dp[len][...])
return dp[len][...]; //dp数组的内容应和dfs调用参数的内容相同,除了是否达到上限
long long cnt = ;
int maxx = (shangxian ? shu[len] : );
for (int i = ; i <= maxx; i++)
{
...;
cnt += dfs(len - ,..., shangxian && i == maxx);
}
if (!shangxian)
dp[len][...] = cnt;
return cnt;
} long long solve(long long x)
{
int k = ;
while (x)
{
shu[++k] = x % ;
x /= ;
}
return dfs(k,...,)
} int main()
{
memset(dp, , sizeof(dp));
scanf("%lld%lld", &l, &r); //有些题目其实并不需要用到long long
printf("%lld\n", solve(r) - solve(l - )); //只有满足区间减法才能用 //while (1);
return ;
}

总结:

1.如果题目中出现求满足区间[l,r]的符合......性质的数的个数,考虑使用数位dp.

2.思考一下:如果我们只能从前往后一位位枚举当前的数位,要做出这道题,我们需要知道哪些量?利用这些来补充到dfs的调用参数中.

3.套用模板.