【记忆化搜索】【超详细】力扣3186. 施咒的最大总伤害

一个魔法师有许多不同的咒语。

给你一个数组 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。

  • 7
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值