数位DP
定义
数位DP是一种动态规划技巧,特别适用于处理与数字的位操作相关的问题,如数字序列的计数、数字的生成等问题。它通过将问题分解为对每一位数字的独立考虑,从而简化问题复杂度,实现高效求解。
数位DP的核心思想是将原问题转化为对每一位数字进行决策的过程,利用动态规划的思想,从最低位向最高位(或相反)逐位考虑,每一步决策都基于当前已经确定的位数的信息。在这个过程中,通常会用一个状态表示当前处理到哪一位以及之前决策产生的某些约束条件(如数字大小、奇偶性等),并使用一个DP数组来记录到达每个状态的有效解的数量或方案。
运用情况
- 数字序列计数:例如,计算某个区间内满足特定条件的数的个数(如:包含偶数个1的数、不包含连续相同数字的数等)。
- 数字序列生成:找出所有满足给定条件的数字序列。
- 数字组合问题:比如,给定一个数字N,求所有由N的非空子集构成的数之和。
- 数字游戏问题:如,猜数字游戏中最小猜测次数等。
注意事项
- 状态定义:清晰准确地定义DP状态是关键,通常包括当前处理的位数、已使用的数字集合或限制条件等。
- 状态转移方程:根据问题的具体条件,合理设计从一位到下一位的转移逻辑,正确计算状态之间的转移。
- 初始化与边界条件:注意DP数组的初始化,尤其是处理最高位或最低位时的特殊处理。
- 遍历顺序:根据问题的特性选择从低到高还是从高到低遍历数字位,确保状态转移的正确性。
- 剪枝:对于一些问题,合理的剪枝可以大幅度减少计算量,提高效率。
解题思路
- 明确问题:首先要明确问题的具体要求,识别出需要根据数位进行决策的点。
- 状态定义:定义DP状态,通常涉及当前处理的位数、前导零的处理、已使用数字的限制等。
- 确定DP数组维度:根据状态定义,决定DP数组的维度和大小。
- 构建状态转移方程:分析如何从前一状态转移到当前状态,这一步是数位DP中最核心的部分。
- 实现与优化:编写代码实现DP算法,同时注意优化,如剪枝、记忆化等技巧以提高效率。
- 边界与初始值处理:确保DP过程的起始状态和边界条件被正确设置。
- 回溯构造答案(如果需要):在计数之外还需要具体方案时,需要根据DP过程回溯构造出满足条件的数字序列。
AcWing 1081. 度的数量
题目描述
运行代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 35;
int l, r, k, b;
int a[N], al;
int f[N][N];
int dp(int pos, int st, int op) //op: 1=,0<
{
//枚举到最后一位数位,是否恰有k个不同的1(也是递归的终止条件)
if (!pos) return st == k;
//记忆化搜索,前提是不贴着上界(可以枚举满这一位所有的数字)
if (!op && ~f[pos][st]) return f[pos][st];
//01数位dp,贴着上界时,本轮能枚举的最大数就是上界数位的数字和1之间的最小值
int res = 0, maxx = op ? min(a[pos], 1) : 1;
for (int i = 0; i <= maxx; i ++ )
{
if (st + i > k) continue;
res += dp(pos - 1, st + i, op && i == a[pos]);
}
return op ? res : f[pos][st] = res;
}
int calc(int x)
{
al = 0; memset(f, -1, sizeof f); //模板的必要初始化步骤
while (x) a[ ++ al] = x % b, x /= b; //把x按照进制分解到数组中
return dp(al, 0, 1);
}
int main()
{
cin >> l >> r >> k >> b;
cout << calc(r) - calc(l - 1) << endl;
return 0;
}
代码思路
-
头文件引入:程序包括了
iostream
、cstring
和algorithm
,分别用于输入输出、内存操作和算法操作。 -
常量定义:
N
定义了数组的最大大小,这里设置为35,意味着可以处理的数字范围是0
到10^34
。 -
变量定义:定义了区间的左右边界
l
和r
,数字的个数k
,底数b
,以及用于存储数字分解的数组a
和数组长度al
。f
是一个二维数组,用于存储中间结果,实现记忆化搜索。 -
递归函数
dp
:这个函数是一个递归函数,用于计算在给定的数位pos
,已经使用了st
个不同的1,是否能够形成满足条件的数。参数op
用于标识是否是边界条件。 -
分解函数
calc
:这个函数将输入的整数x
按照底数b
分解,然后调用dp
函数计算满足条件的数的数量。 -
主函数
main
:读取输入,调用calc
函数计算结果,并输出满足条件的数的数量。
改进思路
-
记忆化搜索:代码中已经使用了记忆化搜索,这有助于减少重复计算。但是,可以进一步优化记忆化搜索的逻辑,确保只存储必要的状态。
-
数组初始化:在使用
memset
初始化f
数组时,使用-1
作为初始值,这有助于在递归过程中检查某个状态是否已经被计算过。 -
递归终止条件:在
dp
函数中,递归的终止条件是pos
为0,这意味着已经处理完所有的数位。这个条件是正确的,但可以进一步优化递归的逻辑,减少不必要的递归调用。 -
循环优化:在
dp
函数的内部循环中,可以进一步优化循环的终止条件,避免不必要的迭代。 -
输入输出优化:虽然这个程序的输入输出操作已经很简洁,但在处理大量数据时,可以考虑使用更快的输入输出方法,比如使用
scanf
和printf
代替cin
和cout
。 -
代码可读性:代码中的变量命名可以进一步改进,以提高代码的可读性。例如,使用更具描述性的变量名,而不是单字母或缩写。
-
错误处理:程序没有包含错误处理逻辑,比如输入数据超出预期范围的情况。在实际应用中,应该添加适当的错误检查和处理。