目录
- [第五章 动态规划](#第五章 动态规划)
第五章 动态规划
1、dfs与记忆化搜索
重复搜索问题
在递归算法中,重复搜索指的是在解决问题的过程中反复计算相同的子问题,导致效率低下的问题。
特别是在具有重叠子问题的场景下,重复搜索会极大地增加算法的时间复杂度。
斐波那契数列F(n) =F(n一1) +F(n一2) 的简单递归解法会导致大量的重复计算。具体表现如下:
- 计算 F(5) 时会计算F(4) + F(3)。
- 计算F(4) 时又会计算F(3) +F(2),其中F(3)被多次重复计算。
- 每个节点的子问题需要重新计算,从而导致指数级增长。
这种情况在许多递归算法中都会出现。
记忆化搜索的实现
记忆化搜索是在递归的基础上增加了一个记录中间结果的缓存(一般是数组),避免重复计算。
斐波拉契序列记忆化搜索的代码
java
static int f(int n){
if(n==1||n==2) return 1;
if(f[n]!=-1) return f[n];
return f[n]=f(n-1)+f(n-2);
}
📖采药
🍎笔记
记忆化搜索:
🍎笔记

📚采药
⭕️记忆化搜索 的 时间复杂度 等价于 动态规划的时间复杂度 。基本上能用动态规划求解的题目都能使用记
忆化搜索,若同学对于后面要学习的动态规划不太能掌握,可以先加强自己对记忆化搜索的实现。
2、动态规划概念
爬楼梯问题
现在你在爬楼梯,你可以一次上一级楼梯,也可以一次上两级楼梯,问上到第n级楼梯的方案数。
如果用递归做法,我们可以写出如下代码:
java
int f(int n){
if(n==1 || n==2) return 1;
return f(n-1)+f(n-2);
}
这种代码会有很多重复计算,例如 f(4) = f(3) + f(2),f(5)= f(4) + f(3),f(3)计算了二次。时间复杂度会很高,为O(2")。
我们用迭代做法,可以写成:
java
void solve(int n){
f[1]=1;
f[2j=1;
for(int i=3;i<=n;i++){
f[i]=f[i-1]+f[i-2];
}
}
这种做法我们也可以称作递推,用已求出的值去推后面的值,也可以称作动态规划。
⭐️动态规划概念
动态规划(DynamicProgramming,简称DP)是一种通过将问题分解为更小的子问题,并存储这些子问题的结果以避免重复计算,从而有效解决复杂问题的算法设计方法。它特别适用于有重叠子问题和最优子结构特性的优化问题。
重叠子问题
动态规划的核心思想是将大问题分解为多个相同类型的子问题。由于这些子问题会重复出现,因此通过存储已经计算过的子问题结果,可以避免重复计算,从而提高效率。
最优子结构
最优子结构意味着问题的最优解可以由其子问题的最优解构造而成。也就是说,问题的最优解依赖于子问题的最优解。
例如在爬楼梯问题中 fn可以由 fn-1,fn-2的和推出。
无后效性
无后效性指的是在动态规划中,某个状态的结果不会依赖于未来的决策。也就是说,一旦子问题的解被计算出来,它就不会因为后续的决策发生改变。
例如在爬楼梯问题中要求fn只需要知道fn-1,fn-2的值。但fn-1,fn-2如何来的我们并不关心了。
动态规划解题步骤
状态确定
首先,需要明确问题的子问题是什么,然后用适当的变量来表示这些子问题的状态。
状态转移方程
状态转移方程用于描述从一个状态到另一个状态的递推关系。
初始化
动态规划需要为边界状态初始化值。
计算结果
根据状态转移方程逐步计算出所有子问题的解,最终得出问题的最优解。
状态定义是最难的,没有什么技巧,只能通过刷题去总结。
动态规划一般也可以叫递推,它和递归的区别为:
动态规划通过利用已知的前一项或前几项的值,逐步推导出下一个值的过程。
递归是一个函数调用自身来解决问题的一种方法。也就是求我之前我还不知道我子问题的解,我需要先求子问题的解。
动态规划和递归求解问题刚好是反过来的。
状态定义(State Definition)对于斐波那契数列问题,我们的目标是计算出第n个斐波那契数。定义状态dp[i]为求解到第i个斐波那契数的结果,即:
dp[i]表示斐波那契数列的第i项。
转移方程(Recurrence Relation)转移方程是根据已经计算的子问题结果来推导当前问题结果的公式。对于斐波那契数列,我们知道:
F(1) = F(2) = 1
对于i≥3, F(i)=F(i-1) +F(i-2)
因此,转移方程可以写成:
dp[i]= dp[i-1] + dp[i-2] (对于i>3)
边界条件(Base Case)边界条件是递推开始时的初始值,即最小子问题的解。对于斐波那契数列:
dp[1] = dp[2] = 1
3、组合数原理-选与不选
📖递归求组合数(模板)
java
static int f(int a, int b) {
if(a==b || b==0)return 1;
return f(a - 1, b - 1) + f(a- 1, b);
}
⭕️注意理解这个选与不选的思想,是后续学习动态规划非常重要的思想。
4、线性dp
📖数字三角形问题

最优子结构
指一个问题的最优解可以通过其子问题的最优解来构建。也就是说,如果我们要得到整个问题的最优解,可以将问题分解为若干子问题,并通过合并这些子问题的最优解得到整体最优解。
- 设dp[i][j]表示从顶点到位置(i,j)的最大路径和。
- 对于位置(i,j),可以从上一层的(i-1,j)或(i-1,j-1)两个位置转移过来。因此,dp[i][j]的状态
转移方程为:
dp[i][j]= a[i][j]+max(dp[i-1][j],dp[i-1][j-1]) - 根据这一递推关系,dp[i][j]的值仅依赖于其上方两个位置的最大路径和。因此,如果我们已经知道dp[i-1][j]和dp[i-1][j-1]的最优解,那么可以通过它们来求解dp[i][j]的最优解。
因为从顶层到达任意一个位置的最优路径都依赖于上一层的最优路径和,这样的递推构成了最优子结构。
无后效性
指状态转移到当前状态后,不再关心之前的状态路径,也就是说状态仅与上一状态有关,而不受之前状态的计算过程影响。
在数字三角形问题中:
- 我们从顶到底计算dp[i][j],每一个dp[i][j]的值仅依赖于dp[i-1][j]和dp[i-1][j-1]。
- 当我们得到dp[i][j]的值后,不再需要关心是通过dp[i-1][j]还是dp[i-1][j-1]得到的路径,也不需
要保留更早之前的状态信息。
因此,数字三角形问题具有无后效性,允许我们在计算中只保留当前行和上一行的状态。
- 状态定义:dp[i][j]表示从顶点到位置(i,j)的最大路径和。
- 状态转移方程:dplill]=a[il]+max(dp[i-1][j-1],dp[i-1][l)
- 初始化:dp数组全部元素为0均可,因为序列中每个元素最小值为非负整数。
- 结果:dp最后一行的最大值
java
public static int maximumPathSum(int[lI] a) {
//使用状态转移方程填充dp数组
for (int i = 1; i <= n; i++) {
//可以从上方或左上方到达,取两者的最大值
for (int j = 1; j <= i; j++) {
dp[i][j]= Math.max(dp[i- 1][j- 1],dp[i- 1][j]) + a[i][j];
}
}
//找到最后一行中的最大值,即为最大路径和
int maxSum = 0;
for (int j = 1; j <= n; j++) {
maxSum = Math.max(maxSum, dp[n][j]);
return maxSum;
}
📖最长上升子序列问题
对于最长上升子序列问题,最优子结构的含义是:
- 假设我们已经知道了某个元素之前的子序列的最长上升子序列长度。
- 若当前元素可以接在该子序列的末尾,那么通过将当前元素加入,形成新的上升子序列的长度将会是之前子序列长度加1。
更具体地说,设dp[i]为以第i个元素为结尾的最长上升子序列的长度,那么对于每个元素arr[i],可以通过遍历其前面的元素arr[j](其中j < i且arr[j] < arr[i]),来更新dp[i]。于是我们得到递推公式:
dp[i] = max(dp[i],dp[j] +1) for all j < i and a[j] < a[i]
最终的解是所有dp[i]中的最大值。
无后效性是指决策过程的结果只依赖于当前状态,而与如何到达当前状态的历史过程无关。在最长上升子序列问题中,当前元素是否可以成为上升子序列的一部分,完全依赖于当前元素与前面的元素是否满足上升关系,而与之前的选择无关。
- 在计算dp[i]时,我们仅依赖于arr[i]和它之前的元素arr[j](满足arr[j]くarr[i]),并根据这些
信息决定dp[i]的值。 - 一旦决定了一个元素是否可以加入上升子序列并更新其dp值,就不会再受到后续元素的影响。
这就是无后效性的体现:从任何一个位置开始构造上升子序列,不会受到其他历史选择的影响。
- 状态定义
dp[i]:以第i个元素结尾的最长上升子序列的长度。 - 状态转移方程
dp[i]=max(dp[i],dp[j] +1) for all j<i and arr[j]<arr[] - 初始化
dp[i] = 1 for all i = 0,1,.,n-1 - 结果
max(dp[0],dp[1],...,dp[n- 1])
java
int ans = 1;
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
if (a[j] < a[i]) {
dp[i]= Math.max(dp[i], dp[j] + 1);
}
ans = Math.max(LSIS, dp[i]);
}
}
📚
📖翻转后1的数量
这种问题也是线性问题,会有多个状态之间进行相互转换。
无操作的部分:对于每个字符S[i],如果没有进行任何翻转,那么dp[i][0]表示到达i时,1的数量最大值(不做任何翻转)。这是一个非常基础的状态,只是根据前一个字符的状态,是否遇到1来进行累加。
翻转的部分:我们需要考虑翻转操作。对于每个字符S[i],我们可以选择在当前字符进行翻转操作(即从0变成1,或者从1变成0),这时候我们需要记录dp[i][1],即在进行了翻转操作后,当前点最大1的数量。
已经翻转的部分:一旦翻转操作执行完毕,我们就不能再进行翻转了。dp[i][2]就是记录在已经进行一次翻转操作后,1的数量的最大值。
状态转移只与当前的选择和之前的状态有关,而不受后续操作的影响。在这个问题中,翻转操作只能进行一次,而一旦翻转完成,后续的操作就不能再改变已经翻转的部分。
具体来说,dp[i][0]和dp[i][1]依赖于前一个状态,dp[i][2]是通过已经完成翻转的情况更新的,符合无后效性的要求。
状态定义:
dp[i][0];//对于第i个字符,还未经过翻转操作1的数量的最大值。
dp[i][1];//对于第i个字符,正在进行翻转操作1的数量的最大值。
dp[i][2];//对于第i个字符,已经进行过一次翻转操作1的数量最大值。
状态转移:c为当前输入字符。
- dp[i][0]只能从dp[i-1][0]转移而来,这步具体为dp[i][0]=dp[i-1][0]+x-'0'。
- dp[[1]可以从dp[i-1][0],dp[i-1][1]转移而来,从dp[i-1][0]转移而来代表从第i个位置开始翻转,从dp[i一1[1]转移而来代表上一个位置还在翻转状态,当前位置继续保持翻转状态,这一步具
体为dp[i][1] =max(dp[i-1|[0],dp[i-1][1]) + (x^1)-'0'。x ^ 1可以让x翻转,因为字符0和字符1在ASCII中只有最后一位二进制位不同。 - dp[i][2]可以从dp[i-1][1],dp[i-1][2]转移而来,从dp[i-1][1]转移代表本次结束翻转,从dp[i-1][2]转移代表之前已经结束翻转过了,取二者最大值,同时注意,如果i=1是不能进行本操作的,这一步具体为如果i>1,dp[i][2]=max(dp[i-1[1],dp[i-1][2)+x-'0'。
结果为max dp[n][0],dp[n][1], dp[n][2]。
以10100为例
我们要根据s="10100"来填充f数组,遵循以下规则:
- f[i][e]:不进行翻转,最多可以得到的1的数量。
- f[i][1]:当前字符进行翻转,最多可以得到的1的数量。
- f[i][2]:已经进行过一次翻转,最多可以得到的1的数量。
我们从头开始,按字符逐步计算每一个f[i][0],f[i][1],和f[i][2]的值。

java
static void solve() {
int n = in.nextInt();
String s=" "+in.next();
for(int i=1;i<=n;i++){
char x=s.charAt(i);
f[i][0]=f[i-1][θ]+x-'0';
f[ijt1j=Math.max(f[i-1][e],f[i-1][1])+(x^1)-'0';
if(i>1) f[i][2]=Math.max(f[i-1][1],f[i-1][2])+x-'0';
}
System.out.println(Math.max(f[n][θ],Math.max(f[n][1],f[n][2])));
}
5、01背包
📖01背包问题概述
暴力思路
枚举其所有可能的组合,可以用dfs或者二进制枚举的方式枚举,每个物品有选和不选两种方式。
例如3种物品,我们有(000,001,010,011,100,101,110,111),该做法复杂度为2n。
java
int dfs(int u,int s){
if(u>n) return 0;
int ans1=0,ans2=0;
if(s+v[u]<=m) ans1=dfs(u+1,s+v[u])+w[u];
ans2=dfs(u+1,s);
return Math.max(ans1,ans2);
}
⭐️状态定义
状态定义
用dp[i][j]
表示前i
个物品在背包容量为j的条件下的最大价值。
i
表示物品的编号。j
表示背包的剩余容量
最优子结构- 如果不放第i个物品,那么当前问题等价于前i-1个物品、背包容量为j的问题,状态为
dp[i-1][j]
。 - 如果放入第1个物品,则问题等价于前i-1个物品、背包容量为j-v[i]的问题,状态为
dp[i-1][ j - v[i] ] + w[i]。
无后效性
对于01背包问题来说,某个状态dp[i][j]一旦通过决策得到,它就不会因后续的决策而改变,因此该问题具有无后效性。
状态转移方程
根据最优子结构,可以得出01背包问题的状态转移方程:
- 如果不选择物品i,则dp[i][j]= dp[i-1][j]。
- 如果选择物品i,则dp[i][j]= dp[i-1][j-v[i]]+w[i]。
则有转移方程
dp[i][j] = max(dp[i-1][j], dp[i- 1] [j- v[i] ] + w[i])
时间复杂度空间复杂度均为n×m。
⭐️01背包(二维数组做法)
java
static void solve(){
n=in.nextInt();
m=in.nextInt();
for(int i=1;i<=n;i++){
v[i]=in.nextInt();
w[i]=in.nextInt();
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
dp[i][j]=dp[i-1][j];
if(j>=v[i]){
dp[i][j]=Math.max(dp[i][j],dp[i-1][j-v[i]]+w[i]);
}
}
}
out.println(dp[n][m]);
}
我们可以看到,对于每一次转移,dp[i][j]只会从dp[i-1][j]转移而来,实际上每次转移只涉及到了两行,能否有什么办法可以只用两行就完成转移?
有一种方式是奇偶性,如果i是奇数,则说明此时在奇数行,我们可以从偶数行转移而来,反之亦然。
因此我们可以只在第0行和第1行进行转移,可以用dp[i%2][j]
和dp[(i-1)%2][j]
相互转移,因为每次转移都是从1~m重新赋值dp[i][j]
,不必担心其他行数据的干扰情况。
%2
我们可以用位运算&1
来代替。
java
static void solve(){
n=in.nextInt();
m=in.nextInt();
for(int i=1;i<=n;i++){
v[i]=in.nextInt(O;
w[ij=in.nextIntO;
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
dp[i&1][j]=dp[i-1&1][j];
if(j>=v[i]){
dp[i&i][j]=Math.max(dp[i&1][j],dp[i-1&1][j-v[i]]+w[i]);
}
}
}
out.println(dp[n&1][m]);
}
⭐️01背包(一维数组做法)
我可以直接用本身进行转移(注意要反向!)
我们可以直接写dp[j]=max(dp[j],dp[j-v[i]]+w[i])
。
(1,6),(2,10)

java
static void solve(){
n=in.nextInt();
m=in.nextInt();
for(int i=1;i<=n;i++){
v[i]=in.nextInt();
w[ij=in.nextInt();
}
for(int i=1;i<=n;i++){
for(int j=m;j>=v[i];j--){
dp[j]=Math.max(dp[j-v[i]]+w[i],dp[j]);
}
}
out.println(dp[m]);
}
📚01背包
🍎笔记

6、完全背包
📖完全背包
暴力思路
枚举其所有可能的组合,可以用dfs,每个物品有选很多次和不选两种方式。
该做法复杂度为2n× k。
java
int dfs(int u,int s){
if(u>n) return 0;
int ans1=0,ans2=0;
if(s+v[u]<=m) ans1=dfs(u+1,s+v[u])+w[u];
ans2=dfs(u+1,s);
return Math.max(ans1,ans2);
}
java
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
for(int k=0;k*v[i]<=j;k++){
f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+w[i]*k);
}
}
}
n循环为考虑几个物品,m循环为考虑几个体积的空间,k循环为选用几次。用f数组记录每种情况的最佳决策。
时间复杂度为O(nm²)。
⭐️状态定义
状态定义
用dp[i][j]表示前i个物品在背包容量为j的条件下的最大价值。
- i表示物品的编号。
- j表示背包的剩余容量
最优子结构
最优子结构是动态规划问题的核心性质之一,表示一个问题的最优解可以由其子问题的最优解构成。
具体到完全背包问题,最优子结构体现在:
对于容量为j的背包,在考虑物品i时,可以有以下选择:
- 不选择物品i:此时背包的最大价值为dp[i-1][j],即前i-1个物品在容量j时的最优解。
- 选择物品i:此时需要至少选一次物品i,背包容量减少v[i],剩余的容量为j-v[i]。此时问题可以简化为容量为j-v[i]的子问题,即dp[i][j-v[i]],再加上当前物品i的价值w[i]。
⭐️完全背包(二维数组做法)
无后效性
对于完全背包问题来说,某个状态dp[i][j]一旦通过决策得到,它就不会因后续的决策而改变,因此该问题具有无后效性。
状态转移方程
根据最优子结构,可以得出完全背包问题的状态转移方程:
- 如果不选择物品i,则dp[i][j]= dp[i-1][j]。
- 如果选择物品i,则dp[i][j]= dp[i][j-v[i]]+w[i]。
则有转移方程
dp[i][j] = max(dp[i- 1][j],dp[i][j]- v[i] + w[i])
时间复杂度为nxm。
java
static void solve(){
n=in.nextInt();
m=in.nextInt();
for(int i=1;i<=n;i++){
v[i]=in.nextInt();
w[ij=in.nextInt();
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= m; j++) {
//如果不选第i个物品
dp[i][j]= dp[i- 1][j];
//如果可以选第i个物品,考虑选几次
if (j >= v[i]) {
dp[i][j]= Math.max(dp[i][j], dp[i][j- v[i]]+ w[i]);
}
}
}
out.println(dp[n][m]);
}
⭐️完全背包(一维数组做法)
对于背包问题,最核心的状态转移依赖于上一层的解,也就是我们不必用一个二维数组来记录每个物品的决策过程。
我们可以通过优化只使用一个一维数组dp[j]来实现背包问题的状态转移。其中,dp[i][j]表示用前i个物品、容量为j的背包的最大价值。可以看到,dp[i][j]只依赖于当前物品之前的状态和该物品的前一状态。因此,实际上只需一个一维数组就可以维护状态。
dp[j] = max(dp[j], dp[j - v[i]] + w[i])
与0/1背包的区别在于,完全背包每个物品可以选择多次。因此,在遍历物品时,需要正序遍历容量(从小到大),以确保每个物品可以被多次选择。
java
static void solve(){
n=in.nextInt();
m=in.nextInt();
for(int i=1;i<=n;i++){
v[i]=in.nextInt();
w[ij=in.nextInt();
}
for(int i=1;i<=n;i++){
for(int j=v[i];j<=m;j++){
dp[j]=Math.max(dp[j-v[i]]+w[i],dp[j]);
}
}
out.println(dp[m]);
}
📚完全背包
🍎笔记
7、多重背包
📖多重背包问题概述
暴力思路
枚举其所有可能的组合,可以用dfs方式枚举,每个物品有选k次和不选两种方式。
java
int dfs(int u,int s){
if(u>n) return 0;
int ans=0;
for(int i=0;s+i*v[i]<=m&&i<=c[i];i++){
ans=Math.max(ans,dfs(u+1,s+i*v[i])+i*w[i]);
}
return ans;
}
⭐️状态定义
状态定义
用dp[i][j]表示前i个物品在背包容量为j的条件下的最大价值。
- i表示物品的编号。
- j表示背包的剩余容量
最优子结构
最优子结构是动态规划问题的核心性质之一,表示一个问题的最优解可以由其子问题的最优解构成。
具体到多重背包问题,最优子结构体现在:
对于容量为j的背包,在考虑物品i时,可以有以下选择:
- 对于每个物品i,可以选择k次(0<k<ci),那么状态转移方程为:dp[i][j]=max(dp[i][j],dp[i-1][j-k·v[i]]+k.w[i])
⭐️例题:多重背包
java
for(int i=1;i<=n;i++){
for(int j=m;j>=O;j--){
for(int k=0;k*v[i]<=j&&k<=s[i];k++){
f[i][j]=max(f[ij[j],f[i-1j[j-k*v[i]]+w[i]*k);
}
}
}
时间复杂度为O(nmk)
⭐️例题:多重背包(二进制优化)
使用二进制优化的方式,可以将多重背包问题转换为0/1背包问题,从而减少时间复杂度。
- 实现思路:将物品数量c:转换为二进制的形式,这样我们可以用2*的组合来代替。
- 例如,假设一个物品的数量为13,可以表示为1+2+4+6,这种形式既可以把1~13中每个数字全部枚举出来。
- 然后我们将该物品拆分成了四件物品,然后去做0/1背包问题。
时间复杂度为(n x m x logk)
java
int p = 1;
for (int i = 1; i<= n ; i++){
int V = in.nextInt();
int W = in.nextIntO;
int S = in.nextIntO;
int k = 1;
while (S > k){
v[p]= V*k;
w[p]= W*k;
S -= k;
k*= 2;
p++;
}
if(S > 0){
v[p] = V*S;
w[p] = W*S;
p ++;
}
}
for(int i=1;i<p;i++){
for(intj=m;j>=v[i];j--){
dp[j]=Math.max(dp[j],dp[j-v[i]]+w[i]);
}
}
System.out.println(dp[m]);
📚多重背包
8、计数问题
9、区间dp
10、数位dp
dfs枚举数位
数位DP一般是用于求解给定一个区间[l,r],问区间内有多少个符合要求的数字的问题。
如果用暴力求解,我们可以写出这样代码:
java
for(int i=l;i<=r;i++){
if(check(i) ans++;
}
这样的代码复杂度很高,最坏为O(n·常数)
现在考虑这样的一个问题:给定一个正整数n,我们如何用dfs枚举1~n。
我们用dfs来依次枚举数位,例如我们现在有一个数字231。
java
static int a[l-{2,3}
static void dfs(int u,boolean limit,int res){
if(u==2){
System.out.println(res);
return;
}
int up=limit?a[u]:9;//如果前面有任意一位没有枚举满,我这一位随便使用。
for(int i=0;i<=up;i++){
dfs(u+1,limit&&(i==up),res*10+i);
}
}
我们调用dfs(0,true,0)就可以枚举完0~231,而数位DP,我们的做法便是在此基础上衍生的记忆化搜索。
🍎
📖234的和
考虑1~r的dfs做法。首先我们要对r进行拆位,然后枚举。
拆位即:
java
static int a[]=new int[20];
static void solve(int r){
int len=0;
while(r!=0){
a[++len]=r%10;
r/=10;
}
}
这样我们便将r的逆序拆入了a数组。
接下来我们需要进行dfs枚举1~r。
java
static int a[]-new int[20];
static void dfs(int u,boolean limit,int res){
if(u==0){
return;
}
int up=limit?a[u]:9;//如果前面有任意一位没有枚举满,我这一位随便使用。
for(int i=0;i<=up;i++){
dfs(u-1,limit&&(i==up),res*10+i);
}
static void solve(int r){
int len=0;
while(r!=0){
a[++len]=r%10;
r/=10;
}
dfs(len,true,0);
}
改写dfs成能求所有2的和。
java
static long dfs(int u,boolean limit,long sum){
if(u==0){
return sum;
}
int up=limit?a[u]:9;//如果前面有任意一位没有枚举满,我这一位随便使用。
long res=0;//记录下当前位选O~up后,能返回的sum值,sum枚举到当前位时存放所有$2$的和。
for(int i=0;i<=up;i++){
if(i==2){
res=res+dfs(u-1,(i==up)&&limit,sum+i)
}else{
res=res+dfs(u-1,(i==up)&&limit,sum)
}
return res;
}
🍎
这里大家画几个案例就明白了,例如sum=32。枚举第一位为0,1,3的时候各自会返回2,为2的时候会返回20,总结果为20+6=26。
改写dfs成能求所有2,3,4的和。
java
static long dfs(int u,boolean limit,long sum){
if(u==0){
return sum;
}
int up=limit?a[u]:9;//如果前面有任意一位没有枚举满,我这一位随便使用。
long res=0;//记录下当前位选O~up后,能返回的sum值,sum枚举到当前位时存放所有$2$的和。
for(int i=0;i<=up;i++){
if(i==2||i==3||i==4){
res=res+dfs(u-1,(i==up)&&limit,sum+i)
}else{
res=res+dfs(u-1,(i==up)&&limit,sum)
}
return res;
}
最终做一个容斥原理,1~r的和减去1~l-1的和。
⭐️改写dfs成能求所有2,3,4的和。
java
static long dfs(boolean lim,int pos,int sum){
if(pos==0) return sum;
if(!lim&&dp[pos][sum]!=-1) return dp[pos][sum];
int up=lim?a[pos]:9;
long res=O;
for(int i=0;i<=up;i++){
if(i==2||i==3||i==4)
res=res+dfs(lim&&i==up,pos-1,sum+i);
else
res=res+dfs(lim&&i==up,pos-1,sum);
}
if(!lim) dp[pos][sum] = res;
return res;
}
记忆化搜索时间复杂度一般为(1og10n·常数)。
🍎
以321为例来看看为什么记忆化搜索可以减少时间复杂度,考虑只求2的和。