算法学习笔记

刷算法

排序

快速排序

主要思想

快速排序是一种分治算法,它通过从数组中选择一个**“枢轴”元素并根据它们是否小于或大于枢轴将其他元素分成两个子数组**。然后递归地对子数组进行排序。快速排序算法的平均时间复杂度为 O ( n l o g n ) O(n log n) O(nlogn)最坏时间复杂度为 O ( n 2 ) O(n^2) O(n2)

解决过程
确定分界点

分界点即为主要思想中提到的“枢纽”元素,一般选择中点对应的数为分界点

调整范围

使用两个指针 i ,j分别指向数组的左右两端,然后向中间靠拢。如果指针对应的数小于分界点,则移到左部反之移到右部。当两个指针相遇的时候,则停止移动指针,开始对左右两部分继续前序操作。当一开始两个指针的位置相同时,则表明这部分只有一个数,就返回。

代码
void quick_sort(int q[], int l , int r) // 快速排序
{
    if(l >= r) return;
    
    int x = q[(l + r) / 2], i = l - 1, j = r + 1; // 先移动 再判断
    while(i < j){
        while(q[++i] < x); // 先移动 保证第一个/最后一个数能被考察
        while(q[--j] > x);
        if(i < j) swap(q[i],q[j]);
    }
    
    quick_sort(q,l,j);
    quick_sort(q,j + 1, r);
}

快速选择(第k小数)

主要思想

运用快速排序算法,但不对整个数组进行排序,仅排序第k个小的数所在的数组。因为快速排序算法将数组分为较小和较大的两部分,所以仅需比较分开位置与k的大小,就能确定下次排序的数组。其时间复杂度为 O ( n ) O(n) O(n)

解决过程

首先利用快速排序算法将数组分为两部分,计算左侧部分的元素数量(sl),并将其与k进行比较。如果k小于或等于sl,则意味着第k小的元素在左侧部分,因此它会递归该部分。否则,它意味着第k小的元素在右侧部分,因此它会递归该部分,并通过减去sl来调整k。当它在子数组中找到第k小的元素时,该函数返回。

代码
int quick_select(int l , int r, int k) // 快速选择
{
    if(l >= r) return q[l];
    
    int x = q[(l + r) / 2], i = l - 1, j = r + 1;
    while(i < j)
    {
        while(q[++i] < x);
        while(q[--j] > x);
        if(i < j) swap(q[i], q[j]);
    }
    
    int sl = j - l + 1; // 左边元素个数
    if(k <= sl) return quick_select(l, j, k);
    else return quick_select(j + 1, r, k - sl);
}

归并排序

主要思想

采用分治法,将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。其时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。该算法为稳定的算法,也就是说值相同的元素不会被交换位置

解决过程

首先将序列分成左右两部分,然后再将各部分继续分解,直到序列只有一个数,然后开始向上合并。合并两序列时,使用双指针,再使用一个临时数组存储当前合并的序列,当某指针指向的值更小时,则将其放入临时数组中,指针向后移动,重复指到两个指针均指向子序列尾部。

代码
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 i = l, j = mid + 1, k = 0;
    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];
}

逆序对数量

主要思想

归并排序归并阶段是将两个有序的子序列合并成一个完整序列。也就是说,当在左部序列中找到比右部序列当前指向的值大的数之后,左部序列之后所有的元素都会与之形成逆序对。所以只需在归并阶段加上对逆序对数量的增加,即可算出总的逆序对数量。

解决过程

分为三种情况,假设分为 Left 和 Right 两部分,左右两边指针分别为 i 和 j。

  • 如果逆序对两元素**均在 Left **,进行左部的归并排序。
  • 如果逆序对两元素**均在 Right **,进行右部的归并排序。
  • 如果逆序对两元素在两侧,归并排序时在左部找到比右部元素大的元素,其后所有的元素都与右部元素组成逆序对。个数为mid-i+1
  • 最后返回个数。
代码
LL merge_sort(int q[], int l, int r)
{
    if(l >= r) return 0;
    int mid = l + r >> 1;
    LL res = 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 ++];
            res += mid - i + 1;
        }
    }
    
    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];
    return res;
}

二分

数的范围


image-20230421212006234


主要思想

通过二分分别查找左边界和右边界,但左右边界的临界情况不同,需要分开讨论。

解决过程
  • 查找左边界:左边界的右边可能存在值相等的情况,所以判断时用>=进行判断中点情况。如果此时中点满足,则表明左边界在中点左侧,则需更新右部的值,反之更新左部的值。
  • **没有条件满足:**如果在查找左边界后,现在的值与输入元素值不等,则表明不存在。
  • 查找右边界:右边界的左边可能存在值相等的情况,所以判断时用<=进行判断中点情况。如果此时中点满足,则表明右边界在中点右侧,则需更新左部的值,反之更新右部的值。
代码
#include <iostream>
using namespace std;

const int N = 100010;

int n, m;
int q[N];

int main()
{
    cin >> n >> m;
    for (int i = 0; i < n; i ++ ) cin >> q[i];
    while (m -- )
    {
        int x;
        cin >> x;
        
        int l = 0, r = n - 1;
        // 寻找左边界 模板1
        while(l < r)
        {
            int mid = l + r >> 1;
            if(q[mid] >= x) r = mid;
            else l = mid + 1;
        }
        
        // 如果连左边界都没有查找到
        if(q[l] != x) cout << "-1 -1" << endl;
        // 寻找右边界 模板2 注意求中点+1是防止进入死循环
        else
        {
            cout << l << " ";
            l = 0, r = n - 1;
            while(l < r)
            {
                int mid = l + r + 1 >> 1;
                if(q[mid] <= x) l = mid;
                else r = mid - 1;
            }
            cout << l << endl;
        }
    }
    
    return 0;
}

高精度

主要思想

对高精度的数使用两个数组存储每一位的值,再在每一位上分别进行运算。

加法

vector<int> add(vector<int> &A, vector<int> &B)
{
    vector<int> C;
    int t = 0; // 进位
    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;
}

减法

减法时需要注意借位情况,可以设置一个t表示借位。每次进行运算时先减去这个借位,然后再进行减法运算。减法运算完后,将t进行加10再取模就能保证为非负数。最后比较一下t是否为负数,如果为负数就表明需要借位,将t设置为1,方便下次运算时先进行-1。需要注意的是,在进行运算前需要对两数进行大小判断,如果是小-大就要额外加上负号。

// 已经比较过大小 A为更大的数
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;
    }
    // 消去前导0 如“002”
    while(C.size() > 1 && C.back() == 0) C.pop_back();
    
    return C;
}

乘法

vector<int> mul(vector<int> &A, int B)
{
    vector<int> C;
    int t = 0;
    for(int i = 0; i < A.size(); i++)
    {
        t = A[i] * B + t;
        C.push_back(t % 10);
        t /= 10;
    }
    if(t != 0) C.push_back(t);
    // 消去前导0
    while(C.size() > 1 && C.back() == 0) C.pop_back();
    
    return C;
}

除法

进行除法时,模拟人的运算方法,从高位开始。如果该数小于除数,那么将该数(可以看成余数)乘10,然后和之后的数相加,再进行相除

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); // 不够除C会在后面加上0
        r = r % B;
    }
    // 因为从高位开始,并且输出时数组相反,所以需要翻转
    reverse(C.begin(), C.end());
    // 消去前导0
    while(C.size() > 1 && C.back() == 0) C.pop_back();
    
    return C;
}

前缀和、差分

一维前缀和

令第i个数 a i a_i ai的前缀和为 S i S_i Si,那么 S i = a 1 + a 2 + . . . + a i S_i = a_1 + a_2 + ... + a_i Si=a1+a2+...+ai

作用

利用前缀和可以快速求出一个区间的和。例如求 [ l , r ] [l,r] [l,r]区间的和,那么 S l , r = S r − S l − 1 S_{l,r} = S_r - S_{l-1} Sl,r=SrSl1

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 1e5 + 10;

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

int main()
{
    cin >> n >> m;
    q[0] = 0, s[0] = 0;
    for (int i = 1; i <= n; i ++ )
    {
        cin >> q[i];
        s[i] = s[i - 1] + q[i];
    }
    
    while (m -- )
    {
        int l,r;
        cin >> l >> r;
        cout << s[r] - s[l-1] << endl;
    }
    
    return 0;
}

二维前缀和

二维数组的前缀和如下图所示, ( i , j ) (i,j) i,j点的前缀和为橙色部分。

image-20230422220345700

若要求 S i , j S_{i,j} Si,j,黄色部分已知,那么 S i , j = S i − 1 , j + S i , j − 1 − S i − 1 , j − 1 + a i , j S_{i,j} = S_{i-1,j} + S_{i,j-1} - S_{i-1,j-1} + a_{i,j} Si,j=Si1,j+Si,j1Si1,j1+ai,j

image-20230422220234914

代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 1010;

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

int main()
{
    cin >> n >> m >> q;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
        {
            cin >> a[i][j];
            s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];
        }
    
    while(q --)
    {
        int x1,y1,x2,y2;
        cin >> x1 >> y1 >> x2 >> y2;
        cout << s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1] << endl;
    }
    
    return 0;
}

一维差分

差分相当于前缀和的逆运算。令数组 a , b a,b a,b ,若 b b b a a a 的差分数组,则满足 a i = b 1 + b 2 + . . . + b i a_i = b_1 + b_2 + ... +b_i ai=b1+b2+...+bi

求一个序列在区间内进行操作后得到的新序列

假设在 a a a [ l , r ] [l,r] [l,r] 区间内,将每个数都加上c。那么只需要进行 b l + c b_l +c bl+c b r + 1 − c b_{r+1} - c br+1c 两个操作,前者保证 l l l 之后的每个 a i a_i ai 都加上c,后者保证 r r r 之后的每个 a i a_i ai 都保持不变。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 1e5 + 10;
int n,m;
int a[N],b[N];

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i];
        b[i] = a[i] - a[i - 1];
    }
    
    while (m -- )
    {
        int l, r, c;
        cin >> l >> r >> c;
        b[l] += c;
        b[r + 1] -= c;
    }
    
    for(int i = 1 ; i <= n; i++) a[i] = a[i - 1] + b[i];
    for(int i = 1 ; i <= n; i++) cout << a[i] << " ";
    
    return 0;
}

二维差分

同二维前缀和,二维差分中 a a a 数组为 b b b 数组的前缀和。

矩阵中一个区间内进行操作后得到新的矩阵

a , b a,b a,b 数组的转换同前缀和理,主要讨论对区间的操作。令两个元素坐标分别为 ( x 1 , y 1 ) (x_1,y_1) (x1,y1) ( x 2 , y 2 ) (x_2,y_2) (x2,y2) ,将以这两元素形成的矩阵中,每个元素加上c。那么可以对 b b b 数组进行操作。假设为图中黄色填充部分为需要操作的部分,那么对 ( x 1 , y 1 ) (x1,y1) (x1,y1) 加上c,会导致红框部分都会加上,那么需要减去蓝色线条与绿色线条部分,并加上多减去的相交部分。

image-20230423190934097

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 1010;

int n, m, q;
int a[N][N], b[N][N];

int main()
{
    cin >> n >> m >> q;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
        {
            cin >> a[i][j];
            b[i][j] = a[i][j] - a[i-1][j] - a[i][j-1] + a[i-1][j-1];
        }
        
    while(q --)
    {
        int x1,y1,x2,y2,c;
        cin >> x1 >> y1 >> x2 >> y2 >> c;
        b[x1][y1] += c;
        b[x1][y2+1] -= c;
        b[x2+1][y1] -= c;
        b[x2+1][y2+1] += c;
    }
    
    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] + b[i][j];
            cout << a[i][j] << " ";
        }
        cout << endl;
    }
    
    return 0;
}

双指针

最长连续不重复子序列

额外使用一个数组记录数出现的次数,如果在这个连续子序列里出现了一次以上,就说明需要移动左端指针,调整子序列。在移动左指针的同时对子序列中数的出现次数也需要进行调整,当这个子序列中没有重复的数后,再次移动右端指针

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 1e5 + 10;

int n;
int q[N],r[N];

int main()
{
    cin >> n;
    for (int i = 0; i < n; i++) cin >> q[i];
    
    int res = 0;
    for (int i = 0, j = 0; i < n; i++)
    {
        r[q[i]]++;
        while (r[q[i]] > 1) r[q[j++]]--;
        res = max(res, i - j + 1);
    }
    cout << res << endl;
    
    return 0;
}

数组元素的目标和

暴力搜索可以得出两个元素都有单调性,即可降维使用双指针。因为两个数组都是单增序列,所以一个数组指向前端,一个指向后端。当两数和大于目标值时移动指向后端的指针。否则移动前端指针

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 1e5 + 10;

int n, m, x;
int a[N], b[N];

int main()
{
    cin >> n >> m >> x;
    for (int i = 0; i < n; i++) cin >> a[i];
    for (int i = 0; i < m; i++) cin >> b[i];
    
    for (int i = 0, j = m - 1; i < n; i++)
    {
        while(j >= 0 && a[i] + b[j] > x) j --;
        if(a[i] + b[j] == x)
        {
            cout << i << " " << j;
            break;
        }
    }
    
    return 0;
}

离散化

离散化是指将间隔很大的点,映射到相邻的数组元素中。减少对空间的需求,也减少计算量。如同区间和一题(原题链接),数组范围太大,而需要用到的数据范围很小,就使用离散化存储在一小段数组中。

在这道题中存储的是需要用到的点的坐标,包括需要增加的点和查询的点。因为可能有多个相同的点,所以需要将其进行去重,并排序。然后再使用前缀和的方式求出查询的和。

8021.png

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

typedef pair<int, int> PII;
// 增加需要10w,查询需要2*10w,共30w
const int N = 300010;

int n, m;
int a[N], s[N];
vector<int> alls; // 存储离散化后的坐标
vector<PII> 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; // 前缀和下标从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.begin(), alls.end()), 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);
        int r = find(item.second);
        cout << s[r] - s[l - 1] << endl;
    }
    
    return 0;
}

区间合并

原题链接

将所有区间按照从小到大排序,从最小的区间开始逐一遍历。定义一个 start 和 end,如果较大区间的开始在 start - end 之内,就更新区间,反之将前一个区间加入到合并完成的数组中,从当前区间继续遍历。

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

const int N = 100010;

int n;
vector<PII> segs; // 存储区间

vector<PII> merge(vector<PII> &segs)
{
    vector<PII> res;
    
    sort(segs.begin(), segs.end());
    // 先将端点设置极限值
    int st = -2e9, ed = -2e9;
    for (auto item : segs)
        if (ed < item.first)
        {
            if (ed != -2e9) res.push_back({st, ed}); 
            st = item.first, ed = item.second;
        }
        else ed = max(ed, item.second); // 更新区间end
    
    if (st != -2e9) res.push_back({st, ed}); // 遍历完成后还需要加入一次
    
    segs = res;
    
    return segs;
} 

int main()
{
    cin >> n;
    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;
}

链表

使用数组模拟链表,会比指针更快,因为 C++ 的 new 方法效率太低,但不太能考虑内存的情况

单链表

head 变量存储头节点的下标,idx 变量存储目前添加到的下标,e[] 数组存储链表的值,ne[] 数组存储当前下标对应的节点指向的节点下标。

// 头插法
void add_to_head(int x)
{
    e[idx] = x;
    ne[idx] = head;
    head = idx;
    idx ++;
}

// 删除下标为 k 的节点指向的节点
void remove(int k)
{
    ne[k] = ne[ne[k]];
}

// 在下标为 k 的节点之后插入值为 x 的节点
void add(int k, int x)
{
    e[idx] = x;
    ne[idx] = ne[k];
    ne[k] = idx;
    idx ++;
} 

双链表

l[] 数组存储当前节点左侧的节点下标,r[] 数组存储当前节点右侧的节点下标。

void init()
{
    // 0下标为head,1下标为tail
    r[0] = 1;
    l[1] = 0;
    idx = 2;
}

// 在第 k 个插入的数右边插入
// 链表左侧插入时 k = 0
// 链表右侧插入时 k = l[1]
// 如果表示在第 k 个插入的数左边插入 那么 k = l[k]
void add(int k, int x)
{
    e[idx] = x;
    l[idx] = k;
    r[idx] = r[k];
    l[r[k]] = idx;
    r[k] = idx;
    idx ++;
}

void remove(int k)
{
    r[l[k]] = r[k];
    l[r[k]] = l[k];
}

模拟栈

使用数组模拟栈,如果起始下标为0,那么栈顶指针就要从-1开始

**栈空:**栈顶指针小于0。

**查询:**输出栈顶指针所指的值。

push:栈顶指针先+1,再赋当前位置的值。

**pop:**栈顶指针-1。

int tt = -1; // 栈顶下标
int stk[N];

void push(int x)
{
    stk[++ tt] = x;
}

void pop()
{
    tt --;
}

void query()
{
    cout << stk[tt] << endl;
}

void empty()
{
    if (tt < 0) cout << "YES" << endl;
    else cout << "NO" << endl;
}

中缀表达式求值

使用一个运算符栈和一个数字栈。扫描表达式,是数字就入栈。是运算符左括号直接入栈;是右括号进行计算,直到运算符栈的栈顶为左括号;是加减乘除就进行判断,如果当前运算符优先级<=栈顶运算符优先级,则先计算直到当前运算符优先级更高否则直接入栈

#include <iostream>
#include <cstring>
#include <stack>
#include <unordered_map>
#include <algorithm>
using namespace std;

const int N = 10010;

stack<int> num;
stack<char> op;

void eval()
{
    // 靠后的数会先出栈
    int b = num.top(); num.pop();
    int a = num.top(); num.pop();
    
    char opt = op.top(); op.pop();
    
    int res = 0;
    if(opt == '+') res = a + b;
    if(opt == '-') res = a - b;
    if(opt == '*') res = a * b;
    if(opt == '/') res = a / b;

    num.push(res);
}

int main()
{
    unordered_map<char, int> pr{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}};
    string str;
    cin >> str;
    for (int i = 0; i < str.size(); i ++)
    {
        char c = str[i];
        if (isdigit(c))
        {
            int x = 0, j = i;
            while (j < str.size() && isdigit(str[j]))
                x = x * 10 + str[j ++] - '0';
            num.push(x);
            i = j - 1;
        }
        // 左括号无优先级 直接入栈
        else if (c == '(') op.push(c);
        // 有括号直接开始计算 直到出现左括号
        else if (c == ')')
        {
            while (op.top() != '(') eval();
            op.pop();
        }
        // 其他运算符 如果栈顶优先级大于此时 则运算
        // 如果栈顶优先级小于此时 直接入栈
        else
        {
            while (!op.empty() && pr[op.top()] >= pr[c]) eval();
            op.push(c);
        }
    }
    // 如果栈中还有 则计算
    while (!op.empty()) eval();
    cout << num.top() << endl; // 结果存储在栈顶
    
    return 0;
}

队列

模拟队列

使用数组模拟队列。需要一个队首指针fr队尾指针bc

**队列空:**fr = bc。

**查询:**输出fr指向的值。

**push:**先赋bc指向的值,再将bc+1。

**pop:**fr+1。

int q[N];
int fr = 0,bc = 0; // fr 队首,bc 队尾

void push(int x)
{
    q[bc ++] = x;
}

void pop()
{
    fr ++;
}

void query()
{
    cout << q[fr] << endl;
}

void empty()
{
    if (fr == bc) cout << "YES" << endl;
    else cout << "NO" <<endl;
}

单调

单调栈

找到数组每个元素左侧离它最近的比它小的元素。组成一个单调栈,如果都没有比其小的,说明之后所有元素都不可能找到在其之前的元素位置了。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 10010;

int stk[N];
int n,tt = 0;

int main()
{
    cin >> n;
    for (int i = 0; i < n; i ++)
    {
        int x;
        cin >> x;
        // 如果栈顶元素更大 则pop
        while (tt && stk[tt] >= x) tt --;
        // 如果栈不空 则找到比当前元素小的 输出当前栈顶元素
        if (tt) cout << stk[tt] << " ";
        // 如果栈空 则左侧均比他大 输出-1
        else cout << -1 << " ";
        // 当前元素入栈
        stk[++ tt] = x;
    }
    
    return 0;
}

单调队列

滑动窗口

原题链接

滑动窗口使用单调队列构建递增的序列。因为存在先进先出的性质,所以队首总是指向最小/大的值。每次循环(一次滑动)需要解决四件事:

  1. **队首是否滑出窗口:**所以队列存储的是元素的下标
  2. 队尾元素是否小/大于等于当前元素:如果是,则不满足单调,队尾元素出队列,进行循环判断
  3. 将当前元素入队:如果不满足2,说明队空或者现在已经单调
  4. 输出具体元素:当指针到窗口的阈值时开始输出
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 1000010;

int a[N], q[N];
int n, k;

int main()
{
    cin >> n >> k;
    for (int i = 0; i < n; i ++) cin >> a[i];
    
    int hh = 0, tt = -1;
    for (int i = 0; i < n; i ++) // 输出窗口内最小值
    {
        if (i - k + 1 > q[hh]) hh ++; // 队首已出窗口
        while (hh <= tt && a[i] <= a[q[tt]]) tt --; // 当前元素小于等于队尾元素
        q[++ tt] = i; // 将当前元素坐标加入队列
        if(i + 1 >= k) cout << a[q[hh]] << " ";
    }
    cout << endl;
    return 0;
}

KMP

主要思想是,找出串中相同的两段,在后续字符匹配不上时,直接将串移至前序相同串的末尾,这样可以减少循环次数。

next数组:1-j这段区间的串中,前缀和后缀相同最大长度。例如abcab这组串中,当指针指向最后的5时,前缀为:{a,ab,abc,abca},后缀为:{b,ab,cab,bcab},故 next[5] = 2。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010, M = 1000010;

int n, m;
char p[N], s[M];
int ne[N];

int main()
{
    cin >> n;
    cin >> p + 1; // 从下标1开始
    // 构造next数组
    for (int i = 2, j = 0; i <= n; i ++)
    {
        while (j && p[i] != p[j + 1]) j = ne[j];
        if (p[i] == p[j + 1]) j ++;
        ne[i] = j;
    }
    cin >> m;
    cin >> s + 1; // 从下标1开始
    // 进行字符串匹配
    for (int i = 1, j = 0; i <= m; i ++)
    {
        // 当后续匹配不上时,直接转向前序相同序列的
        while (j && s[i] != p[j + 1]) j = ne[j];
        if (s[i] == p[j + 1]) j ++;
        if (j == n) // 匹配完成
        {
            cout << i - n << " ";
            j = ne[j];
        }
    }
    
    return 0;
}

Trie树

主要思想是利用树,将仅含一种数据类型的字符串进行存储(例如全为小写字母,全为数字)。

字符串统计

原题链接

存储:利用树对每个字符进行存储,每个字符串完成后,对最后一个字符进行标识(即存储以它结尾的字符串的个数)。

匹配:逐一字符进行查找,如果当前字符没有的话直接返回 0 。如果全部匹配完成后,返回标识数据

void insert(char str[])
{
    int p = 0;
    for (int i = 0; str[i]; i ++)
    {
        int u = str[i] - 'a';
        if (!son[p][u]) son[p][u] = ++ idx; // 如果当前位置没有存储东西
        p = son[p][u];
    }
    cnt[p] ++; // 对字符串末尾进行标识
}

int query(char str[])
{
    int p = 0;
    for (int i = 0; str[i]; i ++)
    {
        int u = str[i] - 'a';
        if (!son[p][u]) return 0;
        p = son[p][u];
    }
    return cnt[p];
}

最大异或对

原题链接
将数字以二进制形式从高位开始存储,整型二进制最多31位,故从 i=30 开始递减,将每位二进制放进Trie树中。从高位开始存储利于找到最大值。查找时,将给定的从高位开始,向高位取反的方向进行查找。

void insert(int x)
{
    int p = 0;
    for (int i = 30; i >= 0; i --)
    {
        int u = x >> i & 1; // 获取第i位的二进制
        if (!son[p][u]) son[p][u] = ++ idx;
        p = son[p][u];
    }
}

int query(int x)
{
    int p = 0, res = 0;
    for (int i = 30; i >= 0; i --)
    {
        int u = x >> i & 1;
        if (son[p][!u]) // 最优情况向取反方向走
        {
            p = son[p][!u];
            res = res * 2 + !u;
        }
        else // 否则往另一方向走
        {
            p = son[p][u];
            res = res * 2 + u;
        }
    }
    return res;
}

*并查集

**解决:**将两个集合合并;询问两个元素是否在一个集合中

基本原理:每个集合用一棵来表示。树根的编号就是整个集合的编号。每个节点存储它的父节点,p[x]表示x的父节点

问题:

  • 如何判断树根: if (p[x] == x)
  • 如何求x的集合编号: while (p[x] != x) x = p[x]; 这一步用路径压缩优化,查询某个点的集合时,将其父节点均直接指向树根。
  • 如何合并两个集合: px是x的集合编号,py是y的集合编号。px = y,即将一个树根直接连接另一个树根

堆本质是一棵完全二叉树(除叶结点外,其余结点均非空,叶结点从左至右排列)。以小根堆为例,每个结点均小于等于其左右儿子,故根节点为最小值

存储:使用一维数组存储,从下标 1 1 1 开始。每个结点的左儿子下标为 2 x 2x 2x,右儿子下标为 2 x + 1 2x + 1 2x+1

操作:

  • down(x):某元素值变大,就将其往下移。需与左右儿子最小值交换。
  • up(x):与down操作相反。只需与父节点比较大小。

解决问题:

  • 插入:在数组尾部插入元素,然后使用up
  • 求最小:数组首部
  • 删除最小:用数组尾部覆盖首部元素,然后使用down
  • 删除任意元素:用数组尾部覆盖被删除元素,然后使用down+up
  • 修改任意元素:修改后使用down+up
  • 初始化堆:若 s i z e = n size = n size=n,那么只需在 1 ∽ 2 / n 1\backsim 2/n 12/n 范围内进行down操作。因为下标在 2 / n 2/n 2/n 内的均不是叶结点,而叶结点不需要down操作。此时的时间复杂度为 O ( n ) O(n) O(n)

排序

思路是每次输出数组首部,然后删除。循环规定的m次

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 100010;

int n, m;
int h[N], size1;

void down(int x)
{
    int t = x;
    if (x * 2 <= size1 && h[x * 2] < h[t]) t = x * 2; // 判断左儿子
    if (x * 2 + 1 <= size1 && h[x * 2 + 1] < h[t]) t = x * 2 + 1; // 判断右儿子
    if (t != x) // 不是down的下标 则交换
    {
        swap(h[x], h[t]);
        down(t);
    }
}

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++) cin >> h[i];
    size1 = n;
    
    for (int i = n/2; i; i --) down(i);
    
    while (m --)
    {
        cout << h[1] << " ";
        h[1] = h[size1];
        size1 --;
        down(1);
    }
    
    return 0;
}

模拟堆

原题链接

题目需要的求的是第 k k k 个插入的数,所以添加两个指针数组hp,ph。hp为h指向p,ph为p指向h。ph[k]表示第 k k k 个插入元素目前在堆中的下标。hp[k]表示堆中下标为 k k k 的元素,是第几个插入的元素,主要用来定位ph数组的位置。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 100010;

int n, m; // m表示第m个插入的数
int h[N], ph[N], hp[N], sz;

void heapswap(int a, int b)
{
    swap(ph[hp[a]], ph[hp[b]]);
    swap(hp[a], hp[b]);
    swap(h[a], h[b]);
}

void up(int x)
{
    while (x/2 && h[x/2] > h[x])
    {
        heapswap(x/2, x);
        x >>= 1;
    }
}

void down(int x)
{
    int t = x;
    if (x * 2 <= sz && h[x * 2] < h[t]) t = x * 2;
    if (x * 2 + 1 <= sz && h[x * 2 + 1] < h[t]) t = x * 2 + 1;
    if (t != x)
    {
        heapswap(t, x);
        down(t);
    }
}

int main()
{
    cin >> n;
    m = 0;
    while (n --)
    {
        string op;
        int k, x;
        cin >> op;
        
        if (op == "I")
        {
            cin >> x;
            sz ++;
            m ++;
            h[sz] = x;
            ph[m] = sz, hp[sz] = m;
            up(sz);
        }
        else if (op == "PM") cout << h[1] << endl;
        else if (op == "DM")
        {
            heapswap(1, sz); // 需要将指针也进行交换,否则会指向错误
            sz --;
            down(1);
        }
        else if (op == "D")
        {
            cin >> k;
            k = ph[k];
            heapswap(sz, k);
            sz --;
            up(k);
            down(k);
        }
        else
        {
            cin >> k >> x;
            k = ph[k];
            h[k] = x;
            up(k);
            down(k);
        }
    }
    
    return 0;
}

二叉树遍历(动画图源:YuXi_0520)

先序遍历

根-左-右,相当于沿着树的外围跑一圈,最后回到根节点。
在这里插入图片描述

中序遍历

左-根-右,相当于垂直投影,即可得到答案。
在这里插入图片描述

后序遍历

左-右-根,相当于葡萄剪枝,一颗颗剪下则需先剪叶子结点
在这里插入图片描述

层序遍历

相当于广度优先搜索,把一层从左到右依次遍历完成后,再转到下一层。

在这里插入图片描述

相关题目

给先序遍历,输出中序遍历。


二叉树遍历 给先序求中序


问题分析

通过题目输入的先序遍历,可得到二叉树。先序遍历依照根-左-右进行,那么每个非空节点都会作为一个根节点,下一个节点即为其左子树的根,直到左子树考察完毕后,转到右子树,即可得到如下二叉树。

在这里插入图片描述

代码
常规解法

运用结构体,使用指针方式存储根节点,注意此题使用先序遍历创建树。

#include <bits/stdc++.h>
using namespace std;
string str;
int k = 0, len;
typedef struct node
{
    char data;
    struct node *lchild;
    struct node *rchild;
} BTNode, *tree; // 定义结构体
tree root = NULL; // 根节点

void create_Tree(tree &BitTree)
{
    if (k >= len)
        return;
    char r = str[k++];
    if (r == '#')
        BitTree = NULL;
    else
    {
        BitTree = new node; // 创建根节点 分配空间
        BitTree->data = r;
        create_Tree(BitTree->lchild); // 首先创建左子树
        create_Tree(BitTree->rchild);
    }
}

void LDR(tree BitTree)
{
    if (BitTree == NULL)
        return;
    LDR(BitTree->lchild);
    cout << BitTree->data << " ";
    LDR(BitTree->rchild);
}

int main()
{
    cin >> str;
    len = str.length();
    create_Tree(root);
    LDR(root);
    return 0;
}
DFS解法

直接使用DFS,如果是空,则返回,输出其父节点,再考察其右子树。相当于针对每个节点都进行左-根-右的操作。

#include <bits/stdc++.h>
using namespace std;
void dfs(){
    char r = getchar();
    if(r == '#') return; // 空的直接返回
    dfs(); // 遍历左子树
    cout << r << " "; // 中序遍历
    dfs(); // 遍历右子树
}

int main()
{
    dfs();
    return 0;
}

01/完全背包问题


在这里插入图片描述


问题分析

这两个问题的区别为:01背包每个背包只能使用一次完全背包每个背包可以使用无数次

两个问题都可以确定的一个规则是:该背包选或不选。再确定是否可以多次选择

那么使用动态规划可以进行求解,他们分别的状态转换方程则为:

01背包:dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]] + w[i])
完全背包:dp[i][j] = max(dp[i-1][j], dp[i][j-v[i]] + w[i])

01背包与完全背包的前一项均相同,代表着不选择该背包。后一项唯一区别是选择之后是否忽略该背包,01背包中减一操作代表着,当前考察的背包不再选择,而完全背包则不需要进行该操作。

代码
01背包
二维数组
#include <bits/stdc++.h>
using namespace std;
int n, v;

int main()
{
    cin >> n >> v;
    int size[n + 1], worth[n + 1];
    for (int i = 1; i <= n; i++)
        cin >> size[i] >> worth[i];
    int dp[n + 1][v + 1];
    memset(dp, 0, sizeof dp);
    for (int i = 1; i <= n; i++)
    {
        for (int j = 0; j <= v; j++)
        {
            if (j >= size[i])
                dp[i][j] = max(dp[i - 1][j], dp[i-1][j - size[i]] + worth[i]);
            else
                dp[i][j] = dp[i - 1][j];
        }
    }
    cout << dp[n][v] << endl;

    return 0;
}
一维数组

利用滚动数组,结合状态转换方程可知,需要逆序考察,因为还未更新过的dp数组值可能会被使用

#include <bits/stdc++.h>
using namespace std;
int n, m;
int main()
{
    cin >> n >> m;
    int v[n + 1], w[n + 1];
    for (int i = 1; i <= n; i++)
        cin >> v[i] >> w[i];
    int dp[m + 1];
    memset(dp, 0, sizeof dp);
    for (int i = 1; i <= n; i++)
    {
        for (int j = m; j >= 0; j--)
        {
            if (j >= v[i])
                dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
        }
    }
    cout << dp[m] << endl;

    return 0;
}
完全背包
二维数组
#include <bits/stdc++.h>
using namespace std;
int n, v;

int main()
{
    cin >> n >> v;
    int size[n + 1], worth[n + 1];
    for (int i = 1; i <= n; i++)
        cin >> size[i] >> worth[i];
    int dp[n + 1][v + 1];
    memset(dp, 0, sizeof dp);
    for (int i = 1; i <= n; i++)
    {
        for (int j = 0; j <= v; j++)
        {
            if (j >= size[i])
                dp[i][j] = max(dp[i - 1][j], dp[i][j - size[i]] + worth[i]); // 这里有变化
            else
                dp[i][j] = dp[i - 1][j];
        }
    }
    cout << dp[n][v] << endl;

    return 0;
}
一维数组

因为未更新的数据不需要使用,所以直接正向进行循环

#include <bits/stdc++.h>
using namespace std;
int n, m;
int main()
{
    cin >> n >> m;
    int v[n + 1], w[n + 1];
    for (int i = 1; i <= n; i++)
        cin >> v[i] >> w[i];
    int dp[m + 1];
    memset(dp, 0, sizeof dp);
    for (int i = 1; i <= n; i++)
    {
        for (int j = m; j >= 0; j--) // 这里有变化
        {
            if (j >= v[i])
                dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
        }
    }
    cout << dp[m] << endl;

    return 0;
}
完全背包
二维数组
#include <bits/stdc++.h>
using namespace std;
int n, v;

int main()
{
    cin >> n >> v;
    int size[n + 1], worth[n + 1];
    for (int i = 1; i <= n; i++)
        cin >> size[i] >> worth[i];
    int dp[n + 1][v + 1];
    memset(dp, 0, sizeof dp);
    for (int i = 1; i <= n; i++)
    {
        for (int j = 0; j <= v; j++)
        {
            if (j >= size[i])
                dp[i][j] = max(dp[i - 1][j], dp[i][j - size[i]] + worth[i]); // 这里有变化
            else
                dp[i][j] = dp[i - 1][j];
        }
    }
    cout << dp[n][v] << endl;

    return 0;
}
一维数组

因为未更新的数据不需要使用,所以直接正向进行循环

#include <bits/stdc++.h>
using namespace std;
int n, m;
int main()
{
    cin >> n >> m;
    int v[n + 1], w[n + 1];
    for (int i = 1; i <= n; i++)
        cin >> v[i] >> w[i];
    int dp[m + 1];
    memset(dp, 0, sizeof dp);
    for (int i = 1; i <= n; i++)
    {
        for (int j = m; j >= 0; j--) // 这里有变化
        {
            if (j >= v[i])
                dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
        }
    }
    cout << dp[m] << endl;

    return 0;
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值