「学习笔记」数位 DP

yizhihongxing

「学习笔记」数位 DP

意义不大的题不写了。

点击查看目录

概述

数位 DP 一般用来解决「在一个较大的区间内统计具有一定特征的数的数量」的问题。

数位 DP 一般有两种做法:

  • 递推法:首先需要预处理出具有一定条件的数的个数,然后将上限按数位拆分开来考虑贡献。
  • 暴搜法:直接记忆化搜索具有特定条件的数的个数。

例题

P2657 [SCOI2009] windy 数

思路

本题使用递推。

\(f_{i,j}\) 表示最高位为 \(j\)\(i\) 位 windy 数数量(\(j\) 可以为 \(0\)),显然:

\[f_{i, j} =
\begin{cases}
1 &i=1\\
\sum_{k=0}^{9}[\left|j - k\right| \ge 2]f_{i - 1, k} &\text{otherwise}
\end{cases}
\]

然后计算答案,把上限 \(x\) 的每一位拆开(假设 \(x\)\(l\) 位),考虑到第 \(i\) 位时,前 \(l-i\) 位与 \(x\) 相同,后 \(i\) 位的方案数用 \(f_{i,j}\) 计算。

但是有一个问题:\(0001\) 去除前导零后合法,但是 \(10001\) 不合法,因此为了保证之后计算的数依然合法,\(f_{4, 0}\) 不会记录像 \(0001\) 这样去除前导零后合法但在最高位再加上一个数后不合法的数的数量。

此时我们需要一个辅助数组 \(g_{i}\),即被漏算了的首位为 \(0\)\(i\) 位数的数量。当第 \(i-1\) 位大于等于 \(2\) 时,首位为 \(0\)\(i\) 位数不会被漏算(因为此时第 \(i\) 位和第 \(i-1\) 差值大于等于为 \(2\),仍然合法),但是第 \(i-1\) 位小于 \(2\) 的数会被漏算,所以易得:

\[g_{i} = g_{i - 1} + f_{i - 1, 0} + f_{i - 1, 1}
\]

计算答案时加上即可。

代码

点击查看代码
const ll N = 15, inf = 1ll << 40, P = 1e9 + 7;
namespace SOLVE {
	ll a, b, f[N][N], g[N];
	inline ll rnt () {
		ll x = 0, w = 1; char c = getchar ();
		while (!isdigit (c)) { if (c == '-') w = -1; c = getchar (); }
		while (isdigit (c)) x = (x << 3) + (x << 1) + (c ^ 48), c = getchar ();
		return x * w;
	}

	inline ll GetAns (ll x) {
		ll len = 0, num[N] = {0}, sum = 0;
		while (x) num[++len] = x % 10, x /= 10;
		for_ (i, len, 1) {
			if (i == len) {
				_for (j, 0, num[i] - 1) sum += f[i][j];
				sum += g[i];
				continue;
			}
			_for (j, 0, num[i] - 1) {
				if (abs (j - num[i + 1]) < 2) continue;
				sum += f[i][j];
			}
			if (abs (num[i] - num[i + 1]) < 2) break;
		}
		return sum;
	}

	inline void In () {
		a = rnt (), b = rnt ();
		return;
	}
	inline void Solve () {
		_for (i, 0, 9) f[1][i] = 1;
		g[1] = 2;
		_for (i, 2, 10) {
			g[i] = g[i - 1] + f[i - 1][0] + f[i - 1][1];
			_for (j, 0, 9) {
				_for (k, 0, 9) {
					if (abs (j - k) < 2) continue;
					f[i][j] += f[i - 1][k];
				}
			}
		}
		return;
	}
	inline void Out () {
		printf ("%lld\n", GetAns (b + 1) - GetAns (a));
		return;
	}
}

P4317 花神的数论题

思路

使用递推。

\(f_{i, j, k}\) 表示有 \(i\) 位,最高位为 \(j(j\in\{0,1\})\),有 \(k\)\(1\)二进制数。显然:

\[\begin{aligned}
f_{i, 0, j} &=
\begin{cases}
1 &j=0\\
f_{i - 1, 0, j} + f_{i - 1, 1, j} &\text{otherwise}
\end{cases}\\
f_{i, 1, j} &=
\begin{cases}
1 &i=1\\
f_{i - 1, 0, j - 1} + f_{i - 1, 1, j - 1} &\text{otherwise}
\end{cases}
\end{aligned}
\]

然后由于要算乘积,要使用快速幂计算有 \(k\)\(1\) 的二进制数的乘积总和。

P4124 [CQOI2016]手机号码

思路

使用暴搜。

定义函数:

ll Dfs (ll p, ll a, ll b, ll sm, ll fr, ll et, ll li)

其中,\(p\) 表示第几位,\(a\) 表示第 \(p\) 位的数,\(b\) 表示第 \(p+1\) 位的数,\(sm\) 表示是否已经存在一样的连续三个数,\(fr\) 是否出现过 \(4\)\(et\) 是否出现过 \(9\)\(li\) 前面几位是否和上限完全一样(这个会影响搜索时下一位的的数,如果完全一样则下一位的的数不能超过上限的这一位,否则可以到 \(9\))。

代码

点击查看代码
ll Dfs (ll p, ll a, ll b, ll sm, ll fr, ll et, ll li) {
	if (fr && et) return 0;
	if (!p) return sm;
	if (!li && f[p][a][b][sm][fr][et] > 0) return f[p][a][b][sm][fr][et];
	ll sum = 0, mx = li ? t[p] : 9;
	_for (i, 0, mx) sum += Dfs (p - 1, i, a, sm || (i == a && i == b), fr || (i == 4), et || (i == 8), li && i == mx);
	if (!li) f[p][a][b][sm][fr][et] = sum;
	return sum;
}
inline ll GetAns (ll x) {
	if (x < 1e10) return 0;
	ll len = 0, sum = 0;
	while (x) t[++len] = x % 10, x /= 10;
	memset (f, -1, sizeof (f));
	_for (i, 1, t[len]) sum += Dfs (len - 1, i, 0, 0, i == 4, i == 8, i == t[len]);
	return sum;
}

haha数

题意

一个正整数,当且仅当在十进制表示下,它的各位非零数字能整除该数本身时(即它的所有非零位的数字都是该数的约数),被称作 haha 数。给定 \(l\)\(r\),求在 \([l, r]\) 范围内 haha 数的个数。

\(1\le l \le r \le9\times10^{18}\)

思路

使用暴搜。

定义函数:

ll Dfs (ll p, ll md, ll sta, ll li)

其中,\(p\) 表示第几位,\(md\) 表示当前的数膜 \(\operatorname{lcm}(1,2,3,4,5,6,7,8,9)=2520\) 的余数,\(sta\) 表示 \(2\sim8\) 的数是否出现过的状态,\(li\) 前面几位是否和上限完全一样。

最后判断合法时,看 \(md\) 是否是 \(sta\) 记录的所有出现过的数的倍数即可。

代码

点击查看代码
inline ll w (ll x) { return (x < 2) ? 0 : (1 << (x - 2)); }
inline bool Check (ll md, ll sta) {
	_for (i, 2, 9) if ((sta & w (i)) && (md % i)) return 0;
	return 1;
}

ll Dfs (ll p, ll md, ll sta, ll li) {
	if (!p) return Check (md, sta);
	if (!li && f[p][md][sta] >= 0) return f[p][md][sta];
	ll sum = 0, mx = li ? t[p] : 9;
	_for (i, 0, mx) sum += Dfs (p - 1, (md * 10 + i) % 2520, sta | w (i), li & (i == mx));
	if (!li) f[p][md][sta] = sum;
	return sum;
}

0和1的熟练

题意

有一个程序员,他使用只有两个按键的键盘打字。这两个按键就是 \(0\)\(1\),只有达到高端境界的人才能出入此境。记住,只有从 \(l\)\(r\) 的所有数都手打过一遍了才能练就如此功夫。作为真正的高手,你一定能快速地回答区间 \([l,r]\) 中有多少数字的二进制表示(不含前导零)中 \(0\) 的个数不少于 \(1\) 的个数,因为你每天都进行一遍这个练习。

\(1\le l<r\le2\times10^9\)

思路

使用暴搜。

定义函数:

ll Dfs (ll p, ll c1, ll len, ll li)

其中,\(p\) 表示第几位,\(c1\) 表示当前的数有几个 \(1\)\(len\) 当前的数变为二进制后有多少位,\(li\) 前面几位是否和上限完全一样。

代码

点击查看代码
ll Dfs (ll p, ll c1, ll len, ll li) {
	if (!p) return len ? (len - c1 >= c1) : 1;
	if (!li && f[p][c1][len] >= 0) return f[p][c1][len];
	ll sum = 0, mx = li ? t[p] : 1;
	_for (i, 0, mx) sum += Dfs (p - 1, c1 + (i == 1), len + (len || i), li & (i == mx));
	if (!li) f[p][c1][len] = sum;
	return sum;
}

苍与红的试炼

题意

辉煌的时代终将落幕吗,只有时间的车轮滚滚向前。

来自苍空之上的,是点亮前路的希望,是划破阴云的曙光。

为协助天城追回那被铅色未来蒙蔽前路之人,你需要完成天城的试炼以取得天城的认可。

漫樱飞舞的古城中,有一些标有数字 \(0\sim9\) 的御守。现在天城给了你一个正整数 \(d\),你需要按照某种顺序排列一些御守,写下由这些御守拼成的数字 \(x\),并保证 \(x\) 能被 \(d\) 整除。

然而想通过「重樱之鬼谋」的考验哪有这么简单。天城还指定了一个正整数 \(s\),你必须保证你所选择的所有御守上的数字之和等于 \(s\) 才能满足要求。同时为了检验你的能力,天城要求你给出满足要求的最小的 \(x\)。当然如果不存在满足要求的 \(x\),你也必须要向天城指出这一点。

跨越时间之长河,斩断命运之枷锁。挑战者啊,穷尽武略与智谋,迎接来自顶点的试炼吧。

\(1\le d\le500, 1\le s\le 5000\)

思路

没有上限,所以直接深搜会爆掉。

那么考虑广搜,放入队列一个数对 \((a, b)\),表示当前数膜 \(d\) 等于 \(a\),每一位数之和等于 \(b\)。显然当 \(a=0,b=s\) 时搜索结束。

状态只有 \(500\times5000=2500000\) 种,记忆化一下即可。

代码

点击查看代码
namespace SOLVE {
	const ll N = 1e7;
	ll d, s, f[N], g[N], jl[N], ans;
	inline ll rnt () {
		ll x = 0, w = 1; char c = getchar ();
		while (!isdigit (c)) { if (c == '-') w = -1; c = getchar (); }
		while (isdigit (c)) x = (x << 3) + (x << 1) + (c ^ 48), c = getchar ();
		return x * w;
	}
	void Print (ll p) {
		if (p == 1) return;
		Print (g[p]);
		printf ("%lld", f[p]);
	}
	inline void In () {
		d = rnt (), s = rnt ();
		return;
	}
	inline void Solve () {
		std::queue <ll> q;
		q.push (0);
		jl[0] = 1;
		ll h = 0, t = 1;
		while (!q.empty ()) {
			ll fr = q.front ();
			++h, q.pop ();
			_for (i, 0, 9) {
				ll dd = (fr % 501 * 10 + i) % d;
				ll ss = fr / 501 + i;
				ll num = dd + ss * 501;
				if (ss > s) continue;
				if (jl[num]) continue;
				f[++t] = i, g[t] = h;
				if (!dd && ss == s) {
					ans = t;
					return;
				}
				jl[num] = 1;
				q.push (num);
			}
		}
		return;
	}
	inline void Out () {
		if (ans) Print (ans), puts ("");
		else puts ("-1");
		return;
	}
}

原文链接:https://www.cnblogs.com/Keven-He/p/NumberDP.html

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:「学习笔记」数位 DP - Python技术站

(0)
上一篇 2023年4月17日
下一篇 2023年4月17日

相关文章

  • JS数据结构之队列结构详解

    JS数据结构之队列结构详解 什么是队列结构? 队列结构是一种遵循先进先出(FIFO)原则的线性数据结构,它可以用来存储一系列待处理的数据,其中队首是最先进入队列的元素,队尾是最后进入队列的元素。 在队列中,添加元素的操作叫做enqueue,移除元素的操作叫做dequeue。同时,队列还包括peek方法,查看队列头的元素,以及isEmpty方法,判断队列是否为…

    数据结构 2023年5月17日
    00
  • Python魔法方法详解

    下面是关于“Python魔法方法详解”的完整攻略。 1. 什么是魔法方法 在Python中,魔法方法是一种特殊的方法,它们以双下划线__开头和结尾。魔法方法在Python中被广泛使用,它们可以用于自定义类的行为,例如实例化、比较、运算等。 2. 常用的魔法方法 2.1 __init__方法 __init__方法是Python中常用的魔法方法之一,它在实例化对…

    python 2023年5月13日
    00
  • C语言数据结构二叉树简单应用

    C语言数据结构二叉树简单应用攻略 1. 什么是二叉树? 二叉树(Binary Tree)是一种树形结构,它的每个节点最多包含两个子节点,它是非线性数据结构,可以用来表示许多问题,例如家族关系、计算机文件系统等等。 2. 二叉树的基本操作 二叉树的基本操作包括插入、删除、查找等等,本攻略主要讲解插入和查找的实现。 插入操作的代码如下: // 二叉树的插入操作 …

    数据结构 2023年5月17日
    00
  • Python实现简单层次聚类算法以及可视化

    Python实现简单层次聚类算法以及可视化 层次聚类是一种常用的聚类算法,它可以将数据集分成不同的层结构。本文中,我们将介绍如何使用Python实现简单层次聚类法以及可视化。我们将分为以下几个步骤: 加载数据集 数据预处理 定义层次聚类法 可视化聚类结果 示例说明 步骤1:加载数据集 在实现层次聚类算法之前,需要加载数据集。在这个例子中,我们将使用Iris数…

    python 2023年5月14日
    00
  • python编程通过蒙特卡洛法计算定积分详解

    以下是关于“Python编程通过蒙特卡洛法计算定积分详解”的完整攻略: 简介 蒙特卡洛法是一种常见的数值计算方法,可以用于计算定积分。本教程将介绍如何使用Python编程通过蒙特卡洛法计算定积分,并讨论如何使用该方法进行数值积分。 步骤 1.导入库和定义函数 首先,我们需要导入必要的库,包括numpy和matplotlib。在Python中,可以使用以下代码…

    python 2023年5月14日
    00
  • C语言实现学生信息管理系统(链表)

    C语言实现学生信息管理系统(链表) 简介 学生信息管理系统是管理学生的一种系统,可以实现添加、查找、删除和修改学生信息等功能。本文将使用C语言实现学生信息管理系统,并通过链表的方式进行实现。 前提条件 在开始之前,我们需要了解如下内容: C语言基础知识 链表的基本概念和使用 系统架构 学生信息管理系统主要包含以下几个模块: 学生信息结构体 添加学生信息 查找…

    数据结构 2023年5月17日
    00
  • Go语言数据结构之二叉树必会知识点总结

    Go语言数据结构之二叉树必会知识点总结 二叉树是一种非常重要的数据结构,它被广泛应用于算法、数据处理等领域。在Go语言中,使用二叉树可以实现很多高级数据结构和算法。本文将为大家介绍二叉树相关的基本知识和操作,以及如何利用Go语言实现二叉树。 什么是二叉树? 二叉树是一种树形结构,由一个根节点和两个子树组成。它的每个节点最多有两个子节点,称为左子节点和右子节点…

    数据结构 2023年5月17日
    00
  • python实现爬山算法的思路详解

    下面是详细讲解“Python实现爬山算法的思路详解”的完整攻略,包括算法原理、Python实现和两个示例说明。 算法原理 爬山算法是一种基于贪心思想的局部搜索算法,其基本思想是从一个随机的起点开始,每次选择当前位置的最优方向,直到达到局部最优解。具体步骤如下: 随机选择一个起点; 计算当前位置的函数值; 在当前位置的邻域内选择一个最优方向; 如果该方向的函数…

    python 2023年5月14日
    00
合作推广
合作推广
分享本页
返回顶部