Acwing_算法基础

算法基础课 第一讲基础算法总结

  • 仅贡本人复习用, 如有不足请大佬指出

插入排序

#include<iostream>
using namespace std;

void insert_sort(int *f, int l, int r) 
{
    for(int i = 1; i <= r; ++i) 
    {
        int k = i, x = f[i]; 
        while(k && f[k - 1] > x) f[k] = f[k - 1], k--; 
        f[k] = x; 
    }
}

快速排序

练习链接: 快速排序
重点理解 快排的实现原理— 用到的是分治的思想。

(1) 确定分界点: 一般分界点有三种选择 q[l], q[(l + r) / 2], q[r] ; 选取其中一个的值假设为x

(2) 调整区间: 将整个序列调整为小于等于x都在x左边, 大于等于x都在x右边

(3) 递归处理左右两段区间, 使得左右区间都有序

本身排序是不稳定的, STL中有对应的sort函数,故在平常刷题的过程不用特地自己写出快排,但理解快排的原理我觉得对自己编码能力还是有用的,这里y总的快排模板,里面还有双指针的用法在里面

快排模板

// 模板1 

void quick_sort(int q[], int l, int r)    // 这样写的l, r 都是左右端点,并不像stl的左开右闭
{ 
    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);
        do j -- ; while (q[j] > x);
        if (i < j) swap(q[i], q[j]);
    }
    quick_sort(q, l, j), quick_sort(q, j + 1, r);  //这里结束之后的i,j 其关系为 i >= j;
    
     //此时j下标的左边为小于x的值,其右边为大于base的值,此时的j下标对应的值小于等于x
     //故依次作为两个区间,在这两个区间内再进行快排,注意此时不要写成如下的形式
     // quick_sort(q,l,i -1); quick_sort(q,i, r);
     //因为这种写法在面对数组为 1, 2 的这种情况会无限递归. l + r >> 1 属于下标下取整,
     //故此时的x对应的值为0, 此时指针i指到第一个位置(下标为0)时即停止,j下标--同样到
     //下标为0的位置停止移动,然后if判断false, 此时 i = j = 0, 故执行quick_sort(q,i,r)会无限递归。
     //故为了不必要的边界考虑,将这个模板熟悉,用j下标即可
     // 如果写成了 quick_sort(q,l,i -1); quick_sort(q,i, r); 
     // 那么相应地  x = q[l + r + 1 >> 1] ; 上取整操作 
}


// 模板2 
void q_sort(int l, int r)
{
    if(l >= r) return; 
    int i = l - 1, j = r + 1, x = q[l + r >> 1]; 
    while(i < j)
    {
        while(q[++i] < x); 
        while(q[--j] > x);
        if(i < j) swap(q[i], q[j]); 
    }
    q_sort(l, j), q_sort(j + 1, r); 
}

快排的拓展 : 快速选择 -->

将一个位置的数字排序正确, 相比于快排每次只是递归一半,故其时间复杂度为 O(N);
相关题目链接: 第k个数

代码 – 其实就是快排模板

void quick_sort(int l, int r, int k)
{
    if(l >= r) return;
    int i = l - 1, j = r + 1, base = a[l + r >> 1];
    while(j > i)
    {
        do ++i;while(a[i] < base);
        do --j;while(a[j] > base);
        if(j > i) swap(a[i], a[j]);
    }   
    
    int len = j - l + 1 ;
    if(len >= k) quick_sort(l, j, k);
    else quick_sort(j + 1, r, k - len);
}

当然stl中也有相对应的库函数 : nth_element()函数, nth_element(a, a + k - 1, a + n);位于头文件 algorithm, 参1, 参3 为 first 和 last用于指定函数的作用范围,参2为指定的位置, 默认的参4缺省为降序规则, 函数将参2指定的位置,指定位置左边的值小于他,其右边的值都大于它, 时间复杂度为O(n);

归并排序

这里对比快速排序, 发现二者都是基于分治的思想, 不同的是我们快排的时候是基于一个数x来进行划分, 即我们会将一个区间分为左右两个区间, 左区间的数值都小于等于x, 右区间的数值都大于等于x, 此时左右区间的个数不一i的那个是相等的. 而归并排序是先递归划分为左右两个个数相等的区间, 然后从最小的左右两个区间有序后依次排序合并.故其基本步骤为:

(1) 确定分界点: 分界点位置为 mid = (l + r) / 2; 这是一个位置, 而不是像快排一样是序列中一个任意的值

(2) 递归排序left, right;

(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]; 
    //前面都合并到一个保存的数组中,最后进行扫尾工作,将其写回原数组
}

二分

可以二分,即存在可以二分的某种性质,可以将左右两边都划为不同的两类,二分本身并不难,但二分的check条件,和边界条件的判断有时候很坑,这里二分建议直接背过y总的模板:

二分边界条件的分析

相关练习链接 :
数的范围

整数二分模板

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

// 区间[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, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
    while (l < r)
    {
        int mid = l + r + 1 >> 1; //注意这里必须上取整 + 1, 不然l = mid, 可能在两个的情况下无限递归. 
        if (check(mid)) l = mid;   
        else r = mid - 1;         
    }
    return l;
}

浮点数二分模板

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

double bsearch_3(double l, double r)
{
    const double eps = 1e-6;   // eps 表示精度,取决于题目对精度的要求
    while (r - l > eps)     // 表示精度,当然也可无脑直接for个100次来达到高精度
     {
        double mid = (l + r) / 2;
        if (check(mid)) r = mid;    // 相比于整数二分,由于浮点数能精确二分,故没有+1的写法
        else l = mid;
    }
    return l;
}
  • 注意负数开奇数方根的情况下, 注意初始的 l 和 r的赋值情况, 这时r不能赋本身, 例如-1000, 其三次方根为- 10, 但如果像正数一样把其右端点赋值为 -1000, 则不能二分出正确的答案.

高精度

由于C++的数据类型不支持大整数,故面对大数运算时,即使是long long也不够用,故需要实现高精度,java自带大整数类,python本身就是高精度,不用考虑。故这里只有C++才需要高精度这个问题, 高精度运算分为高精度加法,减法,乘法,除法;

主要思路: 利用 string 保存大数字, 将其每一位数字储存到vector中

main函数中解决vector的输入

#include<iostream>
#include<vector>
#include<string>
using namespace std;
using VI = vector<int>;

auto main() -> int
{
    string s1, s2;  
    VI v1, v2;
    cin >> s1 >> s2;
    
// 这里的输入采用的是从个位开始push_back的,因为如果发生进位直接在后面push_back即可,不用整个移动数组
    for(int i = s1.size() - 1, j = s2.size() - 1; i >= 0 || j >= 0; --i, --j) 
    {
        if(i >= 0) v1.push_back(s1[i] - '0');    
        if(j >= 0) v2.push_back(s2[j] - '0');
    }
    
    auto v = add(v1, v2);        // 这里将add换为 sub, mul, div 完成减,乘,除法
    
    for(int i = v.size() - 1; i >= 0; --i) cout << v[i] ;    // 逆序输出 
    cout << endl;
    return 0;            
}

(1) 高精度加法

//高精度加法模板1 ----> 通常是两个大数相加
vector<int> add(vector<int> &A, vector<int> &B)
{
    if (A.size() < B.size()) return 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;
}

// 高精度加法模板2 
vector<int> add(vector<int> &v1, vector<int> &v2) 
{
    vector<int> res;
    int t = 0; 
    for(int i = 0, j = 0; i < v1.size() || j < v2.size(); ++i, ++j) 
    {
        if(i < v1.size()) t += v1[i]; 
        if(j < v2.size()) t += v2[j]; 
        res.push_back(t % 10); 
        t /= 10; 
    } 
    if(t) res.push_back(1); 
    return res; 
}

(2) 高精度减法

//高精度减法模板 

//减法注意的一个问题就是出现负数的情况,故这里专门有个check函数进行检测

auto check(VI &A, VI &B) -> bool
{
    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;
}

auto sub(VI &A, VI &B) -> VI
{
    VI 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)% 10);     // 这里的处理是考虑到借位的情况
        if(t >= 0) t = 0;
        else t = -1;                   // 若此时t<0, 说明此时向最高位借位了,故要减掉1
        
        // 或者这里的if-else直接写成一条语句: t = (t < 0) * (-1); 
    }
    while(C.size() > 1 && C.back() == 0) C.pop_back();  // 减法是会产生前导0的,故这里为去除前导0,注意C容器的大小是>=2,即>1
    return C;
}

#if 0
    最后的输出部分, 输入部分参考前面加法模板的
    if(check(v1, v2)) v = sub(v1, v2);
    else cout << "-", v = sub(v2, v1);
    
    for(int i = v.size() - 1; i >= 0; --i)
        cout << v[i] ;
    cout << endl;
    return 0;
#endif 

(3) 高精度乘法

例如存在一个很大的数, 我们这个数的每一位用 a x a_x ax表示,例如一个大数A,表示为 a 8 a 7 a 6 a 5 a 4 a 3 a 2 a 1 a_8a_7a_6a_5a_4a_3a_2a_1 a8a7a6a5a4a3a2a1

此时有一个较小的数num, 当我们运算 A * num 时, 其结果的个位为 ( a 1 a_1 a1*num) % 10, 其向十位的进位为 a 1 ∗ n u m a_1 * num a1num / 10; 我们把其向前的进位记为t, 此时t = ( a 1 ∗ n u m a_1 * num a1num / 10), 此时十位的结果为( a 2 a_2 a2 * num + t) % 10; 此时向百位的进位为 t = ( a 2 a_2 a2 * num + t) / 10; 故这就是乘法的运算过程, 将其转化为下面的代码.

// 高精度乘法 -- 通常是一个大数 乘以一个较小的数 int 类型
vector<int> mul(vector<int> &A, int b)
{
    vector<int> C;

    int t = 0;
    for (int i = 0; i < A.size() || t; i ++ )  //这里或 || t的意思是只要t不为0,就继续执行取余,除10进位操作
    {
        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(); // 乘法产生前导0的原因是 一个数乘以0
    return C;
}

(4) 高精度除法

// 高精度除法  -- 一个大数 除以 一个小数(int)
vector<int> div(vector<int> &A, int b, int &r)   // 这里的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()) C.pop_back();
    return C;
}

前缀和与差分

  1. 一维数组前缀和
    S[i] = a[1] + a[2] + … a[i]
    a[l] + … + a[r] = S[r] - S[l - 1]
 for(int i = 1; i <= n; ++i) cin >> s[i];
    for(int i = 1; i <= n; ++i) s[i] += s[i - 1];  // 求前缀和数组
    
 cout << s[j] - s[i - 1] << endl; // 求某个区间的和

2.二维前缀和
S[i, j] = 第i行j列格子左上部分所有元素的和:
s[i, j - 1] + s[i - 1, j] - s[i - 1][j - 1] + a[i][j];

以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]

公式推导

3.差分

差分相当于求前缀和的逆运算, 即存在数组 a1, a2, a3,…,ai, 此时需要构造数组b,使得ai = b1 + b2 + b3 +,…, + bi; 故 a数组 为 b数组的前缀和数组, b数组为a数组的差分数组。

其应用为这样一种场景: 当需要在a数组的某个区间,例如[l, r] 区间都加上一个常数C的时候,如果按照常规的做法,需要将a数组从这个循环一遍+C, 如果这时有它的差分数组,我们可以让 b l b_l bl += C, 然后 b r + 1 b_{r+1} br+1 -= C; 给区间[l, r]中的每个数加上c:B[l] += c, B[r + 1] -= c. 然后再对其求前缀和

差分代码:

#include<iostream>
using namespace std;

constexpr int N = 1e+5 + 10;
int n,m;
int a[N];

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; ++i)      // 直接由原数组构造差分数组, 由于默认都是0, 故本身就是一个原数组全为0的差分数组,故相当于看做是执行了一个区间为1的插入操作
    {
        int t; cin >> t;
        a[i] += t; a[i + 1] -= t;
    }
    
    while(m--)                       // 区间处理, 在[l, r]区间加上一个C
    {
        int l , r , c; cin >> l >> r >> c;
        a[l] += c; a[r + 1] -= c;
    }
    
    for(int i = 1; i <= n; ++i)     // 求前缀和--> 即处理之后的原数组并输出
    {
        a[i] += a[i - 1];
        cout << a[i] << " ";
    }
    
    cout << endl;
    
    return 0;
}

4.二维差分

差分矩阵的公式:

给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上c:
S[x1, y1] += c, S[x2 + 1, y1] -= c, S[x1, y2 + 1] -= c, S[x2 + 1, y2 + 1] += c

差分矩阵的构造方法即直接就在原本矩阵上的 左上角(i, j), 右下角还是(i, j)进行上面的操作。

公式的推导和例题的题解: 差分矩阵

双指针算法

主要思想 : 使用i, j两个指针指向数组,将原本时间复杂度 O(N2) 的变为 O(N);

主要练习:
799. 最长连续不重复子序列
800. 数组元素的目标和
2816. 判断子序列

位运算

常用位运算

n >> k & 1   //求n的第k位数字: n >> k & 1    //k是从第0位开始算起

n & -n       // 返回n的最后一位1的位置(二进制的第几位), 这里相当于补码与上原码 
     
num = num & (~(1 << k));  // 将 num 的二进制表示的第k位置为 0

num = num | (1 << k);     // 将 num 的二进制表示的第k位置为1

// 将一个十进制数打印出其二进制表示
void fun(int n)
{
    for(int i = 31; ~i; --i) cout << (n >> i & 1); 
    cout << endl;
}

相关练习 801. 二进制中1的个数

离散化

题解 : 离散化例题题解

区间合并

题目链接:acwing803. 区间合并

// 将所有存在交集的区间合并

#include<iostream>
#include<climits>
#include<algorithm>
#include<vector>
using namespace std;
using PII = pair<int, int>; 
constexpr int N = 1e+5 + 10;

vector<PII> vec; 

auto main() -> int
{
    ios::sync_with_stdio(false);
    int n; cin >> n;
    while(n--)
    {
        int l, r; cin >> l >> r; 
        vec.emplace_back(l, r);
    }
    
    sort(vec.begin(), vec.end())   ; 
    int st = INT_MAX, end = INT_MAX, cnt = 0; 
    for(auto &item : vec)
    {
        int l = item.first, r = item.second;    
        if(st == INT_MAX || l > end) st = l, end = r, ++cnt; 
        else end = max(end, r); 
    }
    
    cout << cnt << endl; 
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值