现在的位置: 首页 > 综合 > 正文

一类简单数位统计问题的记忆化搜索解法

2012年11月21日 ⁄ 综合 ⁄ 共 2482字 ⁄ 字号 评论关闭

今天集训内容是按位DP,个人理解可用于整数区间内的特殊类型数的统计问题。XnoZero学长讲的方法是递推DP,但在实际编码中,有一类针对数的总数的统计使用记忆化搜索的方法代码会更加明了,减少了不少状态的表示。这种写法一般用于初始化与逐位统计是一样过程的。

学习了这篇博客,很有启发:http://www.cppblog.com/Yuan/archive/2011/01/25/139299.aspx

举例为今天集训三道比赛题目(前两道比较水)(很遗憾C题求区间最大值的问题不太容易用这个优雅的代码来解决):

HOJ 3038 阶梯数 : http://acm.hit.edu.cn/judge/show.php?Proid=3038

改数字的位数字低位总是大于等于高位数字,例如123就是一个阶梯数,而321不是,现在求出闭区间[a,b]之间的阶梯数。(1 <= a, b <= 2 ^ 31)

考虑阶梯数的每一位,都满足 num < pre ,pre为前一位,可以设计出状态:dp[pos][pre],pos为当前的数位,pre为高一位的值。状态转移方程可为dp[i][j] = sum{dp[i + 1][k]}(0 <= k < pre),但对非数字'9'这样的边界就不太好处理,也可以通过增加一维状态来保存上一节点是否达到最大值来记录。这里给出使用记忆化搜索的主要代码

long long DFS(int pos, int pre, int inf)	// inf储存上界到达标志
{
    if (pos == -1)
        return 1;
    if (!inf && dp[pos][pre] != -1)
        return dp[pos][pre];
    long long ans = 0;
    int end = inf ? digit[pos] : 9;
    for (int i = 0; i <= end; i++)
    {
        if (i >= pre)
            ans += DFS(pos - 1, i, inf && i == end);
    }
    if (!inf)
        dp[pos][pre] = ans;	// 注意到达上界的情况下结果并不完整,不能为后面所用
    return ans;
}
 
long long calc(long long x)
{
    int pos = 0;
    while (x)
    {
        digit[pos++] = x % 10;
        x /= 10;
    }
    return DFS(pos - 1, 0, 1);	// dp数组初始化为-1
}

HOJ 3039:http://acm.hit.edu.cn/judge/show.php?Proid=3039

求出在[a,b]范围内各位数字之和mod n 为0的数的个数(1 <= a, b <= 2 ^31, 1 <= n < 100)

同样比赛的同学反映说这道题跟上一道用递推的话会有很大的不同,这里依旧使用记忆化搜索的同一个模板,状态dp[pos][pre],这里的pre并不是上一个数字,而是存储前面所有位数字加和模n之结果,满足所有的pre` = (pre + i) % n,其中 i 为枚举当前数字。代码的实现上基本与上一题一样,只要将边界的返回值改为 return (pre == 0),将递归更新得部分去掉判断,改为 ans += DFS(pos - 1, (pre + i) % n, inf && i == end);即可。

前两道题涉及到的条件都极少,属于水题,今天的D题是成都网络赛B题,在掌握了数位DP的技巧之后并不算很难的题。见HDU 3652

这题的条件有两个,一个为数位中包含13,同时要求该数为13的倍数,构造这样一个wqb-number可以分为两部分来进行,其一为包含子串“13”(可参考hdu 3555的处理),另一方面,记录状态pre,初始值为0,后面恒满足pre` = (pre * 10 + i) % 13,这样总体上就会满足 __pre = (pre * 10 ^ pos + i) % 13,当__pre为0时可认为成功构造出了13的倍数。关键代码如下:

long long DFS(int pos, int pre, int tag, bool inf)
{
    if (pos == -1)
        return tag == 2 && pre == 0;
 
    if (!inf && dp[pos][pre][tag] != -1)
        return dp[pos][pre][tag];
 
    long long ans = 0;
    int end = inf ? digit[pos] : 9;
 
    for(int i = 0; i <= end; i++)
    {
        // tag 的含义:0表示当前位置为任意数且前面序列中没有出现过“13”;1表示前面没有出现过“13”且当前位置为“1”;2表示前面整个序列中包含“13”.
        int ppre = (pre * 10 + i) % 13, ptag = tag;
        if (tag == 0 && i == 1)
            ptag = 1;
        else if (tag == 1 && i != 1)
            ptag = 0;
        if (tag == 1 && i == 3)
            ptag = 2;
        ans += DFS(pos - 1, ppre, ptag, inf && (i == end));
    }
 
    if (!inf)
        dp[pos][pre][tag] = ans;
    return ans;
}

这一类算法的基本框架:

INIT dp[1...N][1...M][1...K] to -1

DFS( pos, pre, tag, inf)

   if (pos == -1)   return  _if_the_result_exist_

   if (!inf && dp[pos][pre][tag])   return dp[pos][pre][tag]

   INIT ans

   INIT edge(beg || end)

   repeat(i = 1...to...end)            // or beg to end,这里双边界问题过于麻烦,暂不讨论

      do

         modify the pre` && tag`

         ans += DFS(pos - 1, pre`, tag`, inf && (i == end))         // 区间最值问题似乎可以用ans = max(dfs, ans)来处理,但我暂时没有想到完整的解决方案

   if (!inf)

      modify(dp[pos][pre][tag]

   return ans

DFS ENDP

PS:今天的C题是上下界闭区间求最值,暂时还不清楚是我没有掌握明白记忆化搜索技术还是模板应用具有局限性,需要深入研究。

抱歉!评论已关闭.