初探数位DP-hdu2089
2020年04月06日17:12:29 重新看了一遍数位dp, 有了更深的理解:
假设我们先考虑一个简单的问题, 小于87, 且不包含1的的数字有多少,
我们这里先定义dp的含义, dp[i]表示长度为i+1的数字能组成多少种情况, 例如i=0时, 长度为1, 那总共有2,3,4,5,6,7,8,9中情况, 所以dp[0]=8, 那dp[1]则是dp[0]*8, 64种情况.
接下来我们考虑对于87, 如果十位为8, 个位的数字必须限制在小于等于7, 如果十位小于8, 则个位的数字可以任意取 (当然这些都不能取1, 1是不能包含的数字), 因此我们使用记忆化搜索
dfs(int p, bool limit) 表示从第p位开始能得到的结果, limit即表示该位是否能任意取, 当limit=0时, 返回的dfs值才是dp的值, 这也是为什么在返回或者赋值给dp时, 必须加上!limit的原因
前导0的情况
我们可以看到, 在最高位的时候, 我们是可以取到0的, 0即代表这一位没有, 但这里被默认填充上了0, 假设题目中要求不能填充0, 则我们必须允许前导0, 禁止后置的0. 加一个状态即可
一开始刷dp就遇到了数位dp,以前程序设计艺术上看过一点,基本没懂,于是趁今天遇到题目,想把它搞会,但就目前状态来看仍然是似懂非懂啊,以后还要反复搞
这里采用的是记忆化搜索的处理方式,有模板
1 int dfs(int i, int s, bool e) { //i表示当前的位数,s表示状态,e表示后面位数能否任意填 2 if (i==-1) return s==target_s; //最后一位取完,找到一个符合条件的值 3 if (!e && ~f[i][s]) return f[i][s]; //之前位数对应要求的值已经确定,在这里就直接返回 4 int res = 0; //记录符合条件的值 5 int u = e?num[i]:9; //是否能任意填,能任意填则必须小于原来位数上对应的值,否则则可以去到0-9 6 for (int d = first?1:0; d <= u; ++d) //逐个填充值,通常会在下面继续加上一些条件,排除不需要的值 7 res += dfs(i-1, new_s(s, d), e&&d==u); //下个位数 8 return e?res:f[i][s]=res; //可以任意填的话,说明到i位还未确定res没有包含所有情况,不可以任意填说明后面已经确定,即f也可以确定 9 }
2015-04-20 模板
1 int dfs(int p,int s,bool e) { 2 if(p==-1) return 1; 3 if(!e &&dp[p][s]!=-1) return dp[p][s]; 4 int res= 0; 5 int u=e?digit[p]:9; 6 for (int i=0;i<=u;++i) 7 { 8 if(i==4||(s&&i==2)) 9 continue ; 10 res+=dfs(p-1,i==6,e&&i==u); 11 } 12 return e?res:dp[p][s]=res; 13 } 14 int solve(int n) 15 { 16 int len=0; 17 while(n) 18 { 19 digit[len++]=n%10; 20 n/=10; 21 } 22 return dfs(len-1,0,1); 23 }
正确与否有待进一步确认,第一遍看就暂且这么理解吧
f为记忆化数组;
i为当前处理串的第i位(权重表示法,也即后面剩下i+1位待填数);
s为之前数字的状态(如果要求后面的数满足什么状态,也可以再记一个目标状态t之类,for的时候枚举下t);
e表示之前的数是否是上界的前缀(即后面的数能否任意填)。
for循环枚举数字时,要注意是否能枚举0,以及0对于状态的影响,有的题目前导0和中间的0是等价的,但有的不是,对于后者可以在dfs时再加一个状态变量z,表示前面是否全部是前导0,也可以看是否是首位,然后外面统计时候枚举一下位数。
1 #include <iostream> 2 using namespace std ; 3 int f[8][2] ;//f[i][0]:前i位符合要求 f[i][1]:前i位符合要求且i+1位是6 4 int digit[9] ;//digit[i]表示n从右到左第i位是多少 5 int dfs(int i,int s,bool e)//i表示当前位,s表示i位之前的状态,e表示当前位是否可以随意填写 6 { 7 if(i==0) 8 return 1 ; 9 if(!e && f[i][s]!=-1) 10 return f[i][s] ; 11 int res=0 ; 12 int u=e?digit[i]:9 ; 13 for(int d=0 ;d<=u ;d++) 14 { 15 if(d==4 || (s && d==2)) 16 continue ; 17 res+=dfs(i-1,d==6,e&&d==u) ; 18 } 19 return e?res:f[i][s]=res ; 20 } 21 int callen(int n)//计算n的长度 22 { 23 int cnt=0 ; 24 while(n) 25 { 26 cnt++ ; 27 n/=10 ; 28 } 29 return cnt ; 30 } 31 void caldigit(int n,int len)//计算n的digit数组 32 { 33 memset(digit,0,sizeof(digit)) ; 34 for(int i=1 ;i<=len ;i++) 35 { 36 digit[i]=n%10 ; 37 n/=10 ; 38 } 39 } 40 int solve(int n)//计算[0,n]区间满足条件的数字个数 41 { 42 int len=callen(n) ; 43 caldigit(n,len) ; 44 dfs(len,0,1) ; 45 } 46 int main() 47 { 48 int n,m ; 49 memset(f,-1,sizeof(f)) ; 50 while(~scanf("%d%d",&n,&m)) 51 { 52 if(n==0 && m==0) 53 break ; 54 printf("%d\n",solve(m)-solve(n-1)) ;//用[0,m]-[0,n)即可得到区间[n,m] 55 } 56 return 0 ; 57 }
2015-04-20 二次代码,风格变好了很多
1 #include<cstdio> 2 #include<iostream> 3 #include<algorithm> 4 #include<cstring> 5 #include<cmath> 6 #include<queue> 7 #include<map> 8 using namespace std; 9 #define MOD 1000000007 10 const int INF=0x3f3f3f3f; 11 const double eps=1e-5; 12 #define cl(a) memset(a,0,sizeof(a)) 13 #define ts printf("*****\n"); 14 const int MAXN=1005; 15 int n,m,tt; 16 int digit[9],dp[8][2]; 17 int dfs(int p,int s,bool e) { 18 if(p==-1) return 1; 19 if(!e &&dp[p][s]!=-1) return dp[p][s]; 20 int res= 0; 21 int u=e?digit[p]:9; 22 for (int i=0;i<=u;++i) 23 { 24 if(i==4||(s&&i==2)) 25 continue ; 26 res+=dfs(p-1,i==6,e&&i==u); 27 } 28 return e?res:dp[p][s]=res; 29 } 30 int solve(int n) 31 { 32 int len=0; 33 while(n) 34 { 35 digit[len++]=n%10; 36 n/=10; 37 } 38 return dfs(len-1,0,1); 39 } 40 int main() 41 { 42 int i,j,k; 43 #ifndef ONLINE_JUDGE 44 freopen("1.in","r",stdin); 45 #endif 46 memset(dp,-1,sizeof(dp)); 47 while(scanf("%d%d",&n,&m)!=EOF) 48 { 49 if(n==0&&m==0) 50 break; 51 printf("%d\n",solve(m)-solve(n-1)); 52 } 53 }
这里通过输出中间变量来辅助理解
1 int dfs(int i,int s,bool e)//i表示当前位,s表示i位之前的状态(这里表示是否为6),e表示当前位是否可以随意填写 2 { 3 //printf("*****\n"); 4 //printf("--%d %d %d\n",i,s,e); 5 if(i==0) 6 return 1 ; //说明前面的位数已经确定,该方案成立 7 if(!e && f[i][s]!=-1) 8 { 9 //printf("--%d %d %d\n",i,s,e); 10 return f[i][s] ; 11 } 12 13 int res=0 ; 14 int u=e?digit[i]:9 ; 15 //printf("--%d %d %d %d\n",i,s,e,u); 16 for(int d=0 ;d<=u ;d++) 17 { 18 if(d==4 || (s && d==2)) 19 continue ; 20 printf("--%d %d %d %d\n",i,s,e,d); 21 res+=dfs(i-1,d==6,e&&d==u) ; 22 } 23 printf("*************** %d %d %d %d\n",i,s,e,res); 24 return e?res:f[i][s]=res ; 25 }
这里输出了0-200的情况
1 200
--3 0 1 0 //从百位开始计算,之前没有6,所以中间为0,后面可以任意填充所以为1,首先在第一位上填0
--2 0 0 0 //第二位上填0
--1 0 0 0 //第三位上填0,一种情况,res+1,下面一样
--1 0 0 1 //第三位上填1
--1 0 0 2
--1 0 0 3
--1 0 0 5
--1 0 0 6
--1 0 0 7
--1 0 0 8
--1 0 0 9 //第三位上填9
*************** 1 0 0 9 //f[1][0]=9,个位上的情况且十位不含6全部确定共9种,下一步之前res重新清零
--2 0 0 1 //十位填2,之后再确定个位,发现个位上的情况已经确定,于是直接返回f[1][0],res+f[1][0]
--2 0 0 2
--2 0 0 3
--2 0 0 5
--2 0 0 6 //十位填6,之后s变为1,个位需要重新确定
--1 1 0 0
--1 1 0 1
--1 1 0 3
--1 1 0 5
--1 1 0 6
--1 1 0 7
--1 1 0 8
--1 1 0 9
*************** 1 1 0 8 f[1][1]=8 //个位上十位含6共9种情况
--2 0 0 7 //继续枚举十位
--2 0 0 8
--2 0 0 9
*************** 2 0 0 80 //f[2][0]=80
--3 0 1 1 //百位填1 ret+f[2][0]
--3 0 1 2 //百位填2 ret+f[2][0]
--2 0 1 0 //十位填0
--1 0 1 0 //个位填0 ret+1
*************** 1 0 1 1 //个位上只能有0
*************** 2 0 1 1 //十位上只能有00
*************** 3 0 1 161 //返回的是ret
161
正确性有待商榷,待我再多做几道题看看
更多数位dp题可以参见我博客,代码风格基本是统一的