数位DP学习小结

一、学习心得体会

问题描述:

一般体现为,定义某种性质K,问某区间内具有K性质的数的个数

往往给的区间会很大,对区间内的每个数进行判断显然会超时

于是数位DP登场

数位DP,顾名思义,是对数字的每一位进行DP

心得体会:

1.数位DP需要较为熟练的记忆化搜索作为基础,虽然有的题可以直接用循环进行状态转移,但记忆化搜索的状态转移更常用更容易理解

2.时刻记住:abcd这个四位数 = a*1000+b*100+c*10+d

3.关于dp状态的保存,一般一维都是保存数字在整个数中的位置(数位),然后根据题目给定的数字的性质确定二维三维要保存什么状态

感觉数位DP的关键就在于状态的保存(当然状态转移也很重要)一般而言,dp[i][j][k]表达的信息是:具有i性质,j性质,k性质的数的个数

4.在记忆化搜索的过程中,对于数字abcd,搜索是从左往右搜索,但实际上计算是从右往左计算的(递归原理)

5.数位DP需要注意所搜索的范围不能大于原本被视作字符串的那个数字(这个在记忆化搜索中常表现为一个变量标记上界)

同时有时候0的情况也需要特别注意(具体情况具体分析吧)

6.数位DP刚刚入门的时候觉得很难,做多了觉得万变不离其宗,代码长得都差不多,都是套路,唯一的变化也就是定义的数的属性不一样

所以感觉数位DP的难点也就是状态的保存,针对题目给的性质保存相应的状态

7.刚开始写数位DP的时候用循环写状态转移,感觉理解起来没有记忆化搜索清晰,于是后面都是用记忆化搜索写的了

8.理论再强大,理解再深刻,不如多做题,做第一道题的时候感觉似懂非懂,后面做着做着也就渐渐理解了,书读百遍其义自见也是这个理

二、从具体题目中体会

玩了一个数位DP的专题:打开专题

终于做完了数位DP的专题,不过也只是初窥门槛,以后还要继续努力^_^

整体看起来,代码其实大致都一个套路,都是一个dfs 记忆化搜索,一个cal计算

数位DP一般都是用数组的维度保存数的性质,然后dp的值表示具有这样性质的数的个数

个人觉得记忆化搜索比迭代好理解代码看着也舒服,所有除了第一次写的D题,其他都是用记忆化写的

然后说说这个专题,CD两题算是基础题,G题定义的数字的性质蛮新颖,但是想到怎么保存状态也是基础题

H题在基础上加了倍数的判断

AB两题在状态的保存上转了点弯,特别是B题还结合了状态压缩和最长上升子序列

F题比较特别,求的不是数的个数,而是所有满足性质的数的平方和

E题也是数位,不过保存的是二进制数的每一位

(专题整体写下来收获蛮大的^_^)

A - Beautiful numbers 

题意:

定义beautiful 数:这个数能被它的每一位整除

例如12 能被12整除,故12beautiful

求区间[l,r]内的beautiful数的个数

分析:

利用记忆化搜索把小于等于num 的数中的所有beautiful数都搜出来

怎么搜呢?还是按照数字“位”来搜。

现在问题是:

1.怎么判断beautiful数?

2.怎么保存状态?

显然,需要将beautiful数的性质用状态保存下来。

beautiful数需要整除它所有的非零位

那么它只需整除它所有位的最小公倍数即可

数字1~9的最小公倍数为2520(设为mxlcm

考虑这样一种保存状态的方法:

dp[i][j][k]表示长度为 i,所有数位的lcmjmxlcmk的答案

那么需要开一个dp[20][2520][2520]的数组,类型是long long

这数组显然是开不下的,要想办法压缩

对这个数组的第二维,“所有数位的lcmj”,其实j的取值虽然可能达到2520

但是j实际的数最多只有50

于是可以考虑开一个hs数组,hs[j] = id;表示给所有数位的lcmj的编号为id

这样一个dp[20][50][2520]的数组保存状态,那么万事俱备可以搜索了。

搜索的具体方法在代码中介绍

代码:

#define mem(a,x) memset(a,x,sizeof(a))
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
typedef long long ll;
/*
    若一个数能整除它的所有的非零数位,
    那么相当于它能整除个位数的最小公倍数。
    因此记忆化搜索中的参数除了len(当前位)和up(是否达到上界),
    有一个prelcm表示前面的数的最小公倍数,
    判断这个数是否是Beautiful Numbers,还要有一个参数表示前面数,
    但是这个数太大,需要缩小它的范围。
    缩小前面组成的数的范围:
    可以发现所有个位数的最小公倍数是2520,假设当前的Beautiful Numbers是x,
    那么 x % lcm{dig[i]} = 0,
    又 2520%lcm{dig[i]} = 0,
    那么x%2520%lcm{ dig[i] } = 0,x范围由9*10^18变为2520。

*/
ll gcd(ll a,ll b)
{
    return b?gcd(b,a%b):a;
}
ll lcm(ll a,ll b)
{
    return a/gcd(a,b)*b;
}
const int mxlcm = 2520;// 0~9所有数字的 lcm
int  dig[25];//保存数字的每一位
ll dp[25][50][mxlcm+5];//dp[i][j][k]表示长度为 i 所有数字lcm为j  %mxlcm 为 k(即余数为k)的数的个数
int  hs[mxlcm+5];//hs[i]表示所有数位的lcm 为 i的数的编号
/*
    关于上界 up 的作用:
    比如数字是 543
    那么当搜到 5 的时候
    枚举 0~5 其中枚举 4 的时候可以去搜499、
    但是枚举到 5 的时候 599 是大于543的
    所以这里用 up 控制搜索到的数字在 cal计算的数字范围内
*/
ll dfs(int len,int plcm,int pnum,bool up)//记忆化搜索
{     //当前位      前面数字的lcm  前面的数   是否达到上界
    if (len == 0) return pnum%plcm == 0;//整个数都搜完了,即搜到个位,只需单独判断个位即可
    if (!up&&dp[len][hs[plcm]][pnum]!=-1) return dp[len][hs[plcm]][pnum];
    int n = 9;
    if (up)//到界了
    {
        n = dig[len];//达到上界的时候是dig[len],其他时候是9
    }
    ll ans = 0;
    for (int i = 0;i <= n;++i)//枚举这一位可能的数字
    {
        int nnum = (pnum*10 + i)%mxlcm;
        int nlcm = plcm;
        if (i) nlcm = lcm(plcm,i);
        ans += dfs(len-1,nlcm,nnum,up&&(i == n));//所有的可能加起来
    }
    if (!up) dp[len][hs[plcm]][pnum] = ans;
    return ans;
}
ll cal(ll x)//计算[1,num]中beautifun的个数
{
    int len = 0;//将数字的每一位保存在数组dig中
    while (x)
    {
        dig[++len] =x%10;
        x/=10;
    }
    return dfs(len,1,0,1);//传参
}
void init()//hash预处理
{
    int id = 0;mem(dp,-1);//记忆化dp只初始化一次即可
    for (int i = 1;i <= mxlcm;++i)
    {
        if (mxlcm%i == 0) hs[i] = ++id;
    }
}
int main()
{
    int T;init();
    scanf("%d",&T);
    while (T--)
    {
        ll l,r;
        scanf("%I64d %I64d",&l,&r);
        printf("%I64d\n",cal(r) - cal(l-1));
    }
    return 0;
}

B - XHXJ's LIS

题意:

当把数当字符串看的时候,求区间[l,r]最长公共子序列的长度为K的数的个数

我个人觉得这题出的很好,将数位DP,状态压缩和最长公共子序列的nlogn算法结合起来

另写了题解: HDU 4352 XHXJ's LIS(数位DP+状压)


C - 不要62

题意:

区间[l,r]内数字的数位不含62且不含4的数的个数

分析:

这题数据小,可以水过,用dp[i]表示前i个数中满足的数的个数

if ok(i) dp[i] = dp[i-1] + 1  else dp[i] = dp[i-1]  先求出所有dp,然后直接输入输出

用正常的数位DP的方法写的话:

     状态保存:
    dp[len][0]表示长度为len 且不含4和62,最高位不是2的个数
    dp[len][1]表示长度为len 且不含4和62,最高位是2的个数
    状态转移:
    dp[i][0] = 8*dp[i-1][0] + dp[i-1][1] (除去4还有8种可能)
    dp[i][1] = 7*dp[i-1][1] + dp[i-1][1] (除去4还要除去6,否则会构成62)

正常的循环可以写,不过感觉记忆化搜索更好理解写着更清晰,故而用记忆化写的

代码:

#define mem(a,x) memset(a,x,sizeof(a))
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<set>
#include<stack>
#include<cmath>
#include<map>
#include<stdlib.h>
#include<cctype>
#include<string>
using namespace std;
typedef long long ll;
/*
    状态保存:
    dp[len][0]表示长度为len 且不含4和62,最高位不是2的个数
    dp[len][1]表示长度为len 且不含4和62,最高位是2的个数
    状态转移:
    dp[i][0] = 8*dp[i-1][0] + dp[i-1][1] (除去4还有8种可能)
    dp[i][1] = 7*dp[i-1][1] + dp[i-1][1] (除去4还要除去6,否则会构成62)

*/
int dp[10][2];
int dig[10];//保存数字的每一位
int dfs(int len,bool is6,bool up)//当前搜的位,这一位的前一位是不是6,是否为上界
{
    if (len == 0) return 1; //dp[0][0] = 1;
    if (!up&&dp[len][is6]!=-1) return dp[len][is6];
    int ans = 0;
    int n = 9;if (up) n = dig[len];
    for (int i = 0;i <= n;++i)
    {
        if (i==4) continue;// 4
        if (is6&&i==2) continue;// 62
        ans += dfs(len-1,i==6,up&&(i == n));
    }
    if (!up) dp[len][is6] = ans;
    return ans;
}
int cal(int x)
{
    int len = 0;
    while (x)
    {
        dig[++len] = x%10;
        x/=10;
    }
    return dfs(len,0,1);
}
int main()
{
    mem(dp,-1);
    int l,r;
    while (scanf("%d %d",&l,&r)&&(l||r))
    {
        printf("%d\n",cal(r)-cal(l-1));
    }
    return 0;
}

D - Bomb

本渣的第一道数位DP题,对着别人的题解啃了半天,用循环来进行状态转移

(后来深刻体会到记忆化搜索写着更清晰更好理解!)

题意:

区间[1,r]中数位不含49的数的个数(感觉数位DP的题意都是一个调调)

分析:

    dp[i][0] 表示长度为 i 的数中 不含 49 的数的个数
    dp[i][1] 表示长度为 i 的数中 不含 49 但最高位为 9 的数的个数
    dp[i][2] 表示长度为 i 的数中 含49的数的个数

        dp[i][0] = dp[i-1][0] * 10 - dp[i-1][1];
        dp[i][1] = dp[i-1][0];
        dp[i][2] = dp[i-1][2] * 10 + dp[i-1][1];

先把表打好,然后对于具体输入的r具体处理

代码1(循环):

#define mem(a,x) memset(a,x,sizeof(a))
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<set>
#include<stack>
#include<cmath>
#include<map>
#include<stdlib.h>
#include<cctype>
#include<string>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int N = 20;
ll dp[N][3];
void init()
{
    dp[0][0] = 1;
    for (int i = 1;i <= N;++i)
    {
        dp[i][0] = dp[i-1][0] * 10 - dp[i-1][1];
        dp[i][1] = dp[i-1][0];
        dp[i][2] = dp[i-1][2] * 10 + dp[i-1][1];
    }
}
int len;
ll s[30];
void cal(ll x)
{
    len = 0;
    while (x)
    {
        s[++len] = x%10;
        x/=10;
    }
}
int main()
{
    int T;scanf("%d",&T);
    init();
    while (T--)
    {

        ll n;
        scanf("%I64d",&n);
        ++n;cal(n);
        int lr = 0;ll sun = 0;bool fd = 0;
        for (int i = len;i >= 1;--i)
        {
            sun += (ll)(s[i])*dp[i-1][2];
            if (fd) sun += (ll)(s[i])*(dp[i-1][0]);
            if (!fd&&s[i] > 4)
            {
                sun += dp[i-1][1];
            }
            if (lr == 4&&s[i] == 9) fd = 1;
            lr = s[i];
        }
        printf("%I64d\n",sun);
    }
    return 0;
}

代码2(记忆化搜索):

#define mem(a,x) memset(a,x,sizeof(a))
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<set>
#include<stack>
#include<cmath>
#include<map>
#include<stdlib.h>
#include<cctype>
#include<string>
#define Sint(n) scanf("%d",&n)
#define Sll(n) scanf("%I64d",&n)
#define Schar(n) scanf("%c",&n)
#define Sint2(x,y) scanf("%d %d",&x,&y)
#define Sll2(x,y) scanf("%I64d %I64d",&x,&y)
#define Pint(x) printf("%d",x)
#define Pllc(x,c) printf("%I64d%c",x,c)
#define Pintc(x,c) printf("%d%c",x,c)
using namespace std;
typedef long long ll;
/*
	记忆化绝对比循环好理解,我发誓~~~^_^ 
	dp[len][four][have]表示正在处理的位长度为len
	正在处理的前面一位是不是4
	已经处理过的位里面是不是已经有49了 
*/
ll dp[22][2][2];
int dig[22];
ll dfs(int len,bool four,bool have,bool up)
{
	if (len == 0) return have;
	ll &ot = dp[len][four][have];
	if (!up&&~ot) return ot;
	int n = 9;if (up) n = dig[len];
	ll ans = 0;
	for (int i = 0;i <= n;++i)
	{
		bool newhave = have;
		if (four&&i == 9) newhave = 1;
		ans += dfs(len-1,i==4,newhave,up&&i==n);
	}
	if (!up) ot = ans;
	return ans;
} 
ll cal(ll x)
{
	int len = 0;
	while (x)
	{
		dig[++len] = x%10;
		x/=10;
	}
	return dfs(len,0,0,1);
}
int main()
{
	mem(dp,-1);
    int T;Sint(T);
    while (T--)
    {
    	ll n;Sll(n);
    	Pllc(cal(n),'\n');
	}
    return 0;
}


E - Round Numbers

题意:求区间[l,r]内二进制数中0比1多的数的个数

分析:之前做的都是十进制数的数位DP,这个可以转换成二进制数的数位DP(花神的数论题也是二进制上的数位DP)

dp[i][j][k]中i,j,k分别表示长度,0的个数,1的个数,记忆化搜索的时候多传的两个参数分别表示是否到达上界,前一位是否为0

代码:

#define mem(a,x) memset(a,x,sizeof(a))
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<set>
#include<stack>
#include<cmath>
#include<map>
#include<stdlib.h>
#include<cctype>
#include<string>
using namespace std;
typedef long long ll;
int dig[70];
ll dp[70][70][70];//长度 0的个数  1的个数 
ll dfs(int len,int zero ,int one,bool up,bool z)
{
	if (len == 0)
	{
		return z||(zero>=one);
	}
	ll &ot = dp[len][zero][one];
	if (!up&&!z&&ot!=-1) return ot;
	int n = 1;if (up) n = dig[len];
	ll ans = 0;
	for (int i = 0;i <= n;++i)
	{
		if (z&&i==0) ans += dfs(len-1,0,0,up&&(i==n),z&&(i==0));
		else ans += dfs(len-1,zero + (!i),one+i,up&&(i==n),z&&(i==0)); 
	} 
	if (!up&&!z) ot = ans;
	return ans;
} 
ll cal(ll x)
{
	int len = 0;
	while (x)
	{
		dig[++len] = x%2;
		x/=2;
	}
	return dfs(len,0,0,1,1);
}
int main()
{
    ll l,r;mem(dp,-1);
    while (scanf("%lld %lld",&l,&r) == 2)
    {
    	printf("%lld\n",cal(r)-cal(l-1));
	}
    return 0;
}



F - 吉哥系列故事――恨7不成妻

这个题目不同于一般的数位DP求区间内具有某性质的数的个数,而是求区间内具有某性质的所有数的平方和

感觉蛮厉害的样子,另写了题解: HDU 4507 吉哥系列故事——恨7不成妻(数位DP)


G - Balanced Number

题意:

求区间[l,r]内的平衡数的个数

所谓平衡数是指,把这个数的某一数位设置为支点。支点左右两边按|i - p|*dig计算力矩,如果能找到支点使左右力矩相等就是平衡数

例如4139    取3为支点,左边力矩 = 4*2+1*1  = 9,右边力矩 = 9*1 = 9所有是平衡数

分析:

还是根据数的性质保存状态,显然数的性质涉及到力矩,支点的位置

所以dp[i][j][k] :

                                          i : 正在处理的数位
                                          j : 支点的位置
                                         k : 左右力矩之和(正负算,为 0 就是平衡的)
                                        dp[i][j][k]就是具有上述性质的数的个数

    dp[i][j][k] = dp[i-1][j][ k + dig*(i-j)]
    其中 dig 为数位 i 处枚举的可能的数字

    状态转移中j没有转移?支点的位置需要另外枚举

枚举支点位置?会不会出现算某个位置的时候算了一遍数字x,算另一个位置的时候又算了一遍x这样算重复的情况?

想一下,一个数如果是平衡数,那么它的支点位置必然是固定的(0除外)

所以不会算重复,只有最后把重复的0减去就好,0算了len遍,故重复的0有(len-1)个

这里提一个事,这题的dp[i][j][k]保存了3个维度的信息,但是实际上,它在状态转移的时候第二维的j并没有改变

看代码里面的dfs的过程也是,传入的一个参数p从来都没有变过,为什么不省去这一维呢?

首先,dfs的时候确实可以不传参数p,直接让其在全局里面,然后枚举位置的时候改变其值,每次递归的时候可以直接用

但是dp还是应该保存这一维,因为题目是多组数据,秉着算过就记录下来的原理,可以为后面更多组数节约时间

前面B题里面也有一维没有参与状态转移,但是依旧保存下来了是同样的原理(B题的dfs没有传那个不变的参数K)

代码:

#define mem(a,x) memset(a,x,sizeof(a))
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<set>
#include<stack>
#include<cmath>
#include<map>
#include<stdlib.h>
#include<cctype>
#include<string>
using namespace std;
typedef long long ll;
/*
    dp[i][j][k] :
                i : 正在处理的数位
                j : 支点的位置
                k : 左右力矩之和(正负算,为 0 就是平衡的)
                dp[i][j][k]就是具有上述性质的数的个数

    dp[i][j][k] = dp[i-1][j][ k + dig*(i-j)]
    其中 dig 为数位 i 处枚举的可能的数字

    状态转移中j没有转移?支点的位置需要另外枚举
*/
ll dp[22][22][1600];//9*(1+2+3......+18) = 1539
int dig[22];//数字数位上的数字
ll dfs(int len,int p,int sum,bool up)//前3个参数对应dp3个维度的意义,up标记上界
{
    if (len == 0) return sum==0;
    if (sum < 0) return 0;
    if (!up&&dp[len][p][sum]!=-1) return dp[len][p][sum];
    ll ans = 0;
    int n = 9;if (up) n = dig[len];
    for (int i = 0;i <= n;++i)
    {
        ans += dfs(len-1,p,sum+i*(len-p),up&&(i==n));
    }
    if (!up) dp[len][p][sum] = ans;
    return ans;
}
ll cal(ll x)
{
    if (x == -1) return 0;//这题的 l 可以为 0,l-1就是-1了
    int len = 0;
    while (x)
    {
        dig[++len] = x%10;
        x/=10;
    }
    //需要枚举支点的位置
    ll ans = 0;
    for (int i = 1;i <= len;++i)
    {
        ans += dfs(len,i,0,1);
    }
    return ans - (len - 1);//减去重复的0
}
int main()
{
    int T;mem(dp,-1);
    scanf("%d",&T);
    while (T--)
    {
        ll l,r;
        scanf("%I64d %I64d",&l,&r);
        printf("%I64d\n",cal(r)-cal(l-1));
    }
    return 0;
}


H - B-number

题意:

求区间[1,r]内数位含13且可以整除13的数个数

分析:

含13的话类比第D题不要49,整除13类比F题整除7(都是一个调调)

直接类比D题和F题去做,这里就不多说直接上代码了:

PS:网上看了一下别人的题解,别人保存的状态好像和我的有点差别

        不过我的想法也很自然,反正自己看得蛮舒服的。。。。(主要是受前面D题F题的影响,自然而然的思想^_^)

#define mem(a,x) memset(a,x,sizeof(a))
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<set>
#include<stack>
#include<cmath>
#include<map>
#include<stdlib.h>
#include<cctype>
#include<string>
using namespace std;
typedef long long ll;
/*
    dp[i][j][k][l]:
                    i : 正在处理的数位
                    j : %13 = j的数
                    k :这一位的前一位是否为 1(是--1,否--0)
                    l :前面是否已经含 13(是--1,否--0)
*/
ll dp[22][13][2][2];
ll dig[22];
ll dfs(int len, int r,bool is1,bool has,bool up)
{
    if (len == 0) return r == 0&&has;
    if (!up&&dp[len][r][is1][has]!=-1) return dp[len][r][is1][has];
    ll ans = 0;
    int n = 9;if (up) n = dig[len];
    for (int i = 0;i <= n;++i)
    {
        bool nhas = has;
        if (is1&&i==3) nhas = 1;//已经有了13
        ans += dfs(len-1,(i+r*10)%13,i==1,nhas,up&&(i==n));
    }
    if (!up) dp[len][r][is1][has] = ans;
    return ans;
}
ll cal(ll x)
{
    int len = 0;
    while (x)
    {
        dig[++len] = x%10;
        x/=10;
    }
    return dfs(len,0,0,0,1);
}
int main()
{
    ll r;mem(dp,-1);
    while (~scanf("%I64d",&r))
    {
        printf("%I64d\n",cal(r));
    }
    return 0;
}


  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值