具体代码:
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
),并返回这两个数的索引。我们从算法的角度分析该代码。
代码分析
-
数据结构选择: 该代码使用了一个
unordered_map
(哈希表),其目的是为了实现高效的查找操作。unordered_map
的键是数组中的数字,值是该数字对应的索引。通过哈希表,查找某个数是否已经在数组中可以在 O(1) 时间内完成,而不需要遍历整个数组。 -
算法流程:
-
遍历数组: 使用
for
循环遍历nums
数组,对于每个元素nums[i]
,我们尝试找出是否存在一个数已经存储在哈希表中,并且该数与当前元素的和等于目标值target
。 -
哈希查找: 对于每个元素
nums[i]
,算法首先计算target - nums[i]
,并检查这个值是否已经存在于哈希表中。如果找到这个值(即之前某个元素与当前元素之和等于目标值),则返回这两个元素的索引。 -
哈希表更新: 如果
target - nums[i]
不在哈希表中,说明还没有遇到可以与nums[i]
配对的元素。此时,将nums[i]
及其索引i
存入哈希表,以备后续元素使用。
-
-
返回结果: 一旦找到一对满足条件的数,函数立即返回它们的索引。如果遍历结束后没有找到任何满足条件的数,返回空数组。
时间复杂度分析
-
遍历数组:
整个数组被遍历一次,因此时间复杂度是 O(n),其中 n 是数组的长度。 -
哈希查找: 查找和插入哈希表的操作在均摊情况下是 O(1) 的时间复杂度。因此,每次操作(查找和插入)都是常数时间。
因此,整个算法的时间复杂度是 O(n)。
空间复杂度分析
该算法使用了一个 unordered_map
来存储数组中已经遍历过的元素及其索引。在最坏情况下,所有元素都可能被存储在哈希表中,因此空间复杂度是 O(n),其中 n 是数组的长度。
算法的优势
-
时间效率:
使用哈希表使得查找和插入操作能够在常数时间内完成,因此相比于暴力解法(O(n^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
的质数的个数。我们从算法的角度对其进行分析。
代码分析
-
埃拉托色尼筛法简介: 埃拉托色尼筛法是一种高效筛选质数的算法。其基本思想是:
- 对于一个数
i
,如果它是质数,那么i
的倍数一定不是质数。 - 遍历从 2 开始的整数,将每个质数的倍数标记为合数,筛掉非质数,剩下的就是质数。
- 对于一个数
-
数据结构与变量:
vector<int> isPrime(n, 1)
:这是一个大小为n
的布尔向量,初始时假设所有数都是质数(标记为1),会根据算法逐步标记非质数为0。int ans
:用于统计质数的个数。
-
算法流程:
-
初始化筛选表:
初始化一个长度为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,...
),避免重复标记。
- 如果
- 循环从
-
-
返回结果:
- 最后,
ans
中保存的就是小于n
的质数的个数,函数返回这个值。
- 最后,
时间复杂度分析
埃拉托色尼筛法的时间复杂度是 O(n log log n),其中 n
是输入的大小。
-
外层循环: 外层循环遍历从
2
到n-1
的所有数,这部分的时间复杂度是 O(n)。 -
内层筛选: 对每个质数
i
,其所有倍数被标记为非质数。每个数j
只会被标记一次。因此,筛选过程总体上对所有数的标记操作的复杂度是 O(n log log n)。
空间复杂度分析
- 空间复杂度: 该算法的主要空间消耗是存储布尔向量
isPrime
,其大小为n
,因此空间复杂度是 O(n)。
优势与局限性
- 高效性:
- 使用埃拉托色尼筛法可以在 O(n log log n) 的时间复杂度内筛选出所有质数,比朴素的质数判定方法(O(n√n))要高效得多。
- 减少冗余筛选:
- 通过从
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)
。让我们从算法的角度分析这段代码。
代码分析
-
输入与目标: 输入为一个整数
n
,算法的目的是计算出满足条件的(i, j)
数对,并返回结果。具体要求如下:i
和j
需要满足某些特定的条件。- 返回结果是符合条件的三元组数量(通过某种方式定义的三元组)。
-
外层循环:
- 外层循环遍历
i
,范围是1 ≤ i*i < n
。即i
的平方不能超过n
。 - 循环条件
i*i < n
限制了i
的值,使得不会计算不必要的较大i
。
- 外层循环遍历
-
内层循环:
- 内层循环遍历
j
,其中j
的初始值为i+1
(为了保证i < j
),同时i*i + j*j
的值不能超过n
。 - 条件
i*i + j*j <= n
确保不会考虑平方和超过n
的数对(i, j)
。
- 内层循环遍历
-
条件判断:
__gcd(i, j) == 1
:i
和j
必须互质,即它们的最大公约数为 1。这保证了(i, j)
的相对素性。!(i * j % 2)
:i * j
必须是偶数,保证三元组符合特定的性质。
-
累加结果:
- 如果上述条件满足,则计算
n / (i*i + j*j)
,并将其累加到ans
中。 - 通过
n / (i*i + j*j)
的方式,代码似乎在计算某种以(i*i + j*j)
为分母的商,并将商累加到最终结果中。
- 如果上述条件满足,则计算
-
最终返回:
- 最终的答案
ans
乘以 2 后返回,这可能是因为在某种几何、对称或组合的场景下,每个三元组有两种排列方式(例如(i, j)
和(j, i)
)。
- 最终的答案
复杂度分析
-
时间复杂度:
- 外层循环遍历
i
的范围大致是从1
到√n
,因此外层循环的复杂度是 O(√n)。 - 内层循环遍历
j
的范围同样受限于i
和n
,最多也是 O(√n) 的数量级。 - 因此,两个嵌套循环的总时间复杂度大约是 O(n)。
- 外层循环遍历
-
空间复杂度:
- 该算法使用了常数空间,即只使用了几个整数变量,因此空间复杂度是 O(1)。
优势与局限性
-
高效的条件筛选: 通过使用
__gcd(i, j)
和偶数条件!(i * j % 2)
来有效地筛选出符合条件的数对(i, j)
,这减少了不必要的计算。 -
几何与数论背景:
- 该算法涉及了几何与数论的概念,尤其是欧几里得算法(求最大公约数)和平方和的组合问题。特别是,条件
__gcd(i, j) == 1
和i * j
为偶数的限制,可能与某种特定的数学性质相关(例如勾股数或类似的问题)。
- 该算法涉及了几何与数论的概念,尤其是欧几里得算法(求最大公约数)和平方和的组合问题。特别是,条件
-
乘 2 的处理:
- 最后的结果乘以 2,可能是为了处理排列对称性,或者问题中某个性质的双倍计数。这种处理方式在某些几何问题或组合问题中是常见的,例如当考虑顺序不同的对
(i, j)
和(j, i)
时。
- 最后的结果乘以 2,可能是为了处理排列对称性,或者问题中某个性质的双倍计数。这种处理方式在某些几何问题或组合问题中是常见的,例如当考虑顺序不同的对
总结
这段代码通过两层嵌套循环和数论性质(最大公约数和偶数性)来筛选符合条件的数对 (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
时,处于学习状态的学生人数。我们从算法角度对其进行分析。
代码分析
-
输入与输出:
startTime
:表示学生开始学习的时间。endTime
:表示学生结束学习的时间。queryTime
:表示要查询的时间点。- 输出为在
queryTime
时间点处于学习状态的学生人数。
-
算法流程:
- 首先,获取学生数量
n
,即startTime
和endTime
的长度。 - 初始化变量
total
为 0,用于记录满足条件的学生数量。 - 遍历每个学生,对于每个学生
i
:- 如果该学生的
startTime[i]
小于等于queryTime
且endTime[i]
大于等于queryTime
,则说明该学生在queryTime
时正在学习,total
增加1。
- 如果该学生的
- 最后返回
total
,即在查询时间queryTime
时,学习的学生总数。
- 首先,获取学生数量
-
判断条件:
startTime[i] <= queryTime
:表示学生i
的学习开始时间不晚于查询时间。endTime[i] >= queryTime
:表示学生i
的学习结束时间不早于查询时间。- 如果这两个条件都满足,说明学生
i
在查询时间queryTime
时正在学习。
时间复杂度分析
-
时间复杂度:
- 外层循环遍历
startTime
和endTime
,每个学生只检查一次,所以时间复杂度是 O(n),其中n
是学生的数量。
- 外层循环遍历
-
空间复杂度:
- 该算法只用了常数空间来存储变量
n
和total
,因此空间复杂度是 O(1)。
- 该算法只用了常数空间来存储变量
算法优势
- 简单明了:
- 该算法非常直观,仅使用了一次循环和简单的条件判断,易于理解和实现。
- 高效性:
- 算法时间复杂度是 O(n),对于大多数输入规模来说是高效的。
局限性
- 平凡情况:
- 当学生数量为 0 时(即
startTime
和endTime
都为空),代码可以正确返回0
。
- 当学生数量为 0 时(即
- 无法优化的情况:
- 由于每个学生都需要检查是否满足条件,无法通过预处理或其他方法进一步优化时间复杂度,算法已是最优。
结论
这段代码通过遍历所有学生,并根据其开始和结束时间来判断他们是否在 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};}
};
这段代码的任务是找到一个点,该点在给定信号塔影响半径范围内信号最强。我们从算法角度对其进行分析。
代码概述
-
输入:
towers
是一个二维数组,每个元素[x, y, q]
表示一个塔的位置(x, y)
和该塔的信号强度q
。radius
表示信号塔的影响半径,塔的信号强度仅在塔的影响范围内有效。
-
目标:
- 在影响范围内(
radius
),找到信号最强的点(x, y)
。 - 如果有多个点的信号强度相同,优先返回坐标靠左上角的点。
- 在影响范围内(
代码分析
-
变量初始化:
grid
:用于存储 51x51 网格上的信号强度,每个塔对网格中对应位置的信号影响都会存储在该网格上。maxPower
:用于记录网格中的最大信号强度。x, y
:用于记录信号强度最大的坐标。
-
遍历每个塔:
- 对每个塔的坐标
(e[0], e[1])
以及其信号强度e[2]
,计算其影响范围。 - 使用
down
,top
,right
,left
来定义塔的信号影响的矩形区域(在影响范围内的坐标区间)。 - 通过两个嵌套循环,遍历塔在该区域内影响的每个网格点
(i, j)
。
- 对每个塔的坐标
-
计算信号强度:
- 对于每个点
(i, j)
,计算到塔(e[0], e[1])
的平方距离d
。 - 如果距离小于或等于半径
radius
,则将该塔对网格点的信号强度计算并叠加:- 计算公式为:
信号强度 = e[2] / (1 + sqrt(d))
,即信号强度随距离增加而减弱。
- 计算公式为:
- 对于每个点
-
记录最大信号强度和对应的坐标:
- 对每个点
(i, j)
,如果其信号强度超过maxPower
,则更新最大信号强度和坐标(x, y)
。 - 如果信号强度相同,则优先选择
x
较小的点,若x
相同则选择y
较小的点,以保证返回的点最靠左上角。
- 对每个点
-
返回:
- 最终返回信号强度最大的点
(x, y)
。
- 最终返回信号强度最大的点
时间复杂度分析
-
塔的遍历:
- 共有
n
个塔,每个塔影响的范围最多是一个半径为radius
的正方形区域(最多涉及 O(radius^2) 个网格点)。 - 由于网格的最大尺寸为 51x51,因此理论上每个塔最多可以影响 51x51 = 2601 个网格点。
- 共有
-
总的时间复杂度:
- 外层循环遍历每个塔
O(n)
次,内层循环遍历塔的影响范围(最坏情况下最多 2601 次)。 - 因此,总时间复杂度为 O(n * min(radius^2, 2601)),即遍历每个塔并计算其影响的时间。
- 外层循环遍历每个塔
空间复杂度分析
- 空间复杂度:
grid
是一个 51x51 的二维数组,空间复杂度为 O(51^2) = O(2601)。- 其他变量使用的空间是常数级的,因此总的空间复杂度为 O(1) + O(51^2) = O(2601)。
算法优势
- 高效网格计算:
- 通过限制网格大小为 51x51,减少了不必要的复杂度。
- 只计算在半径范围内的网格点,优化了计算过程。
- 信号强度衰减公式:
- 使用
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;}
};
这段代码的任务是计算两个数 a
和 b
之间的公因子(即同时能整除 a
和 b
的整数)的数量。我们从算法角度对其进行分析。
代码分析
-
输入与输出:
- 输入:两个整数
a
和b
。 - 输出:
a
和b
的公因子数量。
- 输入:两个整数
-
步骤解析:
-
最大公约数 (gcd):
- 首先计算
a
和b
的最大公约数g
。两个数的公因子是它们最大公约数的因子,因此只需要计算g
的因子即可。
- 首先计算
-
遍历因子:
- 遍历从
1
到sqrt(g)
,判断每个数i
是否为g
的因子(即g % i == 0
)。 - 如果
i
是g
的因子,那么它和g/i
都是公因子。 - 对于每个符合条件的
i
:- 如果
i * i == g
,说明i
是g
的平方根,因此只增加一次公因子。 - 如果
i * i < g
,则g/i
也是一个不同的公因子,增加两次公因子(一次为i
,一次为g/i
)。
- 如果
- 遍历从
-
-
返回公因子数量:
- 最终将计数的公因子数量返回。
代码示例分析
假设 a = 12
和 b = 18
:
- 计算
gcd(12, 18)
,即最大公约数g = 6
。 6
的因子有1, 2, 3, 6
,因此公因子数量为 4。- 代码遍历
i = 1, 2
,每次找到两个因子:i
和g/i
。
时间复杂度分析
-
计算 gcd:
- 计算两个数
a
和b
的最大公约数的时间复杂度为 O(log(min(a, b))),这是通过欧几里得算法实现的。
- 计算两个数
-
遍历因子:
- 我们遍历从
1
到sqrt(g)
,每次检查g
是否能被整除,因此这部分的时间复杂度是 O(sqrt(g))。
- 我们遍历从
因此,总的时间复杂度是 O(log(min(a, b)) + sqrt(g)),其中 g
是 a
和 b
的最大公约数。
空间复杂度分析
- 空间复杂度:
- 该算法只使用了几个常数变量(如
g
,ans
,i
),因此空间复杂度为 O(1)。
- 该算法只使用了几个常数变量(如
算法优势
-
效率高:
- 通过先计算最大公约数
g
,将问题简化为找到g
的因子,而不是直接在a
和b
上操作。这样可以显著减少不必要的计算。
- 通过先计算最大公约数
-
数学性质的利用:
- 使用最大公约数的性质减少了计算复杂度,同时利用
i
和g/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
。代码中使用了双指针技术(滑动窗口)来查找符合条件的组合。接下来从算法的角度分析该代码。
代码解析
-
输入与输出:
- 输入:一个目标值
target
。 - 输出:所有连续整数组合的集合,每个组合的和等于
target
。
- 输入:一个目标值
-
核心思想:
- 使用两个指针
l
(左边界)和r
(右边界),表示当前窗口的左右边界。 - 初始时,
l
指向 1,r
指向 2,表示当前窗口是[l, r]
之间的整数。 - 计算窗口内整数的和
sum
,如果sum
等于target
,记录这组连续整数。如果sum
小于target
,右边界r
向右扩展;如果sum
大于target
,左边界l
向右收缩。 - 这一过程通过滑动窗口的方式遍历所有可能的连续整数组合,直到
l >= r
。
- 使用两个指针
-
详细步骤:
-
双指针初始化:初始时
l = 1
,r = 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
时,结束循环。
-
-
返回结果:
- 最后返回所有符合条件的组合。
时间复杂度分析
-
双指针遍历:
- 每次循环中,指针
l
和r
至少有一个会移动,直到l
达到target
的一半左右。双指针的移动使得时间复杂度为 O(target)。
- 每次循环中,指针
-
每个区间求和:
- 求和操作是通过公式计算的,因此求和的时间复杂度是 O(1)。
因此,总的时间复杂度为 O(target),在大多数情况下是比较高效的。
空间复杂度分析
- 存储结果:
- 结果存储在二维向量
vec
中,每个找到的解都会被存储,因此空间复杂度主要取决于存储的结果。
- 结果存储在二维向量
- 其他空间开销:
- 除了存储结果外,使用了常数级别的额外空间来存储
l
、r
、sum
和中间结果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 都是整数。该代码通过离散化坐标轴和使用差分数组的方法实现了这一目标。接下来对该代码进行算法解析。
代码解析
-
输入与输出:
- 输入:一个二维数组
circles
,其中每个元素代表一个圆形的坐标和半径,circles[i] = {x, y, r}
表示圆心在 $(x, y)$,半径为 $r$。 - 输出:被所有圆形覆盖的整点坐标的数量。
- 输入:一个二维数组
-
核心思路:
- 网格离散化:首先计算出所有圆形的最右、最上边界,以确定可以处理的最大网格范围。
- 差分数组:在圆形的覆盖区域内,通过使用差分数组
d
来标记整点是否被某个圆形覆盖。这种方法可以有效地减少遍历和判断的次数。 - 圆的扫描:对于每个圆,分为上半部分和下半部分分别处理,并根据圆的半径计算出覆盖的整点范围。
- 计算覆盖区域:通过累加差分数组
d
的值来确定每个整点被覆盖的次数,如果某个点的覆盖次数大于 0,则它是被覆盖的点。
-
详细步骤:
-
确定网格的大小:遍历所有的圆形,计算每个圆形的最右边界
x + r
和最上边界y + r
,以此来确定网格的最大范围maxx
和maxy
。 -
初始化差分数组:定义一个大小为
(maxy + 2) x (maxx + 2)
的二维数组d
,用于记录每个网格点的差分值,初始时所有值为 0。 -
标记圆的覆盖区域:
- 对于每个圆形,分别处理其上半部分和下半部分。
- 对于上半部分,从圆心开始,逐渐向上扫描每一行,计算在当前行中,被圆覆盖的整点的左右边界
x1
和x2
,并使用差分数组标记该行的覆盖范围。 - 对于下半部分,方法类似,只是逐渐向下扫描每一行。
-
计算覆盖的整点数量:
- 在最后的网格扫描中,通过累加差分数组
d
的值,确定每个整点是否被覆盖。如果某个点的累计覆盖值大于 0,则说明该点被覆盖,计入结果。
- 在最后的网格扫描中,通过累加差分数组
-
-
算法的复杂度分析:
-
时间复杂度:遍历每个圆形时,需要根据圆的半径对每一行进行扫描。对于每个圆,扫描和标记的操作与圆的半径相关。假设圆的最大半径为
R
,总的时间复杂度近似为 O(R \cdot n),其中n
是圆的数量。 -
空间复杂度:差分数组的大小是
maxx * maxy
,即覆盖网格的大小。因此,空间复杂度为 O(maxx \cdot maxy)。
-
示例分析
假设输入为 circles = {{2, 2, 1}, {3, 3, 2}}
,即包含两个圆:
- 第一个圆的圆心为 $(2, 2)$,半径为 1。计算其覆盖的整点坐标。
- 第二个圆的圆心为 $(3, 3)$,半径为 2。计算其覆盖的整点坐标。
- 使用差分数组标记这两个圆的覆盖区域。
- 通过累加差分数组,得出被覆盖的整点坐标的总数量。
总结
该算法通过使用差分数组高效地标记并计算圆形覆盖的整点数量,避免了直接遍历网格上所有点的高时间复杂度。通过离散化网格并处理圆的上、下半部分,算法在空间和时间效率上得到了较好的优化。
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'
组成的最大的正方形,并返回其面积。
代码解析
-
输入与输出:
- 输入:二维矩阵
matrix
,每个元素是字符'0'
或'1'
,表示是否是矩形中的某个位置。 - 输出:找到由
'1'
组成的最大的正方形,并返回其面积。
- 输入:二维矩阵
-
核心思路:
- 动态规划的思想用于计算以每个位置为右下角的最大正方形的边长。
- 定义一个二维动态规划数组
dp
,其中dp[i][j]
表示以位置(i, j)
为右下角的最大正方形的边长。
-
动态规划转移方程:
- 对于位置
(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
。
- 对于位置
-
最终面积:
- 最大正方形的面积为
lenth * lenth
,这是返回结果。
- 最大正方形的面积为
详细步骤:
-
初始化:
- 使用
vector<vector<int>> dp(matrix.size(), vector<int>(matrix[0].size(), 0))
初始化动态规划数组dp
,所有元素初始为 0。 lenth
记录当前找到的最大正方形的边长,初始为 0。
- 使用
-
遍历矩阵:
- 双重循环遍历矩阵的每个位置
(i, j)
:- 如果当前值
matrix[i][j] == '1'
,则更新dp[i][j]
为相应的边长,并更新最大边长lenth
。 - 如果
matrix[i][j] == '0'
,则跳过该位置。
- 如果当前值
- 双重循环遍历矩阵的每个位置
-
计算结果:
- 返回
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']
]
- 遍历到
matrix[2][2]
时,由于其上方、左方和左上方的元素都为'1'
,则dp[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
的连续子数组的个数。
代码解析
-
输入与输出:
- 输入:整数数组
nums
和目标值k
。 - 输出:数组中和为
k
的连续子数组的个数。
- 输入:整数数组
-
核心思路:
- 通过累加前缀和来避免重复计算子数组的和。
- 使用一个哈希表(
unordered_map
)来记录每个前缀和出现的次数,快速判断当前前缀和与目标值k
之间的关系。
-
详细步骤:
- 定义一个哈希表
mp
,其中键表示某个前缀和,值表示该前缀和出现的次数。- 初始化
mp[0] = 1
,表示前缀和为 0 的情况出现了一次。
- 初始化
- 通过遍历数组
nums
,逐个累加当前元素到pre
,表示当前位置的前缀和。 - 在每次计算出新的前缀和后,检查
pre - k
是否存在于哈希表mp
中:- 如果存在,说明从某个先前的位置到当前的位置之间的子数组的和为
k
,因此将对应的出现次数加到计数count
中。
- 如果存在,说明从某个先前的位置到当前的位置之间的子数组的和为
- 将当前的前缀和
pre
记录到哈希表中,增加其出现的次数。
- 定义一个哈希表
-
变量解释:
mp
:哈希表,记录每个前缀和出现的次数。count
:统计符合条件的子数组个数。pre
:当前的前缀和。nums
:输入数组。k
:目标值。
动态过程
假设有一个数组 nums = [1, 1, 1]
,目标值 k = 2
:
- 初始时:
mp = {0: 1}
,表示前缀和 0 出现过一次。pre = 0
,count = 0
。
- 遍历数组:
- 第一轮:
- 当前元素
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) 的时间复杂度,非常高效。