数位dp
个人理解是 针对数位计数这类特定问题的高效记忆化搜索方法,用途很窄但比赛很喜欢出...(数位就是按照k进制写 数字中的每一位 有点字符串的意思)
因为只针对数位计数这一特定问题,基础思想不是太简洁。入门时花了不少时间才理解 所以特地写一点入门的内容吧 需要基础记忆化搜索的前置技能
以最简单的问题为例 寻找小于等于x的数里 数位中含49的数的个数
暴力找的话 复杂度O(nlogn)其中显然有很多重复的计算 用数位dp的思想 可以在O(logn)的时间内算出
以数位dp的思想 先把数字处理成一位一位的样子 再从最高位开始搜索 就先这么处理一下
int solve(int x)
{
int pos = 1;
while (x)
a[pos++] = x % 10, x /= 10;
return dfs(pos - 1, 0, 1);
}
dfs里至少有3个参数 当前位 当前状态 和是否有最大限制
第一个 当前位 就是正在搜的位 通常是从大向小搜索 每次-1s 所以开始时是pos-1
第二个 状态数 以题目为例 要找含49的个数 可以设计出3种状态 0是之前没有49 1是前一位是4 需要判断当前位是不是9 2是之前已经出现过49 所以开始时是0
最后一个参数只取01 是限制取值的参数 举个栗子 43449 这个数字从万位开始搜索 万位不是4的时候 其他位可以任意取0-9 万位是4的时候 千位只能取0123 前两位是43的时候 百位只能取01234 否则可以任意取
最后一个参数的意思就是当前是否有取值限制 以决定搜索的下一位取值范围 一开始最高位只能取01234 存在限制 所以取1
然后进入dfs函数 和常规搜索一样 开始时先是判断跳出
int dfs(int pos, int pre, int lim)
{
if (!pos) return pre == 2;
if (!lim&&dp[pos][pre] != -1) return dp[pos][pre];
如果当前位是0 说明搜索到头了 要判断状态是否满足 即pre是否等于2
下一个是记忆化搜索的判断 如果当前搜索没有取值限制 且之前搜索过这个状态 就返回之前搜索过的答案
然后是状态传递
int i, n, up = lim ? a[pos] : 9, sum = 0;
for (i = 0; i <= up; i++)
{
if ((pre == 1 && i == 9) || pre == 2) n = 2;
else if (i == 4) n = 1;
else n = 0;
sum += dfs(pos - 1, n, lim&&i == up);
}
枚举下一位 如果有取值限制 那么最大只能取到当前数位 否则任意取(10进制下就是0-9)sum记录所有子状态的和
然后是状态的传递 和上面参数介绍中的状态变化一样
最后搜索子状态 lim&&i == up的意思是 如果当前有取值限制 且当前位取到最大值 则下一位也有取值限制 否则没有
最后 如果没有取值限制 就记录下这个搜索的值 记忆化搜索的部分
if (!lim) dp[pos][pre] = sum;
return sum;
}
所以完整代码如下
int dfs(int pos, int pre, int lim)
{
if (!pos) return pre == 2;
if (!lim&&dp[pos][pre] != -1) return dp[pos][pre];
int i, n, up = lim ? a[pos] : 9, sum = 0;
for (i = 0; i <= up; i++)
{
if ((pre == 1 && i == 9) || pre == 2) n = 2;
else if (i == 4) n = 1;
else n = 0;
sum += dfs(pos - 1, n, lim&&i == up);
}
if (!lim) dp[pos][pre] = sum;
return sum;
}
int solve(int x)
{
int pos = 1;
while (x)
a[pos++] = x % 10, x /= 10;
return dfs(pos - 1, 0, 1);
}
开始时要将dp数组全部赋值为-1
求0—x的数位统计 输出slove(x) l-r闭区间的数位统计输出slove(r)-slove(l-1) 因为是闭区间所以-1
至于复杂度 记忆化搜索的复杂度就是全部空间的大小 也就是数字位数乘以全部状态数 单次求解时内存不超时间就不会超 多次搜索由于取值限制 还会有一点其他计算 但如果已经记忆化了全部状态 限制状态的值也是由多个子非限制状态叠加 复杂度是递归子状态的次数 目测不超过O(logn)n是记忆化搜索的总大小
这导致memset的时间数量级和算法运行的最大复杂度一样...所以尽量让状态与输入值无关 这样只用memset 1次(详见hdu 4734)
入门的时候费解了半天 如果有这篇文章大概半小时就能了解 了解基本思想后再完善就容易多了
Hdu 2089&&hdu 3555 入门熟悉
Hdu 3652 同时记录2个变量的数位dp 不难
Hdu 4734 有一点trick的简单数位dp 直接按照题目去想会t因为memset()的时间tle 所以这题1w组样例只有500ms时限就是卡memset()
Poj 3252 进阶一点的二进制数位dp 增加了前置0的参数 dfs过程也复杂一些 曾经用组合数学方法a过
Codeforces 55D 墙裂推荐 状态压缩加数位dp 代码很短但满满信息熵 很有优化空间
Hdu 4352 最长递增序列的O(nlogn)算法+状态压缩和操作+数位dp 主要是难想 而且给出关键词还是不好写 唯一好的是代码不长...比赛时能写出这种题大概是银牌线往上级别
Hdu 3709 想到枚举平衡点后就很简单的数位dp
一点细节 这题的前导0可以不放在dfs过程中 因为搜索到pos位时 重复的0就是pos-1个 直接减去就好 也可以加前导0 这样每个计数都会少1(因为0是符合要求的但没有计数) 但求的是差所以不影响结果 特判一下左右边界是0的时候就好 还有一点常数优化 如果当前的平衡值小于0就return0
Zoj 3494 专题里有就说一下吧.. 神题 ac自动机+数位dp+大数加减进制转换 超出了我的芝士领域..暂不强写了...大概区域赛做出来就是金牌线往上的级别..(指相同思维难度编码难度 但从来没出现过类似题目的)
SPOJ BALNUM Balanced Numbers 好题 3进制状态压缩+数位dp 但不太难 开始看错题 样例没过费解好久 能快速独立做出这个难度的题 专题的目的就达到了..
Hdu 4507 腾讯出的鬼畜题 把3个数位dp状态叠在一起 最难的是求平方和...
增加一个状态记录当前数值 return的时候判条件返回平方 过了样例然而wa
正解是推公式得到平方和与当前位数有关 记录3个return值 然后dfs这3个状态...
代码有点复杂但不难 主要是思想鬼畜 还有dfs过程中各种% 容易wa
能快速独立ac这题可以说是非常强了(指不看题解 以现场赛的要求 在1小时内ac)
数位dp的编码实现不是太难 难的是分析题目 设计状态 压缩状态(涉及到位运算 hash 各种数学芝士..)不是单刷一刷数位dp就能练成的
所以入门阶段能独立ac简单数位dp 不需要其他芝士的数位dp看题解思路后能写出代码 就足够了(比如cf 55D 虽然代码不长但需要数学分析功底 3个人都能cf稳4题的话 金牌都就有希望...)