数位DP学习小结
2016-08-06 21:03
375 查看
一、学习心得体会
问题描述:一般体现为,定义某种性质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 能被1、2整除,故12是beautiful数
求区间[l,r]内的beautiful数的个数
分析:
利用记忆化搜索把小于等于num 的数中的所有beautiful数都搜出来
怎么搜呢?还是按照数字“位”来搜。
现在问题是:
1.怎么判断beautiful数?
2.怎么保存状态?
显然,需要将beautiful数的性质用状态保存下来。
beautiful数需要整除它所有的非零位
那么它只需整除它所有位的最小公倍数即可
数字1~9的最小公倍数为2520(设为mxlcm)
考虑这样一种保存状态的方法:
dp[i][j][k]表示长度为 i,所有数位的lcm为j模mxlcm余k的答案
那么需要开一个dp[20][2520][2520]的数组,类型是long long
这数组显然是开不下的,要想办法压缩
对这个数组的第二维,“所有数位的lcm为j”,其实j的取值虽然可能达到2520
但是j实际的数最多只有50个
于是可以考虑开一个hs数组,hs[j] = id;表示给所有数位的lcm为j的编号为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 [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; }
相关文章推荐
- 详解Android应用中屏幕尺寸的获取及dp和px值的转换
- 基于Android中dp和px之间进行转换的实现代码
- Android中dip、dp、sp、pt和px的区别详解
- LFC1.0.0 版本发布
- Android dpi,dip,dp的概念以及屏幕适配
- Android px、dp、sp之间相互转换
- HP data protector软件学习1--基本角色与基本工作流程
- HP data protector软件学习2--软件组成与界面介绍
- android中像素单位dp、px、pt、sp的比较
- Android对px和dip进行尺寸转换的方法
- 关于UI切图与开发 px和dp
- Android根据分辨率进行单位转换-(dp,sp转像素px)
- android 尺寸 dp,sp,px,dip,pt详解
- DP问题各种模型的状态转移方程
- POJ-1695-Magazine Delivery-dp
- nyoj-1216-整理图书-dp
- TYVJ1193 括号序列解题报告
- 对DP的一点感想
- TYVJ上一些DP的解题报告
- soj1005. Roll Playing Games