Problem: LCP 51. 烹饪料理
思路
回溯三问
子集型回溯有两种解法
-
输入视角
- 通过判断集合中的每一个元素 k,认为元素 k 被选入子集或不被选入子集,从而得到答案。其 dfs 形状是一个高度为 n 的二叉树。
- 作为当前节点的元素 k 代表的是这次我处理的是第 k 的元素。当 k 等于集合长度时已经处理完所有元素了,结束 dfs。
-
答案视角
- 每次判断在集合中选择哪一个元素,每次遍历中都枚举选择集合中的每一个数。但是注意由于顺序的问题,1->2和2->1实际上是一样的,所以我们每次从某一个元素 k 开始选择 k 及 k 以后的元素。
- 作为当前节点的元素 k 代表的是这次我会遍历的元素有从 k 开始的剩余元素。当 k 等于集合长度时已经没有元素可以选择了,结束 dfs。
-
两种解法的当前元素 k 看似写法很相似,结束条件也一致,然而代表的含义是完全不同的!
如果不想明白用的是哪一种思考方式,就很非常容易将这两种写法杂糅在一起写。这样做不仅导致意义不明,难于理解,还会在 dfs 的过程中遍历多余节点。
案例分析
- 来看一个案例,答辩视角是在初次做题时写出的杂糅代码,其中混杂了两种写法(甚至写了注释)。重复的节点被遍历了,而且复盘的时候根本说不清楚自己每一步在做什么(很多次在做回溯的时候有的感觉,AC了但无法解释)。
- 作为对比下面写出了两种视角的正确写法,当读者辨别清楚二者的差距时应当能很清楚的理解子集型回溯到底应该做什么。
Code
答辩视角
- 在 dfs 之中使用了 for 循环,遍历元素 k 及之后的元素,这是典型的答案视角写法。可以看到注释中写了 do 和 don’t do 两种情况,这又是典型的输入视角写法。二者混杂在一起就合成了我们最终的三星答辩视角。
- 实际上错误在于没有将题目中的特别条件和回溯的一般条件区分开来。题目中制作料理时有两种可能(可以做或不可以做),这是题目中特殊的的条件,也就是在某一步时,元素可能需要满足一定的条件才能加入集合。然而这和选和不选是完全不一样的! && 这和要不要继续递归也完全没有任何相干!
- 在输入视角中,对当前元素应当先看能不能做,如果能做就是两种选择(选或不选),如果不能做就是一种选择(不选)。
- 在答案视角中,每一个元素也应该看能不能做,如果能做就做,然后递归至子问题,如果不能做就不做,然后递归至子问题。
class Solution {
int res = -1;
int tmp = 0;
public:
int perfectMenu(vector<int>& materials, vector<vector<int>>& cookbooks, vector<vector<int>>& attribute, int limit) {
dfs(0, limit, materials, cookbooks, attribute);
return res;
}
void dfs(int k, int limit, vector<int>& materials, vector<vector<int>>& cookbooks, vector<vector<int>>& attribute) {
if(k == attribute.size()) {
if(limit <= 0) {
res = max(res, tmp);
}
return;
}
for(int i = k; i < attribute.size(); i++) {
int doable = true;
for(int j = 0; j < cookbooks[i].size(); j++) {
if(cookbooks[i][j] > materials[j]) doable = false;
}
// do
if(doable) {
for(int j = 0; j < cookbooks[i].size(); j++) {
materials[j] -= cookbooks[i][j];
}
tmp += attribute[i][0];
limit -= attribute[i][1];
dfs(i + 1, limit, materials, cookbooks, attribute);
for(int j = 0; j < cookbooks[i].size(); j++) {
materials[j] += cookbooks[i][j];
}
tmp -= attribute[i][0];
limit += attribute[i][1];
}
// don't do
dfs(i + 1, limit, materials, cookbooks, attribute);
}
}
};
输入视角,选或不选
class Solution {
int res = -1;
int tmp = 0;
public:
int perfectMenu(vector<int>& materials, vector<vector<int>>& cookbooks, vector<vector<int>>& attribute, int limit) {
dfs(0, limit, materials, cookbooks, attribute);
return res;
}
void dfs(int k, int limit, vector<int>& materials, vector<vector<int>>& cookbooks, vector<vector<int>>& attribute) {
if(k == attribute.size()) {
if(limit <= 0) {
res = max(res, tmp);
}
return;
}
int doable = true;
for(int j = 0; j < cookbooks[k].size(); j++) {
if(cookbooks[k][j] > materials[j]) doable = false;
}
// do
if(doable) {
for(int j = 0; j < cookbooks[k].size(); j++) {
materials[j] -= cookbooks[k][j];
}
tmp += attribute[k][0];
limit -= attribute[k][1];
dfs(k + 1, limit, materials, cookbooks, attribute);
for(int j = 0; j < cookbooks[k].size(); j++) {
materials[j] += cookbooks[k][j];
}
tmp -= attribute[k][0];
limit += attribute[k][1];
}
// don't do
dfs(k + 1, limit, materials, cookbooks, attribute);
}
};
答案视角,选择哪个
class Solution {
int res = -1;
int tmp = 0;
public:
int perfectMenu(vector<int>& materials, vector<vector<int>>& cookbooks, vector<vector<int>>& attribute, int limit) {
dfs(0, limit, materials, cookbooks, attribute);
return res;
}
void dfs(int k, int limit, vector<int>& materials, vector<vector<int>>& cookbooks, vector<vector<int>>& attribute) {
if(k == attribute.size()) {
cout << limit << endl;
if(limit <= 0) {
res = max(res, tmp);
}
return;
}
for(int i = k; i < attribute.size(); i++) {
int doable = true;
for(int j = 0; j < cookbooks[i].size(); j++) {
if(cookbooks[i][j] > materials[j]) doable = false;
}
if(doable) {
for(int j = 0; j < cookbooks[i].size(); j++) {
materials[j] -= cookbooks[i][j];
}
tmp += attribute[i][0];
limit -= attribute[i][1];
}
dfs(i + 1, limit, materials, cookbooks, attribute);
if(doable) {
for(int j = 0; j < cookbooks[i].size(); j++) {
materials[j] += cookbooks[i][j];
}
tmp -= attribute[i][0];
limit += attribute[i][1];
}
}
}
};