• 数位dp【模板 + 老年康复】


    学习博客:

    戳这里

    戳这里

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

    事实上,为什么会想到用数位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减少的运算量为:前面几位固定,后面几位可以任意取的个数统计。

    模板:

     1 typedef long long ll;
     2 int a[20];
     3 ll dp[20][state];//不同题目状态不同
     4 ll dfs(int pos,/*state变量*/,bool lead/*前导零*/,bool limit/*数位上界变量*/)//不是每个题都要判断前导零
     5 {
     6     //递归边界,既然是按位枚举,最低位是0,那么pos==-1说明这个数我枚举完了
     7     if(pos==-1) return 1;/*这里一般返回1,表示你枚举的这个数是合法的,那么这里就需要你在枚举时必须每一位都要满足题目条件,也就是说当前枚举到pos位,一定要保证前面已经枚举的数位是合法的。不过具体题目不同或者写法不同的话不一定要返回1 */
     8     //第二个就是记忆化(在此前可能不同题目还能有一些剪枝)
     9     if(!limit && !lead && dp[pos][state]!=-1) return dp[pos][state];
    10     /*常规写法都是在没有限制的条件记忆化,这里与下面记录状态是对应,具体为什么是有条件的记忆化后面会讲*/
    11     int up=limit?a[pos]:9;//根据limit判断枚举的上界up;这个的例子前面用213讲过了
    12     ll ans=0;
    13     //开始计数
    14     for(int i=0;i<=up;i++)//枚举,然后把不同情况的个数加到ans就可以了
    15     {
    16         if() ...
    17         else if()...
    18         ans+=dfs(pos-1,/*状态转移*/,lead && i==0,limit && i==a[pos]) //最后两个变量传参都是这样写的
    19         /*这里还算比较灵活,不过做几个题就觉得这里也是套路了
    20         大概就是说,我当前数位枚举的数是i,然后根据题目的约束条件分类讨论
    21         去计算不同情况下的个数,还有要根据state变量来保证i的合法性,比如题目
    22         要求数位上不能有62连续出现,那么就是state就是要保存前一位pre,然后分类,
    23         前一位如果是6那么这意味就不能是2,这里一定要保存枚举的这个数是合法*/
    24     }
    25     //计算完,记录状态
    26     if(!limit && !lead) dp[pos][state]=ans;
    27     /*这里对应上面的记忆化,在一定条件下时记录,保证一致性,当然如果约束条件不需要考虑lead,这里就是lead就完全不用考虑了*/
    28     return ans;
    29 }
    30 ll solve(ll x)
    31 {
    32     int pos=0;
    33     while(x)//把数位都分解出来
    34     {
    35         a[pos++]=x%10;//个人老是喜欢编号为[0,pos),看不惯的就按自己习惯来,反正注意数位边界就行
    36         x/=10;
    37     }
    38     return dfs(pos-1/*从最高位开始枚举*/,/*一系列状态 */,true,true);//刚开始最高位都是有限制并且有前导零的,显然比最高位还要高的一位视为0嘛
    39 }
    40 int main()
    41 {
    42     ll le,ri;
    43     while(~scanf("%lld%lld",&le,&ri))
    44     {
    45         //初始化dp数组为-1,这里还有更加优美的优化,后面讲
    46         printf("%lld
    ",solve(ri)-solve(le-1));
    47     }
    48 }
    View Code

    练手题目1:戳这里

    题意:求1-n内有多少个数满足各位之和整除该数。

    解题思路:数位dp,枚举各位之和。

    附ac代码:

     1 #include<iostream>
     2 #include<algorithm>
     3 #include<stdio.h>
     4 #include<string.h>
     5 #include<string>
     6 using namespace std;
     7 typedef long long ll;
     8 int a[22];
     9 ll dp[20][220][220];//不同题目状态不同
    10 int mod;
    11 ll dfs(int pos, int state/*state变量*/, int r/*其他记录点,在这里是余数*/, bool limit/*数位上界变量*/)
    12 {
    13     //递归边界,既然是按位枚举,最低位是0,那么pos==0说明这个数我枚举完了
    14     if(pos == 0) return (state == mod && !r);
    15     /*这里一般返回1,表示你枚举的这个数是合法的,那么这里就需要你在枚举时必须每一位都要满足题目条件,
    16     也就是说当前枚举到pos位,一定要保证前面已经枚举的数位是合法的。
    17     不过具体题目不同或者写法不同的话不一定要返回1 */
    18     //第二个就是记忆化(在此前可能不同题目还能有一些剪枝)
    19     if(dp[pos][state][r] != -1 && !limit) return dp[pos][state][r];
    20     /*常规写法都是在没有限制的条件记忆化,这里与下面记录状态是对应*/
    21     int up = limit?a[pos]:9;//根据limit判断枚举的上界up;
    22     ll ans = 0;
    23     //开始计数
    24     for(int i = 0; i <= up; ++i)
    25     {
    26         if(i + state > mod) break;//剪枝
    27         ans += dfs(pos - 1, state + i, (r * 10 + i) % mod, limit && i == a[pos]);//最后两个变量传参都是这样写的
    28         /*这里还算比较灵活,不过做几个题就觉得这里也是套路了
    29         大概就是说,我当前数位枚举的数是i,然后根据题目的约束条件分类讨论
    30         去计算不同情况下的个数,还有要根据state变量来保证i的合法性,比如题目
    31         要求数位上不能有62连续出现,那么就是state就是要保存前一位pre,然后分类,
    32         前一位如果是6那么这意味就不能是2,这里一定要保存枚举的这个数是合法*/
    33     }
    34     //计算完,记录状态
    35     if(!limit) dp[pos][state][r] = ans;
    36     /*这里对应上面的记忆化,在一定条件下时记录,保证一致性,当然如果约束条件不需要考虑lead,这里就是lead就完全不用考虑了*/
    37     return ans;
    38 }
    39 ll solve(ll n)
    40 {
    41     int pos = 0;
    42     ll x = n;
    43     while(x)//把数位都分解出来
    44     {
    45         a[++pos] = x % 10;//个人老是喜欢编号为[1,pos],看不惯的就按自己习惯来,反正注意数位边界就行
    46         x /= 10;
    47     }
    48     ll ans = 0;
    49     for(int i = 1; i <= 9 * pos; ++i)//枚举模
    50     {
    51         mod = i;
    52         //初始化dp数组为-1
    53         memset(dp, -1, sizeof(dp));
    54         ans += dfs(pos/*从最高位开始枚举*/, 0, 0/*一系列状态 */, true);//刚开始最高位都是有限制的,显然比最高位还要高的一位视为0嘛
    55     }
    56     return ans;
    57 }
    58 int main()
    59 {
    60     int t;
    61     scanf("%d", &t);
    62     ll n;
    63     for(int cas = 1; cas <= t; ++cas)
    64     {
    65         scanf("%lld", &n);
    66         printf("Case %d: %lld
    ", cas, solve(n));
    67     }
    68 
    69 
    70 }
    View Code

    练手题目2:戳这里

    题意:求l-r中有多少个数满足各位>0的数不大于三个。

    解题思路:模板题略作修改。

    附ac代码:

     1 #include <bits/stdc++.h>
     2 using namespace std;
     3 const int maxn = 22;
     4 typedef long long ll;
     5 int a[maxn];
     6 ll dp[maxn][maxn];
     7 ll dfs(int pos, int stat, bool lim)
     8 {
     9     if(!pos) return 1;
    10     if(!lim && stat <= 3 && dp[pos][stat] != -1) return dp[pos][stat];
    11     int up = lim?a[pos]:9;
    12     ll ans = 0;
    13 
    14     for(int i = 0; i <= up; ++i)
    15     {
    16         if(stat + (i > 0) <= 3)
    17         {
    18             ans += dfs(pos - 1, stat + (i > 0), lim && i == a[pos]);
    19         }
    20     }
    21     if(!lim && stat <= 3) dp[pos][stat] = ans;
    22     return ans;
    23 }
    24 ll solv(ll x)
    25 {
    26     int pos = 0;
    27     while(x)
    28     {
    29         a[++pos] = x % 10;
    30         x /= 10;
    31     }
    32 
    33     return dfs(pos, 0, true);
    34 }
    35 int main()
    36 {
    37     int t;
    38     scanf("%d", &t);
    39     memset(dp, -1, sizeof(dp));
    40     ll l, r;
    41     while(t--)
    42     {
    43         scanf("%lld %lld", &l, &r);
    44         printf("%lld
    ", solv(r) - solv(l - 1));
    45     }
    46     return 0;
    47 }
    View Code
  • 相关阅读:
    数值项目的格式化
    ORACLE ERROR CODE代表的意思
    深入了解 Microsoft AJAX Library
    调用MSScriptContro来运算字符串表达式
    C# 调用带参数EXE文件及带启动参数EXE制作
    将DataTable或Ilist<>转换成JSON格式
    客户端控件调用服务器的参数
    调用ICodeCompiler来计算字符串表达式
    录像工具
    今天是开博客园的第一天
  • 原文地址:https://www.cnblogs.com/zmin/p/9438429.html
Copyright © 2020-2023  润新知