首页 > 其他分享 >数位 dp

数位 dp

时间:2024-08-31 09:25:09浏览次数:3  
标签:int pos num limit dp 数位

\(\texttt{0x00}\) 简介

数位 dp 解决的是与数字有关的一类计数问题,在求解过程中常把一个数字的每一位都拆开来看,比如十进制下就是把千位、百位、十位、个位上的数字都拆开来看,其他进制类比十进制。

数位 dp 的问题一般比较显眼,有几个常见形式:

  1. 要求统计满足一定条件的数的数量(即,最终目的为计数);

  2. 这些条件经过转化后可以使用「数位」的思想去理解和判断;

  3. 输入会提供一个数字区间(有时也只提供上界)来作为统计的限制;

  4. 上界很大(比如 \(10^{18}\)),暴力枚举验证会超时。

(from OI Wiki)

在数位 dp 的实现上,我通常采用的是记忆化搜索,这样写不仅容易,而且易于拓展,还可以当板子来背,这已经是 dp 中少见的了。

\(\texttt{0x01}\) 例题

P2657 [SCOI2009] windy 数

题目大意

求 \([l, r]\) 内有多少个数十进制表示下所有的相邻数位数值之差大于等于 \(2\)

思路

考虑从最高位开始填数,在记忆化搜索时记录 \(pos\) 表示当前填到第几位,\(pre\_num\) 表示上一个位置填的数是什么,\(limit\) 记录前面放的数是否顶上界,\(zero\) 记录当前这位之前是否是前导零。

先把上界的每一位抠出来,那么当搜索放第 \(i\) 位时,要先确定这一位能放什么数,若前面都是贴着上界放的,那么这一位最多只能放 \(num_{pos}\),否则就不受限制。

然后在枚举第 \(i\) 位放什么时还要满足相邻数位数值之差大于等于 \(2\) 的限制,这个很好转移。

当然,若是前导零的话还要特别注意,因为这时 \(0\sim 9\) 都可以放,而如果没有考虑到这一点,最高位就只能至少放 \(2\) 了。

在加记忆化时还要注意,若出现了顶上界或前导零的情况是不能记忆化的(当然你也可以多开两维来额外存,不过我觉得没什么必要,这个时候直接暴力搜索就行了,反正也费不了多少时间)。

\(\texttt{Code:}\)

#include <cmath>
#include <vector>
#include <cstring>
#include <iostream>

using namespace std;

const int N = 15;

int l, r;
int f[N][N];
vector<int> num;

int dfs(int pos, int pre_num, bool limit, bool zero) {
    if(pos < 0) return 1; //边界,若放完了最后一位就返回 1,因为我们一直是按要求放的,所以此时也是一种情况
    if(!limit && pre_num >= 0 && f[pos][pre_num] != -1) return f[pos][pre_num]; //记忆化
    int mx = (limit ? num[pos] : 9); //计算上界
    int res = 0;
    for(int i = 0; i <= mx; i++) {
        if(abs(i - pre_num) < 2) continue;
        if(!i && zero) //特判前导零的情况,这时 prenum 设为 -2 确保下一位不受任何限制
            res += dfs(pos - 1, -2, limit && (i == num[pos]), 1);
        else 
            res += dfs(pos - 1, i, limit && (i == num[pos]), 0);
    }
    if(!limit && !zero) f[pos][pre_num] = res;
    return res;
}

int calc(int x) {
    num.clear();
    int tmp = x;
    //先把上界的每一位抠出来
    while(tmp) {
        num.push_back(tmp % 10);
        tmp /= 10;
    }
    //初始化
    memset(f, -1, sizeof f);
    return dfs(num.size() - 1, -2, 1, 1);
}

int main() {
    scanf("%d%d", &l, &r);
    //数位 dp 通常都有这种类似前缀和的形式
    printf("%d\n", calc(r) - calc(l - 1));
    return 0;
}

P2602 [ZJOI2010] 数字计数

题目大意

求 \([l, r]\) 中的数在十进制表示下 \(0\sim 9\) 各个数码分别出现了多少次。

思路

对每个数码分开计算,还是拆成 \(r\) 减去 \(l - 1\) 的形式。

在记忆化搜索时记录一下当前考虑的数码 \(d\) 填了多少次,在所有位填完后再计算即可。

后面都只放主要代码了,因为真的很板子。

\(\texttt{Code:}\)

ll dfs(int pos, ll cnt, bool limit, bool zero, int d) {
    if(pos < 0) return cnt;
    if(!limit && !zero && f[pos][cnt] != -1) return f[pos][cnt];
    int mx = (limit ? num[pos] : 9);
    ll res = 0;
    for(int i = 0; i <= mx; i++)
        res += dfs(pos - 1, cnt + ((!zero || i) && (i == d)), limit && (i == num[pos]), zero && (!i), d);
    if(!limit && !zero) f[pos][cnt] = res;
    return res;
}

Digit Sum

题目大意

求 \([1, N]\) 中有多少个数在十进制表示下数码和是 \(D\) 的倍数。

数据范围:\(1\le N\le 10^{10000},1\le D\le 100\)。

思路

很明显的数位 dp。

首先把上界 \(N\) 的每一位抠出来,然后进行填数,个人喜欢从最高位开始填。

加上记忆化,设 \(f(pos, r)\) 表示在没有顶上界和前导零的情况下,当前填到了第 \(pos\) 位,余数为 \(r\) 的数的个数。

然后在搜索过程中记一下当前数位和 \(\bmod p\) 等于多少,再简单转移一下即可,详细注释在代码中。

这里再讲一下数位 dp 如何分析时间复杂度。

注意到状态数为 \(D\cdot\lg N\),每次转移时最多枚举 \(10\) 个可填的数,所以时间复杂度为 \(O(D\cdot \lg N)\),可以通过此题。

注意!由于最后要 \(-1\),所以为防止减为负数要先加上模数再取模。

\(\texttt{Code:}\)

ll dfs(int pos, int r, bool limit, bool zero) {
    if(pos < 0) return (r == 0);
    if(!limit && !zero && f[pos][r] != -1) return f[pos][r];
    int mx = (limit ? num[pos] : 9);
    ll res = 0;
    for(int i = 0; i <= mx; i++)
        res = (res + dfs(pos - 1, (r + i) % d, limit && (i == num[pos]), zero && (!i))) % mod;
    if(!limit && !zero) f[pos][r] = res;
    return res;
}

P4127 [AHOI2009] 同类分布

题目大意

求出 \([l, r]\) 中各位数字之和能整除原数的数的个数。

思路

若是要求整除的数是同一个数,那就和上一题一样,但若除的数都不一样该怎么办?

那我们就换一种思路,直接枚举数位和,然后在搜索时每填一个数就相应地减去,同时记录一下余数,其他的参数照搬即可。

由于最多有 \(18\) 位,所以要枚举 \(1\sim 162\)。

\(\texttt{Code:}\)

ll dfs(int pos, int sum, int r, bool limit, bool zero) {
    if(sum < 0) return 0;
    if(pos < 0) return !sum && !r;
    if(!limit && !zero && f[pos][sum][r] != -1) return f[pos][sum][r];
    int mx = (limit ? num[pos] : 9);
    ll res = 0;
    for(ll i = 0; i <= mx; i++)
        res += dfs(pos - 1, sum - i, (r * 10 + i) % d, limit && (i == num[pos]), zero && (!i));
    if(!limit && !zero) f[pos][sum][r] = res;
    return res;
}

拓展:

数位 dp 一般会与 Lucas 定理一起食用,毕竟 Lucas 定理就是逐位求组合数。

习题:

P8764 [蓝桥杯 2021 国 BC] 二进制问题
P6218 [USACO06NOV] Round Numbers S
P4124 [CQOI2016] 手机号码
P4317 花神的数论题
P7976 「Stoi2033」园游会
P3413 SAC#1 - 萌数
P3286 [SCOI2014] 方伯伯的商场之旅
P2481 [SDOI2010] 代码拍卖会

标签:int,pos,num,limit,dp,数位
From: https://www.cnblogs.com/Brilliant11001/p/18389858

相关文章

  • 2024.8.30 总结(集训 考 DP)
    上午三个多小时考四道题的DP。赛时会的分:[100](?)+100+[30](?)+100。估分:100+100+0+100。实际分:10+100+0+100。挂巨量的分,挂了120分。下面是一些值得注意的点:T1就是分组背包。DP数组下标要考虑负数可以直接全体加一个值变成非负的,[或者用map之类的](?)(&不......
  • atc 经典dp 26题 题型总结
    题目链接稍微记录下吧。主要想发现他这个题单主人是怎么去分类dp的类型的。借鉴题目不一定要多难。但是题型的分类总结感觉很重要。某种dp的处理方式。。他是相似的。。AB数组前面往i+1,i+2.。。这样的推。C限制只能交叉继承。。不能继承pre一样位置的。他每......
  • Luogu P10997 Partition 题解 [ 蓝 ] [ 轮廓 dp ]
    Partition:一道dp神题,用到了以轮廓线的轨迹来做dp的技巧,和敲砖块这题的状态设计有点相似。观察首先观察样例,发现整张图可以看作是被两条线分隔开的。同时每个颜色的四个方向上又存在一大堆奇怪的性质,很容易发现这两条线一条是从左上到右下的线,另一条是从右下到左上的线。暴......
  • 【dp力扣】买卖股票的最佳时机III
    目录审题通过动态规划固定套路思考:1、定义状态表示(关键)2、推导状态转移方程(重点)对于buy(可买入股票):回顾状态表示:第一种情况:第二种情况:联立两种情况(取两种情况的最大值):​对于own(持有股票)回顾状态表示:第一种情况:第二种情况:(最终结果)联立两种情况(还是取max):3、初......
  • zdppy_cache缓存框架升级,支持用户级别的缓存隔离,支持超级管理员管理普通用户的缓存
    启动服务importzdppy_apiasapiimportzdppy_cachekey1="admin"key2="admin"app=api.Api(routes=[*zdppy_cache.zdppy_api.cache(key1,key2,api)])if__name__=='__main__':importzdppy_uvicornzdppy_uvico......
  • 数位DP小记
    1.基础1.1.问题数位DP解决的一般都是和数字相关的计数问题,常见的有\(l\simr\)中有多少数符合某个关于数位的条件。对于这种问题,我们都是先用前缀和转化成小于等于某个数的问题。下面以P2602[ZJOI2010]数字计数为模板题。1.2记忆化搜索我们先枚举每个数码。我们......
  • 在Android开发中,如何使用SharedPreferences(简称SP)一个轻量级的数据存储方式
    目录全局SharedPreferences工具类代码说明:如何使用这个工具类?在Android开发中,SharedPreferences(简称SP)是一个轻量级的数据存储方式,常用于保存应用的配置信息或少量的数据。为了便于在全局使用,可以将其封装到一个工具类中。以下是一个带有详细中文注释的全局SharedPrefere......
  • 插入类型 DP 学习笔记
    插入类型DP形式多为nnn个元素无法重复使用,需要给定一个排列,满足一定条件或是求有多少个排列满足一定条件。nnn一般在100∼5×103100\sim5\times10^3100∼5×103左右。满足一些函数图像,类似于波浪函数,且答案与每个波浪和波浪的顶点有关(函数的xxx坐标为下标,yy......
  • 在中国使用wordpress建网站的主要有三类人
    在中国,使用WordPress建网站的主要有三类人:做IT技术程序员、海归人士和做外贸的老板。这三类人选择WordPress的原因可以从WordPress的多个优势中找到答案。做IT技术程序员选择WordPress的原因在于其高度的可扩展性和灵活性。WordPress的模块化设计和强大的插件生态系统使得程序......
  • 树形dp的各种应用题型与模板
    ///**//低落...最近做了以及看了树形dp这部分的知识,感觉有必要做一些整理,所以特来此地写下来。我将整理一些树形dp基本的模板与应用以及思想。1.树的直径:树上最长的链概念应该很好懂,那么现在来看看代码(简略版):#include<iostream>usingnamespacestd;structEDGE{ int......