关于740号问题,写了回溯的算法,不太清楚这道题如何定义memo?

来源:11-2 更多经典面试问题

Llizzzc

2024-04-11

import java.util.ArrayList;
class Solution {
    private boolean[] visited;
    public int deleteAndEarn(int[] nums) {
        visited = new boolean[nums.length];
        int res = 0;
        for (int i = 0; i < nums.length; i ++) {
            res = Math.max(delete(nums, i), res);
        }
        return res;
    }

    // 获取删除e之后的最大价值
    public int delete(int[] nums, int index) {
        visited[index] = true;
        ArrayList<Integer> list = new ArrayList<>();
        for (int i = 0; i < nums.length; i ++) {
            if (!visited[i] &&
                    (nums[i] == nums[index] + 1 || nums[i] == nums[index] - 1)) {
                visited[i] = true;
                list.add(i);
            }
        }
        int res = 0;
        for (int i = 0; i < nums.length; i ++) {
            if (!visited[i]) {
                res = Math.max(res, delete(nums, i));
            }
        }
        visited[index] = false;
        for (int i : list) {
            if (visited[i] &&
                    (nums[i] == nums[index] + 1 || nums[i] == nums[index] - 1)) {
                visited[i] = false;
            }
        }
        return res + nums[index];
    } 
}
写回答

1回答

liuyubobobo

2024-04-18

你写的回溯算法是无法通过添加 memo 改成动态规划的。因为你的回溯算法的状态是有后效性的。即后续的状态和之前的状态是相关的。因为在每次操作 delete(nums, index) 的时候,下一次能删除哪个元素,和之前删除过哪个元素息息相关。


请仔细体会一下,你的这个问题确实是动态规划的难点,也是重点。对于动态规划来说,怎么定义状态至关重要。不是能写出回溯算法就能改成动态规划的。


这个问题可以怎么定义状态,请理解一下我的参考代码和对应的状态定义方式(由于我手头的系统写 C++ 方便,所以以下是 C++ 代码,你可以在理解的基础上,改成 Java 代码):


class Solution {

public:
    int deleteAndEarn(vector<int>& nums) {
        
        // 对 nums 的排序不影响最终结果。
        // 这里为什么要对 nums 排序?请将其作为一个思考题,在理解了下面的算法后,回过头回答这个问题。
        sort(nums.begin(), nums.end());
        
        vector<int> memo(nums.size(), -1);
        return dfs(nums, 0, memo);
    }
private:
    // dfs(nums, index) 的定义是,下面在 nums[index...nums.size()) 的范围里删除元素,
    // 可以得到的最大分值是多少
    int dfs(const vector<int>& nums, int index, vector<int>& memo){
        
        // 如果 index >= nums.size(),一个元素都删不了,分值为 0
        if(index >= nums.size())
            return 0;
            
        // 记忆化搜索。如果 memo[index] 不为 -1,
        // 则之前计算过 nums[index...nums.size()) 的范围里删除元素可以得到的最大分值
        // 直接返回
        if(memo[index] != -1)
            return memo[index];
        
        // 索引 i1 指向比 nums[index] 大的下一个元素的索引
        // 如果 nums[index...nums.size()) 为 2, 2, 2, 3, 3, 5, 6
        // 则 i1 指向第一个 3 的索引
        int i1;
        for(i1 = index; i1 < nums.size() && nums[i1] == nums[index]; i1 ++);
        
        // 索引 i2 指向比 nums[index] + 1 大的下一个元素的索引
        // 如果 nums[index...nums.size()) 为 2, 2, 2, 3, 3, 5, 6
        // 则 i2 指向第一个 5 的索引
        // 如果 nums[index...nums.size()) 为 2, 2, 2, 4, 4, 5, 6
        // 则 i2 指向第一个 4 的索引,此时 i2 和 i1 一致
        int i2;
        for(i2 = i1; i2 < nums.size() && nums[i2] == nums[index] + 1; i2 ++);
        
        
        // 面对当前 在 nums[index...nums.size()) 的范围里删除元素,有两个选择
        
        // 选择 1,选择删除 nums[index] 以及所有和 nums[index] 相同的元素。
        // 整个数组中一共有 (i1 - index) 个 nums[index] 这么大的元素,分值加上 nums[index] * (i1 - index)
        // 之后,数组中 nums[i1...i2) 这个范围里的元素不能选了,因为他们比 nums[index] 大一
        // 之后,只要计算在 nums[i2...nums.size()) 的范围里删除元素可以得到的最大分值即可
        int res1 = nums[index] * (i1 - index) + dfs(nums, i2, memo);
        
        // 选择 2,不选择删除 nums[index]
        // 之后,只要计算在 nums[i1...nums.size()) 的范围里删除元素可以得到的最大分值即可
        int res2 = dfs(nums, i1, memo);
        return memo[index] = max(res1, res2);
    }
};

请仔细理解在上述逻辑的状态定义中,这个状态是无后效性的。也就是当我面对在 nums[index....nums.size()) 这个子数组中删除元素,找最大分值的时候,是和在 nums[0...index) 这个区间里怎么删除元素,完全无关的。


继续加油!:)

0
3
Llizzzc
回复
liuyubobobo
回复 liuyubobobo:再次感谢老师的耐心解答!!!
2024-04-29
共3条回复

玩转算法面试-- Leetcode真题分门别类讲解

课程配套大量BAT面试真题,高频算法题解析,强化训练

7410 学习 · 1150 问题

查看课程