【LeetCode】算法技巧专题(持续更新)

持续记录更新

前缀和

目的 :用于快速求解数组或矩阵中连续子区间的和。它的核心思想是预处理,通过一次遍历提前计算出一个辅助数组(前缀和数组),从而将后续的每次区间和查询时间从 O (n) 降低到 O (1)

一维前缀和

定义一个一维数组 arr,其前缀和数组 preSum 的定义如下:

cpp 复制代码
preSum[0] = 0
preSum[i] = arr[0] + arr[1] + ... + arr[i-1] (即前 i 个元素的和)
preSum[i] = arr[i-1] + preSum[i-1]

利用前缀和数组,可以快速计算出原数组中从索引 l 到 r(包含 l 和 r)的子区间和:

cpp 复制代码
sum(l, r) = preSum[r+1] - preSum[l]

例子

bash 复制代码
假设 arr = [1, 2, 3, 4, 5]
前缀和数组 preSum 计算如下:
preSum[0] = 0
preSum[1] = 1
preSum[2] = 1+2 = 3
preSum[3] = 1+2+3 = 6
preSum[4] = 1+2+3+4 = 10
preSum[5] = 1+2+3+4+5 = 15
若要计算 arr[1] 到 arr[3] 的和(即 2+3+4):
sum(1, 3) = preSum[4] - preSum[1] = 10 - 1 = 9

二维前缀和

preSum[i][j] 表示矩阵中从 (0,0) 到 (i-1,j-1) 的子矩阵的和

cpp 复制代码
preSum[i][j] = matrix[i-1][j-1] + preSum[i-1][j] + preSum[i][j-1] - preSum[i-1][j-1](重叠)
当前单元格的值 + 上方子矩阵和 + 左方子矩阵和 - 重复计算的左上子矩阵和
cpp 复制代码
查询公式
若要计算从 (x1,y1) 到 (x2,y2) 的子矩阵和(包含边界):
sum = preSum[x2+1][y2+1] - preSum[x1][y2+1] - preSum[x2+1][y1] + preSum[x1][y1]
python 复制代码
def prefix_sum_2d(matrix):
    if not matrix or not matrix[0]:
        return []
    rows, cols = len(matrix), len(matrix[0])
    pre_sum = [[0]*(cols+1) for _ in range(rows+1)]
    
    for i in range(1, rows+1):
        row_sum = 0
        for j in range(1, cols+1):
            row_sum += matrix[i-1][j-1]
            pre_sum[i][j] = pre_sum[i-1][j] + row_sum
    return pre_sum

# 示例
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
pre_sum_2d = prefix_sum_2d(matrix)
print(pre_sum_2d)
# 输出:
# [
#   [0, 0, 0, 0],
#   [0, 1, 3, 6],
#   [0, 5, 12, 21],
#   [0, 12, 27, 45]
# ]

# 查询子矩阵 (1,1) 到 (2,2) 的和
x1, y1 = 1, 1
x2, y2 = 2, 2
print(pre_sum_2d[x2+1][y2+1] - pre_sum_2d[x1][y2+1] - pre_sum_2d[x2+1][y1] + pre_sum_2d[x1][y1])  # 输出: 28

前缀和优化的应用场景

  • 频繁查询区间和:当需要多次查询数组或矩阵中不同子区间的和时,前缀和优化能显著提升效率。
  • 滑动窗口问题:在滑动窗口类题目中,前缀和可用于快速计算窗口内元素的和
  • 动态规划:某些动态规划问题中,状态转移方程可能涉及区间和,前缀和可用于优化计算

例题1:统计稳定子数组的数目

1、分析题目,定义稳定子数组,即一个非递减区间的子数组 ,一个数组中,有很多块非递减区间的子数组,需要找找各个子数组是否符合非递减的特性

2、怎么统计非递减子数组的子集个数,1+2+3+...+N=N*(N+1)/2

3、判断左右区间覆盖的可能性:1、在一个非递减区间中;2、跨越一个或多个非递减区间

4、寻找规律并计算,这两种可能的计算出来的稳定子数组的个数

5、利用前缀和进行优化,否则超时

本题,统计左右区间覆盖中子数组的个数
情况一:[l,r]在一个非递减区间中

稳定子数组的计算公式为 N = R − L + 1 N=R-L+1 N=R−L+1, N ∗ ( N + 1 ) 2 \frac{N *(N+1)}{2} 2N∗(N+1)

关键是判断出这个稳定子数组中有多少个元素

情况二:[l,r]存在跨1个或多个非递减区间

前缀数组计算公式stable_arr[i] = nums[i]>=nums[i-1] ? stable_arr[i-1] + 1: 1;,计算从临界点开始重新计算存在的稳定子数组

不能直接计算前缀和,因为每次遇到分界点是会重新计算单个元素值,可以额外开一个数组计算前缀和presum_arr[i] = presum_arr[i-1] + stable_arr[i]

使用一个next数组,可以快速找到情况2的left的第一个分界点,不然在q循环中遍历去查找分界点,会超时

cpp 复制代码
class Solution {
public:
    vector<long long> countStableSubarrays(vector<int>& nums, vector<vector<int>>& queries) {
        int N = nums.size();
        vector<long long> ret_arr;

        
        vector<long long> stable_arr(N,0);
        vector<long long> presum_arr(N,0);
        vector<int> next(N,0);
        stable_arr[0]=1;
        presum_arr[0] = stable_arr[0];
        next[N-1]  = N-1;

        for(int i=1;i<nums.size();i++) {
            stable_arr[i] = nums[i]>=nums[i-1] ? stable_arr[i-1] + 1: 1;
            next[N-1-i] = nums[N-1-i]<=nums[N-i]? next[N-i]:N-1-i;
            presum_arr[i] = presum_arr[i-1] + stable_arr[i];
        }
        

        for(int i=0;i<queries.size();i++) {
            int left = queries[i][0];
            int right = queries[i][1];

            long long ret = 0;
            int j = min(next[left],right)+1;
            
            int m = j - left;
            ret += (long long) (m*0.5*(m+1));
            
            if(j<=right) {
                //cout<<presum_arr[right] - presum_arr[j]+1<<" "<<accumulate(stable_arr.begin()+j, stable_arr.begin()+right+1, 0)<<endl; 
                //用前缀和优化,直接相加会超时    
                ret += presum_arr[right] - presum_arr[j]+1;//accumulate(stable_arr.begin()+j, stable_arr.begin()+right+1, 0);
            }
            
            ret_arr.push_back(ret);

        }
        return ret_arr;

    }
};

例题2:连接非零数字并乘以其数字和 II

本题与例题2同样的类型,都是需要在区间中频繁的去查找运算,还是需要预处理+区间运算

分析问题,改如何进行预处理,利用前缀和

  1. 计算区间内有效字符的数字和
  2. 计算区间内有效字符的×10和

直接理解,就是找到区间内的有效字符,然后挨个×10+value并且计算和,但是频繁的区间计算,在q循环中使用遍历查找,必然超时。

有效字符的数字和可以使用前缀和,直接可以得出,关键是第二个,区间内有效字符的×10和

使用前缀,preMul[i]标识[0,i]中有效字符的数字乘积

如何处理[i,j]区间的数字乘积,想到的是preMul[j]-preMul[i-1]*1e(有效字符数)

本题出现了取MOD操作,需要,在每次运算时%MOD,尤其是1e(有效字符数),为了等式有效性,需要在每次×10都%MOD

cpp 复制代码
class Solution {
    const long long MOD = 1000000007;
public:
    
    vector<int> sumAndMultiply(string s, vector<vector<int>>& queries) {
        int N = s.size();
        vector<int> preSum(N+1,0);//统计区间中有效数的和
        vector<long long> preMul(N+1,0);//统计区间中有效数的乘积
        vector<int> preCnt(N+1,0);  //统计区间中存在几个有效字符
        vector<long long> pow10(N+1,0);
        pow10[0]=1;

        for(int i=0;i<N;i++) {
            preSum[i+1] = preSum[i] + (s[i]=='0'? 0:s[i]-'0');
            preCnt[i+1] = preCnt[i] + (s[i]=='0'? 0: 1);
            preMul[i+1] = (s[i]=='0'? preMul[i]:(long long)(preMul[i]*10 + s[i]-'0') % MOD);
            pow10[i+1] = (pow10[i] * 10) % MOD; 
            //cout<<preSum[i+1]<<"-"<<preCnt[i+1]<<"-"<<preMul[i+1]<<endl; 
        }


        vector<int> ret;
        //通过前缀和预处理,频繁查询计算区间结果
        for(int i=0;i<queries.size();i++) {
            int left = queries[i][0];
            int right = queries[i][1];
            

            int cnt = preCnt[right+1] - preCnt[left];
            
            long long x = (preMul[right+1] - preMul[left]*(pow10[cnt])) % MOD;
            if(x<0) {
                x += MOD;
            }

            int sum = preSum[right+1] - preSum[left];
            
            long long ans = ((long long)sum*x) % MOD;
            ret.push_back(ans);

        }
        return ret;
    }
};

回溯算法

子集型

只需要把握边界条件非边界条件

需要理清楚,递归前进的状态(i->i+1,这个i具体是指什么,理清楚递归的时间线),递归终止条件与非终止条件的处理

例题

cpp 复制代码
class Solution {
    vector<string> arr = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
public:

    void dfs(int i, int N,vector<string>& ans,string path,string& digits) {
        if(i==N) {  //终止条件
            ans.push_back(path);
            return;
        }
        //非终止条件,处理
        for(auto e:arr[digits[i]-'0']) {
            //增加新状态进入下一层递归/循环
            dfs(i+1,N,ans,path + e,digits);
            //恢复到原状态

        }
    }

    vector<string> letterCombinations(string digits) {
        vector<string> ans;
        string s;
        //递归的时间线i是指digits中的下标,挨个遍历,一层层向下去循环
        dfs(0,digits.size(),ans,s,digits);
        return ans;
    }
};


需要去构造哪些数可以选的判断条件,终止条件就是叶子节点

0-1选择递归

每一次递归,分别进行选择与不选的递归下一层,不选,就对答案不进行操作(充当一个跳过的作用),选择就append解

这个适合,每次新增状态都可以作为一个解进行记录,不适合需要全部遍历完才知道是否为解。

cpp 复制代码
class Solution {
public:

    void dfs(int i,int N,vector<int> path,vector<int>& nums,vector<vector<int>>& ans) {
        if(i==N) {
            ans.push_back(path);
            return ;
        }

        dfs(i+1,N,path,nums,ans);//不选,充当一个跳过的作用

        //选
        path.push_back(nums[i]);
        dfs(i+1,N,path,nums,ans);
        path.pop_back();

    }
    vector<vector<int>> subsets(vector<int>& nums) {
        int N=nums.size();
        vector<int> path;
        vector<vector<int>> ans;
        dfs(0,N,path,nums,ans);
        //ans.push_back(vector<int>());
        return ans;
    }
};
cpp 复制代码
class Solution {
public:
    bool ifPalindrome(string& s) {
        int left=0;
        int right = s.size()-1;
        while(left<right) {
            if(s[left]!=s[right]) return false;
            left++;
            right--;
        }
        return true;
    }
    vector<vector<string>> ret;

    void dfs(int i, vector<string> ans, string& s,int j){
        if(i==s.size()) {
            ret.push_back(ans);
            return ; 
        }
        if(j==s.size()) return;//处理右边界,必须要返回上一层(j==N-1)去选择
        
        //不选
        dfs(i,ans,s,j+1);

        string ts = s.substr(i,j-i+1);
        if(ifPalindrome(ts)){
            ans.push_back(ts);
            dfs(j+1,ans,s,j+1);
            ans.pop_back();
        }
      
    }
    vector<vector<string>> partition(string s) {
        vector<string> ans;
        dfs(0,ans,s,0);

        return ret;
    }
};

枚举递归

每一次必须选择一个,通过设置选择范围来进行每一层的筛选 ,每次选择都代表需要更新状态/解

这个适合,需要明确知道这一层的解是什么

c 复制代码
class Solution {
public:

    void dfs(int i,int N,vector<int> path,vector<int>& nums,vector<vector<int>>& ans) {
        if(i==N) {
            //ans.push_back(path);
            return ;
        }

        for(int j = i;j<N;j++) {
            path.push_back(nums[j]);
            ans.push_back(path);//每出现一个新状态都需要append一次
            dfs(j+1,N,path,nums,ans);
            path.pop_back();

        }
    }
    vector<vector<int>> subsets(vector<int>& nums) {
        int N=nums.size();
        vector<int> path;
        vector<vector<int>> ans;
        dfs(0,N,path,nums,ans);
        ans.push_back(vector<int>());
        return ans;
    }
};
cpp 复制代码
class Solution {
public:
    bool ifPalindrome(string& s) {
        int left=0;
        int right = s.size()-1;
        while(left<right) {
            if(s[left]!=s[right]) return false;
            left++;
            right--;
        }
        return true;
    }
    vector<vector<string>> ret;

    void dfs(int i, vector<string> ans, string& s){
        if(i==s.size()) {
            ret.push_back(ans);
            return ;
        }

        for(int j=i;j<s.size();j++) {
            string ts = s.substr(i,j-i+1);
            if(ifPalindrome(ts)){
                ans.push_back(ts);
                dfs(j+1,ans,s);
                ans.pop_back();
            }

        }
      
    }
    vector<vector<string>> partition(string s) {
        vector<string> ans;
        dfs(0,ans,s);

        return ret;
    }
};

组合型回溯

子集型回溯的一种剪枝

分析出剪枝条件,哪些条件可以返回,不用往下计算了

例题:组合

cpp 复制代码
class Solution {
public:
    vector<vector<int>> ans;
    vector<int> path;
    int sum=0;

    void dfs(int i,int n,int k,int d) {
        if(sum > n||path.size()>k) return;  //超过了这个范围就返回
        if(path.size()==k && sum==n) {
            ans.push_back(path);
            return ;
        }
        for(int j=i;j<=8;j++) {
            if(9-j<d||n-sum<j) return; //剪枝,还剩下的几个数,剩下数的和是否有可能满足
            path.push_back(j+1);
            sum += j+1;
            dfs(j+1,n,k,d-1);
            path.pop_back();
            sum-=j+1;
        }
        

    }


    vector<vector<int>> combinationSum3(int k, int n) {
        dfs(0,n,k,k);
        return ans;
    }
};

例题:扩号生产

使用0-1选择的回溯模板,变成选'("与')',设置好剪枝条件

  1. count不会小于0,小于0说明当前括号不满足条件,不满足直接剪枝
  2. path长度为2N时判断是否满足
cpp 复制代码
class Solution {
public:
    vector<string> ans;
    

    void dfs(int i,int count,int N,string path) {
        
        /*
            当i=0是一个空的
            i=1时,才有第一个字符
            当1=2*N时,path才刚好符合条件,会先去判断,才会考虑增加,不用担心,只需要确定好边界条件
        */
        
        if(count<0) return;
        if(i==2*N) {
            if(count==0){
                ans.push_back(path);
            }
            return ;
        }
        //选(
        dfs(i+1,count+1,N,path+'(');
        //选)
        dfs(i+1,count-1,N,path+')');
    }





    vector<string> generateParenthesis(int n) {
        string path;
        dfs(0,0,n,path);
        return ans;
    }
};

排列型回溯

全排列问题

dfs(遍历层的idx,未被选择的数字的集合)->子问题:dfs(遍历层的idx+1,未被选择的数字的集合)

其时间复杂度是计算节点数*从根节点到叶子节点的长度= O ( N ∗ N ! ) O(N*N!) O(N∗N!)

N皇后问题

使用回溯法,利用递归来替代多重循环操作操作,转化为树模型进行求解。

  1. 先明确回溯的每一层,也就是i代表什么,可以设置为代表每一行的行号
  2. 明确求解的答案是列号,也就是说在递归时,行号是时间线,自动去递归迭代,然后去查找合法的列号,就能定位一个棋子的坐标
  3. 递归的边界条件,i==n,说明已经遍历了所有行,必须要剪枝,没有必要在进行递归了;合法解的条件,col_vec.size()==n,说明解是合法的
  4. 非边界条件,在[0,N-1]中进行遍历,如何这个列合法,就进行递归dfs(r+1),否者就跳过

N皇后判断一个列号是否合法:需要去遍历以及记录的位置,并挨个去判断是否满足y1-x1==y2-x2(正对角线) || x1+y1==x2+y2(反对角线) || x2==x2,只要符合,就说明这个列号不合法。时间复杂度为O(N)

通过一个函数去判断就比较简洁,如果使用一个set,每更新一个位置,就需要为下面的所有行都进行判断,判断每一行的哪些列号是不合法的,比较复杂。

也可以使用多个哈希表去分别记录,每个元素的x1+y1与x1-y1与x1,只需要通过三个哈希表的判断,就能把判断是否合法的时间复杂度 O ( N ) − > O ( 1 ) O(N)->O(1) O(N)−>O(1)

cpp 复制代码
class Solution {
public:
    vector<vector<int>> ans;
    vector<int> path;


    bool valid(int r,int c) {
        //if(path.size()==0) return true;
        //cout<<r<<","<<c<<endl;
        for(int i=0;i<path.size();i++) {
            //cout<<i<<","<<path[i]<<endl;
            if(path[i]-i==c-r || path[i]+i==c+r ||c == path[i]) return false;
        }
        
        return true;
    }
    
    void dfs(int r,int n) {
        //终止条件
        //可用列数为0 return
        // cout<<r<<"-"<<path.size()<<":";
        // for(auto e:path){
        //     cout<<e<<" ";
        // }
        // cout<<endl;
        if(r==n) {
            if(path.size()==n) {
                ans.push_back(path);
            }
            return;
        }
        
        for(int i=0;i<n;i++) {
            if(!valid(r,i)) continue;
            // cout<<r<<","<<i<<endl;
            // for(auto e:path){
            //     cout<<e<<" ";
            // }
            // cout<<"----"<<endl;
            path.push_back(i);
            dfs(r+1,n);
            path.pop_back();
            
        }

    }
    vector<vector<string>> solveNQueens(int n) {
        dfs(0,n);
        vector<vector<string>> ret;
        
        for(auto& path:ans) {
            vector<string> tmp;
            for(int i=0;i<path.size();i++) {
                string s(n,'.');
                s[path[i]]='Q';
                tmp.push_back(s);
            }
            ret.push_back(tmp);
        }
        return ret;
        
    }
};

动态规划

动态规划的思考

关于动态规划问题,最基础/重要的是确定状态状态转移方程

根据子集型回溯的0/1选择或选哪个的思路

这个问题从回溯的角度去思考

如果选择第一个房子,那么子问题变成了从n-2个房子中找最大的;

如果不选第一个房子,那么子问题变成了从n-1个房子中计算最大的;

cpp 复制代码
dfs(i) = max(dfs(i-1),dfs(i-2)+val[i])
v1-回溯递归
cpp 复制代码
class Solution {
public:
    int dfs(int i,vector<int>& nums) {
        if(i<0) return 0;
        return max(dfs(i-1,nums), dfs(i-2,nums)+nums[i]);

    }
    int rob(vector<int>& nums) {
        return dfs(nums.size()-1,nums);
    }
};

这个是递归回溯代码,但是会超时,因为每计算一个根节点,都要重复去递归计算地下的所有子节点,会非常耗时,使用一个容器,去记录,已经计算过的节点的值,就不需要重复去计算

v2-回溯+记忆化搜索

使用了一个哈希表去记录,并且会在去查找前进行判断哈希表中是否存在,时间复杂度为O(n)=状态个数*单个状态的计算时间

空间复杂度为O(N),自顶向下的递归需要开辟空间

cpp 复制代码
class Solution {
public:
    unordered_map<int,int> arr;
    int dfs(int i,vector<int>& nums) {
        if(i<0) return 0;
        int res;
        if(arr.find(i)!=arr.end()){
            res = arr[i];
        } else{
            res = max(arr.find(i-1)==arr.end()?dfs(i-1,nums):arr[i-1], (arr.find(i-2)==arr.end()?dfs(i-2,nums):arr[i-2]) + nums[i]);
            arr[i]=res;
        }
               
        return res;

    }
    int rob(vector<int>& nums) {
        return dfs(nums.size()-1,nums);
    }
};
  • 自顶向下:递归+记忆化搜索
  • 自底向上:递推,从最低节点向上计算

将回溯递归+记忆化搜索,改成递推计算,回溯中,使用栈进行保存计算结果,递推中,使用数组保存结果

cpp 复制代码
dfs(i) = max(dfs(i-1),dfs(i-2)+val[i])//使用栈保存结果
						||
arr[i] = max(arr[i-1],arr[i-2]+val[i])//使用数组保存结果

不过需要提前设置好,0-10-2的初始值,就像在递归中也通过判断条件设置了if(i<0) return 0;

所以创建一个N+2的数组,提前初始好0,遍历时(开始递归时)从i=[2,N+2)开始遍历

v3-自底向上递推
cpp 复制代码
class Solution {
public:
    int rob(vector<int>& nums) {
        int N = nums.size();
        vector<int> arr(N+2,0);//代表[0,i]个房子累计获得最大的金额
        for(int i=2;i<N+2;i++) {
            arr[i] = max(arr[i-1],arr[i-2]+nums[i-2]);
        }
        return *(arr.end()-1);
    }
};

但此时空间复杂度任然是O(N)因为使用了一个N的数组

不过可以通过公式分析得出,计算arr[i],只需要arr[i-1]与arr[i-2],i-2之前的所有元素都不需要被使用了,所以只需要递推式保存,上一个与上上一个元素的值即可。

v4-斐波那契数列

变成斐波那契数列了

cpp 复制代码
class Solution {
public:
    int rob(vector<int>& nums) {
        //斐波那契数列
        int N = nums.size();
        int f1=0;//前一个
        int f2=0;//前前一个
        int f3;
        for(int i=0;i<N;i++) {
           f3 = max(f1,f2+nums[i]);
           f2=f1;
           f1=f3;
        }
        return f3;
    }
};

背包问题

0-1背包

在确定状态时,也需要确定状态转移的时间线,即i->i+1代表着什么含义

在背包问题中,i代表第i个物品,选择哪些物品,就是代表,哪些物品选或不选(0-1选择)

当前问题:在容量为capacity时,n个物品中价值最大,problem(n,capacity)

这个问题中,状态变化就是,当前第i个物品,选还是不选,会引发状态发生改变

下一个子问题:第i个物品不选:problem(i-1,c)的最大价值;第i个物品选:problem(i-1,c-c[i])+v[i]的最大价值,

状态转移方程:problem(i,c) = max(problem(i-1,c),problem(i-1,c-c[i])+[i])

当然,必须要满足c>=0的情况

例题:目标和

一眼分析,可以使用一个0-1子集回溯算法,每次要么选+,要么选-。

构建递归公式dfs(i,target) 代表,i个数,构成目标target的方案总数

cpp 复制代码
dfs(i,target) = dfs(i-1,target+nums[i]) + dfs(i-1,target-nums[i])//-与+

边界条件,当i<0的情况就是说明已经遍历结束了,递归的范围是[0,N-1],如果递归回到target==0,就说明是一个合法方案,方案数+1

cpp 复制代码
class Solution {
public:

    int dfs(int i,vector<int>& nums,int target) {
        if(i<0) {
            if(target==0) return 1;
            return 0;
        }    
        return dfs(i-1,nums,target-nums[i]) + dfs(i-1,nums,target+nums[i]);
    }

    int findTargetSumWays(vector<int>& nums, int target) {
        return dfs(nums.size()-1,nums,target);
    }
};

也可以转换成0-1背包问题,使用动态规划

设数组中所有合法方案的正数和为p,则负数和为sum-p,有 t a r g e t = p − s u m + p − > p = t a r g e t + s u m 2 target=p-sum+p->p=\frac{target+sum}{2} target=p−sum+p−>p=2target+sum。

且p是一个正整数,必须要满足条件p>=0,并且p%2==0,如果不符合这个条件,就可以直接返回0

然后,问题就变成了,{arr1,arr2,...}数组中,那些数是组成p的,并且满足和为p

也就等效于一个0-1背包问题,恰好满足capacity,的方案数

cpp 复制代码
class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = accumulate(nums.begin(),nums.end(),0);
        int capacity = (sum - target);
        //cout<<capacity<<endl;
        if(capacity < 0 || capacity%2) return 0;
        capacity /=2;
        int N = nums.size();

        vector<vector<int>> arr(N + 1, vector<int>(capacity + 1, 0));
        //int arr[N+1][capacity+1] = {0}//arr[i][j]代表i个数字,组成capacity=j的方案数
        arr[0][0] = 1;

        for(int i=1;i<N+1;i++) {
            for(int j=0;j<capacity+1;j++) {
                if(j<nums[i-1])
                    arr[i][j] = arr[i-1][j];
                else
                    arr[i][j] = arr[i-1][j] + arr[i-1][j-nums[i-1]];
                
                //cout<<i<<","<<j<<"-"<<arr[i][j]<<endl;

            }
        }
        return arr[N][capacity];
        
    }
};

还可以进一步分析空间复杂度,

由于公式计算中,只需要考虑第i个数组与第i-1个数组,之前的数组是不会被使用的,只需要创建两个n大小的数组即可
滚动数组

进一步优化,只使用一个数组,使用倒序遍历,从后往前遍历target

怎么判断优化成一个数组后,是顺序遍历还是倒序遍历?

cpp 复制代码
class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = accumulate(nums.begin(),nums.end(),0);
        int capacity = (sum - target);
        //cout<<capacity<<endl;
        if(capacity < 0 || capacity%2) return 0;
        capacity /=2;
        int N = nums.size();

       
        //int arr[N+1][capacity+1] = {0}//arr[i][j]代表i个数字,组成capacity=j的方案数
        vector<int> arr(capacity + 1, 0);
        arr[0] = 1;

        for(int i=1;i<N+1;i++) {
            for(int j=capacity;j>=nums[i-1];j--) {//倒序,避免覆盖之前的值
                
                arr[j] = arr[j] + arr[j-nums[i-1]];
                
                //cout<<i<<","<<j<<"-"<<arr[i][j]<<endl;

            }
        }

        return arr[capacity];
        
    }
};

完全背包

例题:零钱兑换

典型的完全背包问题

使用层次选择构建状态转移方程
dfs(amount)代表,构成amount最小需要的硬币数

cpp 复制代码
dfs(amount) = min([dfs(coins[i]) for i in range(coins)])+1 

设置边界条件,如果amount==0,就说明已经是合法的构成了,不会进一步递归,返回0

如果amount<0,就说明这条路径的组成不合法,返回INTMAX,这样与min最小化进行区分

直接使用递归会超时,使用一个哈希表,进行记忆化搜索不会超时。

cpp 复制代码
class Solution {
public:
    unordered_map<int,int> umap;
    int dfs(vector<int>& coins,int amount) {
        
        if(amount<0) return INT_MAX;
        if(amount==0) return 0;

        int min_count = INT_MAX;
        for(int i=0;i<coins.size();i++) {

            min_count = min(umap.find(amount-coins[i])==umap.end()?dfs(coins,amount-coins[i]):umap[amount-coins[i]],min_count);
        }
        min_count = min_count==INT_MAX?INT_MAX:min_count+1;
        umap[amount] = min_count;
    
        return min_count;
    }

    int coinChange(vector<int>& coins, int amount) {
        int ans = dfs(coins,amount);
        return ans==INT_MAX?-1:ans;
    }
};

也可以转成0-1选择性

如果加入记忆化搜索,需要设置key={i,amount},记忆化搜索是为了缓存与快速查找之前递归已经计算的值,需要考虑不同环境下,必须要绑定i与amount,不然可能会互相覆盖。

cpp 复制代码
class Solution {
public:
    int dfs(int i,vector<int>& coins,int amount) {
        
        if(i==coins.size()) {
            if(amount==0) return 0;
            return INT_MAX;
        }

        if(amount<coins[i]) return dfs(i+1,coins,amount);//跳过当前节点

        int ans = dfs(i,coins,amount-coins[i]);
        ans = ans==INT_MAX?INT_MAX:ans+1;
        return min(dfs(i+1,coins,amount),ans);

    }

    int coinChange(vector<int>& coins, int amount) {
        int ans = dfs(0,coins,amount);
        return ans==INT_MAX?-1:ans;
    }
};
相关推荐
OJAC1111 小时前
2026高校毕业生1270万!但这些学生却被名企用高薪“提前预定”!
算法
Controller-Inversion1 小时前
岛屿问题(dfs典型问题求解)
java·算法·深度优先
小白程序员成长日记1 小时前
力扣每日一题 2025.11.28
算法·leetcode·职场和发展
Swift社区1 小时前
LeetCode 435 - 无重叠区间
算法·leetcode·职场和发展
sin_hielo1 小时前
leetcode 1018
算法·leetcode
大工mike1 小时前
代码随想录算法训练营第三十一天 | 1049. 最后一块石头的重量 II 494. 目标和 474.一和零
算法
import_random2 小时前
[机器学习]xgboost的2种使用方式
算法
橘颂TA2 小时前
【剑斩OFFER】算法的暴力美学——只出现一次的数字 ||
算法·leetcode·动态规划
想唱rap3 小时前
C++ map和set
linux·运维·服务器·开发语言·c++·算法