📅  最后修改于: 2023-12-03 14:56:10.382000             🧑  作者: Mango
在算法竞赛和实际开发中,有很多涉及到满足给定条件的数字计数问题,这类问题通常需要寻找一种高效的计算方法。本篇文章就来介绍一些常见的求解数字计数问题的方法,以及如何使用这些方法实现高效的代码,满足我们在算法竞赛和实际开发中的需要。
前缀和(Prefix Sum)是数组的一个基础概念,它可以帮助我们高效地回答许多区间询问问题。前缀和的思想是将原始数组中的元素依次累加起来,得到新的数组,并用该数组回答区间查询问题。
对于一个序列 a,其前缀和数组 b 的第 i 项表示原序列 a 中前 i 个数之和。即
b[i] = a[0] + a[1] + ... + a[i]。
利用前缀和,我们可以在 O(1) 的时间复杂度内回答一个区间和查询问题。当然,在得到前缀和数组之后,要回答区间和查询的时间复杂度为 O(1),但我们需要额外花费 O(n) 的时间来计算前缀和数组。
以下为前缀和数组的计算模板:
int n = a.size();
vector<int> s(n + 1, 0);
for (int i = 1; i <= n; ++i) {
s[i] = s[i - 1] + a[i - 1];
}
通过计算前缀和数组,我们可以解决以下问题:
区间和查询
给定一个数组 a,查询区间 [l,r] 的和。
思路:通过计算前缀和数组 s,可以在 O(1) 的时间复杂度内回答一个区间和查询问题。
具体实现:令 s 为 a 数组的前缀和数组,则
s[r + 1] - s[l]
区间平均数查询
给定一个数组 a,查询区间 [l,r] 的平均值。
思路:将区间 [l,r] 的和除以区间长度即可。
具体实现:令 val = (s[r + 1] - s[l]) / (r - l + 1)。
计算子数组的和等于 k 的个数
给定一个数组 a 和一个数 k,计算有多少个子数组的和等于 k。
思路:使用前缀和计算前缀和数组 s,然后使用哈希表来记录每个前缀和出现的次数,从而计算每个前缀和与 k 的差值,在哈希表中查找其出现的次数。
具体实现:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> umap;
umap[0] = 1;
int s = 0, ans = 0;
for (auto x : nums) {
s += x;
if (umap.count(s - k)) ans += umap[s - k];
++umap[s];
}
return ans;
}
我们在实际开发中经常需要对某些数据进行查找,二分查找就是一种高效的查找方法,其时间复杂度为 O(logn)。
二分查找的思想是,将有序数组分成三部分:位于中心的数 num,左边的一部分数组 left,右边的一部分数组 right。比较 num 与目标值 target 的大小关系,如果 num 大于 target,则在 left 中继续查找;如果 num 小于 target,则在 right 中继续查找;如果 num 等于 target,则查找成功。
以下为二分查找的模板:
int L = 0, R = n - 1;
while (L < R) {
int mid = L + R + 1 >> 1;
if (check(mid)) L = mid;
else R = mid - 1;
}
以下为二分查找的应用场景:
查找满足条件的最小值
给定一个有序数组,查找数组中第一个大于等于给定值的元素的位置,即满足 nums[i] >= x 的最小 i。
具体实现:
int lower_bound(vector<int>& nums, int target) {
int n = nums.size();
int L = 0, R = n;
while (L < R) {
int mid = L + R >> 1;
if (nums[mid] >= target) R = mid;
else L = mid + 1;
}
return L;
}
查找满足条件的最大值
给定一个有序数组,查找数组中最后一个小于等于给定值的元素的位置,即满足 nums[i] <= x 的最大 i。
具体实现:
int upper_bound(vector<int>& nums, int target) {
int n = nums.size();
int L = 0, R = n;
while (L < R) {
int mid = L + R + 1 >> 1;
if (nums[mid] <= target) L = mid;
else R = mid - 1;
}
return L;
}
数位 DP(Digit Dynamic Programming)是一种重要的动态规划算法,用于解决数字计数问题。数位 DP 的基本思想是将数字按位分解,从高位到低位进行搜索或动态规划转移,然后组合得到最终的答案。
以下为数位 DP 的模板:
int dfs(int pos, int state /* 其他参数... */) {
// 边界情况
if (pos == -1) return check(state /* 其他参数... */);
int& dp = mem[pos][state];
if (dp != -1) return dp;
int res = 0;
for (int i = 0; i <= 9; ++i) {
res += dfs(pos - 1, update(state, i) /* 其他参数... */);
}
return dp = res;
}
以下为数位 DP 的应用场景:
数字的位数问题
给定两个正整数 L 和 R,计算有多少个满足 L <= n <= R 且 n 的位数小于等于 k 的正整数。
思路:从高位到低位进行搜索,每个位置有 0~9 十种选择。
具体实现:
void init() {
for (int i = 0, j = 1; i <= 9; ++i, j *= 10) {
for (int k = 1; k <= K; ++k) {
dp[0][k][i] = 1;
}
for (int n = 1; n < N; ++n) {
for (int k = 1; k <= K; ++k) {
for (int d = 0; d <= 9; ++d) {
dp[n][k][d] += dp[n - 1][k - (d > 0)][d];
}
}
}
}
}
int solve(int x) {
if (x == 0) return 1;
vector<int> digits;
for (; x; x /= 10) digits.push_back(x % 10);
int res = 0;
int k = digits.size();
for (int i = k - 1; i >= 0; --i) {
int d = digits[i];
for (int j = (i == k - 1); j < d; ++j) {
res += dp[i][K][j];
}
K -= (i > 0 && d == 0);
}
return res + (K == 0);
}
含有特定数字的数字计数问题
给定两个正整数 L 和 R,计算有多少个满足 L <= n <= R 且 n 不包含特定数字(如数字 4)的正整数。
思路:从高位到低位进行搜索,每个位置有 0~9 十种选择。
具体实现:
void init() {
memset(dp, -1, sizeof(dp));
dp[0][0][0] = 1;
for (int i = 1; i <= 10; ++i) {
for (int j = 0; j < 2; ++j) {
for (int s = 0; s <= 81; ++s) {
for (int d = 0; d <= (j ? 9 : digit[i]); ++d) {
dp[i][j || d < digit[i]][s + d] += dp[i - 1][j][s];
}
}
}
}
}
int solve(int x) {
int res = 0;
for (int i = 1, s = 0; i <= 10; ++i) {
digit[i] = (x % 10);
x /= 10;
if (i > 1) {
for (int j = 0; j < 2; ++j) {
for (int sum = 0; sum <= s + (j ? 9 : digit[i]); ++sum) {
res += dp[i - 1][j][sum];
}
}
}
s += digit[i];
}
return res;
}
数位 DP 的时间复杂度一般是 O(位数 * 数字的取值范围 * 其他参数的数量)。由此可见,数位 DP 适合处理数字取值范围较小,但位数较大的问题。