【学习笔记】数位DP(简单题)

前言

本来写的超级多超级繁琐,可是被csdn搞了,然后就没了。现在重写一遍,重新理清思路,只写关键内容。

数位dp

求区间[l,r]内,满足某条件的数的个数,这个条件一般与数的组成有关;或者统计某个量,这个量也与数的组成有关。

解法

首先这样的题一般满足前缀性质,即[l,r]内的答案可以变为[1,r]-[1,l-1]。

  1. 在满足条件的数字很少且方便搜索时,可以先打表再查找。
  2. 从高位到低位遍历每一位数,记忆化搜索得出答案。

题目列表

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 ) &VeryThinSpace; m o d &VeryThinSpace; 13 remain = (remain *10 + i)\bmod 13 remain=(remain10+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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值