第一周总结(基础算法)

一、排序——快速排序

1.算法思想

基本思路

每趟排序过程定义一个基准数,通过比较和交换的方式将序列分成两部分,比基准数小的均放置在基准数左侧,比基准数大的均放置在右侧,通过递归的方法再将左右两侧序列进一步拆分,进行多次上述过程,最终使得整个数据变成有序序列。

大致流程

1.先选定一个基准数,目的在于便于将序列分为两部分。

2.通过比较和交换的方式,将小于基准数的元素均集中在左侧,大于或等于基准数的均集中在右侧,一趟排序过后,左侧元素的大小均小于后侧元素。

3.然后将左右两部分序列视为两个独立序列

分别进行排序,通过递归的方式反复进行上述1、2过程,将序列逐渐拆分排序,最终就会得到一串有序的序列

2.算法实现

代码

#include <stdio.h>
int a[99999999], i;
void qusort(int s[], int start, int end)
{
    int i, j;
    i = start;
    j = end;
    s[0] = s[start];//以首元素为基准数;
    while (i < j)
    {
        while (i < j && s[0] < s[j]) j--;//从右往左依次比较,直到发现一个比基准数小的数
        if (i < j)
        {
            s[i] = s[j];将右侧比基准数小的数移到左侧
            i++;//左指针向右移动
        }
        while (i < j && s[i] <= s[0]) i++;//从左向右以此比较
        if (i < j)
        {
            s[j] = s[i];同理
            j--;
        }
    }
    s[i] = s[0];
    if (start < i)
        qusort(s, start, j - 1);//左半部分
    if (i < end)
        qusort(s, j + 1, end);//右半部分
}
int main()
{
    int n;
    scanf("%d", &n);
    for (i = 1; i <= n; i++)
        scanf("%d", &a[i]);
    qusort(a, 1, n);
    for(int i=1;i<=n;i++)
        printf("%d ", a[i]);
    return 0;
}

图解

代码复杂度

1.时间复杂度:

从上述代码和图解可知该算法的递归深度为logn,每一层比较的数为n,最后几层可能不为n(但也接近),所以比较执行的次数为O(n*logn),但这是最好情况下的复杂的,若该序列是逆序的,则时间复杂度就为最差情况为O(n²)。

2.空间复杂度:

快速排序只是使用数组原本的空间进行排序,但是由于每次拆分之后是递归调用,所以递归调用在运行的过程中会消耗一定的空间,一般情况下的空间复杂度为 O(logn),在最差的情况下空间复杂度为 O(n)。所以我们一般认为快速排序的空间复杂度为 O(logn)。



二、排序——归并排序

1.算法思想

归并排序是分治思想最经典的应用,”分“是指将序列分为两个子序列,”治“则是将两个有序序列合并为一个更大的有序序列。

大致流程:

1.将待排序序列不断拆分为多段子序列,直到每段序列仅剩一个元素为止。

2.将子序列通过比较的方法两两合并,每合并完成一次就会形成一个更大的有序序列,重复执行该步骤直到最后仅剩一串序列,则该序列即为排序完成的目标序列。

2.算法实现

代码(c++)

#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int q[N], tmp[N], n;
void merge_sort(int q[], int l, int r)
{
    if (l >= r) return;
    int mid = l + r >> 1;//找到中间数,将序列拆分为两部分
    merge_sort(q, l, mid);//前半部分
    merge_sort(q, mid + 1, r);//后半部分
    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r)
    {
        // 这里的<=之所以带=,是出于稳定稳定性
        if (q[i] <= q[j]) //较小的数放在前面
            tmp[k++] = q[i++];//tmp为有序序列
        else 
            tmp[k++] = q[j++];//元素每进入tmp一次,指针向后移动
    }
    while (i <= mid) 
//扫尾阶段,若其中一段子序列有一部分未进入tmp,则该序列之后的元素均可直接加在tmp后面
        tmp[k++] = q[i++];
    while (j <= r) 
        tmp[k++] = q[j++];
    for (i = l, j = 0; i <= r; i++, j++) 
        q[i] = tmp[j];
}                                                                                                                                                                  
int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i++) 
        scanf("%d", &q[i]);
    merge_sort(q, 0, n - 1);
    for (int i = 0; i < n; i++) 
        printf("%d ", q[i]);
    puts("");
    return 0;
}

图解

算法复杂度

1.时间复杂度:归并排序的时间复杂度是O(nlogn),且这个时间复杂度是稳定的,不随需要排序的序列不同而产生波动。

2.空间复杂度:O(n)。



三、二分

基本思想:

大致就是数学中的极限思维,我们都知道数学中求零点坐标的其中一个方法叫做二分法,通过不断的二分来精确零点的取值,这里所讲述的二分思想也是如此,那么为什么要学会二分呢,这就不得不说二分的高效性了,我们都知道计算机中的范围是0~2^32,如果我们利用二分去查找一个整数,则最多也就查找32次,这么一听是不是快多了呢。

1.整数二分

算法思路:

定义left、right、mid分别代表左端点、右端点和其中间点,每次对中间点所对应的值进行判断,若中间值与目标值相等,则表示目标值找到了,若大于目标值则说明目标值位于左端,这时则缩小右半部分令right=mid-1,相反若中间值小于目标值则缩小左半部分令left=mid+1,利用新的left、right和mid重复上述操作(显然,能实现上述操作的前提是序列有序,所以利用二分前应确保序列为有序序列)

基本结构:

while (left <= right)
		{
			mid = (left + right)>>1;
			if (left<=right&&chack(mid) {right = mid-1;
			}
			else  left = mid+1;
			}

(口说无凭假把式,我们直接来看一道题吧)

例题讲解

思路:

很好,题目说给的序列必须是升序序列,正好省的我们给排序了,我们先以两端的数为left和right(就是1和4嘛),通过我们上述讲到的二分过程找到指定数,我们发现有些数是重复的,我们要找的是指定数的开头和最后的位置,所以我们找到指定数之后可以在该位置设置两个指针向两边寻找,直到找到的数不为指定数为止。我们还是直接看代码吧,这样更直观点(好吧,其实是我这只蒟蒻的口才就只能到这里了

#include<iostream>
using namespace std;
int arr[100000];
int main()
{
	int n, m;
	cin >> n >> m;
	for (int i = 0; i <n; i++)
		cin >> arr[i];
	while (m--)
	{
		int a, min = 0;
		cin >> a;
		int s = 0;
		int left = 0, right = n-1;//因为下标是从0开始的
		while (left <= right)
		{
			min = (left + right) / 2;
			if (a < arr[min]) {//中间值大于基准数,缩小右半部分
				right = min-1;
			}
			else if (a > arr[min]) {//中间值小于基准数,缩小左半部分
				left = min+1;
			}
			else if (a == arr[min]) {//找到啦,累鼠了赶紧跳出循环!!
				s = 1;//标记一下,证明是真的找到了而不是因为left>right跳出去的
				break;
			}
		}
		int q = min, w = min;
		if (s == 0)cout << -1 << " " << -1 << endl;//没标记到,说明没找到
		else {//开始向两边查找
			while(q>=0){
				if (arr[q-1] != a) {
					break;
				}
				q--;
			}
			while (w < n) {
				if (arr[w+1] != a) {
					break;
				}
				w++;
			}
			cout << q << " " << w << endl;
		}
	}
	return 0;
}

2.浮点二分

算法思路:

相较于整数二分,浮点更能体现出其精确的特点,更能体现数学思维中的二分思想,通过不断的二分缩小范围达到精确到小数的程度(这些话貌似不用敲,但就是感觉这里也该写段思路好跟上面对标一下,我有强迫症qwq

例题讲解:

思路:

若是二次方,我们则可以用sqrt函数,但若是遇到多次方根呢,我们不得不寻找其他思路,若是每个数都式一次那计算量是相当大的(能算出来但你考虑过计算机的感受吗),我们发现通过二分可以高效的达成我们所想要的结果,什么?你问我怎么保留小数点后六位?我们发现逐渐二分精确答案后,最终right和left的差值不会超过0.0000001,由于有四舍五入的情况,所以我们把精度精确到1e-8最后输出结果是保留后六位。具体代码如下。

#include <iostream>
using namespace std;
int main()
{
	double n;
	cin >> n;
	double l = -10000, r = 10000;
	while (r - l > 1e-8)//保留六位小数取1e-8
	{
		double mid = (l + r) / 2;//二分
		if (mid * mid * mid > n) r = mid;//朴实无华的三次方
		else l = mid;
	}

	printf("%lf", l);//C++规定输出为保留6位
	return 0;
}

(怎么样,是不是代码很简短很人畜无害,hhh我也那么感觉)

四、前缀和

简介:前缀和,顾名思义就是前n个元素的总和,在我们计算某区间和时一般思路是将该区间的数依次相加,但这样做法效率较低,算法中前缀和的思想则是将前n个数的总和定义在一个新的前缀和数组里,当要计算某区间和时,仅需要利用前缀和数组进行计算即可,大大提高了效率。

1.一维前缀和

算法思路

一维前缀和是以一维数组为载体实现的,定义一个前缀和数组为S[ ],原数组为arr[ ],则递推公式为:S[i]=S[i-1]+arr[i],S[i]表示前i个元素的和。

小试牛刀

describe:

输入一个长度为 n 的整数序列。

接下来再输入 m 个询问,每个询问输入一对 l,r。

对于每个询问,输出原序列中从第 l 个数到第 r 个数的和。

代码实现:

#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
int a[N], s[N];
int main() 
{
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
		s[i] = s[i - 1] + a[i];//前缀和数组;
	}
	while (m--) {
		int l, r;
		cin >> l >> r;
		if (l > r) {//确保l<r
			l ^= r;
			r ^= l;
			l ^= r;
		}
		cout << s[r] - s[l - 1] << endl;
	}
	return 0;
}

2.二维前缀和

算法介绍:

一维数组的前缀和我们可以理解,无非就是前n个数的总和,那二维数组的前缀和是什么鬼?就算知道了又该怎么计算区间和呢?我们可以很容易想到求两个坐标之间数的和是将其视为一个矩形将里面的数相加,与一维前缀和类似,二维前缀和也是以第一个数(也就是从arr(1,1)开始加,当然你数组要是从(2,2)为起始的下标也不管你,你开心就好),接下来的步骤我放了张图在下面,也不用我做多解释了(解放双手!)

嗯,对,你已经晓得了,若S为前缀和数组,则计算(x1,y1)和(x2,y2)之间数的公式就是:

总和=S[x2,y2]-S[x1-1,y2]-S[x2,y1-1]+S[x1-1,y1-1]

oooh~上面忘了放前缀和的公式,那就在这里放上吧:

for (int i = 1; i <= n; i++)
		for (int j = 1; j <= m; j++){
			scanf("%d", &a[i][j]);
			s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];
			}

OK完工!



五、差分

差分是前缀和的思想的逆运算,该算法旨在便于序列中的某段加上(减去)某个数

1.一维差分

差分数组:

首先给定一个原数组a[ ];a[1],a[2],a[3]....a[n]

构造一个数组b[ ];b[1],b[2],b[3].....b[n] 

使得a[i]=b[1]+b[2]+b[3]+....+b[i]

如此则称数组b[ ]为数组a[ ]的差分数组,即数组a[i]为数组b[ ]的前i项和

构造差分分数组的方法:

void insert(int l, int r, int c)
{
    b[l] += c;
    b[r + 1] -= c;
}

其中构造过程中l、r=i  , c=arr[i]

模板题:

describe:

输入一个长度为 �n 的整数序列。

接下来输入 m 个操作,每个操作包含三个整数 l,r,c,表示将序列中 [l,r] 之间的每个数加上 c。

请你输出进行完所有操作后的序列。

代码实现:

#include<iostream>
using namespace std;
const int N = 100010;
int m, n;
int a[N], b[N];
void insert(int l, int r, int c)//差分
{
    b[l] += c;
    b[r + 1] -= c;
}
int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++)
		scanf("%d", &a[i]);
    //插入的方式形成b[i]
    for (int i = 1; i <= n; i++) 
		insert(i, i, a[i]);
    while (m--)
    {
        int l, r, c;
        scanf("%d%d%d", &l, &r, &c);
        insert(l, r, c);
    }
    for (int i = 1; i <= n; i++)
		b[i] += b[i - 1];
    for (int i = 1; i <= n; i++)
		printf("%d ", b[i]);

    return 0;
}

2.二位差分

思路

若a[][]数组是b[][]数组的前缀和数组,那么b[][]a[][]的差分数组

原数组: a[i][j]

我们去构造差分数组: b[i][j]

使得a数组中a[i][j]b数组左上角(1,1)到右下角(i,j)所包围矩形元素的和。

那么如何构造二维差分数组呢?

代码实现如下:

void insert(int x1, int y1, int x2, int y2, int c) {
    //构建二维差分数组
    a[x1][y1] += c;
    a[x2 + 1][y1] -= c;
    a[x1][y2 + 1] -= c;
    a[x2 + 1][y2 + 1] += c;
}

建议直接背这块板子,因为理解起来确实也挺抽象的(其实是我这只蒟蒻感觉很抽象

模板题:

输入一个 n 行 m 列的整数矩阵,再输入 q 个操作,每个操作包含五个整数x1​,y1​,x2​,y2​,c,其中 (x1​,y1​) 和 (x2​,y2​) 表示一个子矩阵的左上角坐标和右下角坐标。

每个操作都要将选中的子矩阵中的每个元素的值加上 c。

请你将进行完所有操作后的矩阵输出。

代码实现:

#include<iostream>
using namespace std;
const int N = 1010;
int a[N][N], s[N][N];//a为差分数组,s为原数组(前缀和数组)
int n, m, q;
void insert(int x1, int y1, int x2, int y2, int c) {
    //构建二维差分数组
    a[x1][y1] += c;
    a[x2 + 1][y1] -= c;
    a[x1][y2 + 1] -= c;
    a[x2 + 1][y2 + 1] += c;
}
int main() {
    scanf("%d %d %d", &n, &m, &q);
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            scanf("%d", &s[i][j]);
            insert(i, j, i, j, s[i][j]);
        }
    }
    while (q--) {
        int x1, y1, x2, y2, c;
        scanf("%d %d %d %d %d", &x1, &y1, &x2, &y2, &c);
        //更新差分数组
        insert(x1, y1, x2, y2, c);
    }
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            //更新原数组
            s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];
        }//这里是不是跟前缀和很像,呸,什么叫很像,这就是前缀和!!
    }
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            cout << s[i][j] << " ";
        }
        cout << endl;
    }
    return 0;
}


六、双指针

大致分为四种常用思想:

1.普通双指针:一般为两个for循环,如冒泡排序。

2.左右指针:两个指针分别指向开头和末尾,如何相向向中间遍历,直到满足条件或是两指针相遇(如二分查找)

3.快慢指针:两指针的起始点相同,由于条件不同,快指针运动快,慢指针运动慢,直到满足条件或是快指针走到末尾。

4.滑动窗口

在此我们就左右指针和快慢指针来讲解双指针。

左右指针

例题

describe

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

数组下标从 0 开始。

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

数据保证有唯一解。

基于之前有着二分思想的基础,想必思路也是异常的清晰

代码实现:

#include<iostream>
using namespace std;
int n, m, q;
int arr[100000];
int a[100000];
int main()
{
	cin >> n >> m >> q;
	for (int i = 0; i < n; i++) {
		cin >> arr[i];
	}
	for (int i = 0; i < m; i++) {
		cin >> a[i];
	}
	int i = 0, j = m-1, sum = 0;
	while (i < n && j >= 0) 
	{
		if (arr[i] + a[j] < q)i++;
		else if (arr[i] + a[j] > q)j--;
		else if (arr[i] + a[j] == q) {
			cout << i << " " << j;
			return 0;
		}
	}
}

快慢指针

快慢指针的重点在于寻找快慢指针直接的逻辑,明白这点解决题目也就游刃有余了。

例题:

代码实现:

#include <iostream>
#include<algorithm>
#define ll long long
using namespace std;
const int N = 2e5 + 10;
int n, c;
int a[N];
int main()
{
	cin >> n >> c;
	for (int i = 1; i <= n; i++) cin >> a[i];
	sort(a + 1, a + 1 + n);
	int l = 1, r1 = 1, r2 = 1;
	ll ans = 0;
	for (l = 1; l <= n; l++) {
		while (r1 <= n && a[r1] - a[l] <= c) r1++;//快指针
		while (r2 <= n && a[r2] - a[l] < c) r2++;//慢指针
		if (a[r2] - a[l] == c && a[r1 - 1] - a[l] == c && r1 - 1 >= 1)
			ans += r1 - r2;
	}
	cout << ans;
	return 0;
}


七、高精度

概述:

在计算的过程中,若给的数值较大,如几十位上百位的数,这种情况下longlong和double显然是不够的,所以我们需要寻找其他方法来计算这样庞大的数,因此我们引入了高精度这样一类算法。

算法思想:我们发现,1e20若是用字符串形式表示那也仅仅是占用了21个字节,我们可以用字符串的形式来表示较大的数,再通过类似于数学的运算方法来求得最后的结果。

1.高精度加法

我们将较大的数以字符串的形式储存在数组中,通过类似于数学的竖式运算来进行计算:

就像这样。

模板:

#include<iostream>
using namespace std;
char s1[10000000], s2[10000000];
int a[10000000], b[10000000], c[10000000];
int main(void)
{
	cin >> s1 >> s2;
	int la = strlen(s1);//*差不多是位数的意思
	int lb = strlen(s2);
	int lc = max(la, lb) + 1;//+1便于后面操作
	int i = 0;
	for (i = 0; i < la; i++){
		a[la - i] = s1[i] - '0';//将浮点数对应的数字转换为整型
	}
	for (i = 0; i < lb; i++){//同上
		b[lb - i] = s2[i] - '0';
	}
	for (i = 1; i <= lc; i++){//开始加法运算
		c[i] += a[i] + b[i];//*先无脑加
		c[i + 1] += c[i] / 10;//*然后进位
		c[i] %= 10;
//*如果没进位,那没事,进位了,就要取余10,防止炸掉,你当然不想个位数存在10这样的数字
	}
	while (c[lc] == 0 && lc > 0){
		lc--;
	}
	
	for (i = lc; i >= 1; i--){
		cout << c[i];//*输出就完事了
	}
	return 0;
}

2.高精度减法

很显然,与上述加法运算一个道理,直接上代码

#include <iostream>
#include<cstring>
using namespace std;
const int N = 1e5 + 10;
int a[N], b[N], result[N];
string s1, s2;
int lena, lenb, lenc;
bool fu = false;
bool checkzf()//checkzf()为判断a是否大于b,如果大于或等于返回true,否则返回false  
{
	int flag = 0;//标记是否相等 
	if (lena > lenb) return true;//如果a的长度大于b的长度,说明a>b,返回true 
	if (lena < lenb) return false;//如果a的长度大于b的长度,说明a<b,返回false 
	if (lena == lenb){//如果长度相等,有一个a[i]>b[i], 就说明a > b, 否则说明a<b 
		for (int i = lena - 1; i >= 0; i--){
			if (a[i] > b[i]){
				flag = 1;//标记 
				return true;
			}
			if (a[i] < b[i]){
				flag = 1;//标记 
				return false;
			}
		}
	}
	if (flag == 0) return true;//如果不大也不小,就是相等了。 
}
int main()
{
	cin >> s1 >> s2;
	lena = s1.size();
	lenb = s2.size();
	lenc = max(lena, lenb);
	//倒序存储 
	for (int i = 0; i < lena; i++) a[i] = s1[lena - i - 1] - '0';
	for (int i = 0; i < lenb; i++) b[i] = s2[lenb - i - 1] - '0';
	if (checkzf()){//checkzf()为判断a是否大于b,如果大于或等于返回true,否则返回false 
		for (int i = 0; i < lena; i++){
			if (a[i] < b[i]) {//如果当前位的被减数小于减数,就要退位 
				a[i + 1]--;//比i高的一位被借走1 
				a[i] += 10;
			}
			result[i] = a[i] - b[i];
		}
	}
	else
	{
		fu = true;//如果a<b,输出b-a,不要忘记加负号 
		for (int i = 0; i < lenc; i++){//呃呃呃同理哟 
			if (b[i] < a[i]){
				b[i + 1] -= 1;
				b[i] += 10;
			}
			result[i] = b[i] - a[i];
		}
	}
	//删除前缀多余的0 
	int tmp = lenc;
	while (result[tmp] == 0 && tmp > 0) tmp--;
	if (fu) printf("-");//如果fu = true,输出负号
	//倒序输出 
	for (int i = tmp; i >= 0; i--) cout << result[i];
	return 0;
}

注释都标在上面啦,代码是不是一目了然呢

3.高精度乘法

相较于高精度加减法,高精度乘法的难度要大一点,因为这要建立在你已经会高精度加法的前提下,当然,都已经看到这了想必你也已经会加法了,接下来我们来分析一下乘法该如何计算。

依旧是熟悉的竖式计算,这样能更直观的看出乘法法则的规律,我们发现乘法法则是将其中一串数的每一位数均与另一串数相乘,并通过移位的方式相加起来,那么基本的逻辑已经搞懂了,值得注意的是输出结果前要删去前导零,接下来就是代码实现了,上代码!

#include<iostream>
#include<vector>
using namespace std;
vector<int> mul(vector<int>&A,int B)
{
    vector<int>C;
    int t=0;  //进位
    for(int i=0;i<A.size()||t;i++) 
    {
        if(i<A.size())
        t=A[i]*B+t;
        C.push_back(t%10);
        t/=10;
    }
    return C;
}
int main()
{
    string a;
    int B;
    cin>>a>>B;
    vector<int>A;
    for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0');
    auto C=mul(A,B);
    for(int i=C.size()-1;i>=0;i--)
    cout<<C[i];
    return 0;
}

4.高精度除法

直接.....看图说话吧

然后然后看代码说话(qwq好累)

#include <iostream>
#include<cstring>
using namespace std;
const int num = 1e5;
char ca[num];
int a[num], b, c[num];
void demo(char n1[], int n2) {
	int len1 = strlen(n1);
	int len = len1;
	for (int i = 1; i <= len1; ++i) {
		a[i] = ca[i - 1] - '0';
	}
	int x = 0;
	for (int i = 1; i <= len1; ++i) {
		c[i] = (x * 10 + a[i]) / n2;
		x = (x * 10 + a[i]) % n2;
	}
	int pos = 1;
	while (c[pos] == 0)	pos++;
	for (; pos <= len; ++pos) {//输出商 
		cout << c[pos];
	}
	cout << endl;
	cout << x;//输出余数 
}
int main() {
	cin >> ca >> b;
	demo(ca, b);
	return 0;
}


八、位运算

概述

计算机是以二进制来存储数据的,而位运算就是直接对整数在内存中的二进制位进行操作,因此执行效率非常高,计算过程若使用位运算进行操作则会大大提高效率。

位运算符大致有以下几种

(在读上述位运算符时应在前加上“按位”,如“按位与”“按位或”....)

奉上一道简单的题

describe:

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

我们直到按位与运算符是在两数二进制状态下对应位置上的数均为1时结果才唯一,那么我们可以结合按位与和按位右移运算符来解决上述问题。

#include <iostream>
using namespace std;
int arr[100000];
int abc(int n)
{
	int count = 0;
	for (int i = 0; i < 32; i++)
	{
		if (((n >> i) & 1) == 1) {
			count++; 
		}
	}
	return count;
}
int main()
{
	int n = 0;
	cin >> n;
	for (int i = 0; i < n; i++) {
		cin >> arr[i];
	}
	for (int i = 0; i < n; i++) {
		int ret = abc(n);
		cout << abc(arr[i])<<" ";
	}
	return 0;
}

通过一次次的右移和对最右端数是否是1的判断,通过这种方法我们仅需判断至多32次就可以得出答案(因为计算机中二进制数最大为32位)

九、区间合并

概述

给定两个区间[l,r]、[a,b],若a>=l&&a<=r,即这两个区间有重叠的部分(在轴上的位置),则这两个区间可以合并在一起成为一个区间(如[1,3]和[2,6]可以合并为[1,6]),我们称这为区间合并。相反,若a>r或b<l,则这俩区间无交集,则不能实现区间合并。

例题:

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

基本思路:

1. 按区间的左端点进行排序。

2.扫描每个区间,将每个可能有交集的区间进行合并。

易知有包含、相交和相离三种情况。

#include <iostream>
#include<algorithm>
#include<vector>
using namespace std;
typedef pair<int, int> PII;
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;
    cin>>n;
    vector<PII> segs;
    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;
}


十、离散化

概述

离散化,就是把无限空间中有限的个体映射到有限的空间中去,以此提高算法的时空效率。通俗的讲就是在不改变数据相对大小的条件下,对数据进行相应的缩小,可以有效降低时间复杂度。其本质是建立了一段数列到自然数之间的映射关系(value -> index),通过建立新索引,来缩小目标区间,使得可以进行一系列连续数组可以进行的操作比如二分,前缀和等…

什么时候可以用离散化???

当数据只与它们之间的相对大小有关,而与具体是多少无关时,可以进行离散化。

(咋让我想起了质点.....)

离散化处理问题的大致过程为排序->去重->索引

例题

假定有一个无限长的数轴,数轴上每个坐标上的数都是 0。

现在,我们首先进行n次操作,每次操作将某一位置 x 上的数加 c。

接下来,进行 m 次询问,每个询问包含两个整数 l 和 r,你需要求出在区间 [l,r] 之间的所有数的和


#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef pair<int, int> PII;

const int N = 3 * 10e5 + 10;        //n + 2m的最大值为300000

int a[N], s[N];
int n, m;
vector<int> alls;       //存放要记录(被加、被查询)的点的坐标,并对这些坐标进行离散化后映射到a、s
vector<PII> add, query;

int find(int x){        //alls存了坐标,通过坐标x映射出它离散化后的相对位置idx:下标0、1、2...    
	int l = 0, r = alls.size() - 1;        //二分查找的方式    
	while(l < r){        
		int mid = (l + r) / 2;        
		if(alls[mid] >= x) r = mid;
		else    l = mid + 1;
	}
	
	return r + 1;		//+1为了与前缀和数组下标对齐
}

int main(){
	cin >> n >> m;        
	while(n --){        //记录加值的点的信息        
		int x, c;        
		cin >> x >> c;
		
		add.push_back({x, c});
		alls.push_back(x);
    }
    
    while(m --){               //记录要查询的区间的信息        
    	int l, r;        
    	cin >> l >> r;                
		
		query.push_back({l, r});        
		alls.push_back(l);        
		alls.push_back(r);
    }        
    //alls排序并去重,其实就是为了保持这些"坐标"的前后相对性    
    sort(alls.begin(), alls.end());    
    alls.erase(unique(alls.begin(), alls.end()), alls.end()); 
    
    for(auto item : add){		//处理改动
    	int idx = find(item.first);     //映射出相对位置        
    	a[idx] += 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);
        int r = find(item.second);
        cout << s[r] - s[l - 1] << endl;
    }
 
    return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值