【例9.10】机器分配(信息学奥赛一本通- P1266) 机器分配(洛谷P2066)

【题目描述】

总公司拥有高效设备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:循环的嵌套顺序(谁在最外层?)

代码里是三层循环:

  1. 最外层i (枚举公司):代表阶段。必须先算完前 1 个公司,才能算前 2 个公司。这是DP的"地基"。

  2. 中间层j (枚举设备量):代表状态/容量

  3. 最内层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个公司尽可能多拿

  • 代码体现 :内层循环 k1j(从小到大枚举),配合 >= 号。

  • 结果:第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;
}

四、边界情况处理

  1. 初始状态dp[0][j]=0,没有分公司时盈利为0

  2. 不分配设备的情况dp[i][j]初始化为dp[i-1][j],表示不给第i个分公司设备

  3. 设备数为0dp[i][0]=0,有设备才能有盈利

五、复杂度分析

  • 时间复杂度:O(n×m²),三重循环

  • 空间复杂度:O(n×m),三个二维数组

六、扩展思考

  1. 如果n,m很大(比如1000),需要优化空间,用滚动数组

  2. 如果要输出所有最优方案,需要记录所有可能的k值

  3. 如果设备可分割,变为连续问题,需要用实数处理

七、总结

这道题的难点在于状态设计和路径记录的结合。很多动态规划题只需要求最优值,但这题要求具体方案,必须在状态转移时同步记录决策路径。

关键点

  1. 定义清晰的状态:dp[i][j]

  2. 在状态转移时记录决策:b[i][j]

  3. 逆向推导方案时要注意设备总数的更新

  4. 处理相等情况用>=而不是>

掌握这个思路,类似的"求具体方案"的动态规划题都能解决。

相关推荐
roman_日积跬步-终至千里1 小时前
【计算机视觉(2)】图像几何变换基础篇:从平移旋转到投影变换
人工智能·算法·计算机视觉
0 0 01 小时前
CCF-CSP 37-3 模板展开(templating)【C++】
c++·算法
im_AMBER1 小时前
Leetcode 71 买卖股票的最佳时机 | 增量元素之间的最大差值
笔记·学习·算法·leetcode
bulingg2 小时前
聚类方法(kmeans,DBSCAN,层次聚类,GMM,EM算法)
算法·kmeans·聚类
lally.2 小时前
Kaggle Binary Classification with a Bank Dataset逻辑回归实现(准确率0.94539)
人工智能·算法·机器学习·逻辑回归
埃伊蟹黄面2 小时前
二分查找算法
c++·算法·leetcode
野蛮人6号2 小时前
力扣热题100道之78子集
算法·leetcode·职场和发展
悦来客栈的老板2 小时前
AST反混淆实战|reese84_jsvmp反编译前的优化处理
java·前端·javascript·数据库·算法
dragoooon342 小时前
[优选算法专题十一.字符串 ——NO.60~63 最长公共前缀、5最长回文子串、 二进制求和 、字符串相乘]
算法·leetcode·动态规划