数位dp有着很明显的特点,一般来说是给定区间[l,r]求满足某种条件区间中的数有多少个
朴素解法一般是O(n)的而n往往很大(10^8起步)
这时候我们就要想办法优化,于是就有了数位dp
数位有两个基本的原则
-
对于区间数的个数,我们转化为前缀和做(即ans=sum(r)-sum(l-1))
-
逐位确定
我认为第二条很关键,可以说是数位dp的精髓
一般来说数位dp分两步
-
打表 形如f[i,j]到有i位且最高位为j的满足条件的个数
-
统计前缀和
统计前缀和我们需要用到一个非常重要的结论
对于任意一个小于n的数,从高位到低位,必然出现了某一位小于n的那一位
这样我们就可以不受n的限制,用数位开始处理
以bzoj1026这道经典的题目为例
1 var f:array[0..20,0..10] of longint; 2 d:array[0..20] of longint; 3 l,r,t,i,j,k:longint; 4 5 function get(n:longint):longint; 6 var i,s,x,j:longint; 7 begin 8 if (n>=0) and (n<=9) then exit(n); 9 fillchar(d,sizeof(d),0); 10 s:=0; 11 while n<>0 do 12 begin 13 inc(s); 14 d[s]:=n mod 10; 15 n:=n div 10; 16 end; 17 get:=0; 18 for i:=1 to s-1 do //统计位数小于n的位数的满足条件的数目 19 for j:=1 to 9 do 20 get:=get+f[i,j]; 21 //注意这里是不能直接加前导为0的数组,因为我们在计算前导为0的时候,实际上舍掉了后一位为0~1的情况 22 for i:=s downto 1 do //从高到低逐位统计位数为n的位数且小于n的满足条件的个数 23 begin 24 if i<>1 then x:=d[i]-1 else x:=d[i]; //小细节,注意n本身也可能是 25 for j:=0 to x do //当前位小于n的这位的满足条件的数 26 begin 27 if (i=s) and (j=0) then continue; 28 if (s=i) or (abs(j-d[i+1])>=2) then 29 get:=get+f[i,j]; 30 end; 31 if (s<>i) and (abs(d[i+1]-d[i])<2) then break; //如果逐位统计n本身出现了不满足的情况,那显然要直接退出 32 end; 33 end; 34 35 begin 36 readln(l,r); 37 t:=trunc(ln(r)/ln(10))+1; 38 for i:=0 to 9 do //题目的特殊要求 39 f[1,i]:=1; 40 for i:=2 to t do //计算f[i,j] 41 begin 42 for j:=0 to 9 do //注意要包含前导为0的状况 43 for k:=0 to 9 do 44 if abs(j-k)>=2 then 45 f[i,j]:=f[i,j]+f[i-1,k]; 46 end; 47 writeln(get(r)-get(l-1)); 48 end.
(话说我第一次拍这道题花了3h,实在太渣……)
当然我们也不能拘泥于这种套路,我觉得
当表打了没什么用的时候我们可以直接逐位计算,如poj3286(统计0的个数)
这里我们讨论,每一位对0的贡献度
1 var d:array[0..20] of int64; 2 i:longint; 3 l,r:int64; 4 5 function count(n:int64):int64; 6 var i,t:longint; 7 x,y:int64; 8 begin 9 for i:=1 to 18 do 10 if (n<d[i]) then break; 11 t:=i-1; 12 count:=1; 13 for i:=1 to t do 14 begin 15 x:=n div d[i]; //当前位前面所组成的数 16 y:=n mod d[i-1]; //当前位后面所组成的数 17 if (n div d[i-1] mod 10)=0 then //当前位 18 count:=count+d[i-1]*(x-1)+y+1 19 //如果是x0y的情况,0是第k位时,我们分2种情况讨论 20 当小于n的数是p0q的,p∈[1,x-1],那么这个位上的0可以贡献(x-1)*10^(k-1) 21 当这个小于等于n的数是x0q, q∈[0,y]那么这个位上的0可以贡献y+1 22 else count:=count+d[i-1]*x; 23 //如果当前位不是0,那显然比n小的数中肯定存在当前位为0的; 24 设数为p0q, p∈[1,x], q∈[0,10^(k-1)-1] 因为当前位已经小于则显然可以贡献x*10^(k-1) 25 end; 26 end; 27 28 begin 29 d[0]:=1; 30 for i:=1 to 18 do 31 d[i]:=d[i-1]*10; 32 readln(l,r); 33 while (l<>-1) do 34 begin 35 if l=0 then writeln(count(r)) 36 else writeln(count(r)-count(l-1)); 37 readln(l,r); 38 end; 39 end.
数位dp需要大量的思考,有时候看起来对的实际上是错的
但由于这种题目暴力,数据都非常好弄
所以一定要孜孜不倦的对拍,恩恩