记录164
#include<bits/stdc++.h> // 引入万能头文件,包含所有常用的标准库
using namespace std; // 使用标准命名空间
int n,k; // 定义全局变量n(要分的整数)和k(要分的份数)
int dp[205][10]; // 定义二维dp数组,dp[i][j]表示把整数i分成j份的方案数
int main(){ // 主函数入口
cin>>n>>k; // 输入要分的整数n和要分的份数k
// 初始化边界条件:把任意整数i分成1份,只有1种方案(就是它本身)
for(int i=1;i<=n;i++){ // 遍历从1到n的每一个整数
dp[i][1]=1; // 将分成1份的方案数初始化为1
}
// 开始动态规划填表
for(int i=2;i<=n;i++){ // 外层循环:i从2到n,代表当前要分的整数
// 内层循环:j从2到k,且j不能超过i(因为份数不能大于数字本身)
for(int j=2;j<=k&&j<=i;j++){
// 状态转移方程:包含1的方案数(dp[i-1][j-1]) + 不包含1的方案数(dp[i-j][j])
dp[i][j]=dp[i-1][j-1]+dp[i-j][j];
}
}
cout<<dp[n][k]<<"\n"; // 输出把n分成k份的最终方案数
return 0; // 主函数正常结束,返回0
}
题目传送门
https://www.luogu.com.cn/problem/P1025
前言
我是一名专注信奥赛(GESP、CSP-J/S)的教练。
- 如果你觉得这篇题解对你有帮助,欢迎点击关注我的优快云账号,我会持续更新高质量算法解析。
- 我深知算法思维的构建远比单纯通过题目更重要,本系列题解不局限于AC代码的堆砌,而是致力于拆解题目背后的逻辑链条与核心知识点
- 备赛路上若遇瓶颈,欢迎随时评论或私信,我将甄选典型疑难问题,通过视频讲解或撰写专项文章的形式,为你提供深度答疑。
核心解题思路
这道题是一道非常经典的整数划分(动态规划)问题。
-
问题转化(分类讨论模型):
题目要求将整数 n 分成 k 份,且每份不能为空,不考虑顺序。为了避免重复计数,我们可以将所有合法的划分方案分为两类:- 第一类:划分中包含数字 1。既然有一份是 1,那么剩下的问题就变成了:将 n-1 分成 k-1 份。
- 第二类:划分中不包含数字 1。这意味着每一份的数字都至少为 2。既然每一份都至少有 1,我们可以先从每一份中“借走” 1,那么剩下的问题就变成了:将 n-k 分成 k 份。
-
算法设计(状态转移方程):
基于上述分类,我们可以得出极其优美的状态转移方程:
dp[i][j] = dp[i-1][j-1] + dp[i-j][j]
其中 dp[i][j] 表示将整数 i 分成 j 份的方案数。
代码分块详细解释
1. 头文件、全局变量与 DP 数组定义
#include<bits/stdc++.h> // 引入万能头文件,包含所有常用的标准库
using namespace std; // 使用标准命名空间
int n,k; // 定义全局变量n(要分的整数)和k(要分的份数)
int dp[205][10]; // 定义二维dp数组,dp[i][j]表示把整数i分成j份的方案数
- 详细分析:根据题目数据范围,n 最大为 200,k 最大为 6,因此定义 dp[205][10] 绰绰有余。全局数组默认初始化为 0,为后续的动态规划填表做好了准备。
2. 边界条件初始化
int main(){ // 主函数入口
cin>>n>>k; // 输入要分的整数n和要分的份数k
// 初始化边界条件:把任意整数i分成1份,只有1种方案(就是它本身)
for(int i=1;i<=n;i++){ // 遍历从1到n的每一个整数
dp[i][1]=1; // 将分成1份的方案数初始化为1
}
- 详细分析:这是动态规划的基石。无论整数 i 是多少,只要要求分成 1 份,那么方案永远只有 1 种(即 i 本身)。这个边界条件保证了后续状态转移的正确性。
3. 核心逻辑:DP 填表与状态转移
// 开始动态规划填表
for(int i=2;i<=n;i++){ // 外层循环:i从2到n,代表当前要分的整数
// 内层循环:j从2到k,且j不能超过i(因为份数不能大于数字本身)
for(int j=2;j<=k&&j<=i;j++){
// 状态转移方程:包含1的方案数(dp[i-1][j-1]) + 不包含1的方案数(dp[i-j][j])
dp[i][j]=dp[i-1][j-1]+dp[i-j][j];
}
}
- 详细分析:这是代码的灵魂,完美实现了分类讨论的思想。
- 外层循环:从小到大枚举要划分的整数 i。
- 内层循环:枚举要划分的份数 j。注意 j <= i 这个剪枝条件,因为把 3 分成 4 份是不可能的(每份至少为 1)。
- 状态转移:dp[i-1][j-1] 对应“包含 1”的情况;dp[i-j][j] 对应“不包含 1”的情况。两者相加即为总方案数。
4. 输出最终结果
cout<<dp[n][k]<<"\n"; // 输出把n分成k份的最终方案数
return 0; // 主函数正常结束,返回0
}
- 详细分析:经过双重循环填表后,dp[n][k] 中存储的就是将 n 分成 k 份的总方案数,直接输出即可。
核心逻辑总结表
| 代码模块 | 核心变量/操作 | 精炼作用 | 解决的痛点 |
|---|---|---|---|
| 边界初始化 | dp[i][1] = 1 | 设定分成 1 份时的基础方案数 | 为动态规划提供了正确的起点,防止状态转移越界 |
| 包含 1 的转移 | dp[i-1][j-1] | 计算划分中包含 1 的方案数 | 通过提取一个 1,将原问题规模缩小为 n-1, k-1 |
| 不包含 1 的转移 | dp[i-j][j] | 计算划分中不包含 1 的方案数 | 通过从每份中减去 1,将原问题规模缩小为 n-k, k |
| 循环剪枝 | j <= i | 限制份数不能超过整数本身 | 避免了无意义的计算,保证了数组访问的安全性 |
| DP 填表 | 双重 for 循环 | 自底向上计算所有子问题的解 | 避免了递归带来的重复计算,时间复杂度仅为 O(n × k) |
541



被折叠的 条评论
为什么被折叠?



