AcWing算法基础课PART 1基础算法

快速排序

  • 基于分治思想的不稳定排序(特殊情况:若将数组中的每个值变成值与下标的二元组,就能保证数组中所有值都不相同,此时的快排是稳定的)

  • 平均时间复杂度 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n),最坏是 O ( n 2 ) O(n^2) O(n2)

  • 步骤:

    • 确定分界点xq[l] q[(l+r)/2] q[r] 随机
    • 调整区间:小于等于x的在左半边,大于x的在右半边
    • 递归处理两段
  • 暴力做法

    • 开两个额外的数组a[]b[]
    • 扫描q[l ~ r]中的每个数
      • q[i] <= x,则x -> a[]
      • q[i] > x,则x -> b[]
    • a[] -> q[]b[] -> q[]
  • 优雅做法(双指针)

    • 两个指针分别指向数组的两端

    • 左侧指针指向的值小于x时,右移,大于等于x时,停止;右侧指针指向的值大于x时,左移,小于等于x时,停止

    • 如果左侧指针在右侧指针的左侧,则交换指向的值

    • #include <iostream>
      using namespace std;
      const int N = 100005;
      int n, a[N];
      void quick_sort(int a[], int l, int r)
      {
          if(l >= r) return ;
          int x = a[l + r >> 1], i = l - 1, j = r + 1;
          while(i < j)
          {
              do i++; while(a[i] < x);
              do j--; while(a[j] > x);
              if( i < j) swap(a[i], a[j]);
          }
          //处理边界
          quick_sort(a, l, j);
          quick_sort(a, j+1, r);
      }
      int main()
      {
          scanf("%d",&n);
          for(int i=0;i<n;i++) scanf("%d", &a[i]);
          quick_sort(a, 0, n-1);
          for(int i=0;i<n;i++) printf("%d ", a[i]);
          return 0;
      }
      

归并排序

  • 基于分治思想的稳定排序

  • 时间复杂度 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)

  • 基本步骤:

    • 确定分界点:mid = (l + r) / 2
    • 递归排序左右两侧
    • 归并 —— 合二为一 O ( n ) O(n) O(n)
  • img

  • 双指针算法

    #include <iostream>
    using namespace std;
    const int N = 100005;
    int n, a[N], tmp[N];
    void merge_sort(int a[], int l, int r)
    {
        if (l >= r) return ;
        int mid = l + r >> 1;
        merge_sort(a, l, mid);
        merge_sort(a, mid + 1, r);
        int k = 0, i = l, j = mid + 1;
        while(i <= mid && j <= r)
        {
            if (a[i] <= a[j]) tmp[k ++] = a[i ++];
            else tmp[k ++] = a[j ++];
        }
        while(i <= mid) tmp[k ++] = a[i ++];
        while(j <= r) tmp[k ++] = a[j ++];
        for(i = l, j = 0; i <= r; i ++, j ++) a[i] = tmp[j];
    }
    int main()
    {
        scanf("%d", &n);
        for(int i = 0; i < n; i ++) scanf("%d", &a[i]);
        merge_sort(a, 0, n-1);
        for(int i = 0; i < n; i ++) printf("%d ", a[i]);
        return 0;
    }
    

Notes

假定在***待排序***的记录序列中,存在***多个***具有***相同***的关键字的记录,若经过排序,这些记录的***相对次序保持不变***,即在原序列中,r[i] = r[j],且r[i]r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是***稳定***的;否则称为***不稳定***的。

二分

二分的本质并不是单调性,有单调性一定可以二分,但二分不一定需要单调性。

整数二分

本质:边界问题

在这里插入图片描述
在区间内,右边部分满足某种性质,左边部分不满足该种性质,有两个二分模板分别寻找左右两个点。

绿色点【找大于等于给定数的第一个位置(满足某个条件的第一个数)】:取mid = l+r >> 1,在check()函数中判断mid是否符合绿色部分性质。若符合,则表示绿色点在mid的左边部分,此时更新r = mid;若不符合,则表示绿色点在mid的右边部分,此时更新l = mid+1

int bsearch_1(int l, int r)
{
    while (l < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;
        else l = mid + 1;
    }
    return l;
}

红色点【找小于等于给定数的最后一个数(满足某个条件的最后一个数)】:取mid = l+r+1 >> 1,在check()函数中判断mid是否符合红色部分性质。若符合,则表示红色点在mid的右边部分,此时更新l = mid;若不符合,则表示红色点在mid的左边部分,此时更新r = mid-1

mid = l+r+1 >> 1原因:如果mid = l+r >> 1的话,当l = r-1,那么mid = l,如果check(mid)true的话,l = mid,就陷入死循环。

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

总结:

简单总结一下就是在[0,0,0,...,0](共k个数) 里面搜索0

使用第一个会返回位置0(对应最大值最小是…的问题)

使用第二个会返回k - 1(对应最小值最大是…的问题)

也可以看做寻找 第一个<= target的元素 和 最后一个<= target的元素

注:从小到大的数组中,lower_bound(begin, end, num)[begin, end)找第一个大于等于num的元素,upper_bound(begin, end, num)[begin, end)找第一个大于num的元素;从大到小的数组中,lower_bound(begin, end, num)[begin, end)找第一个小于等于num的元素,upper_bound(begin, end, num)[begin, end)找第一个小于num的元素。

浮点数二分

循环条件r - l > eps

eps取值:4位小数1e-6;5位小数1e-7;6位小数1e-8

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

高精度计算

输入/输出

输入:将大整数以字符串形式输入,此时高位在前,低位在后;从字符串尾部按位转换成int型插入vector数组中,此时,低位在前,高位在后,这样便于对最高位进1的操作。

输出:新数组的存放顺序仍为低位在前,高位在后,从数组尾部向前遍历输出即可。

加法

思路:从低位开始,将两个大整数相同位相加,得数为t,需考虑进位,两个个位数相加,如有进位一定是1,个位放入新数组,十位为进位与后一位相加,重复操作直到较长的数组遍历结束,此时若t = 1,即最高位产生进位1,需要将这个进位放入数组中。

#include <iostream>
#include <vector>
#include <string>
using namespace std;
vector<int> add(vector<int> a, vector<int> b)
{
    if(a.size() < b.size()) add(b, a);
    vector<int> c;
    int t = 0;
    for(int i=0;i<a.size();i++)
    {
        t += a[i];
        if(i < b.size()) t += b[i];
        c.push_back(t % 10);
        t /= 10;
    }
    if(t) c.push_back(t);
    return c;
}
int main()
{
    string A, B;
    vector<int> a, b;
    cin >> A >> B;
    for(int i=A.size()-1;i>=0;i--) a.push_back(A[i] - '0');
    for(int i=B.size()-1;i>=0;i--) b.push_back(B[i] - '0');
    vector<int> c = add(a, b);
    for(int i=c.size()-1;i>=0;i--) printf("%d", c[i]);
    return 0;
}

减法

思路:首先要判断两个大整数谁大谁小,从而保证相减运算时一定为大减小。判断大小时,首先看长度,若长度相同,看各个位置,若均相同,直接返回true,结果非负就不用加符号。在进行相减运算时,要考虑借位,借位最多借1,从低位开始按位减,首先给t赋值为a[i] - t,相减结果为t,如果t >= 0,则无需借位,如果t < 0,则需要借位,t + 10,将t或者t + 10放入新数组,合并一下操作,将(t + 10) % 10放入数组。如果t < 0,在下一位运算时,需要减去1,给t赋值1

#include <iostream>
#include <vector>
#include <string>
using namespace std;
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;
}
vector<int> sub(vector<int> a, vector<int> b)
{
    vector<int> c;
    int t = 0;
    for(int i=0;i<a.size();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;
}
int main()
{
    string A, B;
    vector<int> a, b;
    cin >> A >> B;
    for(int i=A.size()-1;i>=0;i--) a.push_back(A[i] - '0');
    for(int i=B.size()-1;i>=0;i--) b.push_back(B[i] - '0');
    vector<int> c;
    if(cmp(a, b)) c = sub(a, b);
    else 
    {
        cout << '-';
        c = sub(b, a);
    }
    for(int i=c.size()-1;i>=0;i--) printf("%d", c[i]);
    return 0;
}

乘法

思路:从个位开始,高精度整数的每一位乘以整数b,相乘的结果个位存入运算结果中,保留其他位数进入下一位的运算。

#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;
        c.push_back(t % 10);
        t /= 10;
    }
    while(c.size() > 1 && c.back() == 0) c.pop_back();
    return c;
}
int main()
{
    string A;
    int b;
    vector<int> a;
    cin >> A >> b;
    for(int i=A.size()-1;i>=0;i--) a.push_back(A[i] - '0');
    vector<int> c = mul(a, b);
    for(int i=c.size()-1;i>=0;i--) printf("%d",c[i]);
    return 0;
}

除法

思路:从最高位开始运算,模拟竖式相除。

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
vector<int> div(vector<int> &a, int b, int &r)
{
    vector<int> c;
    r = 0;
    for(int i=a.size()-1;i>=0;i--)
    {
        r = r * 10 + a[i];
        c.push_back(r / b);
        r %= b;
    }
    reverse(c.begin(), c.end());
    while (c.size() > 1 && c.back() == 0) c.pop_back();
    return c;
}
int main()
{
    string A;
    vector<int> a;
    int b;
    cin >> A >> b;
    for(int i=A.size()-1;i>=0;i--) a.push_back(A[i] - '0');
    int r;
    vector<int> c = div(a, b, r);
    for(int i=c.size()-1;i>=0;i--) cout << c[i];
    cout << endl << r;
    return 0;
}

前缀和与差分

前缀和可以在 O ( n ) O(n) O(n)时间统计和修改,在 O ( 1 ) O(1) O(1)时间内查询统计任意区间之和;差分可看作前缀和的逆运算,可在 O ( 1 ) O(1) O(1)时间操作任意区间。

前缀和

  • 一维

    O ( n ) O(n) O(n) 预处理(加和)s[i] = a[1] + ··· + a[i];

    O ( 1 ) O(1) O(1) 查询区间[l, r]内数的和 s[r] - s[l - 1];

  • 二维

    O ( n m ) O(nm) O(nm) 预处理

    s[i][j]= ∑ i = 1.. i j = 1.. j a [ i ] [ j ] \sum_{i=1..i}^{j=1..j}a[i][j] i=1..ij=1..ja[i][j]

    O ( 1 ) O(1) O(1) 查询

    如图,已知(x1, y1)(x2, y2)两点,求这两个点所构成的矩阵中各数之和,根据容斥原理s=sum-s1-s2-Δs,可知求小矩阵内各数之和需要用大矩阵内所有数之和减去其余空间的各数之和。

    在这里插入图片描述

    初始化 O ( n m ) O(nm) O(nm)

     for(int i=1;i<=n;i++)
         for(int j=1;j<=m;j++)
         {
             cin>>a[i][j];
             a[i][j]+=a[i-1][j]+a[i][j-1]-a[i-1][j-1];
         }
    

    查询 O ( 1 ) O(1) O(1):

    while(q--)
         {
             int x1,y1,x2,y2;
             cin>>x1>>y1>>x2>>y2;
             cout<<(a[x2][y2]-a[x2][y1-1]-a[x1-1][y2]+a[x1-1][y1-1])<<endl;
         }
    

差分

  • 一维

    根据原数组,构造一个差分数组,首位相同,其余各位上的数值表示与前一位的差,通过前缀和运算可得原数组。

    定义b[i]=a[i]-a[i-1],可得a[i]= ∑ j = 1 i b [ j ] \sum_{j=1}^i b[j] j=1ib[j],那么就称b是a的差分数组。

    差分数组可以将对a数组任意区间的同一操作优化到 O ( 1 ) O(1) O(1)

    a数组区间[l,r]同时加上c的操作可转化为:

while(m--)
     {
         int l,r,c;
         scanf("%d%d%d",&l,&r,&c);
         b[l]+=c;
         b[r+1]-=c;
     }

对b数组求前缀和即可得到原数组a:

for(int i=1;i<=n;i++)
     b[i]+=b[i-1];
  • 二维:对于差分矩阵而言,一个元素直接关联到上右下左四个元素,会对右、下两个元素产生直接影响,会对右下方元素产生间接影响,也会受到上、左两个元素的直接影响,从而可以进一步推出,一个元素的右下角元素会受到该元素的两次影响,需要通过加该元素值来抵消一次影响。
b[x1][y1] += c;
b[x2+1][y1] -= c;
b[x1][y2+1] -= c;
b[x2+1][y2+1] += c;

与一维差分一样二维差分可以把对矩阵的同一操作优化到O(1)

img

红色矩形区域同时加上一个数,由图可以得到插入函数:

void insert(int x1,int y1,int x2,int y2,int c)
{
    a[x1][y1]+=c;
    a[x1][y2+1]-=c;
    a[x2+1][y1]-=c;
    a[x2+1][y2+1]+=c;
}

初始化可以视为在(i, j)(i, j)的小矩形内插入a[i][j]

for(int i=1;i<=n;i++)
     for(int j=1;j<=m;j++)
     {
         scanf("%d",&x);
         insert(i,j,i,j,x);
     }

对二维差分数组求二维前缀和可以得到原数组:

for(int i=1;i<=n;i++)
     {
         for(int j=1;j<=m;j++)
         {
             a[i][j]+=a[i-1][j]+a[i][j-1]-a[i-1][j-1];
             cout<<a[i][j]<<" ";
         }
         cout<<endl;
     }

树状数组插入和查询都可以优化到 O ( l o g n ) O(logn) O(logn)。差分和前缀和适合用在查询或修改次数十分巨大的时候,当修改和查询在同一复杂度时适合用树状数组。

位运算

  • n >> k & 1:求n的第k
  • lowbit(n) = n & (-n)
  • -n = ~n + 1

离散化

把无限空间里的有限数据映射到有限空间,主要表示大小关系,不反映具体数值

区间合并

将所有区间按左端点从小到大排序

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值