计数类DP
定义
计数类DP主要是通过动态规划的方法来计算满足特定条件的方案数、组合数等数量相关的问题。
运用情况
- 需要计算不同排列、组合或情况的数量。
- 问题具有明显的阶段性,且每个阶段的选择会对后续阶段产生影响。
- 可以通过逐步构建较小规模问题的解来推导出大规模问题的解。
注意事项
- 状态定义要准确合理,确保能够涵盖所有需要计数的情况。
- 边界条件的处理要小心,避免出现错误。
- 注意状态转移的正确性和完整性,不能遗漏某些可能的情况。
- 避免重复计算,确保 DP 过程的高效性。
解题思路
- 确定状态:仔细分析问题,找到合适的状态表示,通常状态包含问题规模、已有的某些特征等。
- 分析状态转移:找出不同状态之间的联系,即如何从一个状态推导出下一个状态的方案数。
- 初始化:对边界状态或初始状态进行正确的赋值。
- 递推求解:按照状态转移方程逐步计算出更大规模问题的解。
- 得到最终结果:根据问题要求,从最终状态中获取需要的计数结果。
例如,计算从一个起点到一个终点有多少种不同走法的问题,就可以用计数类 DP 来解决,状态可以是当前位置,转移就是根据不同的移动规则来更新方案数。通过合理定义状态和转移方程,就可以准确地计算出总的方案数。
AcWing 900. 整数划分
题目描述
900. 整数划分 - AcWing题库
运行代码
#include <iostream>
#include <cstring>
using namespace std;
const int MOD = 1e9 + 7;
int dp[1001][1001];
int divide(int n, int m) {
if (n == 0) return 1;
if (m == 0) return 0;
if (dp[n][m]!= -1) return dp[n][m];
int res = 0;
for (int i = 0; i <= min(n, m); i++) {
res = (res + divide(n - i, i)) % MOD;
}
dp[n][m] = res;
return res;
}
int main() {
int n;
cin >> n;
memset(dp, -1, sizeof(dp));
cout << divide(n, n) << endl;
return 0;
}
代码思路
dp[n][m]
中的n
表示要划分的整数,m
表示当前划分中允许的最大整数。divide
函数通过递归的方式计算划分方法的数量。如果n
为 0,则表示一种划分成功,返回 1;如果m
为 0 则返回 0。然后通过循环从 0 到min(n, m)
逐步尝试将当前的数拆分成当前最大数和剩余部分,对剩余部分继续递归调用,将所有结果累加并取模更新状态,最后将计算结果存储在dp
数组中。在main
函数中输入n
后,通过调用divide(n, n)
并输出结果。
改进思路
- 可以添加一些注释提高代码的可读性。
- 可以考虑使用更高效的数据结构或算法来优化性能,虽然在这个规模下可能不太明显。
- 可以对代码结构进行一些整理和优化,使逻辑更加清晰。
其它代码
#include <iostream>
using namespace std;
const int N = 1010, mod = 1e9 + 7;
int f[N];
int main()
{
int n;
cin >> n;
f[0] = 1;
for(int i = 1; i <= n; i ++ )
for(int j = i; j <= n; j ++ )
f[j] = (f[j] + f[j - i]) % mod;
cout << f[n] << endl;
return 0;
}
代码思路
-
初始化:首先设置
f[0] = 1
,表示选择0个元素的组合只有1种情况(什么都不选)。 -
双重循环:
- 外层循环变量
i
从1到n,代表当前考虑的是将多少个元素作为一个整体(从1开始是因为至少选1个元素才有组合变化)。 - 内层循环变量
j
从i
到n,表示在考虑将i
个元素作为整体时,可以放置的位置(或理解为累计到目前为止的选择总数)。 - 在内循环中,更新
f[j]
的值为f[j] + f[j - i]
,并取模mod
。这里的意思是,对于已经有j-i
个元素的组合,我们再添加一个由i
个相同元素组成的组合,形成一个新的组合。因此,到达某一总数j
的组合数是之前所有小于j
的组合数累加的结果,体现了组合数学中的加法原理。
- 外层循环变量
-
输出结果:经过上述计算后,
f[n]
即为从1到n的所有整数中选取任意个数的组合总数的和。