目录
模板:
话不多说,直接上模板,刚开始实在搞不懂的话也没问题,会套模板就行(手动滑稽):
typedef long long ll;
int temp[pos];
ll dp[pos][sta]
//如果需要处理前导零,还需加入一个参数bool lead判断是否为前导零
ll dfs ( int pos , int sta , bool limit ){
if(pos==-1) return (根据sta的状况(题意)确定返回值);
if(!limit && dp[pos][sta]!=-1) return dp[pos][sta];
ll ans=0;
//不受限的话就能从0搜到9,否则只能搜到temp[pos]
int end=limit ? temp[pos] : 9;
for(int i=0 ; i<=end ; ++i){
int newsta;
if(题目的要求) 更新sta的状态,即newsta;
ans+=dfs(pos-1,newsta,limit&&i==end);//状态转移,若上一位的数值全部枚举完成,下一位将处于受限状态。
}
//只在非受限时才记录,因为受限时后面枚举的数字发生了变化,其状态是不确定的,不能直接返回,需要重新枚举确定
if(!limit) dp[pos][sta]=ans;
return ans;
}
ll solve(int num){
memset(dp,-1,sizeof(dp));
int pos=0;//别忘了赋初值哟~
while(num){
temp[pos++]=num%10;
num/=10;
}
//limit的初始值始终赋1.
return dfs(pos-1,0,1);//这里注意sta的初始值是赋1还是0,要依题意而定。
}
思想:
OK,除了会套模板之外,还是要稍微了解一下数位DP的原理的嘛~下面就简单的介绍一下:
数位DP本质上是一个记忆化搜索,它的搜索顺序是这样的(以一个数256为例):
首先获得256的数位是3位,然后获得它每一位上的数字为2、 5、 6,接着我们可以做出以下的表格:
2 | 0 | 1 | 2 | |||||||
1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
0 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
-1 | return | return | return | return | return | return | return | return | return | return |
为什么要画这样一个表格呢?因为数位DP的搜索顺序就是从高位出发,即表格的首行出发,然后向表格下一行递归搜索,也就是说数位DP是先搜索000,然后是001、002······009,然后才是010~019、020~029······以此类推直到256(请务必弄清这个搜索顺序!)。由于最高位是处于受限状态的(我们一般都是从高位向低位搜索,最早搜索的是最高位,而最高位一定是受限的,所以现在理解为什么limit的初始值一定是1了吧),只能搜到它在该位上对应的数字temp[pos]=2,也就是我们在这个位置上只能搜索0~2,对于这个位置上搜到的数,都是作为最高位的,比如搜到1,那么它代表的范围就是100~199。继续向下一个位搜索来到10位,对应的数字是5,但是要注意,该位现在并不一定处于受限状态,什么时候才是受限的呢?如果上一位搜到的是0,或者1,很显然它们代表的范围是0~99和100~199,在10位上可以从0搜到9,但如果上一位搜到的是2,它代表的范围是200~256,10位还能从0搜到9吗,显然不能,此时就是受限状态,只能搜到5,同理对于个位的搜索也是一样(看到这里,应该理解了模板的状态转移方程中limit&&i==end的含义了吧)。从个位再往下,已经没有了可搜索的值,此时应该返回满足条件的数了,那么返回多少?这得根据题目来决定,现在假设你搜索到的值是025,而题目的要求是含有2,那么此时就应该返回1,而搜到145则要返回0。
但是到目前为止,似乎这就是换了一个方式的暴力搜索啊,数位DP快速的优势在哪里?我们一开始就讲过,数位DP的搜索不是普通搜索而是记忆化搜索,既然是记忆化搜索当然要有记忆的步骤。当我们搜索完一轮后(比如从000~009),这个时候会需要将当前搜索到的符合条件的数记忆在dp数组中,还是上述的假设,题目的要求是含有2,所以你搜索完000~009后发现了一个合要求的数字002,那么就令dp[pos][sta]=1(pos是当前的数位,sta是当前的状态),那么此后当程序再次搜索到这个范围内时(当然还要求状态sta与记录时的sta一致),比如010~019,程序就不会一个一个的搜索而是直接返回1,由此实现了时间的节省,提高了搜索的效率。最后如果当该数位处于受限状态时,它能搜索的范围就减少了,可能搜不到dp记下的值(不过我举的假设碰巧能搜到,尴尬~),因此不能直接返回,而要重新全部搜索一遍。
例题:
好了,讲了这么多,相信你一定迫不及待想做一两道题来证明自己,ok,满足你的愿望,下面是两道极简单的例题:
1.求从1到n中,只含有0和1的数的个数,n<= (如1~11中,只有1,10,11三个数合要求,答案就为3)
解数位DP题,二话不说,直接模板摆出来,需要我们考虑的只有如何实现sta到newsta之间状态的转移。
对于本题,我们只需要考虑一个数是否只含有0和1,那么就只有两种状态,一种是只含有0和1,记该状态为sta=1,一种是含有其它的数,记该状态为sta=0,由于 的数位不超过10,可以开出数位数组temp[10],记忆化数组dp[10][2](当然为保险起见,数组可以开大一点)。
下面是状态的转移,开始搜索时假设该数字合要求,sta=1,向下搜索,当发现一个数字既不是0也不是1时,状态改变为newsta=0,一直到pos==-1就搜索完了一个数字,如果此时的sta仍然为1,则说明该数字是合要求的,返回1,否则返回0.
分析到这里,程序也就出来了,核心代码如下:
int temp[11];
int dp[11][2];
int dfs(int pos,int sta,bool limit){
//sta==1的意思是当sta=1时就返回1,否则就返回0
if(pos==-1) return sta==1;
if(!limit&&dp[pos][sta]!=-1) return dp[pos][sta];
int ans=0;
int end=limit?temp[pos]:9;
int newsta;
for(int i=0;i<=end;++i){
//状态的转移,只有当当前搜索到的数字是0或1并且前面搜索到数字都合要求时,新状态才能是1
if(i<=1&&sta==1) newsta=1;
//一旦发现某个数字不合要求,新状态就置0
else newsta=0;
ans+=dfs(pos-1,newsta,limit&&i==end);
}
if(!limit) dp[pos][sta]=ans;
return ans;
}
int solve(int num){
memset(dp,-1,sizeof(dp));
int pos=0;
while(num){
temp[pos++]=num%10;
num/=10;
}
//开始时假定每个数都是合要求的,sta赋初值为1
return dfs(pos-1,1,1);
}
2.求从1到n中,共包含了多少个数字7,n<=(如1~17中,7包含一个7,17包含一个7,答案就为2)
和上面一样,继续分析状态的表示,这次我们不是要求满足某个要求的数有多少,而是求一个数字出现的次数有多少,但是核心的思想仍然没有变,既然统计的是7出现的次数,那么sta就记录一个数字中7的个数,注意是一个数字而不是1~n里的所有数字。因为n不超过,所以一个数字中出现7的个数不会超过777777777中7的个数,即9个,所以我们开出数位数组temp[11],记忆化数组dp[11][10].
而sta到newsta之间的状态转移就很明显了,只要新搜到了一个数字7,就将原有的sta+1,即newsta=sta+1,否则不变,当pos==-1搜索完一个数字后,直接返回该数字中7的个数即sta的值。核心代码如下:
int temp[11];
int dp[11][10];
int dfs(int pos,int sta,bool limit){
if(pos==-1) return sta;
if(!limit&&dp[pos][sta]!=-1) return dp[pos][sta];
int ans=0;
int end=limit? temp[pos]:9;
int newsta;
for(int i=0;i<=end;++i){
//此处一定要记得每次循环前都初始化,否则i循环到7后会对后面循环的数字产生影响(newsta++之后若不初始化,i==7之后循环到的所有数字的newsta都被加了1)
newsta=sta;
if(i==7) newsta++;//记录一次搜索中出现的7的个数
ans+=dfs(pos-1,newsta,limit&&i==end);
}
if(!limit) dp[pos][sta]=ans;
return ans;
}
int solve(int num){
memset(dp,-1,sizeof(dp));
int pos=0;
while(num){
temp[pos++]=num%10;
num/=10;
}
//开始时是一个7都没搜到的,所以sta赋初值为0
return dfs(pos-1,0,1);
}