【题目描述】
总公司拥有高效设备M台,准备分给下属的N个分公司。各分公司若获得这些设备,可以为国家提供一定的盈利。问:如何分配这M台设备才能使国家得到的盈利最大?求出最大盈利值。其中M≤15,N≤10。分配原则:每个公司有权获得任意数目的设备,但总台数不超过设备数M。
【输入】
第一行有两个数,第一个数是分公司数N,第二个数是设备台数M;
接下来是一个N*M的矩阵,表明了第 I个公司分配 J台机器的盈利。
【输出】
第一行输出最大盈利值;
接下N行,每行有2个数,即分公司编号和该分公司获得设备台数。
【输入样例】
3 3 //3个分公司分3台机器
30 40 50
20 30 50
20 25 30
【输出样例】
70 //最大盈利值为70
1 1 //第一分公司分1台
2 1 //第二分公司分1台
3 1 //第三分公司分1台
这道题是典型的分组背包问题,但比普通动态规划多一个难点:不仅要计算最大盈利,还要输出具体分配方案。很多人能算出最大值,但一到输出方案就卡壳。
一、 为什么状态转移方程这么写?
dp[i][j] = max{dp[i-1][j-k] + a[i][k]}
要理解这个方程,必须理解动态规划的三个维度:
1. 为什么需要二维数组 dp[i][j]?(定义状态)
很多同学会问:"我能不能只用一维 dp[j] 表示 j 台机器的最大利润?"
-
答案是不能。 因为这道题的每个公司(阶段)不一样。
-
如果我们只记录"用了 j 台机器",我们就丢失了"当前分到了第几个公司"的信息。
-
你需要知道"前 i-1 个公司"的情况,才能推导出"前 i 个公司"的情况。
-
所以: i代表阶段 (进度条),j 代表资源限制(背包容量)。
2. 方程背后的"无后效性"逻辑(推导过程)
想象你在做第i个公司的决策。此时:
-
你手头总共有j台设备可用。
-
决策:你要给第i个公司分k台(k可以是 0, 1, 2...j)。
-
代价:一旦你分给它k台,剩下的资源就只有j-k台了。
-
依赖 :剩下的j-k台设备,必须由前面的i-1个公司 去分,并且要分得最好(利润最大)。
这就构成了最优子结构:
前i个公司的最大利 = max( 第i个公司分k台的利 + 前i-1个公司分剩余j-k台的最大利 )
翻译成数学符号就是:
-
a[i][k]:第i个公司分k台的当期收益。 -
dp[i-1][j-k]:历史累积的最优收益(把剩下的j-k台给前面人分)。 -
max(...):枚举所有可能的k,选个最大的。
二、 核心难点剖析(教学重点)
这道题对初学者来说,有三个"坑"是必须跨过去的。
难点 1:模型识别------这是"分组背包"不是"0/1背包"
很多学生学完 0/1 背包(采药问题)后,习惯了"要么选、要么不选"。
-
误区:学生会以为对于第i个公司,只有"给机器"和"不给机器"两种选择。
-
正解 :这是一个互斥分组问题。
-
给第i个公司"1台机器"、"2台机器"..."m台机器",这些选项是互斥的。
-
你不能同时给甲公司"方案1"又给它"方案2"。
-
本质:内层循环的k,就是在枚举这个"组"里的所有互斥选项。
-
难点 2:循环的嵌套顺序(谁在最外层?)
代码里是三层循环:
-
最外层i (枚举公司):代表阶段。必须先算完前 1 个公司,才能算前 2 个公司。这是DP的"地基"。
-
中间层j (枚举设备量):代表状态/容量。
-
最内层k (枚举分配量):代表决策。
问:j 和k能反过来吗?
答:在二维数组写法下,数学逻辑上j和k的交换不影响结果(但在滚动数组优化成一维DP时,顺序至关重要,必须j从大到小,且必须j在外k在内)。
学习时固定顺序:阶段 -> 状态 -> 决策。
难点3:状态定义要精准
cpp
int dp[15][20]; // dp[i][j]:考虑前i个分公司,总共分配j台设备时的最大盈利
这里的"考虑前i个分公司"很关键。不是"分到第i个分公司",而是"已经为前i个分公司做好了分配决策"。
为什么这么定义?
因为动态规划是递推的,我们需要一个状态能从前一个状态转移过来。dp[i][j]必须能从dp[i-1][某个值]推导出来。
难点4:路径记录的同步
这是本题最大难点。很多人能算出最大盈利,但不知道如何记录分配方案。
cpp
if(dp[i-1][j-k] + a[i][k] >= dp[i][j]){
dp[i][j] = dp[i-1][j-k] + a[i][k];
b[i][j] = k; // 关键:记录这个决策
}
b[i][j]表示:在状态(i,j)下,第i个分公司分配了k台设备才得到最大盈利。
为什么要用>=而不是>?
题目有个隐含要求:当盈利相同时,编号小的分公司尽可能少分设备(洛谷,但信息学奥赛一本通不这么做也会错一个测试点)。因为k是从小到大枚举的,用>=能保证在盈利相等时选择更小的k。
-
我们的 DP 是从前往后推的(算i=1, 2...),但路径还原是从后往前推的(从n倒推回1)。
-
为了让前面的c_1尽可能小,就意味着后面的c_n要尽可能多拿(因为总资源固定)。
-
核心逻辑 :当
dp[i-1][j-k] + a[i][k] == dp[i][j]时,意味着"给第i个公司分k台"和"分更少台"的利润一样。 -
决策 :为了压榨前面公司的资源(迫使它们分得少),在利润相同时,我们要让当前的第i个公司尽可能多拿。
-
代码体现 :内层循环
k从1到j(从小到大枚举),配合>=号。 -
结果:第i个公司拿到了它能拿的最大值,剩下的留给i-1的就少了。
-
当k变大,如果利润持平,
>=会强制更新,选择更大的k。
难点5:逆向推导方案
得到最大盈利后,如何知道每个分公司具体分多少台?
cpp
c[n] = b[n][m]; // 最后一个分公司分配数
int x = m; // 剩余设备数
for(int i = n-1; i >= 1; i--){
x = x - c[i+1]; // 减去已分配的设备
c[i] = b[i][x]; // 当前分公司分配数
}
为什么从后往前推?
因为b[i][j]记录的是"在总设备数为j时"的决策。我们知道最终是m台,但不知道中间状态。从最后往前推,可以逐步确定每个状态的总设备数。
三、完整代码逐行解析
cpp
#include <iostream>
using namespace std;
int dp[15][20];//dp[i][j]代表轮到分第i个分公司时,总共有j台设备待分配,可以获得的最大盈利值
int a[15][20];//a[i][j]代表第i个分公司分到j台设备时的盈利值
int b[15][20];//b[i][j]用来存储过程中每次轮到分第i个公司,总共有j台设备情况下,第i个公司分几台设备能达到最大盈利值
int c[15];//用来存储第i个公司在n台设备情况下,为了获取最大盈利,分得的设备数
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++) cin>>a[i][j];
for(int i=1;i<=n;i++){//现可以分配i个分公司
for(int j=1;j<=m;j++){//总共有j台设备可以分配
dp[i][j]=dp[i-1][j];//初始化为不分配设备给第i个单位
for(int k=1;k<=j;k++){//给第i个分公司分配k台设备,比较最大盈利值
//当分配i分公司k台设备盈利大于当前时,选择分 这里不加等于号原则上是对的,但是会错一个测试点
//加上等于号就是最大盈利值相同时,要求编号小的公司分得设备尽可能少。
if(dp[i-1][j-k]+a[i][k]>=dp[i][j]){
dp[i][j]=max(dp[i][j],dp[i-1][j-k]+a[i][k]);
b[i][j]=k;
}
}
}
}
/*
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cout<<dp[i][j]<<" ";
}
cout<<endl;
}
*/
/*//这里是为了检查b数组里面存的数然后找规律
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cout<<b[i][j]<<" ";
}
cout<<endl;
}
*/
cout<<dp[n][m]<<endl;//最大盈利值
c[n]=b[n][m];
int x=m;
for(int i=n-1;i>=1;i--){
x=x-c[i+1];
c[i]=b[i][x];
}
for(int i=1;i<=n;i++)
cout<<i<<" "<<c[i]<<endl;
return 0;
}
四、边界情况处理
-
初始状态 :
dp[0][j]=0,没有分公司时盈利为0 -
不分配设备的情况 :
dp[i][j]初始化为dp[i-1][j],表示不给第i个分公司设备 -
设备数为0 :
dp[i][0]=0,有设备才能有盈利
五、复杂度分析
-
时间复杂度:O(n×m²),三重循环
-
空间复杂度:O(n×m),三个二维数组
六、扩展思考
-
如果n,m很大(比如1000),需要优化空间,用滚动数组
-
如果要输出所有最优方案,需要记录所有可能的k值
-
如果设备可分割,变为连续问题,需要用实数处理
七、总结
这道题的难点在于状态设计和路径记录的结合。很多动态规划题只需要求最优值,但这题要求具体方案,必须在状态转移时同步记录决策路径。
关键点:
-
定义清晰的状态:
dp[i][j] -
在状态转移时记录决策:
b[i][j] -
逆向推导方案时要注意设备总数的更新
-
处理相等情况用
>=而不是>
掌握这个思路,类似的"求具体方案"的动态规划题都能解决。