基础算法(排序、二分、高精度、前缀与差分、位运算...)

一、排序

1.快排

(1)基本定义及思想

快速排序是由C. A. R. Hoare在1962年提出,对冒泡排序的一种改进,采用了一种分治的策略。通过一趟排序将要排序的数据分割成独立的两部分,其中一部分是所以数据都比另外一部分的数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到数据变成有序序列,平均状况下时间复杂度为O(nlogn)。分治算法都分为三步:分成子问题递归处理子问题子问题合并

(2)动图演示

在这里插入图片描述

(3)模板
void quick(int q[], int l,int r)
{
    if(l >= r) return ;//递归结束
    
    //第一步:分成子问题
    int i = l - 1, j = r + 1;
    int x = q[l + r >> 1]; //用x分割成两部分
    while(i < j)
    {
        do i++; while(q[i] < x); //找到左边大于x的数
        do j--; while(q[j] > x); //找到右边小于x的数
        if(i < j ) swap(q[i], q[j]); //交换
    }

	//第二步:递归处理子问题
    quick(q, l, j); //将分割的左边部分进行递归处理
    quick(q, j + 1, r);  //将分割的右边部分进行递归处理

	//第三步:这里不需要
}

练习题:AcWing 785. 快速排序

2.归并

归并算法也是在分治算法的基础上设计出来的一种排序算法,它可以对指定序列完成升序或降序排序,跟快排的不同主要在于归并问题需要额外空间,而且在最后多了合并子问题这一步骤

(1)动图演示

在这里插入图片描述

(2)模板
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, tmp[r - l + 1];
    while(i <= mid && j <= r)
        if(q[i] <= q[j]) tmp[k++] = q[i++];
        else tmp[k++] = q[j++];
    while(i <= mid) tmp[k++] = q[i++];
    while(j <= r) tmp[k++] = q[j++];

    for(k = 0, i = l; i <= r; k++, i++) q[i] = tmp[k];
}

练习题:AcWing 787. 归并排序

二、二分模板

(1)整数二分

// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
    while (l < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;    // check()判断mid是否满足性质
        else l = mid + 1;
    }
    return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
    while (l < r)
    {
        int mid = l + r + 1 >> 1;
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    return l;
}

练习题:789. 数的范围

(2)浮点数二分

double bsearch_3(double l, double r)
{
    const double eps = 1e-6;   // eps 表示精度,取决于题目对精度的要求
    while (r - l > eps)
    {
        double mid = (l + r) / 2;
        if (check(mid)) r = mid;
        else l = mid;
    }
    return l;
}

练习题:790. 数的三次方根

三、高精度

(1)高精度概念:

什么是高精度算法?它是处理大数字的数学计算方法、在一般的科学计算中,会经常算到小数点后近百位或者更多,当然也可能是几千亿几百亿的大数字。一般这类数字我们统称为高精度数,因其运算的范围大大超出了标准数据类型能表示的运算范围,为了处理高进度,我们可以使用vector数组来存储高精度数中的每一位数字。

(2)模板:高精度加法、减法、乘法、除法

>>高精度加法
vector<int> add(vector<int> &A, vector<int>&B)
{
    vector<int> C; //开一个vector数组来存储加和后的每一位数字 
    int t = 0; //每次存储并加上第i位的数字
    for(int i = 0 ; i < A.size() || i < B.size() ; i++)
    {
        if(i < A.size()) t += A[i];
        if(i < B.size()) t += B[i];
        C.push_back(t % 10);  //插入结果, 注意这里是逆序插入的!
        t /= 10;  //进位
    }
    if(t) C.push_back(1);
    return C;
}

>>   高精度减法
//比较函数,判断输入数据的大小
bool cmp(vector<int>&A, vector<int>&B)
{
    if(A.size() != B.size())return A.size() > B.size();
    for(int i=A.size()-1 ; i>=0 ; i--)
    {
        if(A[i]!=B[i])return A[i]>B[i];
    }
    return true; //判断 a > b 是否为真
}

//减法操作
vector<int> sub(vector<int>&A, vector<int>&B)  //传引用是为了提高效率
{
    vector<int> C; //开一个vector数组来存储减去后的每一位数字 
    
    for(int i = 0, t = 0 ; i < A.size() ;i++) //除法操作需要从最高位开始,即i = 0
    {
        t = A[i] - t;
        if(i < B.size()) t -= B[i];
        C.push_back((t + 10) % 10);  //保证插入的数据为正值
        if(t < 0) t = 1;  //减去的结果为负,借1位
        else t = 0;
    }
    
    while(C.size() > 1 && C.back() == 0) C.pop_back();  //除去前导零
    
    return C;
    
}


>>   高精度乘法
//乘法操作
vector<int> multi(vector<int>&A, vector<int>&B)  //传引用是为了提高效率
{
    vector<int> C(A.size() + B.size() + 5, 0); //开一个vector数组来存储乘上的数字,初始化为0

    for(int i = 0 ; i < A.size() ; i++)
        for(int j = 0 ; j < B.size(); j++)
            C[i + j] += A[i] * B[j];  //用C[i + j] 存储对应位置乘上所得到的数
            
    int t = 0;
    for(int i = 0 ; i < C.size(); i ++)
    {
        t += C[i];
        C[i] = t % 10; //插入数值
        t /= 10;  //更新t
    }
    
    //除去前导零, 这里是因为我们开vector数组C的长度大于了A, B 之和
    while(C.size() > 1 && C.back() == 0) C.pop_back();  
        
    return C;
}



>>   高精度除法
//比较函数
bool cmp(vector<int> &A, vector<int> &B)
{
    if(A.size() != B.size()) return A.size() > B.size();
    for(int i = A.size() - 1 ;i >= 0 ; i--)
    {
        if(A[i] != B[i]) return A[i] > B[i]; 
    }
    return true;  //a > b
}

//减法操作
vector<int> sub(vector<int> &A,vector<int> &B)
{
    vector<int> C;
    int t = 0;
    for(int i = 0;i < A.size() || t; i ++)
    {
        t = A[i] - t;
        if(i < B.size()) t -= B[i];
        C.push_back((t + 10) % 10); //保证插入数值为正
        if(t < 0) t = 1;  //借位
        else t = 0;  
    }
    
    while(C.size() > 1 && C.back() == 0) C.pop_back();  //除去前导零
    
    return C;
}

//除法操作
vector<int> div(vector<int> &A, vector<int> &B, vector<int> &r)
{
    vector<int> C; //存储商的每一位数字
    
    if(!cmp(A,B))  // a < b 直接插入0,余数为a
    {
        C.push_back(0);
        r=A;
        return C;
    }
    
    int j = B.size();
    r.assign(A.end()-j, A.end());  //将区间元素赋值到当前的容器里
    
    while(j <= A.size())
    {
        int k=0;
        while(cmp(r, B))  //当前余数r > 除数 
        {
            r = sub(r, B); r = r - b
            k ++;   //k增加1
        }
        C.push_back(k);
        
        if(j < A.size()) r.insert(r.begin(),A[A.size()-j-1]);
        
        if(r.size() > 1&& r.back() == 0) r.pop_back();
        
        j++;
    }
    
    reverse(C.begin(), C.end());
    while(C.size() > 1 && C.back() == 0) C.pop_back();  
    
    return C;
}

练习题:AcWing 791. 高精度加法AcWing 792. 高精度减法AcWing 793. 高精度乘法 AcWing 794. 高精度除法

四、前缀和与差分

(1)前缀和概念:

前缀和是指某序列的前 n 项和,可以把它理解为数学上的数列前 n 项和,二差分可以看作成前缀和的逆运算。合理的使用前缀和与差分,可以将某些复杂的问题简单化。
简单来说,我们可以有一个原数组 a[n] 和一个前缀和数组 s[n] ,他们说满足以下条件:

s[0] = a[0] 
s[1] = a[0] + a[1]
s[2] = a[0] + a[1] + a[2]
...
依次类推,即可得公式 s[n] = a[n] + a[n - 1] + a[n - 1] + ... + a[1] + a[0] 	
               也即 s[n] = s[n - 1] + a[n]  

(2)一维前缀和、二维前缀和

  • 一维前缀和的得到非常简单,也很好理解,我们只需要在每次遍历的时候,把前一次计算的和 s[n - 1] 加到当前正在计算的位置 s[n] 上,就能够构造出来,即:
    for(int i = 0 ; i <= n ; i++)  cin >> a[i], s[i] = s[i - 1] + a[i];
    
  • 二维前缀和的得到则需要根据图中二维矩阵的运算规律进行推导
    在这里插入图片描述
    由此,我们就可以使用前缀和快速的求出某个区间所有元素的加和、矩阵中某个子矩阵的所有元素之和,如:AcWing 795. 前缀和 || AcWing 796. 子矩阵的和

(3)差分的介绍

差分是求前缀和的逆操作,类似于数学中的求导和积分,对于原数组 a[n] ,构造出一个数组 b[n] ,使得 a[n]b[n] 的前缀和。一般用于快速的对整个数组进行操作,比如将原数组中的 [l, r] 部分的数据全部加上一个常数 c 。如果使用暴力的话,时间复杂度至少为O(n),而使用差分算法的话就可以将时间复杂度降低到O(1)。

(4)差分的运用:一维差分、二维差分

一维差分的构造方法: 
a[0] = 0
b[1] = a[1] - a[0]
b[2] = a[2] - a[1]
b[3] = a[3] - a[2]
...
依次类推, 即可得 b[i] = a[i] - a[i - 1]

由于 a 数组为 b 数组的前缀和,每次在 b[i] 位置上加上或者减去一个数,转化为原数组 a 后都会对该位置后面的元素产生相同的影响,所以我们可以利用这个性质,在时间复杂度为O(1)的基础上,对某一个区间执行加上或者减去一个数的操作,即

在这里插入图片描述

b[l] + c 后,效果使得 a 数组中a[l] 后面的数组都会加上 c 红色部分,a[r] 后面的数据也会改变,所以再让 b[r + 1] - c 绿色部分,就实现了对区间 [l, r] 进行加减的操作,时间复杂度为O(1), 大大提高了效率。

对于二维差分矩阵,我们改如何去构造呢?
在这里插入图片描述
对应练习题:AcWing 797. 差分AcWing 798. 差分矩阵

五、双指针

(1)概念

双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个相同的方向(快慢指针)或者相反方向(对撞指针)的指针进行扫描,从而达到相应的目的。一般来说,在数组遍历的过程中需要注意以下几个细节:双指针的初始位置
双指针的移动方式遍历的结束条件

(2)模板

这里以800. 数组元素的目标和为例

//两指针i,j
// i的初始位置为0, 往后搜索
//j的初始位置为m - 1, 往前搜索
//遇到满足条件的状态(a[i] + b[j] = q)时结束
for(int i = 0 , j = m - 1; i < n ; i ++)  
    {
        while(j > 0 && b[j] + a[i] > q) j--; //j指针不会回退
        if(a[i] + b[j] == q) //check(a, b)判断条件
        {
            cout << i << " " << j << endl;
            return 0;
            
        }

本例还可以用二分思想来做

for(int i = 0 ; i < n ; i++ )
    {
        int x = q - a[i];  //二分
        
        int l = 0, r = m - 1;
        while(l <= r)
        {
            int mid = l + r >> 1;
            if(x == b[mid])
            {
                cout << i << " " << mid << endl; return 0;
            }
            else if(b[mid] > x) r = mid - 1;
            else l = mid + 1;
        }

练习题:AcWing 799. 最长连续不重复子序列AcWing 2816. 判断子序列

六、位运算

(1)简介

现代计算机中所有的数据都是以二进制(0、1)的形式存储在设备中,位运算就是直接对整数在内存中的二进制位进行操作,因此执行效率非常高,在程序中尽量使用位运算进行操作,能够大大提高程序的性能。

(2)图表概览

符号描述运算规则
&按位与两个位都为1时,结果才为1,如:1001 & 1101 = 1001
|按位或两个位都为0时,结果才为0,如:1001 & 1101 = 1101
^异或两个位相同为0,相异为1,如:1001 & 1101 = 0100
~取反0 -> 1 、 1 -> 0
<<左移各二进位全部左移若干位,高位丢弃,低位补0
>>右移各二进位全部右移若干位,对无符号数,高位补0,有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补0(逻辑右移)

(3)模板 AcWing 801. 二进制中1的个数

   while(n--)
   {
       int x, res = 0;
       cin >> x;
       for(int i = 0 ; i <= 31 ; i ++)  //每次右移一位
           if(x >> i & 1) res++;  // x的第k位数字: x >> k & 1若是1,则res++
       cout << res << " ";
   }

这里补充一下计算机中原码、补码和反码的内容

  • 正数的原码、反码、补码相同
  • 负数的原码将对应正数的符号位变为1、反码根据原码按位取反、补码为反码+1

[+9] 原 = 0000 0000 0000 0000 0000 0000 0000 1001
[+9] 反 = 0000 0000 0000 0000 0000 0000 0000 1001
[+9] 补 = 0000 0000 0000 0000 0000 0000 0000 1001

[-9] 原 = 1000 0000 0000 0000 0000 0000 0000 1001
[-9] 反 = 1111 1111 1111 1111 1111 1111 1111 0110
[-9] 补 = 1111 1111 1111 1111 1111 1111 1111 0111

七、离散化

离散化的本质是映射,即将间隔很大的点,映射到相邻的数组元素中,可以减少对空间的要求。这里用一道模板题区间和来理解

在这里插入图片描述
其实映射最大的难点是前后的映射关系,如何能够将不连续的点映射到连续的数组的下标。解决办法就是开辟额外的数组存放原来的数组下标,或者说下标标志,本文是原来上的数轴上的非连续点的横坐标。此处的做法是是对原来的数轴下标进行排序,再去重,为什么要去重呢,因为本题提前考虑了前缀和的思想,其实很简单,就是我们需要求出的区间内的和的两端断点不一定有元素,提前加如需要求前缀和的两个端点,有利于我们进行二分搜索,其实二分搜索里面我们一般假定有解的,如果没解的话需要特判,所以提前加入了这些元素,从而导致可能出现重复元素。

AC代码

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

using namespace std;
const int N = 300010; //n次插入和m次查询相关数据量的上界
int n, m;
int a[N];//存储坐标插入的值
int s[N];//存储数组a的前缀和
vector<int> alls;  //存储(所有与插入和查询有关的)坐标
vector<pair<int, int>> add, query; //存储插入和询问操作的数据

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;
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) {
        int x, c;
        scanf("%d%d", &x, &c);
        add.push_back({x, c});
        alls.push_back(x);
    }
    for (int i = 1; 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());
    //执行前n次插入操作
    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];
    //处理后m次询问操作
    for (auto item : query) {
        int l = find(item.first);
        int r = find(item.second);
        printf("%d\n", s[r] - s[l-1]);
    }

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

leisure-pp

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值