第五章作业
一、回溯法分析 “最小重量机器设计问题”
1.1 “最小重量机器设计问题” 的解空间
简单来说,这个问题的解空间就是所有可能的 “部件 - 供应商” 搭配方案的总和,我们可以用通俗的方式拆解理解:
解的样子:每个解都是一个 “供应商选择清单”,可以看成一个长度为n的列表(对应n个部件)。比如有 2 个部件、3 个供应商,[0,2]就表示 “第 1 个部件选供应商 0,第 2 个部件选供应商 2”(代码里供应商是 0 开始编号,输出时要 + 1 变成自然序号)。
解的总数:每个部件都有m个供应商可选,就像 “每个位置有m种填法”,所以总共有m×m×…×m(n个m相乘,即mⁿ)种可能的方案。比如n=2、m=3,就有3×3=9种不同的搭配方案。
可行的解:不是所有方案都有效,只有 “总价格不超过d” 的方案才是 “可行解”。比如d=10,某方案总价格是 12,就会被排除,不算可行解。
我们要找的解:在所有可行解里,总重量最轻的那个 “供应商选择清单”,就是最终的目标解。
1.2 “最小重量机器设计问题” 的解空间树
解空间树就是把所有可能的方案,用 “树” 的形式画出来,方便我们一步步探索,它是一棵 “m 叉树”(每个节点有m个分支),我们可以结合 “选供应商” 的过程理解:
树的层次对应 “选部件的顺序”:
第 0 层(根节点):就像我们刚开始选供应商,还没给任何部件选供应商,是初始状态(总价格、总重量都是 0)。
第 1 层:对应 “已经给第 1 个部件选完供应商” 的状态,根节点会分出m个分支(对应m个供应商),每个分支对应 “第 1 个部件选某一个供应商” 的选择,第 1 层总共有m个节点。
第 2 层:对应 “已经给前 2 个部件选完供应商” 的状态,第 1 层的每个节点又会分出m个分支(给第 2 个部件选供应商),第 2 层总共有m×m个节点。
……
第n层:对应 “所有n个部件都选完供应商” 的状态,这一层的每个节点都是 “完整方案”(叶子节点),总共有mⁿ个叶子节点,正好对应所有可能的方案。
树的分支含义:每个分支就代表 “给当前部件选某一个供应商” 的决策。比如第 1 层节点的第 2 个分支,就是 “第 1 个部件选供应商 2”,分支上还带着这个选择的 “价格” 和 “重量” 信息。
我们怎么遍历这棵树:代码里用的是 “先往深走、走不通再回头” 的方式(深度优先遍历)。比如先选 “部件 1 - 供应商 0→部件 2 - 供应商 0→…→部件 n - 供应商 0”,走完这条线后,再回头改成 “部件 n - 供应商 1”,依次尝试所有可能,就像 “挨个试完所有搭配”。
1.3 遍历解空间树时每个结点的状态值
遍历树的时候,每个节点就代表 “选到某一步的中间状态”,就像我们做题时 “写到一半的草稿”,里面记录了 4 个关键信息,这些信息能帮我们判断要不要继续往下试:
当前选到第几个部件(i):对应代码里dfs(i)的i,比如i=2,就表示 “已经给前 2 个部件选好供应商,接下来要选第 3 个部件”。根节点是i=0(还没选任何部件),所有部件选完就是i=n(到达叶子节点)。
当前花了多少钱(current_cost):记录前i个部件选完后,总价格是多少。比如选了部件 1 - 供应商 0(价格 3)、部件 2 - 供应商 1(价格 5),current_cost就是 8。如果这个数超过d,就不用继续选了。
当前机器有多重(current_weight):记录前i个部件选完后,总重量是多少。比如部件 1 - 供应商 0(重量 2)、部件 2 - 供应商 1(重量 4),current_weight就是 6。这个数是我们判断 “是否更优” 的关键。
当前选了哪些供应商(current_supplier):这是一个列表,前i个位置已经填好了供应商编号,后n-i个位置还是空的。比如i=2时,列表可能是[0,1,?,?](n=4的情况),等i=n时,这个列表就变成了完整的供应商选择方案。
这些状态就像 “草稿上的记录”,当我们试完一个供应商,再换另一个时,会把草稿上的记录擦掉(比如把current_cost减去刚才选的供应商价格),这就是 “回溯” 的过程。
二、对回溯算法的理解
在我看来,回溯算法就像 “走迷宫找出口(最优解)”,核心逻辑是 “先试一条路,走不通就回头换条路,提前知道走不通的路直接放弃”,比暴力试所有路高效得多。
- 回溯算法的核心:试探→回溯→剪枝
(1)试探:“先顺着一条路往下走”
就像我们给部件选供应商,先给第 1 个部件选供应商 0,再给第 2 个部件选供应商 0,依次往下选,直到把所有部件选完(走到迷宫尽头),看看这个方案是不是可行、是不是最优。这一步对应代码里的 “选择供应商→更新当前成本 / 重量→递归选下一个部件”。
(2)回溯:“走不通就回头,换个方向再试”
当我们试完 “第 n 个部件选供应商 0” 的方案后,不会直接结束,而是 “撤销” 这个选择(把总价格、总重量减回去,把供应商列表里的这个位置清空),然后试 “第 n 个部件选供应商 1” 的方案。就像在迷宫里走到死胡同,退回到上一个路口,换另一条岔路继续走,这就是 “回溯” 的精髓 —— 不浪费之前的探索,只撤销当前步骤的选择,再试其他选项。
(3)剪枝:“提前判断这条路不行,不用走到头”
这是回溯算法的 “优化技巧”,也是比暴力枚举厉害的地方。比如我们选到第 2 个部件时,已经花了 9 块钱,而d=10,剩下的部件就算都选最便宜的,总价格也会超过 10,这时候就不用继续选后面的部件了,直接回头换方案,这就是 “剪枝”—— 把肯定无效的路径提前砍掉,节省大量时间。在本题中,还有一种剪枝:当前总重量加上剩下部件最轻的重量,已经比目前找到的最优重量还重,就算后面选最轻的部件,也不可能更优,这时候也可以直接回头。 - 回溯算法的基本步骤(结合本题作业)
我们可以把回溯算法的流程总结为 3 步,对应作业代码的逻辑:
初始化准备:先把价格、重量数据读进来,设置 “当前最优重量” 为一个很大的数(比如INT_MAX),初始化 “当前选择清单”“最优选择清单”,还会提前计算每个部件的最低价格、最低重量(方便后续剪枝)。
递归试探与回溯:从第 1 个部件开始(i=0),依次尝试每个供应商:
先判断:选这个供应商会不会超预算?会不会后续再怎么选都不可能更轻?如果是,直接跳过(剪枝)。
再选择:记录这个供应商,更新总价格、总重量。
再递归:继续选下一个部件。
最后回溯:选完下一个部件的所有可能后,撤销当前供应商的选择,恢复到之前的状态,再试下一个供应商。
输出结果:所有方案试完后,要么输出找到的最小重量和供应商清单,要么输出 “No solution”(没有可行方案)。 - 回溯算法的小总结
回溯算法不是什么复杂的算法,本质就是 “有策略地枚举”—— 它不像暴力枚举那样盲目试所有方案,而是通过 “回溯” 避免重复计算,通过 “剪枝” 减少无效尝试,在找 “组合优化问题”(比如本题、背包问题、排列问题)的最优解时特别好用。对我们学生来说,学习回溯算法的关键就是理解 “状态记录” 和 “状态回滚”:要知道每一步需要记录哪些信息(比如本题的成本、重量),也要记得试完一个选项后,把状态恢复到原来的样子,这样才能正确尝试下一个选项。
浙公网安备 33010602011771号