离散化

离散化

1. 离散化原理

原理

  • 一般来说,当数据范围很大,而我们只需要数据的相对顺序时,就可以使用保序离散化。

  • 保序离散化:例如给定我们数据-5, 7, 5211314, -1001, -5,我们可以分为如下几步进行离散化:

    (1)排序,得到:-1001, -5, -5, 7, 5211314

    (2)去重,得到:-1001, -5, 7, 5211314

    (3)离散化,可以让这四个数据分别映射到0~3或者1~4,这个需要根据题目选择(例如前缀和或者树状数组应该映射到1~4)。

  • 另外还有一个问题,就是如何得到每个数据映射到的值?这可以使用二分。可以单独使用一个函数实现(例如名为find或者get)。

2. AcWing上的离散化题目

AcWing 802. 区间和

问题描述

分析

  • 根据数据范围可以看出,本题中的数据范围很大,在 [ − 1 0 9 , 1 0 9 ] [-10^9, 10^9] [109,109]之间,但是我们用到的这个区间中的数据最多有 3 × 1 0 5 3 \times 10 ^ 5 3×105个,因此我们可以把使用到的数据映射到1~n(其中n是不重复的数据的个数)。

  • 因为要求某个区间和,上述离散化应该使用保序离散化。同时求区间和,可以使用前缀和技巧,前缀和要求下标从1开始,因此find(返回数据对应离散化后的下标)函数返回的下标应该从1开始。

代码

  • C++
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

typedef pair<int, int> PII;

const int N = 300010;

int n, m;
int a[N], s[N];

vector<int> alls;  // 待离散的数据
vector<PII> add, query;  // 操作

int find(int x) {
    return lower_bound(alls.begin(), alls.end(), x) - alls.begin() + 1;
}

int main() {
    
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i++) {
        int x, c;
        scanf("%d%d", &x, &c);
        add.push_back({x, c});
        
        alls.push_back(x);
    }
    for (int i = 0; i < m; i++) {
        int l, r;
        scanf("%d%d", &l, &r);
        query.push_back({l, r});
        
        alls.push_back(l);
        alls.push_back(r);
    }
    
    // 排序、去重
    sort(alls.begin(), alls.end());
    alls.erase(unique(alls.begin(), alls.end()), alls.end());
    
    for (auto item : add) {
        int x = find(item.first);
        a[x] += item.second;
    }
    
    for (int i = 1; i <= alls.size(); i++) s[i] = s[i - 1] + a[i];
    
    for (auto item : query) {
        int l = find(item.first), r = find(item.second);
        printf("%d\n", s[r] - s[l - 1]);
    }
    
    return 0;
}
  • Java
import java.util.*;

public class Main {

    private static class MyPair {
        public int x;
        public int y;

        public MyPair(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }

    // 寻找 x 对应的离散化后的数据,这里从 1 开始,为了方便计算前缀和
    private static int find(List<Integer> alls, int x) {

        int l = 0, r = alls.size() - 1;
        while (l < r) {
            int mid = (r - l) / 2 + l;
            if (alls.get(mid) >= x) r = mid;
            else l = mid + 1;
        }

        return r + 1;
    }
	
    // Leetcode 0026
    private static int unique(List<Integer> alls) {

        int j = 0;
        for (int i = 0; i < alls.size(); i++)
            if (i == 0 || alls.get(i) != alls.get(i - 1))
                alls.set(j++, alls.get(i));
        // alls[0] ~ alls[j - 1] 所有a中不重复的数
        return j;
    }

    public static void main(String[] args) {

        // 读入数据
        Scanner scan = new Scanner(System.in);
        int n = scan.nextInt();  // 操作的个数
        int m = scan.nextInt();  // 询问的个数
        List<Integer> alls = new ArrayList<>();  // 需要离散化的数据
        List<MyPair> add = new ArrayList<>(); // 操作
        for (int i = 0; i < n; i++) {
            int x = scan.nextInt(), c = scan.nextInt();
            add.add(new MyPair(x, c));
            alls.add(x);
        }
        List<MyPair> query = new ArrayList<>();  // 询问
        for (int i = 0; i < m; i++) {
            int l = scan.nextInt(), r = scan.nextInt();
            query.add(new MyPair(l, r));
            alls.add(l);
            alls.add(r);
        }

        // 算法代码
        int N = n + 2 * m + 10;
        int[] a = new int[N];  // a[i] 代表离散到 i 的原始坐标点对应的数据
        int[] s = new int[N];  // a数组前缀和
        // 离散化:排序、去重
        Collections.sort(alls);
        int u = unique(alls);
        alls = alls.subList(0, u);
        // 处理插入
        for (MyPair pair : add) {
            int x = find(alls, pair.x);  // 获取离散化后的值
            a[x] += pair.y;
        }
        // 预处理前缀和
        for (int i = 1; i <= alls.size(); i++) s[i] = s[i - 1] + a[i];
        // 处理问询
        for (MyPair pair : query) {
            int l = find(alls, pair.x), r = find(alls, pair.y);
            System.out.println(s[r] - s[l - 1]);
        }
    }
}

3. 力扣上的离散化题目

Leetcode 0327 区间和的个数

题目描述:Leetcode 0327 区间和的个数

在这里插入图片描述

分析

  • 本题的考点:树状数组、前缀和、离散化

  • 我们首先求出数组nums的前缀和数组s,当我们考虑s[i]时,我们需要找到存在多少个j 0 ≤ j ≤ i − 1 0 \le j \le i-1 0ji1),使得s[i] - s[j]lowerupper之间。即 l o w e r ≤ s [ i ] − s [ j ] ≤ u p p e r lower \le s[i] - s[j] \le upper lowers[i]s[j]upper,变换一下得到: s [ i ] − u p p e r ≤ s [ j ] ≤ s [ i ] − l o w e r s[i] -upper \le s[j] \le s[i] - lower s[i]uppers[j]s[i]lower

  • 没有离散化之前,当前如果考虑的是s[i],则树状数组中维护的是s[0]~s[i-1]的个数,但是现在存在的问题是s可能为负数,无法作为下标,因此需要进行离散化。

  • 因为j可能取到0,所以我们需要s[0],而s[0]的值为0,因此0也需要被离散化。假设get(x)返回x离散化后对应的值,query(x)返回1~x中数据的个数,则对于当前考虑的s[i],存在的合法的连续子数组的个数为query(get(s[i]-lower)) - query(get(s[i]-upper-1))

  • 因此,我们需要离散化的值有:0s[i]s[i]-lowers[i]-upper-1

  • 因为树状数组的下标是从1开始的,因此获取某个数据离散化后对应的下标需要加上一个偏移量1

代码

  • C++
typedef long long LL;

// 前缀和 + 离散化 + 树状数组(下标从1开始)
class Solution {
public:
    int m;
    vector<int> tr;  // 树状数组
    vector<LL> all;  // 待离散化的数据

    // 返回x在离散化后的数组中的位置(从1开始)
    int get(LL x) {
        return lower_bound(all.begin(), all.end(), x) - all.begin() + 1;
    }

    int lowbit(int x) {
        return x & -x;
    }

    void add(int x, int v) {
        for (int i = x; i <= m; i += lowbit(i)) tr[i] += v;
    }

    int query(int x) {
        int res = 0;
        for (int i = x; i; i -= lowbit(i))res += tr[i];
        return res;
    }

    int countRangeSum(vector<int> &nums, int lower, int upper) {

        int n = nums.size();
        vector<LL> s(n + 1);  // 前缀和
        all.push_back(s[0]);
        for (int i = 1; i <= n; i++) {
            s[i] = s[i - 1] + nums[i - 1];
            all.push_back(s[i]);
            all.push_back(s[i] - lower);
            all.push_back(s[i] - upper - 1);
        }
        sort(all.begin(), all.end());
        all.erase(unique(all.begin(), all.end()), all.end());
        m = all.size();
        tr.resize(m + 1);

        int res = 0;
        // 为什么有下面这句话:https://www.acwing.com/activity/content/code/content/477194/
        // 相当于考虑分析中的 sj = 0 --> lower <= si <= upper 这种情况
        add(get(s[0]), 1);
        for (int i = 1; i <= n; i++) {
            res += query(get(s[i] - lower)) - query(get(s[i] - upper - 1));
            add(get(s[i]), 1);
        }
        return res;
    }
};
  • Java
class Solution {

    int m;  // 离散化后数据的个数
    int[] tr;  // 树状数组(下标从1开始)
    List<Long> alls = new ArrayList<>();  // 待离散化的数据,第一个数对应下标为1

    public int countRangeSum(int[] nums, int lower, int upper) {

        int n = nums.length;
        long[] s = new long[n + 1];
        alls.add(s[0]);
        for (int i = 1; i <= n; i++) {
            s[i] = s[i - 1] + nums[i - 1];
            alls.add(s[i]);
            alls.add(s[i] - lower);
            alls.add(s[i] - upper - 1);
        }
        // 离散化:排序、去重
        Collections.sort(alls);
        alls = alls.subList(0, unique(alls));

        m = alls.size();
        tr = new int[m + 1];

        int res = 0;
        add(get(s[0]), 1);
        for (int i = 1; i <= n; i++) {
            res += query(get(s[i] - lower)) - query(get(s[i] - upper - 1));
            add(get(s[i]), 1);
        }
        return res;
    }

    // 树状数组
    private int lowbit(int x) {
        return x & -x;
    }
    private void add(int x, int v) {
        for (int i = x; i <= m; i += lowbit(i)) tr[i] += v;
    }
    private int query(int x) {
        int res = 0;
        for (int i = x; i > 0; i -= lowbit(i)) res += tr[i];
        return res;
    }

    // 返回数据对应离散化后的值(从1开始)
    private int get(long x) {
        int l = 0, r = alls.size() - 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (alls.get(mid) >= x) r = mid;
            else l = mid + 1;
        }
        return r + 1;
    }
    // 返回去重后应该保留的数据个数: Leetcode 0026 删除排序数组的重复项
    private int unique(List<Long> alls) {
        final int T = 1;
        int k = 0;
        for (int i = 0; i < alls.size(); i++)
            if (k - T < 0 || alls.get(i) != alls.get(k - T))
                alls.set(k++, alls.get(i));
        return k;
    }
}

时空复杂度分析

  • 时间复杂度: O ( n × l o g ( n ) ) O(n \times log(n)) O(n×log(n))n为数组长度。

  • 空间复杂度: O ( n ) O(n) O(n)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值