蓝桥杯每日一题-----数位dp

前言

今天浅谈一下数位dp的板子,我最初接触到数位dp的时候,感觉数位dp老难了,一直不敢写,最近重新看了一些数位dp,发现没有想象中那么难,把板子搞会了,变通也会变的灵活的多!

引入

以一道例题作为数位dp的引入,题目如下,链接
在这里插入图片描述
数据范围为1e9,一般的算法很难把这道题拿下,类似求在一段区间范围内,满足某些条件的数字的个数,并且数据范围很大时就会联想到数位dp算法。

第一个板子

我遇到的数位dp板子有三个,第一个来源于y总的讲解,附上链接和这道题的代码,y总的讲解逻辑清晰,想学习可以直接看视频。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
import java.util.Arrays;
import java.util.Scanner;

public class Main {
    static int[][] f = new int[11][10];
    static int k;
public static void main(String[] args) throws IOException {
    StreamTokenizer sc=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
    sc.nextToken();
    //Scanner scanner = new Scanner(System.in);
    int t = (int)sc.nval;
    while(t> 0) {
        t--;
        sc.nextToken();
        k = (int)sc.nval;
        sc.nextToken();
        int l = (int)sc.nval;
        sc.nextToken();
        int r = (int)sc.nval;
        //int l = scanner.nextInt();
        //int r = scanner.nextInt();
        for (int i = 0; i < 11; i++) {
            Arrays.fill(f[i], 0);
        }
        init();
        System.out.println(dp(r) - dp(l - 1));
        //dp(r);
    }
    return;
}

private static int dp(int num) {
    // TODO Auto-generated method stub
    if(num == 0) {
        return 0;
    }
    int res = 0;
    int last = 0;//上一个位数的数字
    int[] nu = new int[12];
    int n = 1;
    while (num > 0 ) {
        nu[n++] = num%10;
        num = num / 10;
    }
    n--;
    //System.out.println(n);
    for (int i = n; i > 0; i--) {//遍历位数
        int x = nu[i];
        int jj;
        if(i == n) {
            jj = 1;
        }else {
            jj = 0;
        }
        //System.out.println(x);
        //System.out.println(last);
        for (; jj < x; jj++) {//遍历该位数上可以填的数字
        
            if(Math.abs(jj - last) <= k || i == n) {
                //System.out.println("mm" + i);
                res += f[i][jj];
            }
        }
        if(Math.abs(x-last) <= k || i == n) {
            last = x;
            //System.out.println("1111");
        }else {
            break;
        }
        if(i==1) {
            res++;
        }
    }
    //加包含前导0的,其实就是加上不是和num同位数的数字,
    for (int i = 1; i < n; i++) {
        for (int j = 1; j < 10; j++) {//从1开始
            res += f[i][j];
        }
    }
    //System.out.println(res);
    return res;
}

private static void init() {
    // TODO Auto-generated method stub
    for (int i = 0; i < 10; i++) {//初始化只有一位数字的时候,一定符合要求
         f[1][i] = 1;//注意i一定从0开始
    }
    for (int i = 2; i < 10; i++) {//初始化其它位数的数字
        for (int j = 0; j < 10; j++) {//注意,这里可以包含0
            for (int m = 0; m < 10; m++) {
                if(Math.abs(m-j) <= k) {
                    f[i][j] += f[i-1][m];
                }
            }
        }
    }
}
}

第二个板子

  1. 求区间[L,R]内符合条件的数字,转换为求区间[0,L-1]与[0,R]内符合条件的数字个数,答案就是这两个做差。
  2. 在遍历数字的时候,先遍历数字的位数,假设我要求[0,R]内满足条件的数字,R的位数为cnt,在遍历cnt的时候我要注意,不能超过R,如果R是23456,那么第cnt位能够选择的数字范围是[0,2]。如果我第cnt位选择了1,在遍历cnt-1位的时候我就不需要考虑遍历数字是否越界问题,因为cnt位已经决定了后面的位数怎么选都不会比23456大。如果第cnt位选择了2,在遍历cnt-1位时我可以填的范围就变成了[0,3],所以我需要有一个变量记录当前我前面选择的数字是否是贴上界的,令这个变量为limit,如果limit=1,当前位数可以选择的数字上界是a[cnt],否则就是9,其中数组a是把23456拆分的结果,拆分代码如下
int cnt = 0;
	while(n > 0) {
		a[++cnt] = (int) (n%10);
		n/=10;
	}

根据上述分析,会有如下代码,

int up = limit==1?a[cnt]:9;//求当前位数最大能够填的数字
limit&(i==up?1:0)//下一个limit是否还等于1,i表示当前位数填的数字,如果填了最大的那个数,且当前的limit是1,则填下一位数时能够填的最大数字也是受约束的

up表示当前位数可以填的数字上界。

  1. 接下来处理一下前导零,如果前导零的存在不会影响结果,那么不需要处理,否则就要处理一下前导零。比如求包含2和4的数字个数就不需要处理前导0,因为他不影响结果。如果要求数字各个位数不为0
    假设我要求[0,R]内满足条件的数字,R的位数为cnt,在遍历cnt的时候我要注意,在这个位置填的0就是前导0,如果我在这个位置填了0,再去遍历第cnt-1位数字时,这里填的0还是前导0.如果我第cnt位没有填0,那么我在cnt-1位填的0就不是前导0,他是有效的一维数,就像001和101一样,00里面的0都是前导0,是无效的。而10里的0是十位上的0,是有效的。我们用zeros来表示当前位的0是否是前导0,第cnt位的0肯定是前导0,如果第cnt位填了0,第cnt-1位的0才是前导0,否则就不是。所以有

    zeros&(i==0?1:0)//表示下一位的zeros是否是0,i表示当前位填的数字,如果当前位填了0并且当前位的zeros是1,那么下一位的zeros也是1.

    前导0的使用要比上界limit的使用更灵活一点,他是根据题目的要求来使用的。

  2. 这里主要讲记忆化递归。因为这一个板子用的是dfs,而dfs可能会有超时的问题,所以就有了记忆化递归。记忆化递归要标记好当前的状态,所以用来记忆当前状态的数组也是要根据题目设定。
    当题目中有多个测评样例时,进行记忆化的时候要注意不要记住在特定数下的答案。所以有下面的代码

    if(limit == 0) dp[cnt][last][zeros] = res;

    为什么要在limit=0时才进行记忆化呢?因为limit=1说明当前的数是贴上界的,而这个数是受当前这个样例影响的,比如2345这个数字,在遍历到百位数字3时,我是贴上界的,如果千位数字是2,那么此时十位数字只能选0-4,此时得到的res是从0-4递归得到的。但是如果换一个数字2377,在遍历到百位数字3时,我如果是不贴上界的,可选的数字应该是0-9,如果是贴上界的,可选的数字是0-7,明显此时的状态dp[3][3][0]和数字2345的转移是不一样的。所以我只有不贴上界的时候,说明后面的转移都是0-9时才进行记忆。
    读取记忆的时候也是同样的道理,

    if(limit==0&&dp[cnt][last][zeros]!=-1) { return dp[cnt][last][zeros]; }

    只有此时不贴上界的时候才能读取之前的记忆。
    记忆化数组根据具体的题目来设定,并不是统一的,具有高度灵活性,只要解释起来没问题就可以。像小明数这道题没有记忆limit,有些情况也是可以记忆limit的。

分析题目

针对小明数而言,前导0会影响答案,为什么?题目要求相邻两数之差绝对值不超过k,如果存在前导0并且不加以判断那么会认为前导0也是有效数字,那么0后面只能填0-k,但实际前导0应该是无效数字,前面一个数字是前导0,后面可以填0~9中的任意一个数字。如果没有判断前导零,会导致结果比实际的小。 求某些数字相邻位数上的数字之差的绝对值不超过k,来想一下dfs的时候需要什么,必然要有的是当前的位数cnt和是否贴上界limit,刚刚也分析了需要判断前导零,所以有zeros。遍历到cnt位时,来判断一下当前位可以填啥,我要满足相邻位数上的数字之差的绝对值不超过k,我得知道前一个位数我填的是几,所以就有了last,指示前一个位数上填的数字。然后就没有其它的了,所以 dfs(cnt,last,zeros,limit),接下来就直接放代码了。

import java.util.Arrays;
import java.util.Scanner;
public class Main {
	static int dp[][][] = new int[20][20][2];//还要记录当前的状态是不是有前导零!!!!!!!
	static int a[] = new int[20];
	static int k,ans;
	static int nums[] = new int[20];
public static void main(String[] args) {
	Scanner scanner = new Scanner(System.in);
   	int t = scanner.nextInt();
	while(t-- > 0) {
		for(int i = 0;i < 20;i++)
			for(int j = 0;j < 20;j++)
			Arrays.fill(dp[i][j],-1);
		k = scanner.nextInt();
		long l = scanner.nextLong();
		long r = scanner.nextLong();
		System.out.println(solve(r)-solve(l-1));

private static int solve(long n) {
	// TODO Auto-generated method stub
	int cnt = 0;
	while(n > 0) {
		a[++cnt] = (int) (n%10);
		n/=10;
	}
	return dfs(cnt,1,-1,1);
}

private static int dfs(int cnt, int limit, int last,int zeros) {//前导0对答案有影响!!!!!!
	// TODO Auto-generated method stub
	if(cnt==0) {
		return 1;
	}
	if(limit==0&&dp[cnt][last][zeros]!=-1) {
		return dp[cnt][last][zeros];
	}
	int res =0;
	int up = limit==1?a[cnt]:9;
	for(int i = 0;i <= up;i++) {
		if(Math.abs(last-i)<=k||zeros==1) {//3 1 2 0 dp[1][0]
			nums[cnt] = i;
			res += dfs(cnt-1, limit&(i==up?1:0), i,zeros&(i==0?1:0));//120
		}
	}
	if(limit == 0) dp[cnt][last][zeros] = res;	     
	return res;
}
}

如果代码有问题,欢迎在评论区内提出来!第三个板子就不讲啦,其实就是把第二个板子的dfs变成dp,但是个人感觉没有dfs灵活,目前在用第二个板子。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/369905.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

如何以管理员身份删除node_modules文件

今天拉项目&#xff0c;然后需要安装依赖&#xff0c;但是一直报错&#xff0c;如下&#xff1a; 去搜这个问题会让把node_modules文件先删掉 再去安装依赖。我在删除的过程中会说请以管理员身份来删除。 那么windows如何以管理员身份删除node_modules文件呢&#xff1f; wi…

Python绘制热力图

最近投SCI论文的时候&#xff0c;有些实验结果需要热力图展示&#xff0c;所以专门试了一下如何用python绘制热力图&#xff0c;发现简单好用&#xff0c;下面分享给大家具体方法。 一、安装python库 需要安装pandas、seaborn、matplotlib安装包依赖&#xff0c;均用pip一键安…

深入了解 Ansible:全面掌握自动化 IT 环境的利器

本文以详尽的篇幅介绍了 Ansible 的方方面面&#xff0c;旨在帮助读者从入门到精通。无论您是初学者还是有一定经验的 Ansible 用户&#xff0c;都可以在本文中找到对应的内容&#xff0c;加深对 Ansible 的理解和应用。愿本文能成为您在 Ansible 自动化旅程中的良师益友&#…

vue学习91-105

vue的基本认知p91 创建一个空仓库p93 vue 路由 vuex版本 2 3 3 3 4 4 npm的vuex装包npm install vuex --save vuex里有仓库,仓库放vuex核心代码&#xff0c;所有组件都能访问到 const store new Vuex.Store()//访问stored this.$store如何提供$访问vuex的数据p94 核心概念-…

GNSS模块的惯导技术:引领定位科技的前沿

全球导航卫星系统&#xff08;GNSS&#xff09;模块的惯导技术是一项颇具前瞻性的科技&#xff0c;它结合了全球定位系统和惯性导航技术&#xff0c;为各个领域的定位需求提供了更为精准和可靠的解决方案。本文将深入探讨GNSS模块的惯导技术&#xff0c;以及它如何在多个领域中…

DATAX改造支持geometry类型数据同步

数据库使用postgresql安装了postgis插件存储了geometry空间数据&#xff0c;想使用datax做数据同步&#xff0c;但datax本身不支持geometry类型数据&#xff0c;如何改造呢&#xff1f; 1.首先下载已改造支持geometry类型的datax引擎&#xff0c;下载地址 https://download.c…

微信小程序之本地生活案例的实现

学习的最大理由是想摆脱平庸&#xff0c;早一天就多一份人生的精彩&#xff1b;迟一天就多一天平庸的困扰。各位小伙伴&#xff0c;如果您&#xff1a; 想系统/深入学习某技术知识点… 一个人摸索学习很难坚持&#xff0c;想组团高效学习… 想写博客但无从下手&#xff0c;急需…

关于《人工智能法案》你应该知道的真相

什么是《人工智能法案》 《人工智能法案》是欧盟的一项法规&#xff0c;旨在为人工智能引入一个共同的监管和法律框架&#xff0c;保障人类的安全、健康和基本权利&#xff0c;同时促进人工智能的创新和发展。该法案根据人工智能应用可能造成的风险&#xff0c;对其进行了不同…

AcWing算法学习笔记:动态规划(背包 + 线性dp + 区间dp + 计数dp + 状态压缩dp + 树形dp + 记忆化搜索)

动态规划 一、背包问题① 01背包朴素版② 01背包优化版③ 完全背包朴素版④ 完全背包消k版⑤ 完全背包消k优化版⑥ 多重背包朴素版⑦多重背包二进制优化版⑧ 分组背包朴素版⑨ 分组背包优化版 二、线性dp① 数字三角形② 最长上升子序列③ 最长上升子序列打印序列版④ 最长上升…

Pytest自动化测试详解

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 关注公众号&#xff1a;互联网杂货铺&#xff0c;回复1 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 为什么需要自动化测试&#xff1f; 自动化测试有很多优…

CSS 闪电按钮效果

<template><view class="const"><div class="voltage-button"><button>闪电按钮</button><svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox=&q…

02.04

1.信号 include "myhead.h" //定义信号处理函数 void handler(int signo) {if(signo SIGINT){printf("用户按下了ctrl c键&#xff0c;hello world\n");} }int main(int argc, const char *argv[]) {if(signal(SIGINT, handler) SIG_ERR){perror("…

vue前端+nodejs后端通信-简单demo

本文记录vue前端nodejs后端通讯最简单的方法&#xff0c;供广大网友最快速进入全栈开发。 技术架构 前端 vue axios 后端 nodejs express 一、前端部分-搭建VUE 项目 vue create Vnodenpm run serve 启动&#xff1b; 具体操作步骤&#xff0c;请自行百度&#xff0c;这里没…

【JavaEE spring】SpringBoot 统一功能处理

SpringBoot 统一功能处理 1. 拦截器1.1 拦截器快速⼊⻔1.2 拦截器详解1.2.1 拦截路径1.2.2 拦截器执⾏流程 1.3 登录校验1.3.1 定义拦截器1.3.2 注册配置拦截器 2. 统⼀数据返回格式2.1 快速⼊⻔2.2 存在问题2.3 案例代码修改2.4 优点 3. 统⼀异常处理 1. 拦截器 后端程序根据…

快递员的烦恼 - 华为OD统一考试

OD统一考试&#xff08;C卷&#xff09; 分值&#xff1a; 200分 题解&#xff1a; Java / Python / C 题目描述 快递公司每日早晨&#xff0c;给每位快递员推送需要淡到客户手中的快递以及路线信息&#xff0c;快递员自己又查找了一些客户与客户之间的路线距离信息&#xff0…

怎么把物品信息图片批量生成二维码?每张图片单独生码的制作技巧

现在通过扫码来查看人员或者物品信息的方式越来越常见&#xff0c;在合适的位置放置对应的二维码内容&#xff0c;让其他人通过扫码来获取图片信息。那么如果我们将每个信息做成一张图片后&#xff0c;需要将图片生成二维码时&#xff0c;有能够批量生成二维码的方法可以快速处…

【Mysql】整理

Mysql整理与总结 整理Mysql的基本内容供回顾。 参考&#xff1a; [1]. 掘金.MySQL三大日志(binlog,redolog,undolog)详解 [2]. Javaguide.MySQL三大日志(binlog、redo log和undo log)详解

2023-12蓝桥杯STEMA考试 C++ 中高级试卷解析

蓝桥杯STEMA考试 C++ 中高级试卷(12月) 一、选择题 第一题 定义字符串 string a = "Hello C++",下列选项可以获取到字符 C 的是(B)。 A、a[7] B、a[6] C、a[5] D、a[4] 第二题 下列选项中数值与其它项不同的是( C)。 A、 B、 C、 D、 第三题 定义变量 int i =…

幻兽帕鲁服务器自动重启备份-python

幻兽帕鲁服务器自动重启备份-python 1. 前置知识点2. 目录结构3. 代码内容4. 原理解释5. 额外备注 基于python编写的服务器全自动管理工具&#xff0c;能够实现自动定时备份存档&#xff0c;以及在检测到服务器崩溃之后自动重新启动&#xff0c;并且整合了对于frp端口转发工具的…

【高质量精品】2024美赛A题22页word版成品论文+数据+多版本前三问代码及代码讲解+前四问思路模型等(后续会更新)

一定要点击文末的卡片&#xff0c;进入后&#xff0c;即可获取完整资料后续参考论文!! 整体分析:这个题目是一个典型的生态系统建模问题&#xff0c;涉及到动物种群的性比例变化、资源可用性、环境因素、生态系统相互作用等多个方面。这个题目的难点在于如何建立一个合理的数学…