初探数位dp

数位dp有着很明显的特点,一般来说是给定区间[l,r]求满足某种条件区间中的数有多少个

朴素解法一般是O(n)的而n往往很大(10^8起步)

这时候我们就要想办法优化,于是就有了数位dp

数位有两个基本的原则

  1. 对于区间数的个数,我们转化为前缀和做(即ans=sum(r)-sum(l-1))

  2. 逐位确定

我认为第二条很关键,可以说是数位dp的精髓

一般来说数位dp分两步

  1. 打表 形如f[i,j]到有i位且最高位为j的满足条件的个数

  2. 统计前缀和

统计前缀和我们需要用到一个非常重要的结论

对于任意一个小于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.
bzoj1026

(话说我第一次拍这道题花了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.
poj3286

数位dp需要大量的思考,有时候看起来对的实际上是错的

但由于这种题目暴力,数据都非常好弄

所以一定要孜孜不倦的对拍,恩恩

 

转载于:https://www.cnblogs.com/phile/p/4473238.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值