Acwing-基本算法

基本算法

排序算法:

a.快速排序:

主要思想:

  1. 确定分界点:
    x = a[l]
    x = a[r]
    q = a[(l + r) / 2]

  2. 调整范围:
    左边<=x
    右边>x

  3. 递归处理左边和右边

模板:

void quick_sort(int q[], int l, int r)
{
    if (l >= r) return;

    int i = l - 1, j = r + 1, x = q[l + r >> 1];
    while (i < j)
    {
        do i ++ ; while (q[i] < x);         // 可换成 while(q[ ++ i ] < x);
        do j -- ; while (q[j] > x);         // 可换成 while(q[ -- j ] > x);
        if (i < j) swap(q[i], q[j]);
    }
    quick_sort(q, l, j), quick_sort(q, j + 1, r);
}

quick_sort(a, 0, n - 1);           

说明:

  1. 如果x选取q[l],则递归时参数范围选择(l, j)和(j + 1, r)
  2. 如果x选取q[r],则递归时参数范围选择(l, i - 1)和(i, r)
  3. 如果x选取q[l + r >> 1],则递归时参数范围选择哪种都行
  4. 快速排序在对含有重复元素的数组排序时是不稳定的,但可以把元素值和其下标组成二元组{q[i], i}后再排序,这样就能使排序结果稳定。

b.归并排序:

主要思想:

  1. 确定分界点 mid = (l + r) / 2
  2. 递归处理左右两段
  3. 归并(双指针算法,指针表示剩余部分中最小元素的位置)

模板:

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 ++ ];
        else tmp[k ++ ] = q[j ++ ];

    while (i <= mid) tmp[k ++ ] = q[i ++ ];
    while (j <= r) tmp[k ++ ] = q[j ++ ];

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

说明:

在归并步骤时,如果碰到相同元素的插入,每次都选择第1段(左边)的元素插入,则能使归并算法稳定。

二分法:

二分法本质:对于一个区间(l,r),若其满足这样的性质:对于某个边界点的左边,他满足一个性质;对于另一个边界点,其右边又满足另一个性质;(即只要存在某种性质判定能把序列分成连续的两段,典型:区间上的单调性)则二分算法可以找出这两个边界点。

a.整数二分:

对于区间(l,r),不妨设其左边满足的性质为性质1,右边满足的性质为性质2,两者用check()函数来判定,则有:

<1>对于满足性质1的边界点(左边区间的右边界点):

mid=l+r+12if(check(mid))={truel=mid,区间变为[mid,r]falser=mid-1,区间变为[l,mid-1]

<2>对于满足性质2的边界点(右边区间的左边界点):

mid=l+r2if(check(mid))={truer=mid,区间变为[l,mid]falsel=mid+1,区间变为[mid+1,r]

模板:

bool check(int x) {/* ... */} // 检查x是否满足某种性质

// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用(对应于<2>,即区间上check的true段在右,false在左):
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;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用(对应于<1>,即区间上check的true段在左,false在右):
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;
}

给定一个按照升序排列的长度为 n的整数数组,以及 q 个查询。

对于每个查询,返回一个元素 kk的起始位置和终止位置(位置从 0开始计数)。

如果数组中不存在该元素,则返回 -1 -1

输入格式

第一行包含整数 n和 q,表示数组长度和询问个数。

第二行包含 n个整数(均在 1∼100001∼10000 范围内),表示完整数组。

接下来 q 行,每行包含一个整数 kk,表示一个询问元素。

输出格式

共 q 行,每行包含两个整数,表示所求元素的起始位置和终止位置。

如果数组中不存在该元素,则返回 -1 -1

数据范围

1≤n≤1000001≤n≤100000
1≤q≤100001≤q≤10000
1≤k≤100001≤k≤10000

输入样例:

6 3
1 2 2 3 3 4
3
4
5

输出样例:

3 4
5 5
-1 -1

题解:

#include<cstdio>
using namespace std;
const int N=100010;

int t[N];
int n,q;

int main(){
    scanf("%d%d",&n,&q);
    for(int i=0;i<n;i++) scanf("%d",&t[i]);
    while(q--){
        int k;
       scanf("%d",&k);
        int l=0,r=n-1;
        while(l<r){
            int mid =(l+r)>>1;
            if(t[mid]>=k) r=mid;
            else l=mid+1;
        }
        if(t[l]!=k) printf("-1 -1\n");
        else{
            printf("%d ",l);
            int l=0,r=n-1;
            while(l<r){
                int mid=l+r+1>>1;
                if(t[mid]<=k) l=mid;
                else r=mid-1;
            }
            printf("%d\n",l);
        }
    }
    return 0;
}

b.浮点二分:

模板:

bool check(double x) {/* ... */} // 检查x是否满足某种性质

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

前缀和&差分:

前缀和与差分是一对逆运算,类似于离散状态下积分与微分

a.前缀和:

一维前缀和:

定义:

Sk=i=1kai注意下标从1开始!s0=a0=0al+al+1+....ar=SrSl

模板:
int a[N], S[N];

for (int i = 1; i <= n; i++) S[i] = S[i - 1] + a[i];      // 给定数组a,初始化前缀和数组S


cout << S[r] - S[l - 1] << endl;                        // 计算a[l] + ... + a[r]
notes:
  1. 复杂度由O(n)降为O(1)
  2. 数组a和S的第1个元素都不存储(下标为0),而从第2个元素开始存储(下标为1)
  3. 注意遍历范围是1 ~ n
  4. 在一些不涉及a[i]的题目中,不必要存储a[i]的值,只需要存储S[i]就足够

二维前缀和:

Sxy=i=1xj=1yai,j=Sx1,y+Sx,y1Sx1,y1+ax,y

示意图

模板:
int a[N][N], S[N][N];

// 给定数组a
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];

// 没有给定数组a,需要读入并初始化前缀和数组,则可以合并读入和初始化的过程
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];
    }

cout << S[x2][y2] - S[x2][y1 - 1] - S[x1 - 1][y2] + S[x1 - 1][y1 - 1] << endl;      // 使用
notes
  1. 假设数组a中行下标或列下标为0的项都是0

  2. 复杂度由O(m * n)降为O(1)

  3. 读入数组a和初始化前缀和数组S的过程可以合并在一起

  4. 注意遍历范围是1 ~ n

  5. 在一些不涉及a[i]的题目中,不必要存储a[i][j]的值,只需要存储S[i]就足够

b.差分:

差分是前缀和的逆运算,即若a[n]为b[n]前缀和序列,则b[n]为a[n]差分序列。

一维差分

给a[n]区间l,r中的每个数加上c,即在其差分数组吧[n]的b[l]+c,b[r+1]-c从而讲O(n)化为O(1):

模板:
int a[N], B[N];

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

// 初始化差分数组
for (int i = 1; i <= n; i++) {
    scanf("%d", &a[i]);
    insert(i, i, a[i]);
}

// 输出前缀和数组
for (int i = 1; i <= n; i++) {
    B[i] += B[i - 1];
    printf("%d ", B[i]);
}

二维差分

给x1,y1的每个数加上c:

模板:
int B[N][N];            // 二维差分数组

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

// 构造(无需额外的数组a)
int tmp;
for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) {
        scanf("%d", &tmp);
        insert(i, j, i, j, tmp);
    }
}

// 转换成二维前缀和数组
for (int i = 1; i <= n; i++)
    for (int j = 1; j <= m; j++)
        B[i][j] += B[i - 1][j] + B[i][j - 1] - B[i - 1][j - 1];
notes
  1. insert()函数规律: 下标出现2的部分都+1
  2. 范围最大最小的+=c,其它-=c

双指针算法:

核心思想:

对于一个暴力求解的算法:

forint i=0;i<n;i++){
	for(int j=0;j<n;j++)
}

之类的,复杂度为$ O_{(n^2)} O_{(n)}$的算法,常见有快排,归并,KMP都有用到。

模板:

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

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

位运算:

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

离散化:

核心思想:

对于一个稀疏的序列,先把元素存储在vector alls中,排序去重后,再把值映射到长度较小的数组a中,如{1,2,100,200,500}将其映射到{1,2,3,4,5},从而不必去开一个500的数组。操作为去重(同时保序)、找出离散化的值。这一映射过程被称为离散化。

模板:

vector<int> alls; // 存储所有待离散化的值
sort(alls.begin(), alls.end()); // 将所有值排序
alls.erase(unique(alls.begin(), alls.end()), alls.end());   // 去掉重复元素
//unique函数的去重过程实际上就是不停的把后面不重复的元素移到前面来,也可以说是用不重复的元素占领重复元素的位置,返回值是一个迭代器,它指向的是去重后容器中不重复序列的最后一个元素的下一个元素.
// 二分求出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, 2, ...n
}

Example:

题解:

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

using namespace std;

typedef pair<int, int> PII;

const int N = 300010;

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

vector<int> alls;
vector<PII> add, query;

int find(int x)//即找到alls在a[]中的映射值
{
    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()
{
    cin >> n >> m;
    for (int i = 0; i < n; i ++ )
    {
        int x, c;
        cin >> x >> c;
        add.push_back({x, c});

        alls.push_back(x);
    }

    for (int i = 0; i < m; i ++ )
    {
        int l, r;
        cin >> l >> r;
        query.push_back({l, r});

        alls.push_back(l);
        alls.push_back(r);
    }

    // 去重
    sort(alls.begin(), alls.end());
    alls.erase(unique(alls), alls.end());

    // 处理插入
    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];

    // 处理询问
    for (auto item : query)
    {
        int l = find(item.first), r = find(item.second);
        cout << s[r] - s[l - 1] << endl;
    }

    return 0;
}

区间和:

目的:合并所有存在交集的区间

步骤:

  1. 先按左端点排序,然后再合并
  2. 选取第2个区间时,可分为两大类情况
    有交集(包括“包含”和“相交但不包含”两种情况)
    无交集
    对于有交集的情况,只需保留最大的右端点即可
    对于无交集的情况,首先判断是否是空区间(st == -2e9),非空则保存当前区间,并跳至下一个区间
  3. 由于循环内部是先发现新的无交集区间才保存当前指向的区间,因此在循环结束后,还需要单独保存当前区间(注意判断是否为空区间)

2.合并

// 将所有存在交集的区间合并
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;
}
posted @   Yihoyo  阅读(324)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示