数据结构与算法总结6(个人原创,带详细注释代码)

2246. 相邻字符不同的最长路径

给你一棵 (即一个连通、无向、无环图),根节点是节点 0 ,这棵树由编号从 0n - 1n 个节点组成。用下标从 0 开始、长度为 n 的数组 parent 来表示这棵树,其中 parent[i] 是节点 i 的父节点,由于节点 0 是根节点,所以 parent[0] == -1

另给你一个字符串 s ,长度也是 n ,其中 s[i] 表示分配给节点 i 的字符。

请你找出路径上任意一对相邻节点都没有分配到相同字符的 最长路径 ,并返回该路径的长度。

示例 1:

输入:parent = [-1,0,0,1,1,2], s = "abacbe"
输出:3
解释:任意一对相邻节点字符都不同的最长路径是:0 -> 1 -> 3 。该路径的长度是 3 ,所以返回 3 。
可以证明不存在满足上述条件且比 3 更长的路径。 

示例 2:

输入:parent = [-1,0,0,0], s = "aabc"
输出:3
解释:任意一对相邻节点字符都不同的最长路径是:2 -> 0 -> 3 。该路径的长度为 3 ,所以返回 3 。
递归/树dp

注意到本题求的是路径上的个数,那我们求路径长度,也就是边的个数,最后+1即可

首先题目参数并没有给链表二叉树,而是以父亲节点数组形式给出,因此需要我们手动建树

  • 选择二维数组建树,第一维为节点序号,第二维为其子节点序号的列表(数组)
  • 这样我们就可以方便地对每个节点的子节点进行枚举(第一维上遍历所有的序号,对每个序号遍历其子节点列表)

手动建树并找到遍历所有节点方法后,我们可将其与前两题联系起来

前两题是二叉树,只需求出左右子节点的最大链长,并用 当前节点数据 处理以下两个操作

  • 更新全局最大链长(左右两子树通过当前节点连起来作为全局最大链长树)
  • 将当前节点树的最大链长转发给父节点(子树最大链长加上当前节点数据)

而本题为N叉树,因此二叉树的求左右子树变为 对所有子树遍历,因此以上两个步骤在N叉树下变形为

  • 用所有子树的**「前二长」**加上当前节点来更新全局最大链长

    • 由于所有子树都要枚举一遍,我们只需在每个节点处理逻辑内部维护一个变量currentmaxlen:当前节点已访问子树最大链长

    • 那么在枚举每个子树的过程中,即可将 当次枚举子树的链长 加上 已访问子树最大链长 作为 可能的更新全局最大链长 操作

    • ans = max(ans,currentmaxlen+len+1);//len为枚举到的每个子树的链长,1为子树到该节点的链长
      
    • 因为对所有子树枚举一遍,维护一个currentmaxlen可以保证一定能取到「前二长」的子树链长,不用维护两个变量来表示「前二长」

  • 这样枚举完所有子树后,currentmaxlen就变成了所有子树的最大链长,+1作为当前节点树的最大链长转发给父节点即可

  • 对枚举的每个子树链长len,先更新全局ans,再更新当前节点最大链长currentmaxlen,反之则会导致在第二步更新全局ans时,currentmaxlen和len是同一个子树的链长,相当于从最长的子树绕回去,没取到第二长子树的链长

  • 因此以上的关键点在于,由于二叉树——>N叉树,因此

    • 求左右子树链长——>枚举所有子树链长
    • 并枚举过程中维护一个子树最大链长,过程中实时与可能的第二链长更新全局ans,并在枚举完以后作为最大链长返回给父节点

考虑用树形 DP 求直径。枚举子树 x 的所有子树 y,维护从 x 出发的最长路径 currentmaxLen,那么可以更新答案为从 y 出发的最长路径加上 currentmaxLen,再加上 1(边 x——y),即合并从 x 出发的两条路径。递归结束时返回 currentmaxLen。

对于本题的限制,我们可以在从子树 y 转移过来时,仅考虑从满足 **s[y]≠s[x]**的子树 y 转移过来,所以对上述做法加个 if 判断就行了。

由于本题求的是点的个数,所以答案为最长路径的长度加一

注意:
int len = dfs(matrixtree,i,ans,s);

这句进入子树递归并返回子树链长的操作,一定要在if判断进行

否则会导致与父节点字符相同的子树不进入递归失去该子树中更新全局ans的机会

class Solution {
public:
    int dfs(vector<vector<int>> &matrixtree,int node,int &ans,string &s){
        int currentmaxlen = 0;//在每个节点处理逻辑内部维护一个变量currentmaxlen:当前节点 已访问子树最大链长
        for(int i:matrixtree[node]){
            int len = dfs(matrixtree,i,ans,s)+1;//「这句一定不要放到if判断里面,因为无论子树是否因判断条件而被采纳,都应该对子树进入递归更新全局ans」
            if(s[i]!=s[node]){//只有子树满足判断条件,才采纳该子树的深度来更新当前节点的最大长度以及全局ans
                //int len = dfs(matrixtree,i,ans,s); 第一次的错误写法
                ans = max(ans,currentmaxlen+len);//先更新全局ans,再更新当前节点最大链长currentmaxlen
                currentmaxlen = max(currentmaxlen,len);
            }
        }
        return currentmaxlen;
    }
    
    int longestPath(vector<int>& parent, string s) {
        int n = parent.size();
        int ans = 0;
        vector<vector<int>> matrixtree(n);//「用二维数组主动建树」
        for(int i=1;i<n;i++) 
            matrixtree[parent[i]].emplace_back(i);
        dfs(matrixtree,0,ans,s);
        return ans+1;//答案为最长路径的长度加一
    }
};
687. 最长同值路径

示例 1:

输入:root = [5,4,5,1,1,5]
输出:2

示例 2:

输入:root = [1,4,5,4,4,5]
输出:2
递归/树dp
class Solution {
public:
    int dfs(TreeNode* root,int &ans){
        int left=0,right=0;
        if(root->left){
            left = dfs(root->left,ans);//无论左子树数值是否满足题目采纳条件,都要进其递归
            if(root->left->val==root->val) left+=1;
            else left = 0;
        }
        if(root->right){
            right = dfs(root->right,ans);//无论右子树数值是否满足题目采纳条件,都要进其递归
            if(root->right->val==root->val) right+=1;
            else right = 0;
        }
        ans = max(ans,left+right);//考虑在该点折成链的情况
        
        //这里贪心,如果左树不满足条件,则left=0,否则为子树链长+1(子树到当前节点的边长)的更大值
        //也就是只往上传递当前树的链长最大值,如果左右子树都不满足条件,就断链,子树的链长最大值已经在其递归内算过了
        return max(left,right);
    }
    
    int longestUnivaluePath(TreeNode* root) {
        if(!root) return 0;
        int ans = 0;
        dfs(root,ans);
        return ans;
    }
};
337.打家劫舍III

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root

除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。

输入: root = [3,2,3,null,3,null,1]
输出: 7 
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7
状态dp

%5Cleetcode%E5%88%B7%E9%A2%982.assets%5Cimage-20240317001233593.png&pos_id=img-Mat33524-1714418333012)

注意:不选当前节点时,左右儿子可选可不选,讨论左右儿子传入的最大值即可

class Solution {
public:
    pair<int,int> dfs(TreeNode* root){
        if(!root) return make_pair(0,0);
        else{
            auto [l0,l1] = dfs(root->left);
            auto [r0,r1] = dfs(root->right);
            int choose = root->val+l0+r0;
            int notchoose = max(l1,l0)+max(r1,r0);//不选当前节点时,左右儿子可选可不选,讨论左右儿子传入的最大值即可
            return make_pair(notchoose,choose);
        }
    }
    int rob(TreeNode* root) {
        auto [a0,a1]=  dfs(root);//结构化绑定
        return max(a0,a1);
    }
};
556.下一个更大元素 III

给你一个正整数 n ,请你找出符合条件的最小整数,其由重新排列 n 中存在的每位数字组成,并且其值大于 n 。如果不存在这样的正整数,则返回 -1

注意 ,返回的整数应当是一个 32 位整数 ,如果存在满足题意的答案,但不是 32 位整数 ,同样返回 -1

示例 1:

输入:n = 12
输出:21

示例 2:

输入:n = 21
输出:-1

提示:

  • 1 <= n <= 2^31 - 1
  1. 从右往左遍历 找到第一个不是顺序排列的数字,而不是第一个比最右的数小的数
  2. 找到了这个数i之后再最后的数开始往右遍历,找到第一个比i大的数j与i交换,然后再对i后面的所有数重新排序

eg: 2302431 -> 2303124

从右往左遍历先找到第一个比右边小的数字2

然后找到从右往左找到第一个比2大的数字3,交换这两个数字,然后421重新排序为124 最后得到2303124

stl坑点
  • 字符串和数字的相互转换
    • to_string(int)
    • stoi(string)/stol(string)
  • 字符串内元素交换
    • swap(str[i],str[j])
  • 字符串内,没有具体位置的迭代器,有下标,如何反转任意子串
    • reverse(str.begin()+i,str.end())
    • 直接用头尾迭代器加减下标,获得对应位置迭代器
    • reverse(iter1,iter2)函数传入两个迭代器
  • 如何判断字符串转数字后是否超出int范围
    • 先转成long类型,然后和宏INT_MAX,INT_MIN比较
class Solution {
public:
    int nextGreaterElement(int n) {
        string str = to_string(n);//数字转字符串
        int i = str.length()-2;
        while(i>=0&&str[i]>=str[i+1]) i--;
        if(i<0) return -1;
        int j = i+1;
        while(j<str.length()&&str[j]>str[i]) j++;
        swap(str[i++],str[j-1]);//字符串内元素交换
        //while(i<j)
            //swap(str[i++],str[j--]);
        reverse(str.begin()+i,str.end());//直接用头尾迭代器加减下标,获得对应位置迭代器,reverse函数传入两个迭代器
        //i = stoi(str);
        long ans = stol(str);//字符串转数字
        return ans>INT_MAX?-1:(int)ans;//如何判断字符串转数字后是否超出int范围
    }
};
1410. HTML 实体解析器

「HTML 实体解析器」 是一种特殊的解析器,它将 HTML 代码作为输入,并用字符本身替换掉所有这些特殊的字符实体。

HTML 里这些特殊字符和它们对应的字符实体包括:

  • **双引号:**字符实体为 " ,对应的字符是 "
  • **单引号:**字符实体为 ' ,对应的字符是 '
  • **与符号:**字符实体为 & ,对应对的字符是 &
  • **大于号:**字符实体为 > ,对应的字符是 >
  • **小于号:**字符实体为 < ,对应的字符是 <
  • **斜线号:**字符实体为 ,对应的字符是 /

给你输入字符串 text ,请你实现一个 HTML 实体解析器,返回解析器解析后的结果。

示例 1:

输入:text = "&amp; is an HTML entity but &ambassador; is not."
输出:"& is an HTML entity but &ambassador; is not."
解释:解析器把字符实体 &amp; 用 & 替换

示例 2:

输入:text = "and I quote: &quot;...&quot;"
输出:"and I quote: \"...\""
字符串

C++ string的用法和例子

  • replace(int initpos, int substrlen, string replacestr)
    • 其中第三个参数replacestr必须是string类型的,不能是char
    • 所以要么在map处把value换成string类型,其中 " 符号需要转义字符 \,因此为 {"&quot;", "\""}
    • 要么map处存char,但是下面replace改成text.replace(i,j,string(1,mp[str]));,注意怎么用单个char创建临时string
    • 并且char的map中,’ 符号也要转义字符\,因此变成{"&apos;", '\''}
  • substr(int initpos, int substrlen)
  • map的列表初始化 map<…> mp = {{},{},{},…};
class Solution {
public:
    unordered_map<string,string> mp = {
        {"&quot;", "\""},
        {"&apos;", "'"},
        {"&amp;", "&"},
        {"&gt;", ">"},
        {"&lt;", "<"},
        {"&frasl;", "/"}};
    
    string entityParser(string text) {
        for(int i=0;i<text.length();i++){
            if(text[i]=='&'){
                for(int j=4;j<8;j++){
                    if(mp.count(text.substr(i,j))){
                        string str = text.substr(i,j);
                        text.replace(i,j,mp[str]);
                        break;
                    }
                }
            }
        }
        return text;
    }
};
365. 水壶问题

有两个水壶,容量分别为 xy 升。水的供应是无限的。确定是否有可能使用这两个壶准确得到 target 升。

你可以:

  • 装满任意一个水壶
  • 清空任意一个水壶
  • 将水从一个水壶倒入另一个水壶,直到接水壶已满,或倒水壶已空。

示例 1:

输入: x = 3,y = 5,target = 4
输出: true
解释:
按照以下步骤操作,以达到总共 4 升水:
1. 装满 5 升的水壶(0, 5)。
2. 把 5 升的水壶倒进 3 升的水壶,留下 2 升(3, 2)。
3. 倒空 3 升的水壶(0, 2)。
4. 把 2 升水从 5 升的水壶转移到 3 升的水壶(2, 0)。
5. 再次加满 5 升的水壶(2, 5)。
6. 从 5 升的水壶向 3 升的水壶倒水直到 3 升的水壶倒满。5 升的水壶里留下了 4 升水(3, 4)。
7. 倒空 3 升的水壶。现在,5 升的水壶里正好有 4 升水(0, 4)。
裸dfs&自定义哈希

观察题目可知,在任意一个时刻,此问题的状态可以由两个数字决定:X 壶中的水量,以及 Y 壶中的水量。

在任意一个时刻,我们可以且仅可以采取以下几种操作:

把 X 壶的水灌进 Y 壶,直至灌满或倒空;
把 Y 壶的水灌进 X 壶,直至灌满或倒空;
把 X 壶灌满;
把 Y 壶灌满;
把 X 壶倒空;
把 Y 壶倒空。
因此,本题可以使用深度优先搜索来解决。搜索中的每一步以 remain_x, remain_y 作为状态,即表示 X 壶和 Y 壶中的水量。在每一步搜索时,我们会依次尝试所有的操作,递归地搜索下去。这可能会导致我们陷入无止境的递归,因此我们还需要使用一个哈希结合(HashSet)存储所有已经搜索过的 remain_x, remain_y 状态,保证每个状态至多只被搜索一次。

对于基本数据类型,C++已经内置了哈希函数,但是对于自定义类型或者pair等复合类型,需要自己实现哈希函数

自定义哈希函数 unordered_set/map自定义哈希函数

需要特别注意Pairhash::operator() 中的两个const,丢掉任何一个都可能会使你的代码编译错误。

//注意对Pair自定义哈希的写法:结构体struct中重载运算符(),返回值为size_t,括号为{},重载运算符类型为const,哈希写法为hash<int>()(int...)
struct Pairhash{
    //特别注意两个const,丢掉任何一个都可能会使你的代码编译错误。
    size_t operator() (const pair<int,int>& p) const{
        return hash<int>()(p.first)^ hash<int>()(p.second);//两个哈希值异或一下,得到二元哈希值
    }
};        
class Solution {
public:
    bool canMeasureWater(int x, int y, int target) {
        //把重载()运算符的结构体作为哈希表模板第二个参数即可
        unordered_set<pair<int,int>,Pairhash> visited;
        stack<pair<int,int>> stk;
        stk.emplace(0,0);
        while(!stk.empty()){
            //如果该pair状态已经被访问过了,跳过
            if(visited.count(stk.top())){
                stk.pop();
                continue;
            }
            auto [p1,p2] = stk.top();
            if(p1==target||p2==target||p1+p2 == target) return true;
            visited.emplace(stk.top());//标记已经访问
            stk.pop();
            //所有操作都push一遍
            stk.emplace(p1,0);
            stk.emplace(0,p2);
            stk.emplace(x,p2);
            stk.emplace(p1,y);
            stk.emplace(min(x,p1+p2),max(p1+p2-x,0));
            stk.emplace(max(p1+p2-y,0),min(y,p1+p2));
        }
        return false;
    }
};
90. 子集 II

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的 子集

解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

示例 1:

输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]

示例 2:

输入:nums = [0]
输出:[[],[0]]
回溯&去重

先排序,把所有同值元素 归拢到一起

然后在**「同一层递归的循环」**内,「不两次选取相同元素打头

这个if判断 「在同一层的同元素子集去重

为什么是同一层递归的循环内去重?要理解到每一层循环的意义:也就是构建答案子集的第几个数选什么,index表示从nums数组的哪里开始选这第几个数,比如第一层递归dfs(0)就表示构建答案的第一个数在nums[0]-nums[n-1]中选取,而往下递归第二层递归dfs(1)表示构建答案的第二个数在nums[1]到nums[n-1]中选取,因此去重有两个条件:

  • i!=index 使得该层选的第一个数肯定不会被忽略
    • 如果不加该判断,[1,1]的子集应该为{[],[1],[1,1]},但是实际输出为{[],[1]},因为答案第二个数选取的1和第一个数选取的1被去重了,实际上这种情况不应该发生的,答案不同位置的数是可以选取同样的数字的,而同一位置的数不能重复选取,因此传入的第一个index选取是必须的
  • nums[i]==nums[i-1] 使得同位置在该层后面选的相同的数会被忽略掉,也就是去重
class Solution {
public:
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        vector<vector<int>> ans;
        vector<int> test;
        function<void(int)> dfs = [&](int index){
            ans.push_back(test);//每轮递归都有新的子集加入
            for(int i=index;i<nums.size();i++){
                if(i!=index&&nums[i]==nums[i-1]) continue;//当前位置选取的数不重复就可以
                test.push_back(nums[i]);
                dfs(i+1);
                test.pop_back();
            }
        };
        sort(nums.begin(),nums.end());//排序将数组中相同元素弄到一起
        dfs(0);
        return ans;
    }
};
131. 分割回文串

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串。返回 s 所有可能的分割方案。

示例 1:

输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]

示例 2:

输入:s = "a"
输出:[["a"]]
回溯(从答案的视角枚举子串结束位置)

输入中循环+选或不选+递归 = 回溯

对于这题,选或不选设计为从当前开头划分字符串,划分到哪里

从divider开始划分,循环遍历每个结束点下标,结束点+1作新的下标进入下一轮递归作为新的divider

divider初始化为0

class Solution {
public:
    vector<vector<string>> ans;
    vector<string> test;
    bool istrue(string &t){
        int left = 0;
        int right = t.length()-1;
        while(left<right){
            if(t[left]!=t[right]) return false;
            left++,right--;
        }
        return true;
    }
    
    void dfs(int divider,string &s){
        if(divider==s.length()) 
            ans.push_back(test);
        for(int i=divider;i<s.length();i++){
            string t = s.substr(divider,i-divider+1);//遍历s选划分串结束点
            if(istrue(t)){
                test.push_back(t);
                dfs(i+1,s);//传入新的开始点 divider
                test.pop_back();
            }
        }
    }   
    vector<vector<string>> partition(string s) {
        dfs(0,s);
        return ans;
    }
};
输入的视角(逗号选或不选)

假设每对相邻字符之间有个逗号,那么就看每个逗号是选还是不选。

也可以理解成:是否要把 s[i] 当成分割出的子串的最后一个字符。

class Solution {
    bool isPalindrome(string &s, int left, int right) {
        while (left < right)
            if (s[left++] != s[right--])
                return false;
        return true;
    }

public:
    vector<vector<string>> partition(string s) {
        vector<vector<string>> ans;
        vector<string> path;
        int n = s.length();

        // start 表示当前这段回文子串的开始位置
        function<void(int, int)> dfs = [&](int i, int start) {
            if (i == n) {
                ans.emplace_back(path);
                return;
            }

            // 不选 i 和 i+1 之间的逗号(i=n-1 时一定要选)
            if (i < n - 1)
                dfs(i + 1, start);

            // 选 i 和 i+1 之间的逗号(把 s[i] 作为子串的最后一个字符)
            if (isPalindrome(s, start, i)) {
                path.push_back(s.substr(start, i - start + 1));
                dfs(i + 1, i + 1); // 下一个子串从 i+1 开始
                path.pop_back(); // 恢复现场
            }
        };
        dfs(0, 0);
        return ans;
    }
};
198. 打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1:

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:

输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
     偷窃到的最高金额 = 2 + 9 + 1 = 12 。
回溯

先看成一道回溯题,要把大问题变成规模更小的子问题,从第一个房子或者最后一个房子开始思考,因为他们的约束最少

比如考虑最后一个房子选还是不选

  • 如果选,那么问题就变成了前n-2个房子的问题
  • 如果不选,就变成前n-1个房子的问题

以上便是把大问题分解成子问题的步骤,那么这里的问题是什么?见下图

当我们枚举到**「第」** i i i个房子选还是不选的时候,就确定了递归参数中的 i i i

问题定义为求 d f s ( i ) dfs(i) dfs(i),就是**「前」 i i i个房子中得到的最大金额**

如果不选 i i i个房子,就变成从**「前」** i − 1 i-1 i1个房子中得到的最大金额

如果选,就变成从**「前」** i − 2 i-2 i2个房子中得到的最大金额

然后比较这个两个最大金额,取较大者即可

  • 这里特别注意:「前」 i i i个房子中得到的最大金额 是不管房子是如何挑选的,只看金额是否最大
  • 比如[2,1,1,2],那么 dfs为[2,2,3,4],注意dfs[1]也为2与dfs[0]相同,而不是1,因为前两个房子中只看金额最大,肯定是2,而不是说dfs[1]就要挑选第二个房子的金额
  • 或者这么想:
  • 小偷没有偷第i个房屋,那就意味着小偷偷了第i个房屋前面的那间房屋,「所以前i个房屋能够偷窃到的最高金额为dp[i-1]」
  • 由于小偷没有偷第i个房屋所以把它排除掉,「dp[i] (前i个房屋能够偷窃到的最高金额)就等于前i-1个房屋能够偷窃到的最高金额」
  • 也即dp[i] = dp[i-1]
  • 所以逻辑是这样的:当我们枚举到 i i i的时候,只有两种情况,偷还是不偷,将两种情况下所能偷到最多的钱列举出来
    • 不偷:那么 前i个房屋能够偷窃到的最高金额 就等于 前i-1个房屋能够偷窃到的最高金额
    • 偷:前i个房屋能够偷窃到的最高金额 就等于 前i-2个房屋能够偷窃到的最高金额+当前房屋的金额
  • 然后这两者取较大者作为 前i个房屋能够偷窃到的最高金额
  • 比如对[2,1,1,2]中第二个房子
    • 不偷:就是 前1间房子的最大金额 也就是nums[0]
    • 偷:「前0间房子的最大金额 0」 +当前房子金额nums[1]
    • 二者取最大值作为前2房子最大金额 max(nums[0],nums[1])

注意这里的**「前」:在定义DFS或者DP数组的含义时,他只能表示从「一些」元素中算出的结果,而不是从某个**元素算出的结果

边界条件为:

最终的答案即为 d p [ n − 1 ] dp[n−1] dp[n1],其中 n n n 是数组的长度。

或者灵茶山艾府哨兵边界条件,只要不可能的偷窃,收益为0 if(pos<0) return 0;

//回溯backtrace 时间复杂度为指数级别 超时
class Solution {
public:
    int recur(int pos,vector<int>& nums){
        //或者灵茶山艾府写法,只要不可能的偷窃,收益为0 if(pos<0) return 0;
        if(pos==0) return nums[0];//注意边界条件
        else if(pos==1) return max(nums[0],nums[1]);
        return max(recur(pos-1,nums),recur(pos-2,nums)+nums[pos]);
    }
    int rob(vector<int>& nums) {
        return recur(nums.size()-1,nums);
    }
};
记忆化搜索

回溯的搜索树中有重复搜索,因此第一次搜索时将结果存到cache/memo数组中,就可以实现记忆化

时间复杂度降低为 O ( N ) O(N) O(N)

递归搜索 + 保存计算结果 = 记忆化搜索

第二次计算相同结果时,直接返回cache数组中的记录

class Solution {
public:
    int recur(int pos,vector<int>& cache,vector<int>& nums){
        //或者灵茶山艾府写法,只要不可能的偷窃,收益为0 if(pos<0) return 0;
        if(pos==0) return nums[0];//注意边界条件
        else if(pos==1) return max(nums[0],nums[1]);
        if(cache[pos]==-1)//先判断这个值计算过没,计算过就直接返回,否则往下递归计算
            cache[pos] =  max(recur(pos-1,cache,nums),recur(pos-2,cache,nums)+nums[pos]);
        return cache[pos];
    }
    int rob(vector<int>& nums) {
        vector<int> cache(nums.size(),-1);//所有没计算过的值都设置为-1以区别
        return recur(nums.size()-1,cache,nums);
    }
};

自顶向下:递归 记忆化搜索

自底向上:递推

不用递归用循环

第一次的错误写法

cash[1] = nums[1];

这里一定要搞清楚问题的定义!!!

当位置为i时,dfs(i)为「前i个房子能达到的最大金额,不管房子怎么取」!!!

之前想当然把前2个房子能达到的最大金额设置成第二个房子的金额,相当于自己加入了房子的取法,如果nums[1]>nums[2]就错了

也就是大问题分解为子问题的过程中,「所有规模问题的定义和处理都是一样的」,包括初始值dfs[1]=nums[1],没有特例!!!

  • 所以逻辑是这样的:当我们枚举到 i i i的时候,只有两种情况,偷还是不偷,将两种情况下所能偷到最多的钱列举出来
    • 不偷:那么 前i个房屋能够偷窃到的最高金额 就等于 前i-1个房屋能够偷窃到的最高金额
    • 偷:前i个房屋能够偷窃到的最高金额 就等于 前i-2个房屋能够偷窃到的最高金额+当前房屋的金额
  • 然后这两者取较大者作为 前i个房屋能够偷窃到的最高金额
  • 比如对[2,1,1,2]中第二个房子
    • 不偷:就是 前1间房子的最大金额 也就是nums[0]
    • 偷:「前0间房子的最大金额 0」 +当前房子金额nums[1]
    • 二者取最大值作为前2房子最大金额 max(nums[0],nums[1])

边界条件

class Solution {
public:
    int rob(vector<int>& nums) {
        int n =nums.size();
        if(n==1) return nums[0];
        int cash[n];
        cash[0] = nums[0];
        cash[1] = max(nums[0],nums[1]);
        //第一次的错误写法
        //cash[1] = nums[1];
        //这里一定要搞清楚问题的定义!!!当位置为i时,dfs(i)为「前i个房子能达到的最大金额,不管房子怎么取」!!!
        //之前想当然把前2个房子能达到的最大金额设置成第二个房子的金额,相当于自己加入了房子的取法,如果nums[1]>nums[2]就错了
        //也就是大问题分解为子问题的过程中,所有规模的问题定义和处理都是一样的,包括初始值dfs[1]=nums[1],没有特例!!!
        for(int i=2;i<n;i++){
            cash[i] = max(cash[i-2]+nums[i],cash[i-1]);
        }
        return cash[n-1];
    }
};

哨兵初始化

class Solution {
public:
    int rob(vector<int>& nums) {
        int n =nums.size();
        if(n==1) return nums[0];
        int cash[n+2];//整体往后挪2栋房子
        cash[0] = 0;//-2栋房子抢的钱是0
        cash[1] = 0;//-1栋房子抢的钱是0
        
        for(int i=0;i<n;i++){//整体往后挪2栋房子
            cash[i+2] = max(cash[i]+nums[i],cash[i+1]);
        }
        return cash[n+1];
    }
};
空间优化 滚动数组

观察到计算dfs(i)时,只用到上一个状态dfs(i-1)和上上个状态dfs(i-2),再前面的状态都用不到,可以省去,也就不用整个数组存储所有状态,只需两个变量滚动记录上/上上个状态即可

优化:直接将滚动数组pre,post初始化为0,而不是初始化为nums[0],nums[1]

相当于nums[-2],nums[-1] = 0,这样就不用讨论nums元素个数的corner case了,for循环也可以直接从i=0而不是i=2开始

class Solution {
public:
    int rob(vector<int>& nums) {
        int n =nums.size();
        //if(n==1) return nums[0];
        //int pre=nums[0],post=max(nums[0],nums[1]);
        int pre=0,suf=0;//这里直接设两个0即可,相当于nums[-2],nums[-1] = 0,这样就不用讨论nums元素个数了
        //for(int i=2;i<n;i++){
        for(int i=0;i<n;i++){//因为设了-2,-1的状态,从0开始遍历,不用从2
            int cur = max(nums[i]+pre,suf);
            pre = suf;
            suf = cur;
        }
        return suf;
    }
};
152. 乘积最大子数组

给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积

示例 1:

输入: nums = [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。

示例 2:

输入: nums = [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
回溯
  • dfs(index) 定义为以 nums[i] 结尾的子数组的最大乘积

转移方程:dfs(index) = rollmax = max(rollmax*nums[index],nums[index])(dfs(index-1)可能为负数)

由于存在负数,那么会导致最大的变最小的,最小的变最大的。因此还需要维护当前最小值

rollmin = min(rollmin*nums[index],nums[index])

  • 当负数出现时则rollmin与rollmax进行交换再进行下一步计算
class Solution {
public:
    int maxProduct(vector<int>& nums) {
        int ans = -11;
        int rollmax = -11,rollmin = 11;
        for(int i =0;i<nums.size();i++){
            if(nums[i]<0) swap(rollmax,rollmin);
            rollmax = max(rollmax*nums[i],nums[i]);
            
        }
        function<pair<int,int>(int)> dfs = [&](int index){
            if(index<0) return pair(-11,11);
            auto [maxval,minval] = dfs(index-1);
            if(nums[index]<0) swap(maxval,minval);//当负数出现时则rollmin与rollmax进行交换再进行下一步计算
            maxval = max(maxval*nums[index],nums[index]);
            minval = min(minval*nums[index],nums[index]);
            ans = max(ans,maxval);
            return pair(maxval,minval);
        };
        dfs(nums.size()-1);
        return ans;
    }
};
空间优化递推
class Solution {
public:
    int maxProduct(vector<int>& nums) {
        int ans = -11;
        int rollmax = -11,rollmin = 11;
        for(int i =0;i<nums.size();i++){
            if(nums[i]<0) swap(rollmax,rollmin);//当负数出现时则rollmin与rollmax进行交换再进行下一步计算
            rollmax = max(rollmax*nums[i],nums[i]);
            rollmin = min(rollmin*nums[i],nums[i]);
            ans = max(rollmax,ans);
        }
        return ans;
    }
};
139. 单词拆分

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true

**注意:**不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例 1:

输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
     注意,你可以重复使用字典中的单词。
记忆化搜索

回溯「枚举选哪个」思路加一个记忆化数组

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        vector<int> cache(s.length(),-1);
        function<bool(int)> dfs = [&](int index)->bool{
            if(index==s.length()) return true;
            if(cache[index]==-1){
                cache[index] = 0;
                for(string a:wordDict){//枚举选哪个
                    if(a==s.substr(index,a.length())){
                        cache[index]|=dfs(index+a.length());
                        if(cache[index]) break;
                    }
                }
            }
            return (bool)cache[index];
        };
        return dfs(0);
    }
};
递推dp

状态定义:f[i] 表示 s中 [0,i-1]/[0,i) 的子串是否能拆分

转移方程:f[i] = f[j]&&st.find(substr(j,i-j)!=st.end()) 其中 0 < j <= i-1( < i )

  • 枚举[0,i-1]中的分割点j,分解为子问题f[j] -> [0,j-1]是否可被分割,以及当前问题的判断[j,i-1]串是否在字典中

边界条件:f[0] = true

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        unordered_set<string> st(wordDict.begin(),wordDict.end());
        vector<bool> f(s.length()+1,false);
        f[0] = true;
        for(int i= 1;i<f.size();i++){
            for(int j = 0;j<i;j++){//枚举[0,i-1]/[0,i)中的分割点j
                //分解为子问题f[j] -> [0,j-1]是否可被分割,以及当前问题的判断[j,i-1]串是否在字典中
                if(f[j]&&st.count(s.substr(j,i-j))){
                    f[i] = true;
                    break;
                }
            }
        }
        return f[s.length()];
    }
};
97. 交错字符串

给定三个字符串 s1s2s3,请你帮忙验证 s3 是否是由 s1s2 交错 组成的。

两个字符串 st 交错 的定义与过程如下,其中每个字符串都会被分割成若干 非空 子字符串

  • s = s1 + s2 + ... + sn
  • t = t1 + t2 + ... + tm
  • |n - m| <= 1
  • 交错s1 + t1 + s2 + t2 + s3 + t3 + ... 或者 t1 + s1 + t2 + s2 + t3 + s3 + ...

注意:a + b 意味着字符串 ab 连接。

示例 1:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
输出:true

示例 2:

输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"
输出:false
记忆化搜索

状态定义:dfs(i,j) 表示 s1的 「 i 个字符」与 s2 的「 j 个字符」能够组成 s3 的「 i+j 个字符」

  • 因此 i,j 的取值范围为 [0,s1.size()] 前闭后闭

将 s1、s2 和 s3 的长度分别记为 n、m 和 l。

一个显然的情况是若 n+m 不为 l,必然不能用 s1 和 s2 来凑成 s3,返回 false。

定义暴搜函数为 boolean dfs(int i, int j),代表当前处理到 s1 的第 i 个字符,s2 的第 j 个字符,能否凑成 s3 的前 i+j 个字符。

最终答案为 dfs(0, 0)。

根据 s1[i]、s2[j] 和 s3[i+j] 的关系分情况讨论:

  • s1[i]=s3[i+j],可使用 s1[i] 充当 s3[i+j] ,暴搜 s1[i] 已被使用的情况,决策下一位 dfs(i + 1, j)
  • s2[j]=s3[i+j],可使用 s2[j] 充当 s3[i+j] ,暴搜 s2[j] 已被使用的情况,决策下一位dfs(i, j + 1)
  • 当 i+j=l 时,代表我们成功用 s1 和 s2 凑成了 s3,返回 true;而若在任一回合出现 s1[i]≠s3[i+j] 且 s2[j]≠s3[i+j],说明构造无法进行,返回 false。
class Solution {
public:
    bool isInterleave(string s1, string s2, string s3) {
        if(s1.length()+s2.length()!=s3.length()) return false;
        vector<vector<int>> cache(s1.length()+1,vector<int>(s2.length()+1,-1));
        //dfs定义:当前处理到 s1 的第 i 个字符,s2 的第 j 个字符,能否凑成 s3 的前 i+j 个字符。
        function<bool(int,int)> dfs = [&](int i,int j){
            if(i+j==s3.length()) return true;
            if(cache[i][j]==-1){
                cache[i][j] = (i<s1.length()&&s1[i]==s3[i+j]&&dfs(i+1,j))||
                (j<s2.length()&&s2[j]==s3[i+j]&&dfs(i,j+1));
            }
            return (bool)cache[i][j];
        };
        return dfs(0,0);
    }
};
递推

同样注意f[i] [j] 的定义:s1「前」i个字符,s2「前」j个字符能否组成s3「前」i+j字符

因此边界条件f[0] [0] = true(空字符串和空字符串可以组成空字符串)

i,j 的 取值范围为 [0,s1.size()](前s1.size()个字符)

注意:转移方程中的具体字符下标需要 -1,因为 i 定义为「前」i 个字符,而转移方程中比较的是「第」i个字符,因此f[i] [j] 对应字符 s1[i-1], s2[j-1] 与 s3[i+j-1]是否相等

class Solution {
public:
    bool isInterleave(string s1, string s2, string s3) {
        if(s1.length()+s2.length()!=s3.length()) return false;
        vector<vector<bool>> f(s1.length()+1,vector<bool>(s2.length()+1));
        for(int i=0;i<=s1.length();i++){
            for(int j = 0;j<=s2.length();j++){
                f[i][j] = (i>0&&s1[i-1]==s3[i+j-1]&&f[i-1][j])||
                (j>0&&s2[j-1]==s3[i+j-1]&&f[i][j-1]);
                f[0][0] = true;
            }
        }
        return f.back().back();
    }
};
空间优化
class Solution {
public:
    bool isInterleave(string s1, string s2, string s3) {
        if(s1.length()+s2.length()!=s3.length()) return false;
        vector<bool> f(s2.length()+1);
        for(int i=0;i<=s1.length();i++){
            for(int j = 0;j<=s2.length();j++){
                f[j] = (i>0&&s1[i-1]==s3[i+j-1]&&f[j])||
                (j>0&&s2[j-1]==s3[i+j-1]&&f[j-1]);
                if(i==0) f[0] = true;//只有f[0][0]需要初始化为true
            }
        }
        return f.back();
    }
};
494. 目标和

给你一个非负整数数组 nums 和一个整数 target

向数组中的每个整数前添加 '+''-' ,然后串联起所有整数,可以构造一个 表达式

  • 例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1"

返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

示例 1:

输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
01背包/选或不选

设选取的整数和为positive,nums 所有元素不管符号的总和为 sum,则选取的负数的和绝对值为 sum-positive,则可列出表达式

target = positive - (sum-positive) -> 2positive = sum+target -> positive = (sum+target) / 2

问题就变成了,从所有元素中选取和为 (sum+target) / 2 的元素 的组合个数

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

传统的01背包是从前 i 个数中选取和为 c 的最大价值

因此状态定义:dfs(i,c) 为 从前 i 个数中选取组合使得和为 c 的最大价值

转移方程是

dfs(i,c) = max( dfs(i-1,c-weight[i]) + value[i](选当前下标为i的元素), dfs(i-1,c)(不选当前下标为i的元素))

但是这道题不是求组合最大价值,求的是组合个数,因此转移方程不同

回溯
  • 状态定义:dfs(i,c) 为 从「下标」前 i 个数中选取组合使得和为 c 的组合个数
  • 转移方程dfs(i,c) = dfs(i-1,c)(不选下标为i的数)+ dfs(i-1,c-nums[i])(选下标为i的数)
    • 转移方程诠释了「选或不选」
  • 初始状态定位为:从前-1个数中选取和为0的组合数为1,选取和为其他值的组合数为0
    • 不能定义为:从前0个数中选取和为nums[0]的组合数为1,因为nums[0]可选可不选
  • 注意记忆化搜索中,cache数组为二维数组,i取值为[0,nums.size()-1],c取值为[0,target]
  • 当前数大于要选取的和c,绝对不能选当前数字,此时 dfs(i,c) = dfs(i-1,c)(不选下标为i的数)
class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for(int num:nums){
            sum+=num;
        }
        target+=sum;
        if(target<0||target%2==1) return 0;//target+sum = 2positive,因此必为偶数且>0
        target/=2;

        //注意dfs(i,c)的定义:从前i个数(包含第i个数)中选取和为c的组合数量
        function<int(int,int)> dfs = [&](int i,int c){
            if(i<0) return c==0?1:0;//从前-1个数中选取和为0的组合数为1,选取和为其他值的组合数为0
            if(nums[i]>c){//当前数大于要选取的和c,绝对不能选当前数字
                return dfs(i-1,c);
            }
            return dfs(i-1,c)+dfs(i-1,c-nums[i]);
        };
        return dfs(nums.size()-1,target);
    }
};
记忆化搜索

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for(int num:nums){
            sum+=num;
        }
        target+=sum;
        if(target<0||target%2==1) return 0;//target+sum = 2positive,因此必为偶数且>0
        target/=2;

        vector<vector<int>> cache(nums.size(),vector<int>(target+1,-1));

        //注意dfs(i,c)的定义:从前i个数(包含第i个数)中选取和为c的组合数量
        function<int(int,int)> dfs = [&](int i,int c){
            if(i<0) return c==0?1:0;//从前-1个数中选取和为0的组合数为1,选取和为其他值的组合数为0
            if(cache[i][c]!=-1) return cache[i][c];
            if(nums[i]>c){//当前数大于要选取的和c,绝对不能选当前数字
                cache[i][c] = dfs(i-1,c);
                return cache[i][c];
            }
            cache[i][c] = dfs(i-1,c)+dfs(i-1,c-nums[i]);
            return cache[i][c];
        };
        return dfs(nums.size()-1,target);
    }
};
递推

记得有两个状态量,需要二维数组,二重循环,根据上面的分析初始化,

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for(int num:nums){
            sum+=num;
        }
        target+=sum;
        if(target<0||target%2==1) return 0;//target+sum = 2positive,因此必为偶数且>0
        target/=2;

        vector<vector<int>> dp(nums.size()+1,vector<int>(target+1,0));
        dp[0][0] = 1;
        for(int i = 1;i<dp.size();i++){
            for(int c=0;c<=target;c++){
                if(c<nums[i-1]) dp[i][c] = dp[i-1][c];//当前数大于要选取的和c,绝对不能选当前数字
                else dp[i][c] = dp[i-1][c]+dp[i-1][c-nums[i-1]];
            }
        }

        return dp.back()[target];
    }
};
空间优化

观察上面的状态转移方程,注意到 i 维度上每次更新的数值只依赖于 i - 1 的值,因此可以去掉这个维度,原地更新,c 需要倒序更新,防止被覆盖

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for(int num:nums){
            sum+=num;
        }
        target+=sum;
        if(target<0||target%2==1) return 0;//target+sum = 2positive,因此必为偶数且>0
        target/=2;

        vector<int> dp(target+1,0);
        dp[0] = 1;
        for(int i = 1;i<nums.size()+1;i++){
            for(int c=target;c>=0;c--){//倒序更新c
                if(c>=nums[i-1]) dp[c] +=dp[c-nums[i-1]];
            }
        }
        return dp[target];
    }
};
1235. 规划兼职工作

你打算利用空闲时间来做兼职工作赚些零花钱。

这里有 n 份兼职工作,每份工作预计从 startTime[i] 开始到 endTime[i] 结束,报酬为 profit[i]

给你一份兼职工作表,包含开始时间 startTime,结束时间 endTime 和预计报酬 profit 三个数组,请你计算并返回可以获得的最大报酬。

注意,时间上出现重叠的 2 份工作不能同时进行。

如果你选择的工作在时间 X 结束,那么你可以立刻进行在时间 X 开始的下一份工作。

示例 1:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

输入:startTime = [1,2,3,3], endTime = [3,4,5,6], profit = [50,10,40,70]
输出:120
解释:
我们选出第 1 份和第 4 份工作, 
时间范围是 [1-3]+[3-6],共获得报酬 120 = 50 + 70。
01背包&二分

将工作按照结束时间排序,以示例 2 为例,得到下图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

手动计算一下,按照结束时间排序后:

前 1 个工作的最大报酬为 20;
前 2 个工作的最大报酬为 20;
前 3 个工作的最大报酬为前 1 个工作的最大报酬 +70=20+70=90+70=20+70=90+70=20+70=90;
前 4 个工作的最大报酬为前 3 个工作的最大报酬 +60=90+60=150+60=90+60=150+60=90+60=150;
前 5 个工作的最大报酬,如果选了第 5 个工作,那么报酬为前 1 个工作的最大报酬 +100=20+100=120;但也可以不选第 5 个工作,报酬为前 4 个工作的最大报酬,即 150。由于 150>120,不选第 5 个工作更好,因此前 5 个工作的最大报酬为 150。
示例 2 等价于计算前 5 个工作的最大报酬,即 150。

总结一下,我们可以分类讨论,求出按照结束时间排序后的前 i 个工作的最大报酬:

  • 不选第 i 个工作,那么最大报酬等于前 i−1 个工作的最大报酬(转换成了一个规模更小的子问题);

  • 选第 i 个工作,由于工作时间不能重叠,设 j 是最大的满足 endTime[j]≤startTime[i] 的 j,那么最大报酬等于前 j 个工作的最大报酬加上 profit[i](同样转换成了一个规模更小的子问题);

这两种决策取最大值。

注:由于按照结束时间排序,前 j 个工作中的任意一个都不会与第 i 个工作的时间重叠。

定义 f[i] 表示按照结束时间排序后的前 i 个工作的最大报酬,分类讨论:

  • 不选第 i 个工作: f [ i ] = f [ i − 1 ] f[i]=f[i−1] f[i]=f[i1]
  • 选第 i 个工作: f [ i ] = f [ j ] + p r o f i t [ i ] f[i]=f[j]+profit[i] f[i]=f[j]+profit[i],其中 j 是最大的满足 e n d T i m e [ j ] ≤ s t a r t T i m e [ i ] endTime[j]≤startTime[i] endTime[j]startTime[i] 的 j,不存在时为 −1。
  • 两者取最大值,即

f [ i ] = m a x ( f [ i − 1 ] , f [ j ] + p r o f i t [ i ] ) f[i]=max(f[i−1],f[j]+profit[i]) f[i]=max(f[i1],f[j]+profit[i])

由于结束时间是有序的,j 可以用二分查找计算出来

class Solution {
public:
    int jobScheduling(vector<int>& startTime, vector<int>& endTime, vector<int>& profit) {
        vector<vector<int>> bind(startTime.size(),vector<int>(3));
        for(int i =0;i<bind.size();i++){
            bind[i] = {startTime[i],endTime[i],profit[i]};
        }
        //哈希创建三元组后,使用lambda表达式自定义排序sort三元组结束时间
        sort(bind.begin(),bind.end(),[](auto &a,auto &b){return a[1]<b[1];});
        auto upper_bound = [&](int left,int right,int target){
            while(left<=right){
                int mid = (right-left)/2+left;
                if(bind[mid][1]<=target) left = mid+1;//找第一个<=当前时间段开始时间的结束时间
                else right = mid-1;
            }
            return right;
        };
        //f多了一个哨兵头,使得二分查找返回-1时(即没有任何三元组的结束时间<=当前选中的开始时间)正常处理
        vector<int> f(bind.size()+1,0);
        for(int i = 1;i<f.size();i++){
            int j = upper_bound(0,i-1,bind[i-1][0]);
            f[i] = max(f[i-1],bind[i-1][2]+f[j+1]);//01背包/选或不选,「注意这里下标映射是j+1」
        }
        return f.back();
    }
};
322. 零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1

你可以认为每种硬币的数量是无限的。

示例 1:

输入:coins = [1, 2, 5], amount = 11
输出:3 
解释:11 = 5 + 5 + 1
完全背包

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • dfs(i,c) 为 从前 i 个硬币中选取组合使得和为 c 的最小硬币个数
  • 转移方程:因为可以重复选,因此dfs(i,c) = min(dfs(i-1,c)(不选当前下标i的硬币),dfs(i,c-weight[i])+1(选取下标为i的硬币后,还是从前i个硬币中选,体现了重复,硬币数量+1))
class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<vector<int>> cache(coins.size(),vector<int>(amount+1,-1));
        function<int(int,int)> dfs = [&](int i,int c)->int{
            if(i<0) return c==0?0:INT_MAX/2;//如果不合法,返回无穷大,这样后面min()就会取另一个值
            if(coins[i]>c) return dfs(i-1,c);//如果当前数大于要选的和,直接不选
            if(cache[i][c]==-1){
                cache[i][c] = min(dfs(i,c-coins[i])+1,dfs(i-1,c));
            }
            return cache[i][c];
        };
        int ans = dfs(coins.size()-1,amount);
        return ans>=INT_MAX/2?-1:ans;
    }
};
递推

我们给数组i维度多了一格来表示i = -1的边界情况,但是没有给c如此,因为

if(c<coins[i-1]) cache[i][c] = cache[i-1][c];//如果当前c小于元素值,绝对不能选

这个判断已经蕴含了c的边界条件了

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<vector<int>> cache(coins.size()+1,vector<int>(amount+1,INT_MAX/2));
        cache[0][0] = 0;
        for(int i=1;i<cache.size();i++){
            for(int c = 0;c<=amount;c++){
                if(c<coins[i-1]) cache[i][c] = cache[i-1][c];//如果当前c小于元素值,绝对不能选
                else cache[i][c] = min(cache[i-1][c],cache[i][c-coins[i-1]]+1);
            }
        }
        return cache.back()[amount]>=INT_MAX/2?-1:cache.back()[amount];
    }
};
空间优化

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这里第二重循环中的c需要正序循环而非逆序,因为如果选取下标为i的硬币,转移过来的式子是dfs(i,c-coins[i])

因此正序遍历

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<int> cache(amount+1,INT_MAX/2);
        cache[0] = 0;
        for(int i=0;i<coins.size();i++){
            for(int c =0;c<=amount;c++){
                if(c>=coins[i]) cache[c] = min(cache[c],cache[c-coins[i]]+1);//根据转移方程确定遍历顺序
            }
        }
        return cache[amount]<INT_MAX/2?cache[amount]:-1;
    }
};
  • 14
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值