📌  相关文章
📜  满足给定条件的 [L, R] 范围内的数字计数(1)

📅  最后修改于: 2023-12-03 14:56:10.382000             🧑  作者: Mango

满足给定条件的 [L, R] 范围内的数字计数

在算法竞赛和实际开发中,有很多涉及到满足给定条件的数字计数问题,这类问题通常需要寻找一种高效的计算方法。本篇文章就来介绍一些常见的求解数字计数问题的方法,以及如何使用这些方法实现高效的代码,满足我们在算法竞赛和实际开发中的需要。

前缀和

前缀和(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];
}

通过计算前缀和数组,我们可以解决以下问题:

  1. 区间和查询

    给定一个数组 a,查询区间 [l,r] 的和。

    思路:通过计算前缀和数组 s,可以在 O(1) 的时间复杂度内回答一个区间和查询问题。

    具体实现:令 s 为 a 数组的前缀和数组,则

    s[r + 1] - s[l]

  2. 区间平均数查询

    给定一个数组 a,查询区间 [l,r] 的平均值。

    思路:将区间 [l,r] 的和除以区间长度即可。

    具体实现:令 val = (s[r + 1] - s[l]) / (r - l + 1)。

  3. 计算子数组的和等于 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;
}

以下为二分查找的应用场景:

  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;
    }
    
  2. 查找满足条件的最大值

    给定一个有序数组,查找数组中最后一个小于等于给定值的元素的位置,即满足 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

数位 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 的应用场景:

  1. 数字的位数问题

    给定两个正整数 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);
    }
    
  2. 含有特定数字的数字计数问题

    给定两个正整数 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 适合处理数字取值范围较小,但位数较大的问题。