位运算,集散化与区间合并(附例题)

位运算

1,如何求一个数的二进制表示中第k个元素

  • 先将第 k 位移动到最后一位,采用右移操作: n >> k
  • 查看最后一位是多少: x & 1
  • 前两步结合起来 : n >> k & 1

例如下面程序,最终输出 1010,即 10 的二进制表示

#include <iostream>
using namespace std;
int main()
{
	int n = 10;
	for(int k = 3; k >= 0; k --) cout << (n >> k & 1); 
	return 0;
}

2,lowbit() 运算

lowbit (x) 返回 x 的最后一位 1 的位置(树状数组基本操作之一)
例子: x = 1010, lowbit(x) = 10
x = 10010000, lowbit(x) = 10000

具体操作的时候,只需将 x & -x 即可,原理如下:
-x = ~x + 1(取反加1)
例子:
x = 1010100010000
~x = 0101011101111
~x + 1 = 0101011110000
x & -x = x & (~x+1) = 0000000010000 = 10000

例题:二进制中 1 的个数

题目链接: 二进制中 1 的个数.

题目:
给定一个长度为n的数列,请你求出数列中每个数的二进制表示中1的个数。

输入格式
第一行包含整数n。第二行包含n个整数,表示整个数列。

输出格式
共一行,包含n个整数,其中的第 i 个数表示数列中的第 i 个数的二进制表示中1的个数。

数据范围
1≤n≤100000,
0≤数列中元素的值≤109

输入样例:
5
1 2 3 4 5

输出样例:
1 1 2 1 2

本题采用 lowbit() 操作来做 ~ 具体代码如下

#include <iostream>
using namespace std;

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

int main()
{
    int n;
    cin >> n;
    while(n--)
    {
        int x;
        cin >> x;
        
        int ans = 0;
        while(x)
        {
            x -= lowbit(x);// x 每次减去 x 的最后一位 1 
            ans ++;//统计减了多少次,即 x 中有多少个 1
        }
        cout << ans << " ";
    }
    return 0;
}

离散化(整数)

离散化是指以下情况:
假如现在有一个值域为 0 - 10^9 ,元素个数为 10 ^ 5 的数组,为了不定义一个这么大的数组,我们可以采用离散化操作
例子: 在 1,5,8,388,10000 位置上各有一个数,我们可以将其映射到 1,2,3,4, 5 的位置,便于后续计算
其实这个操作相当于将在大范围内分散分布的数聚集起来(压缩),且不改变其相对顺序
映射过程中可能有以下问题需要解决:
1,原数组中有重复元素
2,如何算出原数组离散化后的值

1,代码模板

vector<int> alls;//存储所有待离散化的值
sort(alls.begin(),alls.end());//将所有值排序
alls.erase(unique(alls.begin(),alls.end()),alls.end());//去掉重复元素
//unique() 的作用是去重,并将重复元素放到数组最后,返回第一个重复元素下标,之后从这个下标开始到数组结尾,
//用 erase 删除元素,即去掉了所有重复元素

//二分法求出 x对应的离散化的值
int find(int x)//找到第一个大于等于 x 的位置
{
	int l = 0,r = all.size() - 1;
	while(l < r)
	{
		int mid = l + r >> 1;
		if(alls[mid] > = x) r = mid;
		else l = mid + 1;
	}
	return r + 1;//映射到 1,2,...., n(如果不加1,映射到 0,1,..., n - 1)
}

2,例题:区间和

题目链接: 区间和.

题目:
假定有一个无限长的数轴,数轴上每个坐标上的数都是 0。现在,我们首先进行 n 次操作,每次操作将某一位置 x 上的数加 c。接下来,进行 m 次询问,每个询问包含两个整数 l 和 r,你需要求出在区间 [ l , r ] 之间的所有数的和。

输入格式
第一行包含两个整数 n 和 m。接下来 n 行,每行包含两个整数 x 和c。再接下里 m 行,每行包含两个整数 l 和 r。

输出格式
共 m 行,每行输出一个询问中所求的区间内数字和。

数据范围
−10^9 ≤ x ≤ 10 ^ 9,
1 ≤ n , m ≤ 10 ^ 5,
−10^ 9 ≤ l ≤ r ≤ 10 ^ 9,
−10000 ≤ c ≤ 10000

输入样例:
3 3
1 2
3 6
7 5
1 3
4 6
7 8

输出样例:
8
0
5

解题思路:
本题中 n 个坐标涉及的范围很大,但是相比之下,坐标个数并没有那么多,稀疏分布在坐标轴上,所以我们可以采用离散化操作,将这些坐标映射到 从 1 开始的坐标上去,这样我们不用开一个长度为 10^9 的数组,只需要开 n + 2m 大小的数组即可(因为最多有 n + 2m 个不同的坐标值,m次询问每次两个坐标)。询问时要求一下区间和,这块儿可以用之前记录过的前缀和来做。

前缀和指南: 前缀和与差分数组(附练习题).

代码如下(附注释):

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

typedef pair<int,int> PII;

const int N = 300010;//这里 N 设置为300000 是因为,我们可能用到 n + 2*m 个坐标
int a[N], s[N];//离散化后,n 次操作中的每一个坐标 x 都被映射到 a[N] 数组中,之后利用 a[N] 来计算及保存对应坐标位置加 c 后的结果
               //s[N] 用来计算 a[N] 的前缀和
int n, m;
               
vector<int> alls;//用来离散化
vector<PII> add, quary;//add 保存输入的加操作对应的坐标和加数,quary 保存查询时的左右坐标

int find(int x)
{
    int l = 0, r = alls.size() - 1;
    while(l < r)
    {
        int mid = l + r >> 1;
        if(alls[mid] >= x) r = mid;
        else l = mid + 1;
    }
    return r + 1;//映射到 a[1],...,a[k]
}

int main()
{
    cin >> n >> m;
    
    //输入及保存加操作坐标和加数
    while(n--)
    {
        int x, c;
        cin >> x >> c;
        alls.push_back(x);
        add.push_back({x, c});
    }
    
    //输入及保存查询操作的坐标
    while(m--)
    {
        int l, r;
        cin >> l >> r;
        alls.push_back(l);
        alls.push_back(r);
        
        quary.push_back({l, r});
    }
    
    //去重
    sort(alls.begin(),alls.end());
    alls.erase(unique(alls.begin(), alls.end()), alls.end());
    
    //处理给某个坐标值对应的数加一个数 c 的操作
    for(auto item:add)
    {
        int i = find(item.first);
        a[i] += item.second;
    }
    
    //求前缀和
    for(int i = 1; i <= alls.size(); i ++) s[i] = s[i - 1] + a[i];
    
    //处理查询操作
    for(auto item : quary)
    {
        int l = find(item.first);
        int r = find(item.second);
        cout << s[r] - s[l - 1] << endl;
    }
    
    return 0;
}

补充 unique() 的实现方法:
虽然C++有这个函数,但是对于 JAVA 和 Python 来说,需要再写一个unique 函数,函数实现及应用如下所示:

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

typedef pair<int,int> PII;

const int N = 300010;//这里 N 设置为300000 是因为,我们可能用到 n + 2*m 个坐标
int a[N], s[N];//离散化后,n 次操作中的每一个坐标 x 都被映射到 a[N] 数组中,之后利用 a[N] 来计算及保存对应坐标位置加 c 后的结果
               //s[N] 用来计算 a[N] 的前缀和
int n, m;
               
vector<int> alls;//用来离散化
vector<PII> add, quary;//add 保存输入的加操作对应的坐标和加数,quary 保存查询时的左右坐标

vector<int>::iterator unique(vector<int> &a)
{
    int j = 0;
    for(int i = 0; i < a.size(); i ++)
    {
        if(!i || a[i] != a[i - 1])//i = 0 或 a[i] 为重复元素中第一个出现的元素
            a[j ++] = a[i];//a[0] ~ a[j - 1] 保存了a[] 中所有不重复元素,a[j] 开始到最后的元素无用,可以删去
    }
    return a.begin() + j;//返回指向第一个要删除的元素的下标
}

int find(int x)
{
    int l = 0, r = alls.size() - 1;
    while(l < r)
    {
        int mid = l + r >> 1;
        if(alls[mid] >= x) r = mid;
        else l = mid + 1;
    }
    return r + 1;//映射到 a[1],...,a[k]
}

int main()
{
    cin >> n >> m;
    
    //输入及保存加操作坐标和加数
    while(n--)
    {
        int x, c;
        cin >> x >> c;
        alls.push_back(x);
        add.push_back({x, c});
    }
    
    //输入及保存查询操作的坐标
    while(m--)
    {
        int l, r;
        cin >> l >> r;
        alls.push_back(l);
        alls.push_back(r);
        
        quary.push_back({l, r});
    }
    
    //去重
    sort(alls.begin(),alls.end());
    alls.erase(unique(alls), alls.end());
    
    //处理给某个坐标值对应的数加一个数 c 的操作
    for(auto item:add)
    {
        int i = find(item.first);
        a[i] += item.second;
    }
    
    //求前缀和
    for(int i = 1; i <= alls.size(); i ++) s[i] = s[i - 1] + a[i];
    
    //处理查询操作
    for(auto item : quary)
    {
        int l = find(item.first);
        int r = find(item.second);
        cout << s[r] - s[l - 1] << endl;
    }
    
    return 0;
}

区间合并

将所有存在交集的区间合并
1,按照区间左端点排序
2,合并区间,分为三种情况
1)两区间无交集
2)一个区间在另一个区间内
3)两区间有部分交集
分情况对以上情况进行判断解决即可

例题1:区间合并

题目链接: 区间合并.

题目:
给定 n 个区间 [li,ri],要求合并所有有交集的区间。注意如果在端点处相交,也算有交集。输出合并完成后的区间个数。
例如:[1,3]和[2,6]可以合并为一个区间[1,6]。

输入格式
第一行包含整数n。接下来n行,每行包含两个整数 l 和 r。

输出格式
共一行,包含一个整数,表示合并区间完成后的区间个数。

数据范围
1 ≤ n ≤ 100000,
−1 0^ 9 ≤ li ≤ ri ≤10 ^ 9

输入样例:
5
1 2
2 4
5 6
7 8
7 9

输出样例:
3

就是一个纯区间合并题,代码如下:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

typedef pair<int,int> PII;
const int N = 100010;

int n;
vector<PII> segs;

void merge(vector<PII> &segs)
{
    vector<PII> ans;
    sort(segs.begin(),segs.end());//c++ 内默认先按pair 第一个元素排序,之后再按第二个排序
    
    int sl = -2e9, sr = -2e9;//当前维护的区间两端点,开始默认负无穷(2e9是根据题目范围定的)
    for(auto seg : segs)
    {
        if(sr < seg.first)//枚举到的区间的左端点在当前区间右边,说明两区间无交集,可以将当前维护区间保存起来,
        {                //用枚举到的这个区间去匹配后面的区间
            if(sl != -2e9) ans.push_back({sl, sr});//需保证当前区间不是我们最开始预设的负无穷区间
            sl = seg.first, sr = seg.second;
        }
        else sr = max(sr, seg.second);//两区间有交集,只需令当前区间右端点为两区间右端点最大值即可
    }
    if(sl != -2e9) ans.push_back({sl, sr});//将最后区间保存进去,且避免 segs 为空的情况(这时候ans里面也应是空的)
    
    segs = ans;
}

int main()
{
    cin >> n;
    for(int i = 0; i < n; i ++)
    {
        int l, r;
        cin >> l >> r;
        segs.push_back({l, r});
    }
    
    merge(segs);
    
    cout << segs.size() << endl;
    
    return 0;
}

例题2,格子染色

题目链接: 格子染色.

题目:
在二维平面上有一个无限的网格图形,初始状态下,所有的格子都是空白的。现在有n个操作,每个操作是选择一行或一列,并在这行或这列上选择两个端点网格,把以这两个网格为端点的区间内的所有网格染黑(包含这两个端点)。问经过n次操作以后,共有多少个格子被染黑,重复染色同一格子只计数一次。

输入格式
第一行包含一个正整数n。
接下来n行,每行包含四个整x1,y1,x2,y2,表示一个操作的两端格子坐标。
若x1=x2则是在一列上染色,y1=y2则是在一行上染色。保证每次操作是在一行或一列上染色。

输出格式
包含一个正整数,表示被染黑的格子的数量。

数据范围
1≤n≤10000,
−109≤x1,y1,x2,y2≤109

输入样例:
3
1 2 3 2
2 5 2 3
1 4 3 4

输出样例:
8

题解指南: 一个我认为不错的格子染色题解.

代码:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

typedef long long ll;

struct Node{
    int l, r ,k;//左右端点,k表示行或列序号
    bool operator < (const Node & w)const{//重载,先按行或列,再按区间左右端点
                                       //括号中的const表示参数a对象不会被修改,最后的const表明调用函数对象不会被修改
        if(k != w.k) return k < w.k;
        if(l != w.l) return l < w.l;
        else return r < w.r;
    }
};

vector<Node> cols,rows;
int n;
ll ans = 0;

void merge(vector<Node> &segs)
{
    sort(segs.begin(), segs.end());
    int sl = -2e9, sr = -2e9, k = -2e9;
    vector<Node> tmp;
    for(auto seg : segs)
    {
        if(seg.k == k)//同一行/列,合并区间
        {
            if(sr < seg.l)
            {
                if(sl != -2e9) tmp.push_back({sl, sr, k}), ans += sr - sl + 1;
                sl = seg.l, sr = seg.r;
            }
            else sr = max(sr, seg.r);
        }
        else         //不同行/列,保存当前区间 {sl, sr, k}
        {
            if(sl != -2e9) tmp.push_back({sl, sr, k}), ans += sr - sl + 1;
            sl = seg.l, sr = seg.r, k = seg.k;
        }
    }
    if(sl != -2e9) tmp.push_back({sl, sr, k}), ans += sr - sl + 1;//保存最后一个区间
    segs = tmp;
}

int main()
{
    cin >> n;
    while(n--)
    {
        int x1,y1,x2,y2;
        cin >> x1 >> y1 >> x2 >> y2;
        if(x1 == x2) 
        {
            cols.push_back({min(y1, y2), max(y1, y2), x1});
        }
        else
            rows.push_back({min(x1, x2), max(x1, x2), y1});
    }
    
    merge(cols), merge(rows);//分别合并行, 列区间,之后再去掉行列交点
    
    for(auto col : cols)
        for(auto row : rows)
            if(row.k >= col.l && row.k <= col.r && col.k >= row.l && col.k <= row.r)
                ans --;
    
    cout << ans << endl;
    
    return 0;
}

参考链接:
1,C++ operator关键字(重载操作符).
2,C/C++对bool operator < (const p &a)const的认识,运算符重载详解(杂谈).

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值