一个魔法师有许多不同的咒语。
给你一个数组 power ,其中每个元素表示一个咒语的伤害值,可能会有多个咒语有相同的伤害值。
已知魔法师使用伤害值为 power[i] 的咒语时,他们就 不能 使用伤害为 power[i] - 2 ,power[i] - 1 ,power[i] + 1 或者 power[i] + 2 的咒语。
每个咒语最多只能被使用 一次 。
请你返回这个魔法师可以达到的伤害值之和的 最大值 。
示例 1:
输入:power = [1,1,3,4]
输出:6
解释:
可以使用咒语 0,1,3,伤害值分别为 1,1,4,总伤害值为 6 。
示例 2:
输入:power = [7,1,6,6]
输出:13
解释:
可以使用咒语 1,2,3,伤害值分别为 1,6,6,总伤害值为 13 。
代码(超内存,完成度:537/554)
class Solution {
public:
long long maximumTotalDamage(vector<int>& power) {
int count = 0;
unordered_map<int, int> group;
for(int num : power){
group[num]++;
count = max(count, num);
}
vector<pair<int,int>> freq(count + 1);
for (const auto& entry : group) {
freq[entry.first] = {entry.first, entry.second};
}
if (freq.size() == 0) return 0;
if (freq.size() == 1) return freq[0].first * freq[0].second;
if (freq.size() == 2) return max(freq[0].first * freq[0].second, freq[1].first * freq[1].second);
vector<int> dp(freq.size());
dp[0] = freq[0].first * freq[0].second;
dp[1] = max(dp[0], freq[1].first * freq[1].second);
dp[2] = max(dp[1],freq[2].first * freq[2].second);
for(int i = 3; i < freq.size();i++){
dp[i] = max(dp[i-1], dp[i-3] + freq[i].first * freq[i].second);
}
return dp[freq.size() - 1];
}
};
在做这一题的时候,很像打家劫舍的问题,一开始的想法是构造一个初始化为0的数组group,然后列出状态转移方程max(dp[i-1], dp[i-3] + freq[i].first * freq[i].second)
来解决问题,这个思路本身没有错,但是在提交的时候发现超出内存限制,原因是构造的freq有许多空间没有用到(都是0),所以需要用其他方法来避免对这些多余空间的占用。
记忆化搜索
class Solution {
public:
long long maximumTotalDamage(vector<int>& power) {
unordered_map<int, int> group;
for(int num : power){
group[num]++;
}
vector<pair<int, int>> a(group.begin(), group.end());
sort(a.begin(), a.end(), [](const auto& a, const auto& b) {
return a.first < b.first;
});
int n = a.size();
vector<long long> memo(n, -1);
auto dfs = [&](auto&& dfs, int i) -> long long{
if(i < 0){
return 0;
}
long long& res = memo[i];
if(res != -1){
return res;
}
int j = i;
auto& [x,y] = a[i];
while(j && a[j-1].first >= x-2){
j--;
}
return res = max(dfs(dfs,i-1), dfs(dfs,j-1) + (long long) x * y);
};
return dfs(dfs, n-1);
}
};
记忆化搜索运用了递归的思想,也运用了动态规划的思想,自上而下进行计算。
第一个要点是auto dfs = [&](auto&& dfs, int i) -> long long
中的&为什么要这样放。
捕获列表中的&:
& 表示按引用捕获外部作用域中的变量。这允许lambda函数在外部作用域中修改这些变量。
在这个上下文中,&捕获列表确保lambda函数能够访问和修改所有在其外部定义的变量,例如memo和a。
参数列表中的auto&& dfs:
这里的auto&& dfs是一个递归lambda函数的参数,表示该lambda函数自身的引用。
auto&&是一种通用引用(也叫转发引用),它可以绑定到任何类型(左值或右值引用)。
auto&& dfs允许我们定义一个递归lambda函数,因为在递归调用中,我们需要一个对自身的引用。
第二个是记忆话搜索可以用递归树来表示,如图:
在示例1中:power = [1,1,3,4]
,答案输出为6。
return dfs(dfs, n-1);
上面这一行代码返回的是{4,1},也就是最顶层的根节点。
auto dfs = [&](auto&& dfs, int i) -> long long{
if(i < 0){
return 0;
}
long long& res = memo[i];
if(res != -1){
return res;
}
int j = i;
auto& [x,y] = a[i];
while(j && a[j-1].first >= x-2){
j--;
}
return res = max(dfs(dfs,i-1), dfs(dfs,j-1) + (long long) x * y);
};
memo的作用
在dfs这个lambda函数内部,此时传进来的参数 i = 2,这时候首先要做的事情就是,将res指向memo[i], long long& res = memo[i];
。设立memo这个数组的目的就是,储存在第 i 个元素之前的时候,可以最多造成的伤害。当memo[i]中的值被更改的时候,由于&res是对memo[i]的引用,也就是说,memo[i]被更改也就是res被更改,这时候就直接返回res。
if(res != -1){
return res;
}
直接返回res有什么意义呢?原因是为了避免重复运算,在后面会提到。
接着auto& [x,y] = a[i];
代表[x,y]是对a[i]的引用,[x,y]指向{4,1},x=4,y=1。这时候设立一个整型 j,他的目的是如果这时候选择了伤害为4的咒语,那么他就会自动搜索刚好满足a[j-1].first >= x-2
这个伤害的咒语,然后再进行j--
,j - 1 一定会满足小于x-2,最后在return中返回 j - 1。
return res = max(dfs(dfs,i-1), dfs(dfs,j-1) + (long long) x * y)
最后返回两种情况(选择伤害为4的咒语或不选择)中的最大值,在递归树中,也就是所具有最大数值的分支。dfs(dfs,i-1)
代表不选,指向{3,1},dfs(dfs,j-1) + (long long) x * y
代表选,指向{1,2}。那么如何判断每个分支的数值呢?
接下来请看上面的递归树进行理解
在实际递归的运算过程中,采用的是深度优先遍历,也就是计算{4,1}的res需要第二层的{3,1}和{1,2}都返回给它一个值,他会先看{3,1}有没有返回给它一个值,发现{3,1}没有返回给它一个值,他就会计算{3,1}的两个子节点的返回值来计算{3,1}的值是多少,他就会先看第三层{1,2}有没有返回给他一个值,发现也没有,就会去计算第三层{1,2}的两个子节点的返回值来计算{1,2}的值,由于第三层的{1,2}是第一个元素了,所以他的两个子节点都只能返回它0。
这时候我们计算第三层{1,2}的值,他的值(也就是res)的计算要比较两种情况的最大值,也就是要选伤害为 1 的咒语还是不选择?如果不选择的话,他的左边的字节点会返回给它一个0,也就是代码中的dfs(dfs,i-1)
。如果选择的话呢,右边的字节点也会返回给它一个0,但是要注意的是,当选择的时候,{1,2}的res的计算需要加上它本身 1 * 2 = 2,为什么要加,因为它选择了。选择的情况对应代码dfs(dfs,j-1) + (long long) x * y
。这时候不选择的话,第三层{1,2}的res为0,选择的话,res为 0 + 1 * 2 = 2,取两种情况最大值2,所以第三层{1,2}的res也就是2,这时候要储存memo[0] = 2。
这时候往上去计算{3,1}的res,这时候{3,1}不选择伤害为3的咒语伤害的情况也就是{1,2}给他的返回值,同样对应代码dfs(dfs,i-1)
,这时候还差选择的情况,也就是需要计算{3,1}右边的子节点返回给它的值,右边的子节点返回给它的值是0,但是因为选择了伤害为3的咒语,所以所以{3,1}的res也就是0 + 3 = 3,取不选和选两种情况的最大值,也就是{3,1}的res为3,这时候储存memo[1] = 3。
接下来{4,1}的左子节点返回了他的res = 3,也就是{4,1}不选择情况的res是3,这时候可以开始计算选择伤害为4的咒语时候的res是多少,也就是要计算右子节点返回的值 + 4 * 1
。右子节点也就是第二层的{1,2},这时候就不需要像刚刚一样计算左右子节点返回值是多少了,因为{1,2}的res我们在前面已经算出等于2,储存在了memo[0]中。即使你再算一遍,{1,2}无论如何就是两种情况,计算方式和前面的完全相同。这时候也就是为什么要使用memo来储存res,这样可以避免多余的运算,可以返回上面看加粗的memo的作用这个标题,就能理解了。回到我们刚刚问题,不选择伤害为4咒语的情况返回的res是3,选择的情况就是右子节点返回的res = 2,然后还要加上4,所以选择的情况的res是6。然后比较选择和不选择两种情况的最大值,所以{4,1}的res = 6,储存memo[2] = 6。
这时候就计算出了施咒的最大伤害是6。