模板 - 动态规划 - 数位dp
#include<bits/stdc++.h> using namespace std; #define ll long long int a[20]; ll dp[20][20/*可能需要的状态1*/][20/*可能需要的状态2*/];//不同题目状态不同 ll dfs(int pos,int state1/*可能需要的状态1*/,int state2/*可能需要的状态2*/,bool lead/*这一位的前面是否为零*/,bool limit/*这一位是否取值被限制(也就是上一位没有解除限制)*/) //不是每个题都要处理前导零 { //递归边界,最低位是0,那么pos==-1说明这个数枚举完了 if(pos==-1) return 1;/*这里返回1,表示枚举的这个数是合法的,那么这里就需要在枚举时必须每一位都要满足题目条件,也就是说当前枚举到pos位,一定要保证前面已经枚举的数位是合法的。 */ //第二个就是记忆化(在此前可能不同题目还能有一些剪枝) if(!limit && !lead && dp[pos][state1][state2]!=-1) return dp[pos][state1][state2]; /*常规写法都是在没有限制的条件记忆化,这里与下面记录状态对应*/ int up=limit?a[pos]:9;//根据limit判断枚举的上界up ll ans=0; //开始计数 for(int i=0;i<=up;i++)//枚举,然后把不同情况的个数加到ans就可以了 { int new_state1=???; int new_state2=???; /* 计数的时候用continue跳过不合法的状态,不再搜索 */ //合法的状态向下搜索 ans+=dfs(pos-1,new_state1,new_state2,lead && i==0,limit && i==a[pos]);//最后两个变量传参都是这样写的 } //计算完,记录状态 if(!limit && !lead) dp[pos][state1][state2]=ans; /*这里对应上面的记忆化,在一定条件下时记录,保证一致性,当然如果约束条件不需要考虑lead,这里就是lead就完全不用考虑了*/ return ans; } ll solve(ll x) { //可能需要特殊处理0或者-1 if(x<=0) return ???; int pos=0; while(x)//把数位分解 { a[pos++]=x%10;//编号为[0,pos),注意数位边界 x/=10; } return dfs(pos-1/*从最高位开始枚举*/,0/*可能需要的状态1*/,0/*可能需要的状态2*/,true,true);//刚开始最高位都是有限制并且有前导零的,显然比最高位还要高的一位视为0嘛 } int main() { memset(dp,-1,sizeof(dp)); //一定要初始化为-1 ll le,ri; while(~scanf("%lld%lld",&le,&ri)) { printf("%lld\n",solve(ri)-solve(le-1)); } }
其实另一种计数写法对别的题目有一定的启发性,需要特别注意的是,无论哪种写法的dp结果中存的数字都是和le与ri无关的。所以在数位受限时不能取用计算过的dp值,也不能更新dp值,不受限的情况可以重复利用。
无注释版:
#include<bits/stdc++.h> using namespace std; #define ll long long int a[20]; ll dp[20][MAXS1][MAXS2]; ll dfs(int pos,int s1,int s2,bool lead,bool limit) { if(pos==-1) { return ?; } if(!limit && !lead && dp[pos][s1][s2]!=-1) return dp[pos][s1][s2]; int up=limit?a[pos]:9; ll ans=0; for(int i=0; i<=up; i++) { int ns1=op1(s1); int ns2=op2(s2); ans+=dfs(pos-1,ns1,ns2,lead && i==0,limit && i==a[pos]); } if(!limit && !lead) dp[pos][s1][s2]=ans; return ans; } ll solve(ll x) { if(x<=0) return ?; int pos=0; while(x) { a[pos++]=x%10; x/=10; } return dfs(pos-1,INITS1,INITS2,true,true); } int main() { memset(dp,-1,sizeof(dp)); ll le,ri; while(~scanf("%lld%lld",&le,&ri)) { printf("%lld\n",solve(ri)-solve(le-1)); } }
一个更简单的模板,去掉了很多奇奇怪怪的东西,比如前导0,前导0的确应该特殊考虑而不能一概而论。
int dfs(int i, int s, bool e) { if (i==-1) return s==target_s; if (!e && ~f[i][s]) return f[i][s]; int res = 0; int u = e?num[i]:9; for (int d = first?1:0; d <= u; ++d) res += dfs(i-1, new_s(s, d), e&&d==u); return e?res:f[i][s]=res; }
看起来清爽多了,其中:
f为记忆化数组;
i为当前处理串的第i位(权重表示法,也即后面剩下i+1位待填数);
s为之前数字的状态(如果要求后面的数满足什么状态,也可以再记一个目标状态t之类,for的时候枚举下t);
e表示之前的数是否是上界的前缀(即后面的数能否任意填)。
for循环枚举数字时,要注意是否能枚举0,以及0对于状态的影响,有的题目前导0和中间的0是等价的,但有的不是,对于后者可以在dfs时再加一个状态变量z,表示前面是否全部是前导0,也可以看是否是首位,然后外面统计时候枚举一下位数。
注意:
不满足区间减法性质的话,不能用solve(r)-solve(l-1)。
看了学长的部分博客之后发现其实使用f[i][j][st]表示以j开头的i位数满足条件st的数的个数也是可以的。待更新。