算法基础1.3:双指针,位运算,离散化,区间合并

双指针

for (int i = 0, j = 0; i < n; i ++ )
{
    while (j < i && check(i, j)) j ++ ;

    // 具体问题的逻辑
}
常见问题分类:
    (1) 对于一个序列,用两个指针维护一段区间
    (2) 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作

双指针算法是用的非常多的一种算法,比方说归并排序中有一步是要把两个有序序列合并,两个指针分别指向两个序列的开头,每一次分别移动一下,当两个指针移动完的时候,排序过程就结束了,算是第一大类,所以说归并排序就算是一个双指针算法了,双指针算法还有很多种,第二大类的话是更多的一种情况,就是指向一个序列。比方说快排划分的过程,就是其中一个指针指向开头,另外一个指针指向结尾,两个指针实际上维护的是一段区间。一般来说的话只有这么两大类,双指针算法的用途非常多,一般的写法无外乎就一种情况,首先是一重for循环,然后i从零开始,j从一点开始,i整个扫描一遍,每一次i更新完之后,更新一下j,里面大概长这个样子,while j小于i,第一个判断条件是j的范围,一定要在合法的范围之内,并且满足某一种性质,比方说check(i,j),只要满足某一种性质,j就加加,一般来说,代码的结构都是这样的,然后while后面是写每道题目的具体逻辑,这是双指针算法的一个通用模板,写的时候先把这个模板写下来,然后再具体的去想一下每道题目该如何做。

双指针算法最核心思想是可以优化,本来是两个指针,一共有n的平方种不同组合,本来如果暴力写,可能是i从零到n循环一遍,并且j从零到n也循环一遍,时间复杂度是On的平方的,但是如果用双指针算法,所有双指针算法都是On的,如果用两个指针扫描一个序列,虽然说循环看起来是两重循环,但是每一个指针在所有循环里面总共移动的次数是不超过n的,两个指针总共移动的次数就不超过2n,所以双指针算法的核心思想就是将朴素算法优化到On,运用了某些性质,可以使得本来是需要枚举所有的情况的,现在就只需要枚举On个情况了。然后来看几个例子,比方说举一个最简单的例子,输入一个字符串,然后把其中的每一个单词输出出来,单词是拿空格隔开的,每个单词之间只有一个空格,比方说abcdefghi,要输出abc,def和ghi,把它们每个单词输出,每个单词占一行,首先第一个指针是从零开始,一直枚举到结束为止,此时来定义第二个指针j等于i,假定字符串的开头没有空格,并且每两个单词之间只有一个空格,然后写的时候每一次i循环,都保证i指向单词的第一个位置,然后要找到当前这个单词的最后一个位置,比方说现在的单词是abcdefghi,i最开始指向a,然后j是想找到a单词的末尾,用n来表示字符串的长度,然后只要j没有走到终点, j指向的不是空格,j就加加,通过这样一个while循环的话啊,最开始i指向a,j是从i开始的,每次从i开始,然后往后扫描,直到扫描到空格为止,所以当while循环结束的时侯,j是指向空格的,此时从i到j- 1之间的所有的字母就是当前这个单词的所有字母,后面的内容就是这道问题的具体逻辑了,来循环一下,定义k从i开始,k小于j,k加加,然后把当前这个字母输出出来就可以了,最后输出一个回车,每个单词占一行。然后当前这层循环结束之后,让i指向j,i就会指向空格,下一次i在结尾的时候加加就会指向下一个字母了,最后的话,让i=j就可以跳过整个区间 。

这个模板是为了帮助节省思考的时间,双指针算法的概念很广泛,不是说只是可以维护两个窗口,双指针算法指的是凡是具有模板这样形式的算法都可以被称为双指针算法,核心思想就是可以把n方的复杂度,本来要枚举i和j两重循环,然后我们从中挖掘了某些性质,可以让我们只枚举On个状态,把时间复杂度从n方变成了On,凡是具有这样性质的题目,都可以被称为双指针算法,包括快排的划分,归并排序的归并,kmp算法都是这样的一个问题。

例题:

给定一个长度为 n的整数序列,请找出最长的不包含重复的数的连续区间,输出它的长度。

输入格式

第一行包含整数 n。

第二行包含 n 个整数(均在 0∼10的5次方范围内),表示整数序列。

输出格式

共一行,包含一个整数,表示最长的不包含重复的数的连续区间的长度。

数据范围

1≤n≤10的5次方

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

去重不行,因为要是连续的一段,

首先凡是双指针问题,一般来说都可以从暴力的做法来想,首先想一下暴力做法是什么,暴力做法就是假设i是连续序列的终点,先枚举终点,再枚举起点,for循环i从0开始,i<n,i++,然后再枚举起点,假设起点是j,j小于等于i,起点和终点可以是一个数,然后再check一下,从i到j这一段是不是成立?如果成立的话,就更新一下答案,res=max(res,i-j+1)一般来说,双指针算法都可以这样朴素做法,时间复杂度显然是On方的,然后来想一下如何去优化它,本质上是找一下i和j有什么样的规律,其实I和j是有单调性的,比方说如果用红颜色的指针表示i,用绿颜色的指针来表示j,假设当i往后移动了一个位置移到下个点的时候,j表示的是i往左最远可以走到多远,基本思路枚举i的思路和朴素做法是一样的,枚举一下每一个i,看一下以每一个i为右端点的区间,它的左端点,离它最远是在什么位置,比方说12235,当i指向一的时候,j就指向一,当i指向二的时候,j还是可以指向一的,因为前两个是没有重复数字的,当i指向第三个数的时候,j就只能是指向同一个数了,因为当i指向第三个数字的时候,j不管是指向一还是指向二,都是会有重复数字的,因此i和j就只能指向同一个位置了,当i指向下一处的时候,j还可以指向2,因为2和3没有重复的,当i指向最后一个数字的时候,j也是可以指向二的,这里绿颜色的指针表示的是它离红颜色最左能到什么位置,使得绿颜色指针和红颜色指针之间是没有重复数字的。每一次把红颜色的指针往后移动一个位置然后再求一下新的i这个位置,对应的j最靠左的话可以在什么位置?可以发现一个很重要的性质,就是当红颜色指针往后移动的时候,绿颜色指针每一次都是红颜色指针匹配的最靠左的这个位置,绿颜色的指针一定也是会往后走的,不会往前走。假设说现在红颜色指针只能停在一个位置,红颜色指针往后移动之后,新的红颜色指针对应的绿颜色指针,如果要是往前动的话,就矛盾了,因为新红颜色和新绿颜色指针之间没有重复元素的话,那么把上一个绿指针往前移动,也不会有重复元素,一个没有重复元素的区间,内部的所有子区间内部都是没有重复元素的,如果i往后走了一格,新的i对应的j在我们旧的j左边,因此对于这个旧的i而言,旧j就一定不能在原来的位置了,j肯定取到新i对应的j是更好的,所以说就矛盾了,因此,当红颜色指针往后移动的时候,绿颜色的指针也一定是往后移动,具有单调性。所以说朴素做法就可以优化了,就是可以只枚举i就可以了,然后每一次的看一下j要不要往后走,就while j小于等于i,并且check i,j,就判断一下从j到i之间有没有重复元素,check的含义是j到i这个区间里面,如果包含重复元素,j就需要往左移动一格,直到移动到j和i之间没有重复元素为止。每一次后面就是更新一下答案就可以了,res=max res,i-j+1,两个指针总共会走2n次,红颜色指针走n步,绿颜色指针共走n步,所以两个指针加在一块总共会走2n步。时间复杂度就可以从n方优化成on了。所以说可以发现双指针算法,其实就是把朴素算法,发现一些性质,尤其是发现一些单调性,使得本来需要枚举On方个状态,然后现在只需要枚举On个状态了。就可以用这个双指针算法,把时间复杂度从n方优化成On。

关键是check j是怎么判断的,可以直接开一个数组,记一下就可以了,由于这个题目的数据范围比较小,它每个数都是在零到十万范围内,所以说可以直接开一个十万的数组,然后动态的记录当前这个区间里面每个数出现多少次,比方说,可以开一个s数组,记录一下当前这个区间里面每个数出现多少次,每一次i往后移动一格,相当于是在区间里面加入了一个新的数,然后j每一次往后移动一格,相当于从当前这个区间里面就有一个数就出去了,就可以动态的去统计出来这个区间里面有多少个数,然后前一个i结束的时候,它对应的j,这个区间里边是没有重复元素的,那么当新加了一个i,有重复的数了,说明新重复的数就必然是ai,然后check j就可以简写成aj不等于ai就可以了,就当这个区间里面有重复元素的话,重复的就一定是ai这个值,j往后走的时候一定要把ai这个值去掉一个才可以。

#include <iostream>

using namespace std;

const int N = 100010;

int n;
int q[N], s[N];      //s存的是当前j到i这个区间里面每一个数出现的次数。

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);

    int res = 0;
    for (int i = 0, j = 0; i < n; i ++ )
    {
        s[q[i]] ++ ;     //新加入一个数
        while (j < i && s[q[i]] > 1) s[q[j ++ ]] -- ;    //有重复的数,把它拿出去,表示这个数减1
        res = max(res, i - j + 1);       //更新答案
    }

    cout << res << endl;

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/40066/
来源:AcWing

模拟一下例子12235,第一次是i指向一,j指向一,当i取第一个数的时候,j就应该指向第一个数,当i往后移动一个数的时候,此时1和2都出现了一次,二出现的次数是一,所以while循环就不成立了,当i指向第三个数的时候,二就出现了两次。所以此时while循环就会执行,首先第一步先把一剔除,j指向二,此时ji区间里面还是有两个二,还是不行,然后再提出一个二,此时可以了,所以说当i指向第三个数的时候,j就指向第三个数,当i指向下一个数的时候,那此时这个区间里面是2和3,每个数出现一次,所以成立,j指向第三个数,当i指向最后一个数的时候,此时区间里边每一个数还是只出现了一次,所以此时j指向第三个数OK了。

双指针算法的思路总结一下,都是通过暴力。先写一个暴力解的做法,然后看一下I和j之间有没有什么单调关系,有单调关系的话,就可以利用这种单调关系啊,来把我整个枚举的状态数量从n方变成On,从而使总共的时间复杂度从n方变成On。

例题2:

给定两个升序排序的有序数组 A和 B,以及一个目标值 x。

数组下标从 00 开始。

请你求出满足 A[i]+B[j]=x的数对 (i,j)。

数据保证有唯一解。

输入格式

第一行包含三个整数 n,m,x,分别表示 A的长度,B的长度以及目标值 x。

第二行包含 n个整数,表示数组 A。

第三行包含 m 个整数,表示数组 B。

输出格式

共一行,包含两个整数 i 和 j

数据范围

数组长度不超过 10的5次方。
同一数组内元素各不相同。
1≤数组元素≤10的9次方

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

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int n, m, x;
int a[N], b[N];

int main()
{
    scanf("%d%d%d", &n, &m, &x);
    for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);
    for (int i = 0; i < m; i ++ ) scanf("%d", &b[i]);

    for (int i = 0, j = m - 1; i < n; i ++ )
    {
        while (j >= 0 && a[i] + b[j] > x) j -- ;     //找单调性,一个i对应j大于等于x,j是最靠左的一个,                                                                       总和x不变,i变大,j就会变小
        if (j >= 0 && a[i] + b[j] == x) cout << i << ' ' << j << endl;
    }

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/40069/
来源:AcWing

模板题:

给定一个长度为 n 的整数序列 a1,a2,…,an 以及一个长度为 m 的整数序列 b1,b2,…,bm

请你判断 a 序列是否为 b序列的子序列。

子序列指序列的一部分项按原有次序排列而得的序列,例如序列 {a1,a3,a5}是序列 {a1,a2,a3,a4,a5}的一个子序列。

输入格式

第一行包含两个整数 n,m

第二行包含 n 个整数,表示 a1,a2,…,an

第三行包含 m 个整数,表示 b1,b2,…,bm

输出格式

如果 a 序列是 b 序列的子序列,输出一行 Yes

否则,输出 No

数据范围

1≤n≤m≤10的5次方
−10的9次方≤ai,bi≤10的9次方

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

#include <iostream>
#include <cstring>

using namespace std;

const int N = 100010;

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

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);
    for (int i = 0; i < m; i ++ ) scanf("%d", &b[i]);

    int i = 0, j = 0;
    while (i < n && j < m)
    {
        if (a[i] == b[j]) i ++ ;   //匹配成功i往后走一步
        j ++ ;      //不管匹配成功与否,j都往后走一步
    }

    if (i == n) puts("Yes");
    else puts("No");

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/589289/
来源:AcWing

位运算

求n的第k位数字: n >> k & 1
返回n的最后一位1:lowbit(n) = n & -n

首先最常用的一种操作是求一个整数n的二进制表示里面第k位数字是几,比方说n=15,n的二进制表示为1111,我们说的第k位的下标,个位是第零位,十位是第一位,以此类推。

位运算的一个基本思路是第一步先把第k位移到最后一位啊,用右移运算,n右移k,把n的第k位数字移到个位,第二步看一下个位是几,比方说想看一下x的个位是几,那就是让x&一就可以了,把两步结合到一块就可以得到个公式,n>>k&1,假设n等于10,for循环一共四位,然后依次输出来n右移k&1,这样输出的就是n的二进制表示,从最高位开始,然后把它的每个二进制数字输出,输出1010,

第二个操作,lowbit操作,是树状数组的一个基本操作,作用是返回x的最后一位一是多少,举个例子,比方说x的二进制表示是1010,lowbit x返回的是二进制的10,是2,x等于101000,lowbit x就等于1000,返回的是一个二进制数,它最高位的一就是x的最后一位一,

lowbit x怎么实现的?lowbit x实现的时候其实就是x&-x。首先啊,在C++里面,一个整数的负数是原数的补码,补码是取反加1,所以在C++里面,负x二进制表示是和取反x+1的二进制表示是一样的。因此x&-x的话就等于x&取反x+1,比方说x等于1010.....10000.00,x取反等于0101.....01.....1,x的最后一位一,就是取反x的最后一位零,取反x+1,加上一之后就是后面所有1全部变成0,最后一位零的位置变成一,并且不会再往前进位了,0101......10...0,x&取反x+1,反x和x前面是完全不一样,因此最后得出00......0100......0

最简单的应用是可以统计x里边一的个数,思想是每一次把我们x的最后一位一去掉。当x=0的时候,里面就没有一了,减了多少次,就说明x里面有多少个一。

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

输入格式

第一行包含整数 n。

第二行包含 n个整数,表示整个数列。

输出格式

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

数据范围

1≤n≤100000
0≤数列中元素的值≤10的9次方

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

#include <iostream>

using namespace std;

int main()
{
    int n;
    scanf("%d", &n);
    while (n -- )
    {
        int x, s = 0;
        scanf("%d", &x);

        for (int i = x; i; i -= i & -i) s ++ ;    //每次去掉一个1,s就++

        printf("%d ", s);
    }

    return 0;
}

离散化

离散化,这里特指整数的离散化,而且是有序,保序的离散化。

首先是离散化的基本的含义,首先可能有一些数值域比较大,但是个数比较少,比方一个序列里面可能只有十的五次方个数,但是它的值域非常大,0到10的九次方。有些题目可能要以这些值为下标来做,总不能开一个长度是10的9次方的一个数组,因此就要把这个序列映射到从零开始的连续的自然数,比方说有一个数组a,1,3,100,2000,500000,然后把它映射到从0开始的自然数,把它映射到01234,这个过程就被称为离散化。

离散化里面一般有两个问题,第一个问题是a数组里边可能有重复元素,所以需要去重。由于这一步比较复杂,因此提供一个可以背过的模板。

vector<int> alls; // 存储所有待离散化的值
sort(alls.begin(), alls.end()); // 将所有值排序
alls.erase(unique(alls.begin(), alls.end()), alls.end());   // 去掉重复元素

unique()函数的底层原理

基本的实现思路也是双指针算法,比方说数组是11222345556,所有不同的元素一定满足这样一个性质,要么它是第一个,它就一定是不同的,第二个是它跟前一个元素不一样,只有这两种情况,要么它只要满足这个性质的话,那么它就是每一段相同的第一个数,所以只要把所有满足这样的数拿出来,那么这些数就是所有不同的元素,比方说这组数的第一个1,第一个2,3,4,第一个5和6,把所有满足这个性质的数拿出来就可以了。双指针算法,第一个指针i是遍历所有的数,第二个指针j是存一下当前存到了第几个不同的数,遍历的时候时时刻刻保证j小于等于i。

vector<int>::iterator unique(vector<int> &a)    //返回的应该是vector int的一个迭代器

{
    int j = 0;
    for (int i = 0; i < a.size(); ++i) {
        if (!i || a[i] != a[i - 1])//如果是第一个元素或者该元素不等于前一个元素,即不重复元素,我们就把它存到数组前j个元素中
            a[j++] = a[i];//每存在一个不同元素,j++,a[0]到a[j-1]是a中不重复的数
    }
    return a.begin() + j;//返回的是前j个不重复元素的下标
}

unique函数的作用是将数组当中所有的元素去重,把不重复的数放到前面去,并且返回去重之后的数组的尾端点,剩余的重复元素,函数结束完之后会被换到后面的位置,unique函数会把无重数组最后的下标返回回来,然后再把这冗余的一部分元素直接erase删掉就可以了

第二个问题是如何算出x离散化后的值,因为a数组是有序的,离散化的过程就是a的值映射到下标,离散化就是找一下x这个值在a中的下标是多少,因为a是有序的,这一步是可以用二分来做的。下面给出模板:

// 二分求出x对应的离散化的值
int find(int x) // 从左往右找到第一个大于等于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; // ,返回这个位置的下标+1,从1开始映射,映射到1, 2, ...n

例题:

假定有一个无限长的数轴,数轴上每个坐标上的数都是 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

如果数据范围比较小,所有的下标的范围是十的五次方以内,前缀和就可以做了,先预处理开个数组,最开始数组上的每一处都是零,然后每一次给某一个数加上一个数,比方说要在x上加上c,就直接让a[x]+=c就可以了,然后再对a数组求一遍前缀和,对于每一个询问,就直接返回Sr减去Sl-1

这个题的数据范围虽然大到了负的10的9次方到10的9次方,但是总共涉及到的数的个数是很少的,n和m的范围只有十的五次方,因此,首先n个操作,最多只会用到n个x,然后对于每一个询问操作,会有两个坐标l和r,所以是2m个下标,总共加到一块最多也是n+2 m个下标,n和m都是十万,虽然数据范围跨度是20万,但是只会用到其中的3×10的五次方,绝大部分的坐标都是没有用到的。这是离散化问题的一个很经典的性质,就是整个的值域跨度很大,但是它非常的稀疏,一共有2×10的九次方个数,但是只用到了其中3×10的五次方个数。求一下下标从l到r之间所有数的和,其实是想找一下x在l到r之间的所有的数,所以这儿就可以用一种保序的离散化的方式来把所有用到的下标映射成从1开始的自然数,由于要求前缀和,所以映射的时候是从一开始映射,每一次比方说想给x这个位置加上c,就先找到x离散化之后的值是多少,然后在它离散化之后的值的位置上加上一个数就可以了,比方说离散化之后的值是k的话,那么就让ak+=c,然后在求某一个区间里面所有数的和的时候也是,就假设要求一下l和r之间所有数的和,那么先把l和r离散化到它们对应的这个下标的位置,比方说l离散化之后是kl,然后r离散化之后是kr,那么就求一下从这个akl到akr之间所有数的和就可以了。

这是这道题目的一个基本的想法,由于值域很大,但是里面的数很稀疏,然后要用到的只有它们之间的相对关系,每一次要求所有数的和的时候,其实只要求一下l和r之间所有数的和就可以了,跟它们的这个值的绝对关系是没有关系的,跟它们的相对大小关系是有关系的,所以说就可以先把里边所有用到过的不同的数映射成从一开始的自然数就可以了,那么这个题其实就变成了所有数的数据范围就是在1到30万之间,,那么就可以用前面学过的前缀和的方式来把这个题完美解决掉了,这里去做映射的时候其实是把数组上的每一个值映射到它的下标,排上去之后,下标就是它映射的值。比方说a数组排完序是1,2,100,2000,30000,它的下标是01234,所以说映射完之后其实就是下标,就把每个数映射成它的下标,1就映射成0,以此类推。

由于本题可能有多组数据是针对同一个数组下标操作的,因此我们可以将所有用到的数组下标装在一个下标容器alls内去重,然后再逐一为相同的数组下标增加数值c,再通过对应前缀和相减求得区间 l~r 之间的数的值

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

using namespace std;

typedef pair<int, int> PII;    //把所以操作读进来,每个操作两个数,用pair来存

const int N = 300010;     //插入一个和查询两个,n和m都是十万级别,加起来可能有30万,因此设                                                    置为30万

int n, m;
int a[N], s[N];   //a为存的数,s为前缀和

vector<int> alls;//存入下标容器
vector<PII> add, query;//add增加容器,存入对应下标和增加的值的大小
//query存入需要计算下标区间和的容器
int find(int x)   //求一下x离散化之后的结果
{
    int l = 0, r = alls.size() - 1;
    while (l < r)//二分查找大于等于x的最小的值的下标
    {
        int mid = l + r >> 1;
        if (alls[mid] >= x) r = mid;    //第一个模板
        else l = mid + 1;
    }
    return r + 1;//因为使用前缀和,其下标要+1可以不考虑边界问题
}

int main()
{
    cin >> n >> m;
    for (int i = 0; i < n; i ++ )
    {
        int x, c;
        cin >> x >> c;
        add.push_back({x, c});//存入下标即对应的数值c

        alls.push_back(x);//存入数组下标x=add.first
    }

    for (int i = 0; i < m; i ++ )
    {
        int l, r;
        cin >> l >> r;
        query.push_back({l, r});//存入要求的区间,因为是pair,所以要加大括号

        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);//将add容器的add.secend值存入数组a[]当中,
        a[x] += item.second;//在去重之后的下标集合alls内寻找对应的下标并添加数值
    }

    // 预处理前缀和
    for (int i = 1; i <= alls.size(); i ++ ) s[i] = s[i - 1] + a[i];  //注意i<=,因为是映射到从1到alls.size

    // 处理询问
    for (auto item : query)
    {
        int l = find(item.first), r = find(item.second);//在下标容器中查找对应的左右两端[l~r]下标,然后通过下标得到前缀和相减再得到区间a[l~r]的和
        cout << s[r] - s[l - 1] << endl;
    }

    return 0;

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/40105/
来源:AcWing

区间合并

区间合并的一个具体的应用场景是是给很多很多的区间,区间如果有交集,就把它合并成同一个区间,把它们并集当成我们新的区间,题目是给我们输入n个区间,然后让我们把这n个区间所有有交集的区间进行合并,然后输出的合并之后的区间个数,区间合并算法就是快速的把这n个区间有交集的进行合并。这里会有一些边界问题,可以特殊规定,比方说两个区间,如果要是只有端点相交,也算它是可以合并成同一个区间。来模拟一下样例,比方说第一个区间是1到2,第二个区间是2到4,第三个区间是5到6,第四个区间是7到8,第五个区间是7到9,首先前两个区间可以合并,有公共的端点二,合并之后的结果就是1到4,第二个区间5到6是不可以合并的,第三个区间7到9是可以合并的,所以最终合并完之后一共是可以得到三个区间,

具体做法是首先第一步是按照所有区间的左端点排序,第二步是扫描整个区间,扫描的过程当中把所有可能有交集的区间进行合并,比方说左端点是start,简称st,右端点是end,简称ed,维护当前的区间啊,在按照左端点从小到大从前往后扫描的过程当中,假设当前已经扫描到了第二个区间,第二个区间和当前这个区间的关系有三种,首先第一种关系是在第一个区间的内部,第二种关系是有交集,但是不在内部,第三种关系是没有交集。

然后分别看一下这三种情况该如何来更新现在维护的这个区间,首先对于第一种情况而言,更新完之后st和ed不变,两个区间有交集,取并集还是本来这个区间,对于第二种情况而言,相当于区间延长了,新的ed就该移到后面的位置了,第三种情况是没有交集的,而且是按照左端点从小到大的顺序来扫描所有区间,所以第二个区间的左边点一定是在第一个区间右端点的后面,因此,从当前这个区间开始,后面的所有区间都和当前维护的这个区间没有任何交集,那么当前维护的这个区间就可以放到答案里面去了,然后把维护的这个区间更新成第二个区间,更新st和ed。这是基本思路

来模拟一下样例,首先第一个区间是最早开始维护的区间,然后扫描第二个区间,如果第二区间是和当前这个区间是有交集的,就把维护的这个区间变成维护的区间和当前这个区间的一个并集,如果发现下一个区间的左端点是在当前维护的区间的右端点的右边,那么当前维护的这个区间和后面所有区间都不会有任何的交集了,当前这个区间就可以拿出来了,放到答案里面去。然后当前维护的区间变成与原区间没有交集的区间,然后再继续往后走,看下一个区间,如果下一个区间和当前区间也没有交集,枚举的时候有可能先枚举第一个区间,也有可能先枚举第二个区间,不管哪个区间,新的区间和当前维护的这个区间是没有任何交集的,那么当前这个区间和后面的所有区间就没有任何的交集了,那么,这个区间也可以拿出来放到我们答案里面去。然后再去看下一个区间,和当前这个区间是有交集的。因此就要把当前维护的这个区间更新成两个区间交集,最后得到的就是三个没有任何交集的区间了

跟双指针算法不太一样,因为只有一个指针,从前往后去扫描的是每一个区间,只扫描了一遍,跟区间有关的问题有很多,大部分都是贪心,不是按照左端点排序,就是按照右端点排序,或者是按照这个左端点和右端点双关键字排序。

例题:

给定 n个区间 [li,ri],要求合并所有有交集的区间。

注意如果在端点处相交,也算有交集。

输出合并完成后的区间个数。

例如:[1,3]和 [2,6]可以合并为一个区间 [1,6]。

输入格式

第一行包含整数 n。

接下来 n 行,每行包含两个整数 l和 r。

输出格式

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

数据范围

1≤n≤100000
−10的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; //定义了一个名为Pll的数据结构,用来表示一对整数,表示区间的起始和结束位置。

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);    //否则说明有交集,更新当前区间的结束位置为两者中较                                                                      大的值

    if (st != -2e9) res.push_back({st, ed});    //将最后一个区间加入结果向量,得到最终的合并后的区间

    segs = res;    //将结果赋值给传入的区间向量
}

int main()
{
    int n;
    scanf("%d", &n);  //读入区间的数量

    vector<PII> segs;  //存储区间的数量
    for (int i = 0; i < n; i ++ )
    {
        int l, r;    //读入每个区间的左右端点
        scanf("%d%d", &l, &r);
        segs.push_back({l, r});   //存储到区间向量中
    }

    merge(segs);    //调用合并函数

    cout << segs.size() << endl;   //返回合并之后的区间数量

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/40108/
来源:AcWing

end.....................................................................................................................................................

  • 28
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值