LeetCode 热题 HOT 100 打卡计划 | 第二十八天 | 每日进步一点点

1,062 阅读7分钟

图片.png

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第29天,点击查看活动详情

416. 分割等和子集

思路

(动态规划) O(n * m)

给定一个只包含正整数的非空数组 nums[],判断是否可以将这个数组分割成两个子集,并且每个子集数字的和恰好等于整个数组的元素和的一半。

样例:

图片.png

如样例所示,nums = [1,5,11,5],数组可以分割成 [1, 5, 5][11],因此返回ture。从题意来看,这个问题可以转换成0-1背包问题,如何看出来的? 我们不妨将换种表述方式:

将大小为n的数组看成n件物品,数组元素和sum的一半看成一个容量为sum / 2的背包, 每件物品只能使用一次,每件物品的体积是nums[i],求解是否可以选出一些物品,使得这些物品的总体积恰好为背包的容量,因此可以使用动态规划求解,下面我们来讲解具体做法。

首先,如果sum 是奇数,则不可能将数组分割成元素和相等的两个子集,因此直接返回false。接下来我们去定义状态表示和推导状态转移方程。

状态表示: f[i][j]表示从前i个数中选若干个数,是否使得这些数字的和恰好等于j。因此f[i][j]有两种状态,true或者false

状态计算:

假定nums[]数组下标从1开始,如何确定f[i][j]的值?

一般去考虑最后一步,那么对于当前的数字 nums[i],可以选取也可以不选取:

  • 1、不选 nums[i],那么我们就从前i - 1个数中选,看是否使得这些数字的和恰好等于j,即 f[i][j] = f[i - 1][j]
  • 2、选择nums[i] ,在背包可以装下的情况下,那么相应的背包容量就要减去nums[i] ,f[i][j]的状态就可以从f[i - 1][j - nums[i]]转移过来,即f[i][j] = f[i - 1][j - nums[i]]

综上,两种情况只要有一个为true,那么f[i][j]就为true。因此状态转移方程为f[i][j] = f[i - 1][j] | f[i - 1][j - nums[i]]

初始化:

f[0][0] = true:在前0个数中,我们可以一个数都不去选,因此从前0个数中选,使得这些数字的和恰好等于0的状态为true,其余的状态都初始化为false

实现细节:

在推导状态转移方程时,我们假设的nums[]数组下标是从1开始的,而实际中的nums[]数组下标是从0开始的,因此在代码的编写过程中,我们需要将所有nums[i]的下标减去 1,与使用的语言保持一致。

时间复杂度分析: O(n * m),nnums数组的大小,m数组元素和的一半。

空间复杂度分析: O(n * m)

c++代码

 class Solution {
 public:
     bool canPartition(vector<int>& nums) {
         int n = nums.size(), sum = 0;
         for(int x : nums) sum += x;
         if(sum % 2) return false;
         int m = sum / 2;
         vector<vector<bool>> f(n + 1, vector<bool>(m + 1, false));
         f[0][0] = true;
         for(int i = 1; i <= n; i++){
             for(int j = 1; j <= m; j++){
                 if(j >= nums[i - 1]) f[i][j] = f[i - 1][j - nums[i - 1]] | f[i - 1][j];
                 else f[i][j] = f[i - 1][j];
             }
         }
         return f[n][m];
     }
 };  

java代码

 class Solution {
     public boolean canPartition(int[] nums) {
         int n = nums.length, sum = 0;
         for(int x : nums) sum += x;
         if(sum % 2 != 0) return false;
         int m = sum / 2;
         boolean[][] f = new boolean[n + 1][m + 1];
         f[0][0] = true;
         for(int i = 1; i <= n; i++){
             for(int j = 1; j <= m; j++){
                 if(j >= nums[i - 1]) f[i][j] = f[i - 1][j - nums[i - 1]] || f[i - 1][j];
                 else f[i][j] = f[i - 1][j];
             }
         }
         return f[n][m];
     }
 }

一维优化

我们可以发现,在计算 f[i][j]的过程中,每一行f[i][j]的值只与上一行的f[i - 1][j]有关,因此考虑去掉前一维,状态转移方程为:f[j] = f[j] | f[j - nums[i]]

如果此时我们继续考虑第二层循环j从小往大计算,即:

 for (int i = 1; i <= n; i++){                //为了下标对应,实际nums[i]应取nums[i - 1]
     for (int j = nums[i]; j <= m; j++){
          f[j] = f[i] | f[j - nums[i]];   
     }
 }    

此时的状态便与二维的状态不等价了,因为在计算第i层的状态时,我们从小到大枚举, j - nums[i]严格小于j,那么f[j-nums[i]]一定会先于f[j]被计算出来,于是我们计算出来的f[j - nums[i]]仍为第i层状态,这样f[j - nums[i]]等价于f[i][j-nums[i]] ,实际上f[j - nums[i]]应该等价于f[i - 1][j - nums[i]]

为了解决这个问题只需要将j从大到小枚举。

 for (int i = 1; i <= n; i++){                //为了下标对应,实际nums[i]应取nums[i - 1]
     for (int j = m; j >= nums[i]; j -- ){
          f[j] = f[i] | f[j - nums[i]];   
     }
 }    

因为我们从大到小枚举j,而 j - nums[i]严格小于j,于是我们在计算f[j] 的时候f[j - nums[i]]还未被第i层状态更新过,那么它存的就是上一层(i - 1层)的状态,即f[i - 1][j - nums[i]]

空间复杂度分析: O(n)

c++代码

 class Solution {
 public:
     bool canPartition(vector<int>& nums) {
         int n = nums.size(), m = 0;
         for (int x: nums) m += x;
         if (m % 2) return false;
         m /= 2;
         vector<bool> f(m + 1);
         f[0] = true;
         for (int i = 1; i <= n; i++)
             for (int j = m; j >= nums[i - 1]; j -- )
                 f[j] = f[j] | f[j - nums[i - 1]];
         return f[m]; 
     }
 };

437. 路径总和 III

思路

(dfs) O(n^2)

我们遍历每一个节点 node,搜索以当前节点node为起始节点往下延伸的所有路径,并对路径总和为 targetSumtarget的路径进行累加统计。

时间复杂度分析: 遍历整颗树需要O(n)的时间,搜索每条路径需要O(n)的时间,故总的时间复杂度为O(n^2)。

c++代码

 /**
  * Definition for a binary tree node.
  * struct TreeNode {
  *     int val;
  *     TreeNode *left;
  *     TreeNode *right;
  *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
  *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  * };
  */
 class Solution {
 public:
     int res = 0;
     int pathSum(TreeNode* root, int targetSum) {
         if(!root) return 0;
         dfs(root, targetSum);
         pathSum(root->left, targetSum);
         pathSum(root->right, targetSum);
         return res;
     }
     void dfs(TreeNode* root, int sum){
         if(!root) return ;
         sum -= root->val;
         if(!sum) res++;
         dfs(root->left, sum);
         dfs(root->right, sum);
     }
 };

(前缀和 + 哈希) O(n)

求出二叉树的前缀和,统计以每个节点node节点为路径结尾的合法路径的数量,记录一个哈希表cnt,维护每个前缀和出现的次数。

对于当前节点root,前缀和为cur,累加 cnt [cur - sum]的值,看看有几个路径起点满足。

递归函数设计:

 void dfs(TreeNode* root, int sum, int cur)

root是当前遍历的节点,sum是目标数,cur是当前经过的路径之和。

如下图:

图片.png

时间复杂度分析: 树中的每个节点被遍历一遍,故时间复杂度为O(n)。

c++代码

 /**
  * Definition for a binary tree node.
  * struct TreeNode {
  *     int val;
  *     TreeNode *left;
  *     TreeNode *right;
  *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
  *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  * };
  */
 class Solution {
 public:
     unordered_map<int, int>cnt;
     int res = 0;
     int pathSum(TreeNode* root, int targetSum) {
         cnt[0] = 1;  //前缀和0出现了一次
         dfs(root, targetSum, 0);
         return res;
     }
 ​
     void dfs(TreeNode* root, int sum, int cur){
         if(!root) return ;
         cur += root->val;
         res += cnt[cur - sum];
         cnt[cur]++;
         dfs(root->left, sum, cur), dfs(root->right, sum, cur);
         cnt[cur]--;
     }
 };

438. 找到字符串中所有字母异位词

思路

(滑动窗口,哈希表) O(n)

1、定义两个哈希表hshphs哈希表维护的是s字符串中滑动窗口中各个字符出现多少次,ht哈希表维护的是t字符串各个字符出现多少次。

2、定义两个指针jij指针用于收缩窗口,i指针用于延伸窗口,则区间[j,i]表示当前滑动窗口。首先让ij指针都指向字符串s开头,然后枚举整个字符串s ,枚举过程中,不断增加i使滑动窗口增大,相当于向右扩展滑动窗口。

3、每次向右扩展滑动窗口一步,将s[i]加入滑动窗口中,而新加入了s[i],相当于滑动窗口维护的字符数加一,即hs[s[i]]++

4、当hs[s[i]] > hp[s[i]时,说明hs哈希表中s[i]的数量多于hp哈希表中s[i]的数量,此时我们就需要向右收缩滑动窗口,j++并使hs[s[j]]--,即hs[s[j ++ ]] --

5、当i - j + 1 == p.size(),我们将起始索引j加入答案数组中。

时间复杂度分析: O(n)

c++代码

 class Solution {
 public:
     vector<int> findAnagrams(string s, string p) {
         unordered_map<char, int> hs, hp;
         for(int c : p) hp[c]++;
         vector<int> res;
         for(int i = 0, j = 0; i < s.size(); i++){
             hs[s[i]]++;
             while(hs[s[i]] > hp[s[i]])  hs[s[j++]]--;       
             if(i - j + 1 == p.size()){
                 res.push_back(j);
             }
         }
         return res;
     }
 };