dp即动态规划,一个很神奇的词,与其说是算法,不如认为是一种概念。
(有的时候你做完题,甚至都不知道自己用的是dp…)
我觉得吧,dp,就是把全局的最优解,拆分成部分最优解。
(也可能不是最优解,但与全局最优解有关)
一种牵一发而动全身的感觉。
数位dp,就是和数字排列有关,解决超长数字范围问题(比如范围到1e200000)。
思路
1、范围有多少位,就建多少个数组,每个数组代表该数位。
比如1e6 ,有6位,建8位就够了,dp[8]。
dp[1]代表[1,9],dp[2]代表[10,99]
2、分解超长数字范围。
以分解dp[2]为例:
dp[2]即[10,99],我们需要知道上一个数位,即dp[1],[1,9];
dp[2]=[10,19]+[20,29]+[30,39]……;
[10,19],[20,29],[30,39],就是dp[1]分别加上前缀1、2、3;
所以分解dp[2],就是用dp[1]判断10次。
同理其他数位。
3、判断状态。
以dp[i][j][k]为例,表示符合 i 状态、j状态、k状态 的情况。
(初始化为-1)
第一个i 一般为数位,而后面的j、k状态则随着题目要求变化需要自行定义。
实现
用dfs递归每个数位来实现:
for(int i=0;i<=up/*代表数位*/;i++){
if(/*条件*/){
dfs(pos-1,/*下一状态*/,limit&&i==up);
}
else if(){
}
**注意是 else if 不是if**
**血一样的教训**
}
值得注意的是,递归数位时,我们需要知道我们的数位有没有到范围的上限:
比如 0~20时 :从0开始循环,最高为2(上限);
0开始时,接着从0~9开始(上限)。
我们可以发现上限有两种情况:
- 数位上的数字
- 9
如何判断:
(a[]记录数位)
int up=limit?a[pos]:9;
……
limit&&i==up
dfs结束后记录结果:
if(!limit&&!lead){
dp[pos][]=ans;
}
每次递归时再剪个枝,重复状态不判断:
if(!limit&&dp[pos][]!=-1){
return dp[pos][];
}
dfs的意思是,递归给你的状态是还没被记录的状态,递归时确认上状态正确再递出去本次状态。如果状态不符合要求,本状态将被退回,不计入。
以 poj 3252 为例:
//求 0比1多的数
#include <iostream>
#include <string.h>
#include <stdio.h>
#define maxn 40
using namespace std;
int start,finish;
int dp[maxn][maxn][maxn];//i 数位 j 0的个数, k 1的个数
int a[maxn];
int dfs(int pos,int zero,int one,bool lead/*前导零*/,bool limit){
if(pos<=0)return (lead||zero>one);
if(!limit&&!lead&&dp[pos][zero][one]!=-1){
return dp[pos][zero][one];
}
int up=limit?a[pos]:1;
int ans=0;
for(int i=0;i<=up;i++){
if(!lead){//不是首位
if(i){
ans+=dfs(pos-1,zero,one+1,0,limit&&i==up);
}
else{
ans+=dfs(pos-1,zero+1,one,0,limit&&i==up);
}
}
else{//是首位
if(i){//首位是1
ans+=dfs(pos-1,0,0,0,limit&&i==up);
}
else{ //首位是0
ans+=dfs(pos-1,0,0,1,limit&&i==up);
}
}
}
if(!limit&&!lead){
dp[pos][zero][one]=ans;
}
return ans;
}
int solve(int n){
int pos=0;
while(n){
if(n&1)a[++pos]=1;
else a[++pos]=0;
n>>=1;
}
return dfs(pos,0,0,1,1);
}
int main()
{
while(~scanf("%d%d",&start,&finish)){
memset(dp,-1,sizeof(dp));
printf("%d\n",solve(finish)-solve(start-1));
}
return 0;
}