算法基础(一)


补一点算法基础,涉及主要思想和模板


快速排序-分治思想

1.确定分界点q[l+r]或随机值
2.调整区间
3.递归处理左右两端
以下是模板O(nlogn)

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];//令指针i,j指向区间外侧
    while (i < j)
    {
        do i ++ ; while (q[i] < x);
        do j -- ; while (q[j] > x);
        if (i < j) swap(q[i], q[j]);//将<=x放左端,>=x放右端,若不满足,swap交换
    }
    quick_sort(q, l, j), quick_sort(q, j + 1, r);
    //对左右两端再递归以上两过程,直到每段只有一个数,即全部有序
}

归并排序

  1. 确定分界点 mid=(l+r)/2
  2. 递归排序,对数组不断等长拆分,直到一个数的长度
  3. 归并合二为一(双指针)
    以下是模板O(nlogn)
void msort(int q[], int l, int r)
{
    if (l >= r) return;

    int mid = l + r >> 1;
    msort(q, l, mid);
    msort(q, mid + 1, r);//拆分

    int k = 0, i = l, j = mid + 1;//合并,i,j分别指向两段起点,k指向tmp起点
    while (i <= mid && j <= r)
        if (q[i] <= q[j]) tmp[k ++ ] = q[i ++ ];//枚举,如果q[i] <= q[j],放q[i]进tmp,否则放q[j]进
        else tmp[k ++ ] = q[j ++ ];

    while (i <= mid) tmp[k ++ ] = q[i ++ ];//把左段或右段剩余的数放入tmp
    while (j <= r) tmp[k ++ ] = q[j ++ ];

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

二分

关于二分,我之前的理解还是太浅,仅限于以下这种标准二分

int search(vector<int>& nums, int target) {
        int left=0;int right = nums.size() - 1;
        while(left<=right)
        {
            int mid=left+(right-left)/2;
            if(nums[mid]>target)
            {
                right=mid-1;
            }
            else if(nums[mid]<target)
            {
                left=mid+1;
            }
            else{
                return mid;
            }
        }
        return -1;
    }

下面是它的变种

二分查找

查找最后一个<=q的数的下标(最大化查找)

int find(int q){
  int l=0,r=n+1;//开区间
  while(l+1<r){ //l+1=r时结束
    int mid=l+r>>1;
    if(a[mid]<=q) l=mid;
    else r=mid;
    }
    return l;
}

查找第一个>=q的数的下标(最小化查找)

int find(int q){
  int l=0,r=n+1;//开区间
  while(l+1<r){ //l+1=r时结束
    int mid=l+r>>1;
    if(a[mid]>=q) r=mid;
    else l=mid;
    }
    return r;
}

二分答案

最大化答案(最小化同理略)

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

int bsearch(int l, int r)//注意上下界取两端外侧
{
    while (l+1 < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) l= mid;    // check()判断mid是否满足性质
        else r = mid;
    }
    return l;
}

相关题目
Luogu 2440 木材加工

高精度

高精度计算针对c++而言,用数组存储和模拟运算过程来实现

高精度加法

  1. 数字用字符串读入
  2. 把字符串倒序存入两个整型数组A,B
  3. 从低位到高位循环计算相加,取出当前位的值(存余),保留进位

以下用vector来写

vector<int> add(vector<int> &A, vector<int> &B)
{
    if (A.size() < B.size()) return add(B, A);//检查大小,特殊情况处理

    vector<int> C;//创建了一个新的向量 C 来存储相加后的结果
    int t = 0;
    for (int i = 0; i < A.size(); i ++ )
    {
        t += A[i];
        if (i < B.size()) t += B[i];//判断是否在B的范围内
        C.push_back(t % 10);//取出当前位的值
        t /= 10;//保留进位
    }

    if (t) C.push_back(t);//处理最高位的进位
    return C;
}

高精度减法

  1. 字符串读入
  2. 把字符串翻转存入两个整型数组A,B
  3. 若A<B,则交换A,B,输出负号
  4. 从低位到高位,逐位求差,借位,存差
  5. 把数组C从高位到低位依次输出
vector<int> sub(vector<int> &A, vector<int> &B)
{
    vector<int> C;
    for (int i = 0, t = 0; i < A.size(); i ++ )
    {
        t = A[i] - t;//维护一个借位t
        if (i < B.size()) t -= B[i];
        C.push_back((t + 10) % 10);//处理负数情况,进行借位,存差
        if (t < 0) t = 1;//t<0则需要借位,否则t为0
        else t = 0;
    }
    while (C.size() > 1 && C.back() == 0) C.pop_back();//去除前导0
    return C;
}

高精度乘法

  1. 用字符串读入高精度数字
  2. 把字符串翻转存入整型数组A中
  3. 从低位到高位,累加乘积,取出当前值,保留进位
  4. 把数组从高位到低位依次输出
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;
}

高精度除法

高精度/低精度

  1. 字符串读入
  2. 翻转存入整型数组A
  3. 从高位到低位(与前面不同),当前被除数,存商,求余数
  4. 把C数组从高位到低位依次输出
vector<int> div(vector<int> &A, int b, int &r)
{
    vector<int> C;
    r = 0;//创建了一个整数 r 用于存储余数
    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;
}

前缀和差分

一维前缀和

prefix[i] = arr[1] + arr[2] + ... arr[i]=prefix[i-1]+arr[i]//未雨绸缪先算好
arr[l] + ... + arr[r] = prefix[r] - prefix[l - 1]//求区间和直接调用

二维前缀和

以求子矩阵的和为例

prefix[i][j]=prefix[i-1][j]+prefix[i][j-1]-prefix[i-1][j-1]+arr[i][j]
prefix[i][j]= 第i行j列格子左上部分所有元素的和
以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
prefix[x2][y2]-prefix[x1-1][y2]-prefix[x2][y1-1]+prefix[x1-1][y1-1]

一维差分

差分和前缀和是一对互逆运算,把原序列 a 的区间[ l, r ]加 c,等价于其差分序列 b 的点 b [ l ]加 c ,点 b [ r +1]减c,即把原序列的"区间操作"转化为差分序列的"两点操作"。多次区间完成后(注意),再利用前缀和还原。

//给区间[l, r]中的每个数加上c:b[l] += c, b[r + 1] -= c
for(int i=1;i<=n;i++)cin>>a[i];
    for(int i=1;i<=n;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++)b[i]+=b[i-1];
    for(int i=1;i<=n;i++)cout<<b[i]<<' ';

二维差分

以差分矩阵为例,输入原始矩阵,初始化差分数组b,进行区间修改,还原出修改后的矩阵,输出结果

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

双指针

核心是一种优化技巧,解决序列的区间问题

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

    // 具体问题的逻辑
}

这个模板使用两个指针 i 和 j,其中 i 主要用于遍历整个序列,而 j 则在 i 的前面移动,以满足特定条件。在 while 循环之后,要根据具体问题填写逻辑。

位运算

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

离散化

离散化是一种常见的算法技巧,用于处理一些数据范围较大的情况,将原始数据映射到一个较小的区间内,以便于后续的处理。这个技巧通常用于处理一些需要对原始数据进行排序或者进行二分查找的场景。
以下是离散化的基本步骤和实现模板:

vector<int> alls; // 存储所有待离散化的值
sort(alls.begin(), alls.end()); // 将所有值排序
alls.erase(unique(alls.begin(), alls.end()), alls.end());   // 去掉重复元素

// 二分求出x对应的离散化的值
int find(int x) // 找到第一个大于等于x的位置
{
    int l = -1, r = alls.size();
    while (l+1< r)
    {
        int mid = l + r >> 1;
        if (alls[mid] >= x) r = mid;
        else l = mid;
    }
    return r + 1; // 映射到1, 2, ...n
}

区间合并

区间合并算法通常用于将一组有重叠或相邻的区间合并成尽可能少的不重叠的区间,或者找出覆盖所有区间的最少区间数量。

这种算法常见于涉及区间的问题,如日程安排、区间调度、会议安排等。它的基本思想是对区间进行排序,然后遍历排序后的区间数组,根据当前区间与之前合并区间的关系进行合并或者更新,最终得到合并后的区间数组。

// 将所有存在交集的区间合并
void merge(vector<PII> &segs)
{
    vector<PII> res;//初始化结果数组

    sort(segs.begin(), segs.end());//排序区间数组
//使用两个变量 st 和 ed 分别表示当前合并区间的起始位置和结束位置
    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);
//遍历排序后的区间数组 segs,对于每个区间 seg:
//如果当前区间的起始位置 seg.first 大于 ed,说明当前区间与之前的合并区间无交集,需要将之前的合并区间加入到结果数组 res 中,并更新新的合并区间的起始位置和结束位置为当前区间的起始位置和结束位置。
//如果当前区间的起始位置 seg.first 小于等于 ed,说明当前区间与之前的合并区间有交集,更新合并区间的结束位置 ed 为当前区间的结束位置和原合并区间的结束位置的最大值。
    if (st != -2e9) res.push_back({st, ed});//添加最后一个合并区间

    segs = res;//更新传入的区间数组
}

单链表

以下链表实现使用数组来模拟链表结构(静态链表)

// head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点
int head, e[N], ne[N], idx;

// 初始化
void init()
{
    head = -1;
    idx = 0;
}

// 在链表头插入一个数a
void insert(int a)
{
    e[idx] = a, ne[idx] = head, head = idx ++ ;
}

// 将头结点删除,需要保证头结点存在
void remove()
{
    head = ne[head];
}

insert(int a) 函数用于在链表的头部插入一个值为 a 的节点。它的实现步骤如下:

  1. 将新节点的值 a 存入数组 e 中的当前索引 idx 处。
  2. 将新节点的下一个节点索引设为当前头节点的索引 head。
  3. 更新链表的头节点索引 head 为新节点的索引 idx。
  4. 增加 idx 的值,以便下次插入节点时使用新的索引。

双链表

// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
int e[N], l[N], r[N], idx;

// 初始化左右端点
void init()
{
    //0是左端点,1是右端点
    r[0] = 1;//左端点0的右指针指向右端点1
    l[1] = 0;// 右端点1的左指针指向左端点0
    idx = 2;//从第二个节点开始插入数据
}

// 在节点a的右边插入一个数x
void insert(int a, int x)
{
    e[idx] = x;   // 插入节点的值为 x
    l[idx] = a;   // 插入节点的左指针指向节点 a
    r[idx] = r[a]; // 插入节点的右指针指向节点 a 的后一个节点
    l[r[a]] = idx; // a 的后一个节点的左指针指向插入节点
    r[a] = idx++; // a 的右指针指向插入节点
}

// 删除节点a
void remove(int a)
{
    l[r[a]] = l[a]; // 将 a 的后一个节点的左指针指向 a 的前一个节点
    r[l[a]] = r[a]; // 将 a 的前一个节点的右指针指向 a 的后一个节点
}

栈(Stack)是一种常见的数据结构,它基于后进先出(LIFO,Last-In-First-Out)的原则。栈通常用于需要临时存储和管理数据的场景,比如函数调用、表达式求值、内存管理等。栈具有以下特点:

  1. 后进先出(LIFO):最后入栈的元素最先出栈,这是栈的基本特性。
  2. 只能在栈顶操作:在栈中,只能对栈顶元素进行插入、删除等操作,无法直接访问或修改栈中的其他元素。

模板实现了一个基于数组的栈结构

// tt表示栈顶
int stk[N], tt = 0;

// 向栈顶插入一个数
stk[ ++ tt] = x;

// 从栈顶弹出一个数
tt -- ;

// 栈顶的值
stk[tt];

// 判断栈是否为空,如果 tt > 0,则表示不为空
if (tt > 0)
{

}

队列

队列(Queue)是一种常见的数据结构,它基于先进先出(FIFO,First-In-First-Out)的原则。队列通常用于需要按照先后顺序处理数据的场景,比如任务调度、缓冲区管理、消息传递等。队列具有以下特点:

  1. 先进先出(FIFO):最先入队的元素最先出队,这是队列的基本特性。
  2. 只能在队首和队尾操作:在队列中,元素只能从队尾入队(enqueue)和从队首出队(dequeue),无法在队列中间插入或删除元素。

普通队列

// hh 表示队头,tt表示队尾
int q[N], hh = 0, tt = -1;

// 向队尾插入一个数
q[ ++ tt] = x;

// 从队头弹出一个数
hh ++ ;

// 队头的值
q[hh];

// 判断队列是否为空,如果 hh <= tt,则表示不为空
if (hh <= tt)
{
    // 队列不为空
}

循环队列

// hh 表示队头,tt表示队尾的后一个位置
int q[N], hh = 0, tt = 0;

// 向队尾插入一个数
q[tt ++ ] = x;
if (tt == N) tt = 0;

// 从队头弹出一个数
hh ++ ;
if (hh == N) hh = 0;

// 队头的值
q[hh];

// 判断队列是否为空,如果hh != tt,则表示不为空
if (hh != tt)
{
    // 队列不为空
}

单调栈

单调栈(Monotonic Stack)是一种特殊的栈数据结构,它具有以下特点:

  1. 单调性:单调栈中的元素具有单调性,即栈中的元素满足一定的单调性要求,可以是单调递增(递增栈)或单调递减(递减栈)。
  2. 实时维护:单调栈在入栈和出栈操作时,保持栈内元素的单调性。当新元素要入栈时,会与栈顶元素进行比较,根据单调性要求来确定是否弹出栈顶元素,以保持栈的单调性。
  3. 应用广泛:单调栈常用于解决一些与区间最值相关的问题,如寻找某个元素右边(或左边)第一个比它大(或小)的元素,以及求解滑动窗口最大值等问题。

单调栈通常用于解决数组中元素的单调性问题,例如:在一个数组中,找到每个元素左边第一个比它大的元素。

常见模型:找出每个数左边离它最近的比它大/小的数
int tt = 0;
for (int i = 1; i <= n; i ++ )
{
    while (tt && check(stk[tt], i)) tt -- ;//检查栈顶元素 stk[tt] 是否存在,检查当前元素 i 是否满足某个条件(通常是比栈顶元素大/小)
    stk[ ++ tt] = i;
}

整体上,这段代码的作用是维护一个单调递增(或递减)的栈 stk,并且在遍历数组元素的过程中,对每个元素找到左边离它最近的满足某个条件的数。

单调队列-滑动窗口

与单调栈类似,解决问题时通常用于处理滑动窗口相关的场景

  1. 单调性:单调队列中的元素满足一定的单调性要求,可以是单调递增或单调递减。
  2. 实时维护:单调队列在入队和出队操作时,保持队列内元素的单调性。当新元素要入队时,会与队尾元素进行比较,根据单调性要求来确定是否弹出队尾元素,以保持队列的单调性。
  3. 适用范围:单调队列通常用于解决与滑动窗口相关的问题,如求解滑动窗口中的最大值、最小值、平均值等。

单调队列和滑动窗口结合使用的目的正是为了维护一段与当前滑动窗口相对应的索引下标数组,这段数组的特点是所对应的原数组在当前滑动窗口中实时满足单调性(单增或单减)

常见模型:找出滑动窗口中的最大值/最小值
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int a[N],q[N];
int main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    int n,k;cin>>n>>k;
    for(int i=1;i<=n;i++)cin>>a[i];
    int hh=1,tt=0;
    for(int i=1;i<=n;i++){//枚举序列
        while(hh<=tt&&a[q[tt]]>=a[i])tt--;//队尾出队,越小越优
        q[++tt]=i;//队尾入队
        if(q[hh]<i-k+1)hh++;//队头出队
        if(i>=k)cout<<a[q[hh]]<<' ';//使用最小值
    }
    cout<<'\n';
    hh=1,tt=0;
    for(int i=1;i<=n;i++){//枚举序列
        while(hh<=tt&&a[q[tt]]<=a[i])tt--;//队尾出队,越大越优
        q[++tt]=i;//队尾入队
        if(q[hh]<i-k+1)hh++;//队头出队
        if(i>=k)cout<<a[q[hh]]<<' ';//使用最大值
    }
    puts("");
}

整体上,这段代码的作用是维护一个单调递增(或递减)的队列 q(索引下标数组),并且在遍历滑动窗口的过程中,实时获取窗口中的最大值或最小值。

KMP

KMP算法是一种字符串匹配算法,用于在一个文本串(主串)中查找一个模式串(子串)的出现位置。它的核心思想是在匹配过程中,当发现不匹配的字符时,利用已经部分匹配的信息来避免重复的比较,从而提高匹配的效率。
为什么要考虑前后相等(最长相同前缀和后缀的长度):考虑一个字符串"ABCDABD",我们以它的第7个字符 “D” 为例,它之前的字符串是 “ABCDAB”。现在我们观察字符串 “ABCDAB”,发现它的前缀 “AB” 和后缀 “AB” 是相等的,长度为2。而在匹配过程中,如果在子串中的位置7处失配了,我们可以利用这个信息,因为在位置2之前的匹配已经成功了。这样我们可以直接将匹配指针滑动到 “AB” 的后面,而不是从头开始比较。
Next 数组:用于在匹配过程中快速调整指针的位置,部分匹配表 ne[] 是一个数组,其长度与模式串 p[ ] 的长度相同,用于存储每个位置对应的最长公共前后缀的长度。在 KMP 算法中,ne[i] 表示的是模式串 p[ ] 中以第 i 个字符结尾的子串(不包括第 i 个字符)的最长公共前后缀的长度。下面是一个示例 ne[ ] 数组的展示:

假设模式串 p[] 为:“ababcab”,则 ne[] 数组为:

i     0  1  2  3  4  5  6  7
p[i]  a  b  a  b  c  a  b
ne[i] 0  0  1  2  0  1  2

上面的表格中,ne[0] 因为表示空串的最长公共前后缀的长度,所以是 0。ne[1] 表示以第一个字符 “a” 结尾的子串的最长公共前后缀的长度,也是 0。接着,ne[2] 表示以 “ab” 结尾的子串的最长公共前后缀的长度,是 1,以此类推。

该例为求出模式串 P 在字符串 S 中所有出现的位置的起始下标。

#include<bits/stdc++.h> 
using namespace std; 

const int N = 100010, M = 1000010; 

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

int main(){ 
    cin>>n>>p+1>>m>>s+1; // 从标准输入读入整数 n 和 m,以及字符串 p 和 s

    // 构建部分匹配表 ne[]
    for(int i=2, j=0; i<=n; i++){ // 从第二个字符开始遍历字符串 p
        while(j && p[i] != p[j + 1]) // 如果 j 不为零且 p[i] 与 p[j + 1] 不相等
            j = ne[j]; // 回退 j 到 ne[j] 所指向的位置
        if(p[i] == p[j + 1]) // 如果 p[i] 与 p[j + 1] 相等
            j++; // j 向后移动一位
        ne[i] = j; // 更新部分匹配表 ne[i] 的值为 j
    }

    // 在字符串 s 中查找字符串 p 的匹配位置
    for(int i=1, j=0; i<=m; i++){ // 从第一个字符开始遍历字符串 s
        while(j && s[i] != p[j + 1]) // 如果 j 不为零且 s[i] 与 p[j + 1] 不相等
            j = ne[j]; // 回退 j 到 ne[j] 所指向的位置
        if(s[i] == p[j + 1]) // 如果 s[i] 与 p[j + 1] 相等
            j++; // j 向后移动一位
        if(j == n){ // 如果 j 等于字符串 p 的长度 n,表示找到了一个匹配
            printf("%d ", i - n); // 输出匹配的起始位置
            j = ne[j]; // 回退 j 到 ne[j] 所指向的位置,继续查找下一个匹配
        }
    }

    return 0; // 返回主函数结束
}

模板题
Luogu3375


部分参考来源 https://www.cnblogs.com/dx123/

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值