方法一:
对每一位数字为某一个值时的情况分步计算
难度:低
实用性:低,仅在要求判断数位中是否出现某个数字时可用
例如求数字中没有连续62且没有4的情况
比456小的数,可以这么考虑,
4 5 6
4 5 (0~6)
4 (0~4) (0~9)
(0~3) (0~9) (0~9)
先计算百位为0,1,2,3时能有多少个数满足(0~399)
然后计算十位为0,1,2,3,4时能有多少个数满足(000~049)
最后计算十位为0~6时能有多少个数满足(000~006)
这里说明一下0~49等价于400~449
假如这些数字的前面出现了4或62,则这些数字不计
没有出现,则前面的数字的值没有影响
讲解一下实际操作
dp[][],第一维表示位数(个,十,百等),第二维表示该位上的数字是几
先赋初值,显然个位上除4以外每一个的值都为1
然后状态转移方程为
dp[i][j]=dp[i-1][0]+ dp[i-1][1]+…+dp[i-1][9]
然后要加入62和4的判断
FOR(i,2,6)
{
FOR(j,0,9)
{
if(j==4) continue;
FOR(k,0,9)
{
if(k==4||(j==6&&k==2)) continue;
dp[i][j]+=dp[i-1][k];
}
}
}
其中j的位数在k的左边,用于判断62是否存在
初始化是比较简单的,但在求解过程中中有很多易错点
先把读入的数字每一位都排出来
while(k)
{
a[++cnt]=k%10;
k/=10;
}
a[cnt+1]=0;
这里一定要保证数组最大位的前一位为0,因为后续判断中会用到这一位
首先要从高位到低位
比如456,我们先算了0~399,由于有4,后面的数字都是非法的
所以高位的数字决定低位的数字是否有效
所以出现这些数字时即可结束循环
假如百位上是3,我们要计算0~299,所以我们实际上计算了
0~99
100~199
200~299
也就是比3小的数字,因此循环的范围为0~(a[i]-1)
这个计算方式在计算最后一位时是会少算一个数字的
比如123,我们实际上算了0~122,所以要算的数字还要自增1
ROF(i,cnt,1)
{
FOR(j,0,a[i]-1)
{
if((a[i+1]==4&&j==9)) continue;
ans+=dp[i][j];
}
if((a[i+1]==4&&a[i]==9)) break;
}
以上方法是非常基础的,书写易出错且用处非常有限
方法二:
记忆化搜索dp
难度:高
实用性:高,几乎一切情况
这里先补充一下dp的认知
以数塔为例
如果你枚举所有的状态,会出现很多非常相似的状态,它们之间只有很小的区别,而且你会进行大量的重复运算,如果找到某个规律能够利用所有被计算过的状态,并运用到将要进行的运算中,效率将会大大提升。
然而数塔的情况非常单调,当你推理状态时,状态仅用一个一维数组就能完全表示。但即便如此,我们依旧可以进行记忆化搜索。
记忆化搜索即———在判断过该状态后,下次再遇到时,直接借用过去的结果。
代码:
int dfs(int x,int y)
{
if(dp[x][y])
return dp[x][y];
if(x>n||y>n) return 0;
dp[x][y]=max(dfs(x+1,y),dfs(x+1,y+1))+a[x][y];
return dp[x][y];
}
那么dp[1][1]就是我们需要的答案,而且其运算顺序与常规的状态方程写法一致。
因此,我们可以发现,dp和记忆化搜索有着极其密切的联系,如果目前看不懂数位dp的搜索,不如先用记忆化搜索重写以前的dp题加深理解。
以下给出记忆化搜索的数位dp的一个实例
ll dfs(int pos,int sta,int limit)
//3到4四个参数,为当前位,状态(可能用两个数表示状态),和上限
{
//cout<<pos<<" "<<sta<<" "<<f[sta]<<endl;
if(pos==0)//到最后一位数字,判断该数字是否满足条件
{
return f[sta]==1;
}
if(!limit&&dp[pos][sta]!=-1)
//在位数相同,状态相同时,直接借用过去结果,limit是是否达到上限值,比如上次算出的是1000到1999,与现在2000到2999刚好同一情况,但上限(初始输入)小于2999,所以不能借用
{
return dp[pos][sta];
}
int up=limit?a[pos]:9,s,g=sta,k=sta;
ll ans=0;
FOR(i,0,up)
{
if(i==0)
{
if(sta!=0)
{
s=g%3;
g/=3;
if(s==0||s==1) k=sta+t[i];
else k=sta-t[i];
}
}
else
{
s=g%3;
g/=3;
if(s==0||s==1) k=sta+t[i];
else k=sta-t[i];
}
ans+=dfs(pos-1,k,limit&&i==up);
}
if(!limit)//没达到上限时,记录该状态
dp[pos][sta]=ans;
return ans;
}
ll solve(ll x)
{
int pos=0;
while(x)
{
a[++pos]=x%10;
x/=10;
}
a[pos+1]=0;
return dfs(pos,0,1);
}
题目对应SPOJ BALNUM ,实际做题按需求改写,没有什么泛用性,但结构基本一致。
顺带先讲一下这道题,题目要求在l和r中有多少BALNUM,BALNUM是指,数位中的奇数出现偶数次,偶数出现奇数次,没有出现的不考虑,如1,111,11111,22,2222都是BALNUM。
用三进制表示0到9的状态,0为没出现,1为奇数次,2为偶数次。
由于只有1和2加入计算,所以可以通过以下二进制处理出所有符合条件的状态数字
注:t[j]为3的j次方
FOR(i,1,pow(2,10)-1)
{
int l=i,ret=0;
FOR(j,0,9)
{
if(l&1) ret+=((j%2)==1?2*t[j]:t[j]);
l>>=1;
}
//cout<<i<<" "<<ret<<endl;
f[ret]=1;
}
就不放完整代码了,用时为0ms