【双指针、位运算、离散化、区间合并】思路讲解及代码实现

简介: 用一篇Blog来讲解下最近学到的双指针、位运算、离散化、区间合并等算法,为日后的刷题打下坚实的基础。

Halo,这里是Ppeua。平时主要更新C语言,C++,数据结构算法......感兴趣就关注我吧!你定不会失望。


🌈个人主页:主页链接


🌈算法专栏:专栏链接


       我会一直往里填充内容哒。


🌈LeetCode专栏:专栏链接


   目前在刷初级算法的LeetBook 。若每日一题当中有力所能及的题目,也会当天做完发出


🌈代码仓库:Gitee链接


🌈点击关注=收获更多优质内容🌈


8036289d7eca4338aa038be5de3d3ee2.jpg


用一篇Blog来讲解下最近学到的双指针、位运算、离散化、区间合等算法,为日后的刷题打下坚实的基础。


双指针:


双指针,顾名思义就是两个指针。

在解题当中,往往分为两种情况:指向两个不同的序列,指向一个序列。


其核心思想是由暴力算法优化而来,通常暴力算法的时间复杂度为O(n^2),这样解题的过程中显然无法凸显出算法之美(doge


基于此,我们优化了一下解题方式,其核心思想就是有:两个指针i,j当符合某种条件的时候j++,两个指针都不会走已经走过的路


如何理解呢?下面我们来看一个例子


寻找不重复最长区间,就是一个区间里的数字出现次数<=1


3afef88c9d924c4eba661a65e67cfc09.gif


之后返回最优解即可:本题就是每当j移动时就更新下区间


代码模板:


#include<iostream>
using namespace std;
const int N=100010;
int n=0,ans=0;
int a[N],s[N];
void in()
{
    cin>>n;
    for(int i=0;i<n;i++)cin>>a[i];
    return;
}
int main()
{
    in();
    for(int i=0,j=0;j<n;j++)
    {
        s[a[j]]++;
        while(s[a[j]]>1)
        {
            s[a[i]]--;
            i++;
        }
        ans=max(ans,j-i+1);
    }
    cout<<ans;
}


Leetcode相关题目推荐:


b3d75345117c4291b42a49003c95b29b.png


至此双指针算法结束.


位运算:


这里先简单介绍一下数据的存储格式,在x86也就是32位的环境下数据是以32个1或0存储在计算机当中的。


抽象来看一个int类型的数据,就可以存储32种情况(有代表1,无代表0)


通常情况下,我们可以利用位运算来存储某几种情况是否出现,若不用位运算,我们就另外需要开辟几个空间来存储这几种情况的bool值。


所以n>>k&1就可以很好的解释上面的算法。


087d40e6898142418cc97ef670bd0ff3.jpg


n向右位移k位(这里的k为4),之后与1的二进制位进行&的比较,若有1则输出1.


比较常用的还有一种算法 lowbit(x)


作用是输出一个数二进制位中的最后一个1,通常是用来统计一个数二进制中有几个1等问题。


代码就简单的一行,我们来一起看看他的原理是怎样的。


先来理解一个概念对一个数取相反数 得到的是这个数的补码。(不看符号位)

然后执行&运算,他就会输出最后一位1所代表的十进制。


31d44949a34242b19b5dcbb7d4aa8b10.jpg


代码模板:


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


至此位运算结束.


区间合并:


这个算法比较少用,其大意是将所有有重合的区间进行合并。


d0b012e7ee894413a45f594946d1505d.jpg


首先我们先将所有端点的头位置(st)进行排序,将尾位置(ed)作为比较依据


所以就有以下这三种情况


1ca2cebe2c9a4dd39f19736831536892.jpg


第一种情况是ed1<ed的情况 也就是这整个区间被包含在比较区间当中,所以我们做的是保留原区间进行下一个比较。

第二种情况是ed2>ed的情况 也就是比较区间被包含在了其中,所以我们要将比较区间尾端更新,进行下一个比较。

第三种情况是st3>ed 因为我们已经将所有端头进行排序,所以出现这种情况只能说明上一个模板已经排完序了,这时候我们更新比较区间就可以了。


代码模板:


#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
typedef pair<int,int> PII;
vector<PII>segs;
void merge(vector<PII>&segs)
{
    vector<PII>res;
    sort(segs.begin(),segs.end());
    int st=-2e9,ed=-2e9;
    for(auto seg:segs)
    {
        if(ed<seg.first)//第三种情况 
        {
            if(st!=-2e9)res.push_back({st,ed});//因为已经排完序 将上一个区间先放入答案集
            st=seg.first,ed=seg.second;//更新维护区间
        }
        else ed=max(ed,seg.second);//max只能放两个数据类型相同的值
    }
    if(st!=-2e9)res.push_back({st,ed});//放最后一个区间 
    segs=res;
}
int main()
{
    int n=0;
    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();
}


至此区间合并算法结束.


离散化:


离散化先来介绍下是用来干什么的:他是用来解决跨度(10^9)很大,但非常稀疏的数据(10^5),若用数组来存储,则会很浪费空间。


举个例子:


bbe869214df34f0eadba9084a12e8432.jpg



图中的key相当于数组下标,而value则是对应的值。我们想要在key为1  10000  5000000的三个地方加上值1,因为数组的空间是连续的,所以这样会造成非常大的空间浪费。


所以为了避免这种浪费,想到了数学中离散化的方法。


首先先来考虑几个问题:


1、key可能会有重复如何去重?(若不去重则会出现相同的下标)


2、如何算出a[i]离散化后的值呢?


先来解决第一个问题:去重


先来认识两个函数,erase与unique。

erase是vector的方法,目的是去除连续的区间

unique是去除指定区间内连续相同数据的函数(从名字也可以来理解 unique唯一的),并返回去除后的尾坐标。

下面我们先对容器进行预处理


sort(alss.begin(),alss.end());
    alss.erase(unique(alss.begin(),alss.end()),alss.end());//去除重复下标


至于第二个问题我们暂且按下不表。


拿acwing上的一道例题来举例:


f099d72cac1a41c68bf4a867395822fa.png


分析题目可以得到,我们要做的有两件事,第一 在x的位置上加上常数c,第二访问l,r区间和,看其数据描述,很符合我们上方提到的离散化模型,接下来我们就开始试一试。


先定义变量,其中alss用来存储下标,而add用来将存储的下标与相加的数进行绑定,query用来存储查找的区间。

const int N=300010;
typedef pair<int, int>PII;
int n,m;
int a[N],s[N];//a为离散数组 s为前缀和
vector<int>alss;//原始数组数组
vector<PII>add,query; 


接下来进行数据读入。


cin>>n>>m;
    for(int i=0;i<n;i++)
    {
        int x,c;
        cin>>x>>c;
        add.push_back({x,c});//将对应坐标与要加的值放在一起之后访问
        alss.push_back(x);//将要访问的下标存入hash
    }


接下来读入要访问的区间,这里区间也等于是两个坐标,为了之后方便求和,我们也将他放入alss坐标集中

for(int i=0;i<m;i++)
    {
        int l,r;
        cin>>l>>r;
        query.push_back({l,r});//将访问下标存入pair
        alss.push_back(l);//将访问下标也映射到hash中
        alss.push_back(r);
    }


接下来就是上文提到的去重等问题这里不过多赘述


    sort(alss.begin(),alss.end());
    alss.erase(unique(alss.begin(),alss.end()),alss.end());//去除重复下标


接下来就是将add中要加的坐标x与要加的数r进行加减。

首先先访问一下要加的坐标是哪个。

这里解答刚刚还剩下的问题,用二分查找去寻找在alss中x放在了哪里,之后返回alss中hash过的坐标,放入a中,最后在a中进行操作。


for(auto item:add)
    {
        int x=find(item.first);//为什么不直接将已经排完序的vector下标各加一作为hash值?
        a[x]+=item.second;//因为这样不知道你要访问的值是哪个 也就是说 在alss中 下标是作为value的存在而你要用的是把这个value做成key key对应的value才是你要操作的空间
    }//总的来说就是节省了时间 也可以一个for去找用的那个值 然后返回其下标但这样无疑慢了很多


二分查找函数


int find(int x)
{
    int l=0,r=alss.size()-1;
    while(l<r)
    {
        int mid=l+r>>1;
        if(alss[mid]>=x)r=mid;
        else l=mid+1;
    }
    return r+1;//整体向前偏移1
}


最后就是求前缀和,同样要访问的l,r坐标还是通过二分查找去寻找,前缀和前几篇blog已经讲过,不懂的朋友再去看看吧,这里就不再赘述

for(int i=1;i<=alss.size();i++)s[i]=s[i-1]+a[i];//求前缀和
    for(auto item:query)
    {
        int l=find(item.first),r=find(item.second); 
        cout<<s[r]-s[l-1]<<endl;
    }    


代码模板:


#include<iostream>
#include <utility>
#include<algorithm>
#include<vector>
using namespace std;
const int N=300010;
typedef pair<int, int>PII;
int n,m;
int a[N],s[N];//a为离散数组 s为前缀和
vector<int>alss;//原始数组数组
vector<PII>add,query; 
int find(int x)
{
    int l=0,r=alss.size()-1;
    while(l<r)
    {
        int mid=l+r>>1;
        if(alss[mid]>=x)r=mid;
        else l=mid+1;
    }
    return r+1;
}
int main()
{
    cin>>n>>m;
    for(int i=0;i<n;i++)
    {
        int x,c;
        cin>>x>>c;
        add.push_back({x,c});//将对应坐标与要加的值放在一起之后访问
        alss.push_back(x);//将要访问的下标存入hash
    }
    for(int i=0;i<m;i++)
    {
        int l,r;
        cin>>l>>r;
        query.push_back({l,r});//将访问下标存入pair
        alss.push_back(l);//将访问下标也映射到hash中
        alss.push_back(r);
    }
    sort(alss.begin(),alss.end());
    alss.erase(unique(alss.begin(),alss.end()),alss.end());//去除重复下标
    for(auto item:add)
    {
        int x=find(item.first);//为什么不直接将已经排完序的vector下标各加一作为hash值?
        a[x]+=item.second;//因为这样不知道你要访问的值是哪个 也就是说 在alss中 下标是作为value的存在而你要用的是把这个value做成key key对应的value才是你要操作的空间
    }//总的来说就是节省了时间 也可以一个for去找用的那个值 然后返回其下标但这样无疑慢了很多
    for(int i=1;i<=alss.size();i++)s[i]=s[i-1]+a[i];//求前缀和
    for(auto item:query)
    {
        int l=find(item.first),r=find(item.second); 
        cout<<s[r]-s[l-1]<<endl;
    }    
    return 0;
}


至此离散化算法结束.


完结撒花:


🌈本篇博客的内容【双指针、位运算、离散化、区间合并思路讲解及代码实现】已经结束。


🌈若对你有些许帮助,可以点赞、关注、评论支持下博主,你的支持将是我前进路上最大的动力。


🌈若以上内容有任何问题,欢迎在评论区指出。若对以上内容有任何不解,都可私信评论询问。


🌈诸君,山顶见!

目录
相关文章
|
存储 C语言
函数指针数组:更高效的代码实现方式——指针进阶(二)
函数指针数组:更高效的代码实现方式——指针进阶(二)
64 0
|
小程序 算法
如何降低代码的冗余度(指针的妙用)——探索指针数组,数组指针,函数指针,函数指针数组,回调函数的奥妙
如何降低代码的冗余度(指针的妙用)——探索指针数组,数组指针,函数指针,函数指针数组,回调函数的奥妙
56 0
|
1月前
|
存储 搜索推荐 C语言
深入C语言指针,使代码更加灵活(二)
深入C语言指针,使代码更加灵活(二)
|
1月前
|
存储 程序员 编译器
深入C语言指针,使代码更加灵活(一)
深入C语言指针,使代码更加灵活(一)
|
6月前
|
C语言
C语言指针带代码
C语言指针带代码
54 3
|
1月前
|
C语言
深入C语言指针,使代码更加灵活(三)
深入C语言指针,使代码更加灵活(三)
深入C语言指针,使代码更加灵活(三)
|
3月前
|
存储 大数据 测试技术
掌握 GoLang 中的指针:高效代码的提示和技巧
掌握 GoLang 中的指针:高效代码的提示和技巧
|
5月前
|
Go C++
代码随想录——双指针/滑动窗口(二)
代码随想录——双指针/滑动窗口(二)
|
5月前
代码随想录——双指针(一)
代码随想录——双指针(一)
|
6月前
DAY-2 | 哈希表、指针与区间划分:字符种数统计问题
```markdown ## 题干 [牛客网链接](https://www.nowcoder.com/practice/eb94f6a5b2ba49c6ac72d40b5ce95f50) ## 题解 1. **查表法(哈希表)**:利用数组标记出现过的 ASCII 值小于127的字符,首次出现计数,重复则忽略。 2. **指针与区间划分(回头法)**:遍历字符串,对每个字符检查其前所有字符是否重复,重复则不计数。 ## 方法总结 - 哈希表在去重问题中非常实用,可多做相关练习。 - 使用`continue`时注意避免死循环,确保循环变量会改变。 - 多回顾此类问题以巩固理解。 ```
44 2