关于数位DP的学习

---恢复内容开始---

因为最近做比赛经常会出现数位DP,便尝试着去学学看数位DP。

先给出两篇论文的链接:

数位计数问题解法研究

浅谈数位类统计问题

然后也是寻找了很多大牛的博客,学习了很多(但是没学会囧。),现在先总结一下已经学到的东西

“在信息学竞赛中,有这样一类问题:求给定区间中,满足给定条件的某个D 进制数或
此类数的数量。所求的限定条件往往与数位有关,例如数位之和、指定数码个数、数的大小
顺序分组等等。题目给定的区间往往很大,无法采用朴素的方法求解。此时,我们就需要利
用数位的性质,设计log(n)级别复杂度的算法。解决这类问题最基本的思想就是“逐位确定”
的方法。下面就让我们通过几道例题来具体了解一下这类问题及其思考方法。”——刘聪

事实上,为什么会想到用数位DP来做,就是因为限定条件往往和数位有关,而仔细地朴素的暴力方法中,所做的重复的工作太多。这样的条件会使得DP(记忆化搜索)有用武之地。

目前我所接触到的大多数的题,都是可以通过记录某些值(比如数位等)来减少重复的运算。当然,因为此类题的特殊性,可以编写check函数确定代码的正确性。

再偷用某个大牛的一句话:其实数位DP(或者说所有记忆化搜索)都是可以看做通过搜索来填满状态的值。

首先先想想数位DP的运行模式

如果我们要统计[0,54321]中满足某个条件的个数,需要将其拆分为

[00000,09999][10000,19999],[20000,29999],[30000,39999],[40000,49999],

[50000,50999],[51000,51999],[52000,52999],[53000,53999],

[54000,54099],[54100,54199],[54200,54299],

[54300,54309],[54310,54319],

[54320,54321]

为什么要这么分呢?随便举个例子,如果我们统计过了[0000,9999]中的满足条件(或者其他各种不满足条件的状态)的个数,那么分别在加上前缀,就可以判断出有多少个满足条件的个数。目的是为了将大的区间划分为小的区间进行求解。

因此,总结一句话,数位DP减少的运算量为:前面几位固定,后面几位可以任意取的个数统计。

比如分析一道简单题:HDU 3652,通过我这个渣渣的错误历程来分析一些细节上的问题

先贴错误代码

 1 #include<cstdio>
 2 #include<cstring>
 3 #include<algorithm>
 4 using namespace std;
 5 int dp[20][20][20];
 6 int num[20];
 7 int dfs(int pos,int mod,int pre,int stat,int limit){
 8     if(pos==0) return mod==0&&stat;
 9     if(!limit&&dp[pos][mod][pre]!=-1) return dp[pos][mod][pre];
10     int ans=0;
11     int end=limit?num[pos]:9;
12     for(int i=0;i<=end;i++){
13         int nmod=(mod*10+i)%13;
14         int nstat=(pre==1&&i==3)||stat;
15         ans+=dfs(pos-1,nmod,i,nstat,limit&&i==end);
16     }
17     if(!limit) dp[pos][mod][pre]=ans;
18     return ans;
19 }
20 int cal(int x){
21     int cnt=0;
22     memset(num,0,sizeof(num));
23     while(x){
24         num[++cnt]=x%10;
25         x/=10;
26     }
27     return dfs(cnt,0,0,0,1);
28 }
29 int main()
30 {
31     int i,j;
32     int n;
33     memset(dp,-1,sizeof(dp));
34     while(scanf("%d",&n)!=EOF){
35         int ans=cal(n);
36         printf("%d\n",ans);
37     }
38     return 0;
39 }

我选取的记录参数有三个:pos当前处理位,mod前缀和余数,还有前一位的数字pre

但是运算结果却始终会小于等于正确的答案,为什么呢?

想了想,发现其实是因为参数含义的问题。

分析一下,如果我将pre作为一个关键参数记录下来,其实我并不能区分我记录的是后面几位能不能随机取的个数。

即当下一次搜索到pos,mod,pre的时候,不能确定前面是否有13,或者以前搜索的DP[pos][mod][pre]中的数的个数是否有13。

因此应该把记录的参数改为pos,mod,stat(表示为记录状态,0为不含13,1为只含前一位为1,2为前缀含有13)。

因此得到下面的AC代码

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int dp[20][20][3];
int num[20];
int dfs(int pos,int mod,int stat,int limit){
    if(pos==0) return mod==0&&stat==2;
    if(!limit&&dp[pos][mod][stat]!=-1) return dp[pos][mod][stat];
    int ans=0;
    int end=limit?num[pos]:9;
    for(int i=0;i<=end;i++){
        int nmod=(mod*10+i)%13;
        int nstat;
        if(stat==1&&i==3||stat==2) nstat=2;
         else if(i==1) nstat=1;
          else nstat=0;
        ans+=dfs(pos-1,nmod,nstat,limit&&i==end);
    }
    if(!limit) dp[pos][mod][stat]=ans;
    return ans;
}
int cal(int x){
    int cnt=0;
    memset(num,0,sizeof(num));
    while(x){
        num[++cnt]=x%10;
        x/=10;
    }
    return dfs(cnt,0,0,1);
}
int main()
{
    int i,j;
    int n;
    memset(dp,-1,sizeof(dp));
    while(scanf("%d",&n)!=EOF){
        int ans=cal(n);
        printf("%d\n",ans);
    }
    return 0;
}
View Code

好了讲完了

转载于:https://www.cnblogs.com/acalvin/p/3760082.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值