算法leecode笔记

具体代码:

class Solution {
public:vector<int> twoSum(vector<int>& nums, int target) {unordered_map<int, int> hashtable;for (int i = 0; i < nums.size(); ++i) {auto it = hashtable.find(target - nums[i]);if (it != hashtable.end()) {return {it->second, i};}hashtable[nums[i]] = i;}return {};}
};

这段代码实现了经典的“两数之和”(Two Sum)问题,解决的是在一个整数数组中找到两个数,其和等于指定的目标值(target),并返回这两个数的索引。我们从算法的角度分析该代码。

代码分析

  1. 数据结构选择: 该代码使用了一个 unordered_map(哈希表),其目的是为了实现高效的查找操作。unordered_map 的键是数组中的数字,值是该数字对应的索引。通过哈希表,查找某个数是否已经在数组中可以在 O(1) 时间内完成,而不需要遍历整个数组。

  2. 算法流程

    • 遍历数组: 使用 for 循环遍历 nums 数组,对于每个元素 nums[i],我们尝试找出是否存在一个数已经存储在哈希表中,并且该数与当前元素的和等于目标值 target

    • 哈希查找: 对于每个元素 nums[i],算法首先计算 target - nums[i],并检查这个值是否已经存在于哈希表中。如果找到这个值(即之前某个元素与当前元素之和等于目标值),则返回这两个元素的索引。

    • 哈希表更新: 如果 target - nums[i] 不在哈希表中,说明还没有遇到可以与 nums[i] 配对的元素。此时,将 nums[i] 及其索引 i 存入哈希表,以备后续元素使用。

  3. 返回结果: 一旦找到一对满足条件的数,函数立即返回它们的索引。如果遍历结束后没有找到任何满足条件的数,返回空数组。

时间复杂度分析

  1. 遍历数组
    整个数组被遍历一次,因此时间复杂度是 O(n),其中 n 是数组的长度。

  2. 哈希查找: 查找和插入哈希表的操作在均摊情况下是 O(1) 的时间复杂度。因此,每次操作(查找和插入)都是常数时间。

因此,整个算法的时间复杂度是 O(n)。

空间复杂度分析

该算法使用了一个 unordered_map 来存储数组中已经遍历过的元素及其索引。在最坏情况下,所有元素都可能被存储在哈希表中,因此空间复杂度是 O(n),其中 n 是数组的长度。

算法的优势

  1. 时间效率
    使用哈希表使得查找和插入操作能够在常数时间内完成,因此相比于暴力解法(O(n^2)),该算法大大提高了时间效率。

  2. 一次遍历即可找到解
    由于在遍历的过程中同时进行查找和存储操作,只需一次遍历即可找到符合条件的数对,而不需要两层循环。

总结

这段代码巧妙地利用哈希表提高了查找效率,能在 O(n) 时间内解决两数之和的问题。算法的核心思想是在遍历数组时,实时查找是否存在可以与当前元素配对的另一个元素,从而达到快速求解的目的。

具体代码

class Solution {
public:int countPrimes(int n) {vector<int> isPrime(n, 1);int ans = 0;for (int i = 2; i < n; ++i) {if (isPrime[i]) {ans += 1;if ((long long)i * i < n) {for (int j = i * i; j < n; j += i) {isPrime[j] = 0;}}}}return ans;}
};

这段代码实现了用 埃拉托色尼筛法 来统计小于 n 的质数的个数。我们从算法的角度对其进行分析。

代码分析

  1. 埃拉托色尼筛法简介: 埃拉托色尼筛法是一种高效筛选质数的算法。其基本思想是:

    • 对于一个数 i,如果它是质数,那么 i 的倍数一定不是质数。
    • 遍历从 2 开始的整数,将每个质数的倍数标记为合数,筛掉非质数,剩下的就是质数。
  2. 数据结构与变量

    • vector<int> isPrime(n, 1):这是一个大小为 n 的布尔向量,初始时假设所有数都是质数(标记为1),会根据算法逐步标记非质数为0。
    • int ans:用于统计质数的个数。
  3. 算法流程

    • 初始化筛选表
      初始化一个长度为 n 的布尔向量 isPrime,其中 isPrime[i] == 1 表示 i 是质数,isPrime[i] == 0 表示 i 不是质数。初始时,所有数都被认为是质数,除了0和1。

    • 遍历筛选

      • 循环从 i = 2 开始,直到 i < n
        • 如果 isPrime[i] == 1,表示 i 是质数,那么增加 ans 计数。
        • 如果 i * i < n,则继续将 i 的倍数从 i * i 开始全部标记为非质数(即 isPrime[j] = 0,其中 j = i * i, i * i + i, i * i + 2*i,...),避免重复标记。
  4. 返回结果

    • 最后,ans 中保存的就是小于 n 的质数的个数,函数返回这个值。

时间复杂度分析

埃拉托色尼筛法的时间复杂度是 O(n log log n),其中 n 是输入的大小。

  1. 外层循环: 外层循环遍历从 2n-1 的所有数,这部分的时间复杂度是 O(n)。

  2. 内层筛选: 对每个质数 i,其所有倍数被标记为非质数。每个数 j 只会被标记一次。因此,筛选过程总体上对所有数的标记操作的复杂度是 O(n log log n)。

空间复杂度分析

  1. 空间复杂度: 该算法的主要空间消耗是存储布尔向量 isPrime,其大小为 n,因此空间复杂度是 O(n)。

优势与局限性

  1. 高效性
    • 使用埃拉托色尼筛法可以在 O(n log log n) 的时间复杂度内筛选出所有质数,比朴素的质数判定方法(O(n√n))要高效得多。
  2. 减少冗余筛选
    • 通过从 i * i 开始标记 i 的倍数,可以避免重复标记更小的数。例如,当 i = 2 时,i * 2 等小倍数已经被筛掉,因此从 i * i 开始是一个优化步骤。

总结

这段代码利用 埃拉托色尼筛法 高效地筛选出小于 n 的所有质数,并统计它们的个数。时间复杂度 O(n log log n) 使得该算法适用于较大的输入规模。算法的核心是通过遍历每个数,并将它的倍数标记为非质数,从而筛选出质数。

具体代码

class Solution {
public:int countTriples(int n) {int ans=0;for (int i=1;i*i<n;++i)for (int j=i+1;i*i+j*j<=n;++j)if (__gcd(i,j)==1&&!(i*j%2))ans+=n/(i*i+j*j);return ans*2;}
};

 

这段代码实现了一个计算三元组的算法,具体任务是计算满足某些条件的数对 (i, j)。让我们从算法的角度分析这段代码。

代码分析

  1. 输入与目标: 输入为一个整数 n,算法的目的是计算出满足条件的 (i, j) 数对,并返回结果。具体要求如下:

    • ij 需要满足某些特定的条件。
    • 返回结果是符合条件的三元组数量(通过某种方式定义的三元组)。
  2. 外层循环

    • 外层循环遍历 i,范围是 1 ≤ i*i < n。即 i 的平方不能超过 n
    • 循环条件 i*i < n 限制了 i 的值,使得不会计算不必要的较大 i
  3. 内层循环

    • 内层循环遍历 j,其中 j 的初始值为 i+1(为了保证 i < j),同时 i*i + j*j 的值不能超过 n
    • 条件 i*i + j*j <= n 确保不会考虑平方和超过 n 的数对 (i, j)
  4. 条件判断

    • __gcd(i, j) == 1ij 必须互质,即它们的最大公约数为 1。这保证了 (i, j) 的相对素性。
    • !(i * j % 2)i * j 必须是偶数,保证三元组符合特定的性质。
  5. 累加结果

    • 如果上述条件满足,则计算 n / (i*i + j*j),并将其累加到 ans 中。
    • 通过 n / (i*i + j*j) 的方式,代码似乎在计算某种以 (i*i + j*j) 为分母的商,并将商累加到最终结果中。
  6. 最终返回

    • 最终的答案 ans 乘以 2 后返回,这可能是因为在某种几何、对称或组合的场景下,每个三元组有两种排列方式(例如 (i, j)(j, i))。

复杂度分析

  1. 时间复杂度

    • 外层循环遍历 i 的范围大致是从 1√n,因此外层循环的复杂度是 O(√n)。
    • 内层循环遍历 j 的范围同样受限于 in,最多也是 O(√n) 的数量级。
    • 因此,两个嵌套循环的总时间复杂度大约是 O(n)。
  2. 空间复杂度

    • 该算法使用了常数空间,即只使用了几个整数变量,因此空间复杂度是 O(1)。

优势与局限性

  1. 高效的条件筛选: 通过使用 __gcd(i, j) 和偶数条件 !(i * j % 2) 来有效地筛选出符合条件的数对 (i, j),这减少了不必要的计算。

  2. 几何与数论背景

    • 该算法涉及了几何与数论的概念,尤其是欧几里得算法(求最大公约数)和平方和的组合问题。特别是,条件 __gcd(i, j) == 1i * j 为偶数的限制,可能与某种特定的数学性质相关(例如勾股数或类似的问题)。
  3. 乘 2 的处理

    • 最后的结果乘以 2,可能是为了处理排列对称性,或者问题中某个性质的双倍计数。这种处理方式在某些几何问题或组合问题中是常见的,例如当考虑顺序不同的对 (i, j)(j, i) 时。

总结

这段代码通过两层嵌套循环和数论性质(最大公约数和偶数性)来筛选符合条件的数对 (i, j),并基于平方和的商进行累加。它的时间复杂度大约是 O(n),适合于较大的 n 值。

class Solution {
public:int busyStudent(vector<int>& startTime, vector<int>& endTime, int queryTime) {int n = startTime.size();int total = 0;for (int i = 0; i < n; i++)if (startTime[i] <= queryTime && endTime[i] >= queryTime)total++;return total;}
};

这段代码实现了一个简单的统计问题,即计算在给定查询时间 queryTime 时,处于学习状态的学生人数。我们从算法角度对其进行分析。

代码分析

  1. 输入与输出

    • startTime:表示学生开始学习的时间。
    • endTime:表示学生结束学习的时间。
    • queryTime:表示要查询的时间点。
    • 输出为在 queryTime 时间点处于学习状态的学生人数。
  2. 算法流程

    • 首先,获取学生数量 n,即 startTimeendTime 的长度。
    • 初始化变量 total 为 0,用于记录满足条件的学生数量。
    • 遍历每个学生,对于每个学生 i
      • 如果该学生的 startTime[i] 小于等于 queryTimeendTime[i] 大于等于 queryTime,则说明该学生在 queryTime 时正在学习,total 增加1。
    • 最后返回 total,即在查询时间 queryTime 时,学习的学生总数。
  3. 判断条件

    • startTime[i] <= queryTime:表示学生 i 的学习开始时间不晚于查询时间。
    • endTime[i] >= queryTime:表示学生 i 的学习结束时间不早于查询时间。
    • 如果这两个条件都满足,说明学生 i 在查询时间 queryTime 时正在学习。

时间复杂度分析

  1. 时间复杂度

    • 外层循环遍历 startTimeendTime,每个学生只检查一次,所以时间复杂度是 O(n),其中 n 是学生的数量。
  2. 空间复杂度

    • 该算法只用了常数空间来存储变量 ntotal,因此空间复杂度是 O(1)。

算法优势

  1. 简单明了
    • 该算法非常直观,仅使用了一次循环和简单的条件判断,易于理解和实现。
  2. 高效性
    • 算法时间复杂度是 O(n),对于大多数输入规模来说是高效的。

局限性

  1. 平凡情况
    • 当学生数量为 0 时(即 startTimeendTime 都为空),代码可以正确返回 0
  2. 无法优化的情况
    • 由于每个学生都需要检查是否满足条件,无法通过预处理或其他方法进一步优化时间复杂度,算法已是最优。

结论

这段代码通过遍历所有学生,并根据其开始和结束时间来判断他们是否在 queryTime 时正在学习。算法时间复杂度是 O(n),非常高效,适用于绝大多数场景。

class Solution {
public:vector<int> bestCoordinate(vector<vector<int>>& towers, int radius) {int n = towers.size();vector<vector<int>> grid(51, vector<int>(51));int maxPower = 0;int x = 0, y = 0;for(auto&e: towers){int down = max(0, e[0] - radius);int top = min(50, e[0] + radius);int right = min(50, e[1] + radius);int left = max(0, e[1] - radius);for(int i = down; i <= top; ++i){for(int j = left; j <= right; ++j){int d = abs(i-e[0])*abs(i-e[0]) + abs(j-e[1])*abs(j-e[1]);if(d <= radius*radius){grid[i][j] += e[2] / (1 + sqrt(d));if(grid[i][j] > maxPower){maxPower = grid[i][j];x = i;y = j;}else if(grid[i][j] == maxPower){if(x > i){x = i;y = j;}else if(x == i && y > j){y = j;}}}}}}return {x, y};}
};

这段代码的任务是找到一个点,该点在给定信号塔影响半径范围内信号最强。我们从算法角度对其进行分析。

代码概述

  1. 输入

    • towers 是一个二维数组,每个元素 [x, y, q] 表示一个塔的位置 (x, y) 和该塔的信号强度 q
    • radius 表示信号塔的影响半径,塔的信号强度仅在塔的影响范围内有效。
  2. 目标

    • 在影响范围内(radius),找到信号最强的点 (x, y)
    • 如果有多个点的信号强度相同,优先返回坐标靠左上角的点。

代码分析

  1. 变量初始化

    • grid:用于存储 51x51 网格上的信号强度,每个塔对网格中对应位置的信号影响都会存储在该网格上。
    • maxPower:用于记录网格中的最大信号强度。
    • x, y:用于记录信号强度最大的坐标。
  2. 遍历每个塔

    • 对每个塔的坐标 (e[0], e[1]) 以及其信号强度 e[2],计算其影响范围。
    • 使用 down, top, right, left 来定义塔的信号影响的矩形区域(在影响范围内的坐标区间)。
    • 通过两个嵌套循环,遍历塔在该区域内影响的每个网格点 (i, j)
  3. 计算信号强度

    • 对于每个点 (i, j),计算到塔 (e[0], e[1]) 的平方距离 d
    • 如果距离小于或等于半径 radius,则将该塔对网格点的信号强度计算并叠加:
      • 计算公式为:信号强度 = e[2] / (1 + sqrt(d)),即信号强度随距离增加而减弱。
  4. 记录最大信号强度和对应的坐标

    • 对每个点 (i, j),如果其信号强度超过 maxPower,则更新最大信号强度和坐标 (x, y)
    • 如果信号强度相同,则优先选择 x 较小的点,若 x 相同则选择 y 较小的点,以保证返回的点最靠左上角。
  5. 返回

    • 最终返回信号强度最大的点 (x, y)

时间复杂度分析

  1. 塔的遍历

    • 共有 n 个塔,每个塔影响的范围最多是一个半径为 radius 的正方形区域(最多涉及 O(radius^2) 个网格点)。
    • 由于网格的最大尺寸为 51x51,因此理论上每个塔最多可以影响 51x51 = 2601 个网格点。
  2. 总的时间复杂度

    • 外层循环遍历每个塔 O(n) 次,内层循环遍历塔的影响范围(最坏情况下最多 2601 次)。
    • 因此,总时间复杂度为 O(n * min(radius^2, 2601)),即遍历每个塔并计算其影响的时间。

空间复杂度分析

  1. 空间复杂度
    • grid 是一个 51x51 的二维数组,空间复杂度为 O(51^2) = O(2601)。
    • 其他变量使用的空间是常数级的,因此总的空间复杂度为 O(1) + O(51^2) = O(2601)。

算法优势

  1. 高效网格计算
    • 通过限制网格大小为 51x51,减少了不必要的复杂度。
    • 只计算在半径范围内的网格点,优化了计算过程。
  2. 信号强度衰减公式
    • 使用 1 + sqrt(d) 来表示信号衰减,这是一种合理的模型,能够很好地模拟信号随距离衰减的情况。

结论

该算法通过遍历每个塔的影响范围,计算网格点上的信号强度,并找到信号最强的点 (x, y)。时间复杂度是 O(n * min(radius^2, 2601)),适用于中等规模的塔和网格。通过合理的条件判断,确保了结果的正确性和效率

class Solution {
public:int commonFactors(int a, int b) {int ans = 0, g = gcd(a, b);for (int i = 1; i * i <= g; ++i)if (g % i == 0) {++ans; // i 是公因子if (i * i < g)++ans; // g/i 是公因子}return ans;}
};

这段代码的任务是计算两个数 ab 之间的公因子(即同时能整除 ab 的整数)的数量。我们从算法角度对其进行分析。

代码分析

  1. 输入与输出

    • 输入:两个整数 ab
    • 输出:ab 的公因子数量。
  2. 步骤解析

    • 最大公约数 (gcd)

      • 首先计算 ab 的最大公约数 g。两个数的公因子是它们最大公约数的因子,因此只需要计算 g 的因子即可。
    • 遍历因子

      • 遍历从 1sqrt(g),判断每个数 i 是否为 g 的因子(即 g % i == 0)。
      • 如果 ig 的因子,那么它和 g/i 都是公因子。
      • 对于每个符合条件的 i
        • 如果 i * i == g,说明 ig 的平方根,因此只增加一次公因子。
        • 如果 i * i < g,则 g/i 也是一个不同的公因子,增加两次公因子(一次为 i,一次为 g/i)。
  3. 返回公因子数量

    • 最终将计数的公因子数量返回。

代码示例分析

假设 a = 12b = 18

  • 计算 gcd(12, 18),即最大公约数 g = 6
  • 6 的因子有 1, 2, 3, 6,因此公因子数量为 4。
  • 代码遍历 i = 1, 2,每次找到两个因子:ig/i

时间复杂度分析

  1. 计算 gcd

    • 计算两个数 ab 的最大公约数的时间复杂度为 O(log(min(a, b))),这是通过欧几里得算法实现的。
  2. 遍历因子

    • 我们遍历从 1sqrt(g),每次检查 g 是否能被整除,因此这部分的时间复杂度是 O(sqrt(g))。

因此,总的时间复杂度是 O(log(min(a, b)) + sqrt(g)),其中 gab 的最大公约数。

空间复杂度分析

  1. 空间复杂度
    • 该算法只使用了几个常数变量(如 g, ans, i),因此空间复杂度为 O(1)。

算法优势

  1. 效率高

    • 通过先计算最大公约数 g,将问题简化为找到 g 的因子,而不是直接在 ab 上操作。这样可以显著减少不必要的计算。
  2. 数学性质的利用

    • 使用最大公约数的性质减少了计算复杂度,同时利用 ig/i 作为因子配对的方式,使得遍历只需要到 sqrt(g) 为止,进一步优化了算法。

结论

该算法通过先计算最大公约数,然后遍历其因子,找出两个数的所有公因子。时间复杂度为 O(log(min(a, b)) + sqrt(g)),效率较高,适合处理大多数输入。

class Solution {
public:vector<vector<int>> fileCombination(int target) {vector<vector<int>>vec;vector<int> res;for (int l = 1, r = 2; l < r;){int sum = (l + r) * (r - l + 1) / 2;if (sum == target) {res.clear();for (int i = l; i <= r; ++i) {res.emplace_back(i);}vec.emplace_back(res);l++;} else if (sum < target) {r++;} else {l++;}}return vec;}
};

这段代码的任务是找到所有连续整数的组合,使得这些整数的和等于目标值 target。代码中使用了双指针技术(滑动窗口)来查找符合条件的组合。接下来从算法的角度分析该代码。

代码解析

  1. 输入与输出

    • 输入:一个目标值 target
    • 输出:所有连续整数组合的集合,每个组合的和等于 target
  2. 核心思想

    • 使用两个指针 l(左边界)和 r(右边界),表示当前窗口的左右边界。
    • 初始时,l 指向 1,r 指向 2,表示当前窗口是 [l, r] 之间的整数。
    • 计算窗口内整数的和 sum,如果 sum 等于 target,记录这组连续整数。如果 sum 小于 target,右边界 r 向右扩展;如果 sum 大于 target,左边界 l 向右收缩。
    • 这一过程通过滑动窗口的方式遍历所有可能的连续整数组合,直到 l >= r
  3. 详细步骤

    • 双指针初始化:初始时 l = 1r = 2,表示考察从 1 到 2 的连续整数。

    • 计算区间和:计算区间 [l, r] 的整数和 sum,公式为 sum = (l + r) * (r - l + 1) / 2。这是等差数列求和公式。

    • 判断和与目标值的关系

      • 如果 sum == target,则找到一组解,将该区间 [l, r] 的整数存入 res,并将其添加到 vec
      • 如果 sum < target,说明窗口内的数之和还不够,需要将右边界 r 扩大,以增加和的大小。
      • 如果 sum > target,说明窗口内的数之和过大,需要将左边界 l 向右移动,以减小和的大小。
    • 结束条件:当左边界 l 大于或等于右边界 r 时,结束循环。

  4. 返回结果

    • 最后返回所有符合条件的组合。

时间复杂度分析

  1. 双指针遍历

    • 每次循环中,指针 lr 至少有一个会移动,直到 l 达到 target 的一半左右。双指针的移动使得时间复杂度为 O(target)。
  2. 每个区间求和

    • 求和操作是通过公式计算的,因此求和的时间复杂度是 O(1)。

因此,总的时间复杂度为 O(target),在大多数情况下是比较高效的。

空间复杂度分析

  1. 存储结果
    • 结果存储在二维向量 vec 中,每个找到的解都会被存储,因此空间复杂度主要取决于存储的结果。
  2. 其他空间开销
    • 除了存储结果外,使用了常数级别的额外空间来存储 lrsum 和中间结果 res

因此,空间复杂度为 O(结果数量 * 区间长度),最坏情况下是 O(target)。

示例分析

假设 target = 9,代码执行如下:

  • 初始时 l = 1, r = 2,计算区间和 sum = (1 + 2) * (2 - 1 + 1) / 2 = 3,小于 target,因此扩展右边界 r
  • l = 1, r = 3,计算 sum = (1 + 3) * (3 - 1 + 1) / 2 = 6,小于 target,扩展右边界 r
  • l = 1, r = 4,计算 sum = (1 + 4) * (4 - 1 + 1) / 2 = 10,大于 target,缩小左边界 l
  • l = 2, r = 4,计算 sum = (2 + 4) * (4 - 2 + 1) / 2 = 9,等于 target,记录 [2, 3, 4]
  • l = 3, r = 4,计算 sum = (3 + 4) * (4 - 3 + 1) / 2 = 7,小于 target,扩展右边界 r

最终返回 [[2, 3, 4]]

总结

该算法使用滑动窗口法有效地查找目标值的连续整数组合,时间复杂度为 O(target),能够处理较大的目标值,并且代码结构简单易懂。

class Solution {
public:int countLatticePoints(vector<vector<int>>& circles) {int maxx = 0, maxy = 0;for(auto& c : circles) {maxx = max(maxx, c[0] + c[2]);maxy = max(maxy, c[1] + c[2]);}int d[maxy + 2][maxx + 2];memset(d, 0, sizeof(d));for(auto& c : circles) {for(int y = c[1] + c[2], x1 = c[0], x2 = c[0]; y >= c[1]; --y) {while((x1 - c[0]) * (x1 - c[0]) + (y - c[1]) * (y - c[1]) <= c[2] * c[2]) --x1;while((x2 - c[0]) * (x2 - c[0]) + (y - c[1]) * (y - c[1]) <= c[2] * c[2]) ++x2;d[y][x1+1]++;d[y][x2]--;}for(int y = c[1] - c[2], x1 = c[0], x2 = c[0]; y < c[1]; ++y) {while((x1 - c[0]) * (x1 - c[0]) + (y - c[1]) * (y - c[1]) <= c[2] * c[2]) --x1;while((x2 - c[0]) * (x2 - c[0]) + (y - c[1]) * (y - c[1]) <= c[2] * c[2]) ++x2;d[y][x1+1]++;d[y][x2]--;}}int res = 0;for(int y = 0; y <= maxy; ++y) {for(int x = 0, cur = 0; x <= maxx; ++x) {cur += d[y][x];if(cur > 0) res++;}}return res;}
};

 

这段代码的任务是计算由若干个圆形覆盖的整点坐标的数量。整点坐标是指坐标轴上的点 (x, y),其中 x 和 y 都是整数。该代码通过离散化坐标轴和使用差分数组的方法实现了这一目标。接下来对该代码进行算法解析。

代码解析

  1. 输入与输出

    • 输入:一个二维数组 circles,其中每个元素代表一个圆形的坐标和半径,circles[i] = {x, y, r} 表示圆心在 $(x, y)$,半径为 $r$。
    • 输出:被所有圆形覆盖的整点坐标的数量。
  2. 核心思路

    • 网格离散化:首先计算出所有圆形的最右、最上边界,以确定可以处理的最大网格范围。
    • 差分数组:在圆形的覆盖区域内,通过使用差分数组 d 来标记整点是否被某个圆形覆盖。这种方法可以有效地减少遍历和判断的次数。
    • 圆的扫描:对于每个圆,分为上半部分和下半部分分别处理,并根据圆的半径计算出覆盖的整点范围。
    • 计算覆盖区域:通过累加差分数组 d 的值来确定每个整点被覆盖的次数,如果某个点的覆盖次数大于 0,则它是被覆盖的点。
  3. 详细步骤

    • 确定网格的大小:遍历所有的圆形,计算每个圆形的最右边界 x + r 和最上边界 y + r,以此来确定网格的最大范围 maxxmaxy

    • 初始化差分数组:定义一个大小为 (maxy + 2) x (maxx + 2) 的二维数组 d,用于记录每个网格点的差分值,初始时所有值为 0。

    • 标记圆的覆盖区域

      • 对于每个圆形,分别处理其上半部分和下半部分。
      • 对于上半部分,从圆心开始,逐渐向上扫描每一行,计算在当前行中,被圆覆盖的整点的左右边界 x1x2,并使用差分数组标记该行的覆盖范围。
      • 对于下半部分,方法类似,只是逐渐向下扫描每一行。
    • 计算覆盖的整点数量

      • 在最后的网格扫描中,通过累加差分数组 d 的值,确定每个整点是否被覆盖。如果某个点的累计覆盖值大于 0,则说明该点被覆盖,计入结果。
  4. 算法的复杂度分析

    • 时间复杂度:遍历每个圆形时,需要根据圆的半径对每一行进行扫描。对于每个圆,扫描和标记的操作与圆的半径相关。假设圆的最大半径为 R,总的时间复杂度近似为 O(R \cdot n),其中 n 是圆的数量。

    • 空间复杂度:差分数组的大小是 maxx * maxy,即覆盖网格的大小。因此,空间复杂度为 O(maxx \cdot maxy)。

示例分析

假设输入为 circles = {{2, 2, 1}, {3, 3, 2}},即包含两个圆:

  1. 第一个圆的圆心为 $(2, 2)$,半径为 1。计算其覆盖的整点坐标。
  2. 第二个圆的圆心为 $(3, 3)$,半径为 2。计算其覆盖的整点坐标。
  3. 使用差分数组标记这两个圆的覆盖区域。
  4. 通过累加差分数组,得出被覆盖的整点坐标的总数量。

总结

该算法通过使用差分数组高效地标记并计算圆形覆盖的整点数量,避免了直接遍历网格上所有点的高时间复杂度。通过离散化网格并处理圆的上、下半部分,算法在空间和时间效率上得到了较好的优化。

auto __unsync_ios_stdio = ios::sync_with_stdio(false);
auto __untie_cin = cin.tie(nullptr);class Solution {
public:int maximalSquare(vector<vector<char>>& matrix) {vector<vector<int>> dp(matrix.size(),vector<int>(matrix[0].size(),0));int lenth = 0;for(int i = 0;i<matrix.size();i++){for(int j = 0 ;j<matrix[0].size();j++){if(matrix[i][j]=='1'){if(i==0||j==0)dp[i][j] = 1;else dp[i][j] = min(min(dp[i-1][j],dp[i-1][j-1]),dp[i][j-1])+1;lenth = max(lenth,dp[i][j]);}else{dp[i][j] = 0;}}}return lenth*lenth;}
};

这段代码用于解决最大正方形问题,给定一个由 '0''1' 组成的二维矩阵,要求找到由 '1' 组成的最大的正方形,并返回其面积。

代码解析

  1. 输入与输出

    • 输入:二维矩阵 matrix,每个元素是字符 '0''1',表示是否是矩形中的某个位置。
    • 输出:找到由 '1' 组成的最大的正方形,并返回其面积。
  2. 核心思路

    • 动态规划的思想用于计算以每个位置为右下角的最大正方形的边长。
    • 定义一个二维动态规划数组 dp,其中 dp[i][j] 表示以位置 (i, j) 为右下角的最大正方形的边长。
  3. 动态规划转移方程

    • 对于位置 (i, j)
      • 如果 matrix[i][j] == '1'
        • 如果 (i, j) 是第一行或第一列,则它自身形成的最大正方形边长为 1(dp[i][j] = 1)。
        • 否则,dp[i][j] = min(dp[i-1][j], dp[i-1][j-1], dp[i][j-1]) + 1。即可以取它的左边、上边和左上方位置的最小值加 1 来形成新的正方形。
      • 如果 matrix[i][j] == '0',则 dp[i][j] = 0
    • 同时,动态更新最大正方形的边长 lenth
  4. 最终面积

    • 最大正方形的面积为 lenth * lenth,这是返回结果。

详细步骤:

  1. 初始化

    • 使用 vector<vector<int>> dp(matrix.size(), vector<int>(matrix[0].size(), 0)) 初始化动态规划数组 dp,所有元素初始为 0。
    • lenth 记录当前找到的最大正方形的边长,初始为 0。
  2. 遍历矩阵

    • 双重循环遍历矩阵的每个位置 (i, j)
      • 如果当前值 matrix[i][j] == '1',则更新 dp[i][j] 为相应的边长,并更新最大边长 lenth
      • 如果 matrix[i][j] == '0',则跳过该位置。
  3. 计算结果

    • 返回 lenth * lenth,即最大正方形的面积。

代码效率分析

  • 时间复杂度

    • 由于需要遍历矩阵中的每个元素,因此时间复杂度为 $O(m \times n)$,其中 $m$ 是矩阵的行数,$n$ 是矩阵的列数。
  • 空间复杂度

    • 动态规划数组 dp 需要与输入矩阵相同大小的空间,因此空间复杂度为 $O(m \times n)$。

示例分析

假设输入为:

matrix = [  ['1', '0', '1', '0', '0'],['1', '0', '1', '1', '1'],['1', '1', '1', '1', '1'],['1', '0', '0', '1', '0']
]
  1. 遍历到 matrix[2][2] 时,由于其上方、左方和左上方的元素都为 '1',则 dp[2][2] = 2,表示以此位置为右下角的最大正方形边长为 2。
  2. 最终最大的正方形边长为 2,面积为 2 * 2 = 4

总结

这段代码使用动态规划高效地解决了最大正方形问题,通过构建 dp 数组来记录每个位置可以形成的最大正方形边长。最终返回最大正方形的面积。

class Solution {
public:int subarraySum(vector<int>& nums, int k) {unordered_map<int, int> mp;mp[0] = 1;int count = 0, pre = 0;for (auto& x:nums) {pre += x;if (mp.find(pre - k) != mp.end()) {count += mp[pre - k];}mp[pre]++;}return count;}
};

这段代码解决了一个经典的问题:给定一个整数数组 nums 和一个目标值 k,要求找出数组中所有和为 k 的连续子数组的个数。

代码解析

  1. 输入与输出

    • 输入:整数数组 nums 和目标值 k
    • 输出:数组中和为 k 的连续子数组的个数。
  2. 核心思路

    • 通过累加前缀和来避免重复计算子数组的和。
    • 使用一个哈希表(unordered_map)来记录每个前缀和出现的次数,快速判断当前前缀和与目标值 k 之间的关系。
  3. 详细步骤

    • 定义一个哈希表 mp,其中键表示某个前缀和,值表示该前缀和出现的次数。
      • 初始化 mp[0] = 1,表示前缀和为 0 的情况出现了一次。
    • 通过遍历数组 nums,逐个累加当前元素到 pre,表示当前位置的前缀和。
    • 在每次计算出新的前缀和后,检查 pre - k 是否存在于哈希表 mp 中:
      • 如果存在,说明从某个先前的位置到当前的位置之间的子数组的和为 k,因此将对应的出现次数加到计数 count 中。
    • 将当前的前缀和 pre 记录到哈希表中,增加其出现的次数。
  4. 变量解释

    • mp:哈希表,记录每个前缀和出现的次数。
    • count:统计符合条件的子数组个数。
    • pre:当前的前缀和。
    • nums:输入数组。
    • k:目标值。

动态过程

假设有一个数组 nums = [1, 1, 1],目标值 k = 2

  1. 初始时:
    • mp = {0: 1},表示前缀和 0 出现过一次。
    • pre = 0count = 0
  2. 遍历数组:
    • 第一轮:
      • 当前元素 x = 1,前缀和 pre = 1
      • 查找 pre - k = 1 - 2 = -1,不存在于 mp 中。
      • 更新哈希表:mp = {0: 1, 1: 1}
    • 第二轮:
      • 当前元素 x = 1,前缀和 pre = 2
      • 查找 pre - k = 2 - 2 = 0,存在于 mp 中(次数为 1)。
      • 更新计数:count = 1
      • 更新哈希表:mp = {0: 1, 1: 1, 2: 1}
    • 第三轮:
      • 当前元素 x = 1,前缀和 pre = 3
      • 查找 pre - k = 3 - 2 = 1,存在于 mp 中(次数为 1)。
      • 更新计数:count = 2
      • 更新哈希表:mp = {0: 1, 1: 1, 2: 1, 3: 1}

最终结果为 count = 2,即存在两个子数组 [1, 1][1, 1],它们的和为 2。

算法复杂度

  • 时间复杂度

    • 遍历数组 nums 一次,每次操作都涉及常数时间的哈希表查找和更新,因此时间复杂度为 O(n),其中 n 是数组的长度。
  • 空间复杂度

    • 需要使用一个哈希表来记录前缀和,最坏情况下哈希表的大小为 O(n),所以空间复杂度为 O(n)。

总结

这段代码使用了前缀和加上哈希表来有效解决连续子数组和为 k 的问题,避免了暴力解法的嵌套循环,通过动态记录前缀和和快速查询,实现了 O(n) 的时间复杂度,非常高效。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/879600.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Git之如何删除Untracked文件(六十八)

简介&#xff1a; CSDN博客专家、《Android系统多媒体进阶实战》一书作者 新书发布&#xff1a;《Android系统多媒体进阶实战》&#x1f680; 优质专栏&#xff1a; Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a; 多媒体系统工程师系列【…

【计算机网络】UDP 协议详解及其网络编程应用

文章目录 一、引言二、UDP1、UDP的协议格式2、UDP 报文的解包和分用3、UDP面向数据报的特点 三、UDP输入输出四、UDP网络编程 一、引言 UDP&#xff08;User Datagram Protocol&#xff0c;用户数据报协议&#xff09;是一种网络通信协议&#xff0c;它属于传输层的协议。是一…

HTTP中的Cookie与Session

一、背景 HTTP协议是无状态无连接的。 无状态&#xff1a;服务器不会保存客户端历史请求记录&#xff0c;每一次请求都是全新的。 无连接&#xff1a;服务器应答后关闭连接&#xff0c;每次请求都是独立的。 无状态就导致服务器不认识每一个请求的客户端是否登陆过。 这时…

【贪心算法】贪心算法

贪心算法简介 1.什么是贪心算法2.贪心算法的特点3.学习贪心的方向 点赞&#x1f44d;&#x1f44d;收藏&#x1f31f;&#x1f31f;关注&#x1f496;&#x1f496; 你的支持是对我最大的鼓励&#xff0c;我们一起努力吧!&#x1f603;&#x1f603; 1.什么是贪心算法 与其说是…

Spring为什么要用三级缓存解决循环依赖?

Spring为什么要用三级缓存解决循环依赖&#xff1f; 1. Spring是如何创建一个bean对象2. Spring三级缓存2.1 一级缓存&#xff1a;单例池&#xff0c;经历过完整bean生命&#xff0c;单例Bean对象2.2 二级缓存&#xff1a;提前暴露的Bean2.3 三级缓存&#xff1a;打破循环 3. S…

计算机网络通关学习(一)

简介 之前我通过王道的考研课进行了计算机网络的学习&#xff0c;但是在秋招准备过程中发现之前的笔记很多不足&#xff0c;学习的知识不够深入和巩固&#xff0c;所以再重新对《图解HTTP》&《图解TCP/IP》进行深度学习后&#xff0c;总结出了此篇博客&#xff0c;由于内容…

08_Python数据类型_字典

Python的基础数据类型 数值类型&#xff1a;整数、浮点数、复数、布尔字符串容器类型&#xff1a;列表、元祖、字典、集合 字典 字典&#xff08;Dictionary&#xff09;是一种可变容器模型&#xff0c;它可以存储任意类型对象&#xff0c;其中每个对象都存储为一个键值对。…

存储数据的树形结构

目录 1、二叉查找树 2、平衡二叉树AVL Tree 3 、平衡多叉树B-Tree 4、BTree树 5 、红黑树 红黑树的应用 6.平衡树的旋转 mysql 索引数据结构&#xff1a; Btree 索引是B树在数据库中的一种实现&#xff0c;最为常见的。B树 中的B代表平衡&#xff0c;而不是二叉 1、二…

带你如何使用CICD持续集成与持续交付

目录 一、CICD是什么 1.1 持续集成&#xff08;Continuous Integration&#xff09; 1.2 持续部署&#xff08;Continuous Deployment&#xff09; 1.3 持续交付&#xff08;Continuous Delivery&#xff09; 二、git工具使用 2.1 git简介 2.2 git的工作流程 2.3 部署g…

如何用 Scrapy 爬取网站数据并在 Easysearch 中进行存储检索分析

做过数据分析和爬虫程序的小伙伴想必对 Scrapy 这个爬虫框架已经很熟悉了。今天给大家介绍下&#xff0c;如何基于 Scrapy 快速编写一个爬虫程序并利用 Easysearch 储存、检索、分析爬取的数据。我们以极限科技的官网 Blog 为数据源&#xff0c;做下实操演示。 安装 scrapy 使…

3. Python计算水仙花数

Python计算水仙花数 一、什么是水仙花数&#xff1f; 百度答案 二、怎样使用Python计算水仙花数&#xff1f; 这里需要for循环&#xff0c;if判断&#xff0c;需要range()函数&#xff0c;需要知道怎么求个位数&#xff0c;十位数&#xff0c;百位数… 1. For循环 语句结…

CTFHub技能树-SQL注入-整数型注入

一、手动注入 思路&#xff1a;注入点->库->表->列->数据 首先使用order by探测有几列 http://challenge-215beae2f0b99b12.sandbox.ctfhub.com:10800/?id1 order by 2 我们发现order by 2 的时候有回显&#xff0c;到了order by 3 的时候就没有回显了&#xf…

k8s的环境配置

一、前期系统环境准备 准备3台主机&#xff1a;硬盘50G cpu2个 内存2G 1、3台主机同时配置 1&#xff09;关闭防火墙与selinux、NetworkManager [rootk8s-master ~]# systemctl stop firewalld[rootk8s-master ~]# systemctl disable firewalldRemoved symlink /etc/systemd/…

CSS---序号使用css设置,counter-reset、counter-increment、content配合实现备注文案的序号展示

直接上代码&#xff0c;全代码copy即可使用! <template><div class"reminder"><span class"Bold_12_body" style"line-height: 8vw">温馨提示&#xff1a;</span><br /><div class"rule-container"…

【Hot100】LeetCode—84. 柱状图中最大的矩形

目录 1- 思路题目识别单调栈 2- 实现⭐84. 柱状图中最大的矩形——题解思路 3- ACM 实现 原题链接&#xff1a;84. 柱状图中最大的矩形 1- 思路 题目识别 识别1 &#xff1a;给定一个数组 heights &#xff0c;求解柱状图的最大面积 单调栈 使用 Stack 来实现&#xff0c;遍…

go语言中的数组指针和指针数组的区别详解

1.介绍 大家知道C语言之所以强大&#xff0c;就是因为c语言支持指针&#xff0c;而且权限特别大&#xff0c;c语言可以对计算机中任何内存的指针进行操作&#xff0c;这样自然而然也会带来一些不安全的因素&#xff0c;所以在golang中&#xff0c;「取消了对指针的一些偏移&…

【C语言】分支和循环专题应用

分支和循环专题应用 1、随机数生成1.1rand1.2 srand函数介绍1.3 time函数介绍1.4 设置随机数的范围 2、猜数字游戏的代码及实现 通过了分支和循环的介绍学习之后&#xff0c;我们可以运用分支和循环语句写出一些有趣的代码了&#xff0c;让我们来一起探索吧&#xff01; 写一个…

node.js 中的进程和线程工作原理

本文所有的代码均基于 node.js 14 LTS 版本分析 概念 进程是对正在运行中的程序的一个抽象&#xff0c;是系统进行资源分配和调度的基本单位&#xff0c;操作系统的其他所有内容都是围绕着进程展开的 线程是操作系统能够进行运算调度的最小单位&#xff0c;其是进程中的一个执…

远程桌面内网穿透是什么?有什么作用?

远程桌面内网穿透指的是通过特定技术手段&#xff0c;将处于内网中的电脑或服务器&#xff0c;通过外部网络&#xff08;互联网&#xff09;进行访问。内网穿透的主要作用是解决在内网环境下&#xff0c;远程设备与外部互联网之间的连接问题&#xff0c;允许用户从外部访问内网…

.Net Gacutil工具(全局程序集缓存工具)使用教程

GAC介绍&#xff1a; GAC&#xff08;Global Assembly Cache&#xff09;全局程序集缓存&#xff0c;是用于存放.Net应用程序共享的程序集。 像平常我们在Visual Studio中引用系统程序集时&#xff0c;这些程序集便来自于GAC。 GAC默认位置为&#xff1a;%windir%\Microsoft…