前言
本来写的超级多超级繁琐,可是被csdn搞了,然后就没了。现在重写一遍,重新理清思路,只写关键内容。
数位dp
求区间[l,r]内,满足某条件的数的个数,这个条件一般与数的组成有关;或者统计某个量,这个量也与数的组成有关。
解法
首先这样的题一般满足前缀性质,即[l,r]内的答案可以变为[1,r]-[1,l-1]。
- 在满足条件的数字很少且方便搜索时,可以先打表再查找。
- 从高位到低位遍历每一位数,记忆化搜索得出答案。
题目列表
HDU 3555 含49.
CF 1036C 非0数位不超过3.
HDU 2089 不含62,不含4.
BZOJ 1026 相邻数位之差大于等于2
BZOJ 1833 求所有数码的出现次数
HDU 3652 含13,被13整除。
HDU 3555 Bomb
求[0,N]内含49的数的个数,写了四种不同的写法,终于找到一种好理解的。
记忆化搜索
状态表示:solve(upper,bit,status) 表示[0,upper]内满足条件的数字个数,upper的位数为bit(防中间0),当前的状态是status(0/1/2分别表示 无4/有4/有49)。
状态边界:bit=0时,如果status为2,答案是1,否则是0.
状态转移:记front为upper的首位(第bit高位),首位值i从0到front遍历,当i<front时递归的upper就是一个各位全为9的数,否则就原数的下一位;递归的bit就是当前bit-1;对status设置一个状态机:起始是0,检测到4是1,检测到49是2.
状态记录:为了避免超时,用table[bit][status]来记录记录当upper各位全为9时的答案。
手动跑一下这个过程会理解的更深刻。
代码中,p10数组表示10的幂次;getbit函数用于获得n的位数;求upper的首位:upper/p10[bit-1],求upper删去首位后的值:upper%p10[bit-1].
vector<ll> p10(1,1);
ll table[19][3];
inline int getbit(ll n)
{
return upper_bound(p10.begin(), p10.end(), n) - p10.begin();
}
//求[0,upper]内含49的数的个数,
//status 0/1/2 无4/有4/有49
ll solve(ll upper, int bit, int status)
{
//printf("%I64d %d %d\n",upper,bit,status );
if(bit==0)
return status==2;
if(upper == p10[bit]-1 && table[bit][status]!=-1)
return table[bit][status];
ll res = 0, front = upper/p10[bit-1];
for(int i=0;i<=front;i++)
{
ll nxt_up = i==front ? upper%p10[bit-1] : p10[bit-1]-1;
int nxt_st = status;
if(status!=2)
{
if(i==4)
nxt_st = 1;
else if(status==1 && i==9)
nxt_st = 2;
else
nxt_st = 0;
}
res += solve(nxt_up, bit-1, nxt_st);
}
if(upper == p10[bit]-1)
table[bit][status] = res;
return res;
}
int main(void)
{
for(int i = 1; i <= 18; i++)
p10.push_back(p10.back() * 10);
memset(table,-1,sizeof(table));
int T = read();
while(T--)
{
ll N = read();
cout <<solve(N, getbit(N), 0) << endl;
}
return 0;
}
Codeforces 1036C Classy Numbers
求[L,R]内非0数位不超过3的数
状态表示:solve(upper, bit, need)表示[0,upper]内非0数位不超过need的个数。
状态边界:bit=0时返回1,因为need<0的情况已经在转移过程中卡掉了
状态转移:新的upper、bit同上题,遍历到0时status不变,其他情况在status大于0时减一。
状态记录:同上题
好生气,这题套模板5分钟左右就做完了,而cf第一次做的时候做了30多分钟,果然题数压制才是硬道理。
vector<ll> p10(1,1);
ll table[19][4];
inline int getbit(ll n)
{
return upper_bound(p10.begin(), p10.end(), n) - p10.begin();
}
//求[0,upper]内非0数位不超过3的个数
//status 0/1/2/3 可用0/1/2/3个
ll solve(ll upper, int bit, int status)
{
//printf("%I64d %d %d\n",upper,bit,status );
if(bit==0)
return 1;
if(upper == p10[bit]-1 && table[bit][status]!=-1)
return table[bit][status];
ll res = 0, front = upper/p10[bit-1];
for(int i=0;i<=front;i++)
{
ll nxt_up = i==front ? upper%p10[bit-1] : p10[bit-1]-1;
if(i==0)
res += solve(nxt_up, bit-1, status);
if(i && status)
res += solve(nxt_up, bit-1, status-1);
}
if(upper == p10[bit]-1)
table[bit][status] = res;
return res;
}
int main(void)
{
for(int i = 1; i <= 18; i++)
p10.push_back(p10.back() * 10);
memset(table,-1,sizeof(table));
int T = read();
while(T--)
{
ll L = read(), R = read();
cout << solve(R, getbit(R), 3) - solve(L-1,getbit(L-1),3) << endl;
}
return 0;
}
HDU 2089 不要62
求[n,m]内不含62与4的数字个数。
先对status设计一个状态机,起始为0,检测到6为1,检测到62或4为2,其中01都是合法状态。
但是写起来就会发现,能达到状态为2的可以直接卡掉,不用下传状态。
状态表示: solve(upper,bit,status)表示[0,upper]内不含62与4的数的个数。
状态边界: bit=0时返回1.
状态转移: 卡掉status=2的情况,然后下传即可。
状态记录: 同以上题
vector<ll> p10(1,1);
ll table[19][3];
inline int getbit(ll n)
{
return upper_bound(p10.begin(), p10.end(), n) - p10.begin();
}
//求[0,upper]内不含62和4的数字个数
//status 0/1/2 其它/6/62或4
ll solve(ll upper, int bit, int status)
{
if(bit==0)
return 1;
if(upper == p10[bit]-1 && table[bit][status]!=-1)
return table[bit][status];
ll res = 0, front = upper/p10[bit-1];
for(int i=0;i<=front;i++)
{
if((i==2 && status) || i==4)
continue;
ll nxt_up = i==front ? upper%p10[bit-1] : p10[bit-1]-1;
res += solve(nxt_up, bit-1, i==6);
}
if(upper == p10[bit]-1)
table[bit][status] = res;
return res;
}
int main(void)
{
for(int i = 1; i <= 18; i++)
p10.push_back(p10.back() * 10);
memset(table,-1,sizeof(table));
while(1)
{
ll L = read(), R = read();
if(L==0)
break;
cout << solve(R, getbit(R), 0) - solve(L-1,getbit(L-1),0) << endl;
}
return 0;
}
BZOJ 1026 Windy数
题外话:发现bzoj(八中oj),hysbz(衡阳市八中),lydsy(大视野在线测评)其实说的是一个OJ。
求[A,B]内相邻两数位相差大于等于2的数字个数。
因为可以卡掉非法情况,本题的status可以直接记录上一位。需要注意的是区分前导0和非前导0的情况。
状态表示: solve(upper,bit,status)表示[0,upper]内上一位位status时的windy数个数,其中当status为11时表示前导0.
状态边界: bit=0时返回1,因为非法状态全卡掉了。
状态转移: 遍历0到upper最高位内所有与status相差大于等于2的数,向下转移。注意如果status为11时,0也按11算,表示前导0只会转移到前导0,非前导0只会转移到非前导0.
状态记录: 同以上题
写这题之前总结了一下板子(namespace DigitDP),等板子成熟了再独立出来。
namespace DigitDP
{
vector<ll> p10(1,1);
ll table[19][15]; /*change: 表的定义*/
inline int getbit(ll n)
{
return upper_bound(p10.begin(), p10.end(), n) - p10.begin();
}
/*change: 函数目标,状态含义*/
//求[0,upper]内相邻数位相差大于等于的情况
//status: 上一位,11表示没有
ll solve(ll upper, int bit, int status)
{
if(bit==0)
return 1; /*change: 边界情况*/
if(upper == p10[bit]-1 && table[bit][status]!=-1)
return table[bit][status];
ll res = 0, front = upper/p10[bit-1];
for(int i=0;i<=front;i++)
{
ll nxt_up = i==front ? upper%p10[bit-1] : p10[bit-1]-1;
/*change: 状态舍弃,状态转换*/
if(abs(i-status)<2) continue;
int nxt_st = i;
if(i==0 && status==11) nxt_st = 11;
res += solve(nxt_up, bit-1,nxt_st );
}
if(upper == p10[bit]-1)
table[bit][status] = res;
return res;
}
void init()
{
for(int i = 1; i <= 18; i++)
p10.push_back(p10.back() * 10);
memset(table,-1,sizeof(table));
}
}using namespace DigitDP;
int main(void)
{
init();
ll L = read(), R = read();
cout << solve(R, getbit(R), 11) - solve(L-1,getbit(L-1),11) << endl;
return 0;
}
BZOJ 1833 count 数字计数
求[a,b]中各个数码(0-9)分别出现了多少次。
一道数位计数题,而且要算10个答案,想不到在递归边界结算答案的方法,只能在过程中去计算,用结构体去同时统计所有答案。
状态表示: solve(upper,bit,state)表示1到upper的答案,state表示是否需要计算0的答案。
边界状态: bit = 0时返回全0
状态转移: 遍历i从0到upper首位,当且仅当i=0且state为0时下传state=0.
很快就写完了,但是调的心态爆炸,甚至给用来对拍的以前ac代码找出了bug。。。后来发现是快读没开long long,心态爆炸+1.
本题中得到的启示是 边界求值或者过程求值最好只选一个,使得代码简洁。
这也会使得,边界求值求得的是[0,x]的答案,而过程求值求得的是[1,x]的答案。
struct Item
{
int vis;
ll num[10];
Item()
{
vis = 1;
memset(num,0,sizeof(num));
}
Item operator + (const Item& b) const
{
Item c;
for(int i=0;i<10;i++)
c.num[i] = num[i] + b.num[i];
return c;
}
}table[19][2],item0;
namespace DigitDP
{
vector<ll> p10(1,1);
inline int getbit(ll n)
{
return upper_bound(p10.begin(), p10.end(), n) - p10.begin();
}
/*change: 函数目标,状态含义*/
//求[1,upper]内所有数码的出现次数
//status: 是否需要计算0(前导0)
Item solve(ll upper, int bit, int status)
{
if(bit==0)
return item0; /*change: 边界情况*/
if(upper == p10[bit]-1 && table[bit][status].vis)
return table[bit][status];
Item res;
for(int i=0, front = upper/p10[bit-1];i<=front;i++)
{
ll nxt_up = i==front ? upper%p10[bit-1] : p10[bit-1]-1;
/*change: 状态舍弃,状态转换*/
if(i || status)
res.num[i] += nxt_up + 1;
res = res + solve(nxt_up, bit-1, i || status);
}
if(upper == p10[bit]-1)
table[bit][status] = res;
return res;
}
void init()
{
for(int i = 1; i <= 18; i++)
p10.push_back(p10.back() * 10);
for(int i=0;i<19;i++)
for(int j=0;j<2;j++)
table[i][j].vis = 0;
}
}using namespace DigitDP;
int main(void)
{
init();
ll L = read(), R = read();
Item itemr = solve(R,getbit(R),0);
Item iteml = solve(L-1,getbit(L-1),0);
for(int i = 0;i<9;i++)
cout << itemr.num[i] - iteml.num[i] << " ";
cout << itemr.num[9] - iteml.num[9] <<endl;
return 0;
}
HDU 3652 B-number
求[1,n]内含13且能被13整除的数字个数。
状态表示 : solve(upper,bit,status,remain)表示答案,status表示是否出现了13,remain表示当前的数字模13的余数。
状态边界 :bit=0时,只有status为2且remain为0返回1.
状态转移 :status转移同上,
r
e
m
a
i
n
=
(
r
e
m
a
i
n
∗
10
+
i
)
 
m
o
d
 
13
remain = (remain *10 + i)\bmod 13
remain=(remain∗10+i)mod13,因为模运算对乘法和加法具有分配律。
//DigitDP.cpp 数位dp通用
namespace DigitDP
{
vector<ll> p10(1,1);
ll table[19][3][14]; /*change: 表的定义,以及表的使用*/
inline int getbit(ll n)
{
return upper_bound(p10.begin(), p10.end(), n) - p10.begin();
}
/*change: 函数目标,状态含义*/
//求[0,upper]内含13且能被13整除的数字个数
//status: 0/1/2 无1/有1/有13 remain:除以13的余数
ll solve(ll upper, int bit, int status, int remain)
{
if(bit==0)
return status==2 && remain==0; /*change: 边界情况*/
if(upper == p10[bit]-1 && table[bit][status][remain]!=-1)
return table[bit][status][remain];
ll res = 0;
for(int i=0, front = upper/p10[bit-1]; i<=front; i++)
{
ll nxt_up = i==front ? upper%p10[bit-1] : p10[bit-1]-1;
int nxt_st = status;
/*change: 状态舍弃,状态转换*/
if(status!=2)
{
if(i==1) nxt_st = 1;
else if(status==1 && i==3) nxt_st = 2;
else nxt_st = 0;
}
res += solve(nxt_up, bit-1, nxt_st, (remain*10+i)%13);
}
if(upper == p10[bit]-1)
table[bit][status][remain] = res;
return res;
}
void init()
{
for(int i = 1; i <= 18; i++)
p10.push_back(p10.back() * 10);
memset(table,-1,sizeof(table));
}
}using namespace DigitDP;
int main(void)
{
int n;
while(scanf("%d",&n)!=EOF)
{
printf("%I64d\n",solve(n,getbit(n),0,0) );
}
return 0;
}