【算法】基础算法模板

文章目录

  • 一、快速排序
  • 二、归并排序
  • 三、二分
    • 1. 二分的本质
    • 2. 整数二分
    • 3. 实数二分
  • 四、前缀和
    • 1. 一维前缀和
    • 2. 二维前缀和
  • 五、差分
    • 1. 一维差分
    • 2. 二维差分
  • 六、常用位运算
    • 1. 求二进制的第 k 位
    • 2. lowbit
  • 七、其他常用算法
    • 1. 去重
    • 2. 表达式求值
    • 3. 单调栈
    • 4. 单调队列
    • 5. 并查集

一、快速排序

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

应用:快速选择 第k个数

//如果第k个数在左就在左区间找,在右就在右区间找
//由此保证答案在区间中
int quick_sort(int a[], int l, int r, int k)
{
    //区间长度为1时就是答案
    if(l == r) return a[l];

    int i = l - 1, j = r + 1, x = a[l + r >> 1];
    while(i < j)
    {
        while(a[++i] < x);
        while(a[--j] > x);
        if(i < j) swap(a[i], a[j]);
    }
	
    //一趟快排后 前j个的数有哪些已经确定
    int lcnt = j - l + 1;
    if(k <= lcnt) return quick_sort(a, l, j, k);
    return quick_sort(a, j + 1, r, k - lcnt);
}

二、归并排序

int tmp[N];
void merge_sort(int a[], int l, int r)
{
    //递归出口 区间长度为0或1时已经有序
    if(l >= r) return;
    
    //先把左右区间都排好序
    int mid = (l + r) / 2;
    merge_sort(a, l, mid);
    merge_sort(a, mid + 1, r);
    
    //再有序合并两个区间到辅助数组
    int i = l, j = mid + 1, k = 0;
    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(int i = l, j = 0; i <= r; i++, j++) a[i] = tmp[j];
}

应用:逆序对的个数

int tmp[N];
int merge_sort(int a[], int l, int r)
{
    //递归出口 区间长度为0或1时逆序对个数为0
    if(l >= r) return 0;

    int mid = l + r >> 1;
    //先分别求左右区间内部的逆序对个数
    int res = merge_sort(a, l, mid) + merge_sort(a, mid + 1, r);

    //再求两个区间之间的逆序对个数
    int i = l, j = mid + 1, k = 0;
    while(i <= mid && j <= r)
    {
        if(a[i] <= a[j]) tmp[k++] = a[i++];
        else
        {
            tmp[k++] = a[j++];
            res += mid - i + 1;
        }
    }
	
    while(i <= mid) tmp[k++] = a[i++];
    while(j <= r) tmp[k++] = a[j++];

    for(int i = l, j = 0; i <= r; i++, j++) a[i] = tmp[j];
	
    //最后返回总共的逆序对个数
    return res;
}

三、二分

1. 二分的本质

根据某种性质,将一段区间分成有这个性质和没有这个性质的两段,二分出的就是这两段的边界。

因此有单调性一定可以二分,没单调性也可能可以二分。

2. 整数二分

  1. 先确定答案所在区间 [ L , R ] [L, R] [L,R]
  2. 考虑用什么性质来二分
  3. 每次更新区间都要包含答案
  4. L = = R L == R L==R 时,区间长度为 1 1 1,就是答案
//第一种写法
while(l < r)
{
    int mid = (l + r) / 2;
    if(check(mid)) r = mid;
    else l = mid + 1;
}

//第二种写法
while(l < r)
{
    int mid = (l + r + 1) / 2;//(1)
    if(check(mid)) l = mid;//(2)
    else r = mid - 1;
}
//注意(1)(2)
//当 r = l + 1 时,如果 mid = (l + r) / 2 = l => l = mid = l,就会死循环
//因此改为 mid = (l + r + 1) / 2

二分一定有答案,可以根据二分答案判断题目是否有解。

3. 实数二分

double eps = 1e-8;//经验值,一般比题目保留位数多两位
while(r - l > eps)
{
    double mid = (l + r) / 2;
    if(check(mid)) l = mid;
    else r = mid;
}
//r - l <= eps 时,就认为 l 或 r 是答案了

四、前缀和

建议下标从 1 1 1 开始

1. 一维前缀和

//定义
a[1] + ... + a[i] = s[i]

//核心操作
s[i] = s[i - 1] + a[i]
a[l] + ... + a[r] = s[r] - s[l - 1]

2. 二维前缀和

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

五、差分

建议下标从 1 1 1 开始

1. 一维差分

//差分就是前缀和的逆运算
b[i] = a[i] - a[i - 1]

//核心操作
//给区间[l, r]中的每个数加上c
//只需对差分数组b这样操作
b[l] += c, b[r + 1] -= c
//然后对b求前缀和就是操作后的原数组

2. 二维差分

//给以(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;

六、常用位运算

1. 求二进制的第 k 位

设最低位为第 0 0 0

//右移 k 位,再 & 1
x >> k & 1

2. lowbit

返回二进制最后一个1

//eg: x = 1110 => -x = 0010
//x & -x = 0010
int lowbit(int x)
{
	return x & -x;
}

七、其他常用算法

1. 去重

vector<int> v;

sort(v.begin(), v.end());
v.erase(unique(v.begin(), v.end()), v.end());

2. 表达式求值

#include
using namespace std;

stack<int> num;//操作数栈
stack<char> op;//运算符栈

void eval()
{
    int b = num.top(); num.pop();
    int a = num.top(); num.pop();
    char c = op.top(); op.pop();

    int res;
    if(c == '+') res = a + b;
    else if(c == '-') res = a - b;
    else if(c == '*') res = a * b;
    else res = a / b;

    num.push(res);
}

int main()
{
    //运算符优先级表
    unordered_map<char, int> p{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}};

    string s; cin >> s;
    for(int i = 0; i < s.size(); i++)
    {
        if(isdigit(s[i]))
        {
            int x = 0, j = i;
            while(j < s.size() && isdigit(s[j]))
                x = x * 10 + s[j++] - '0';
            i = j - 1;
            num.push(x);
        }
        else if(s[i] == '(') op.push(s[i]);
        else if(s[i] == ')')
        {
            while(op.top() != '(') eval();
            op.pop();
        }
        else
        {
            while(!op.empty() && op.top() != '(' && p[op.top()] >= p[s[i]]) eval();
            op.push(s[i]);
        }
    }
    while(!op.empty()) eval();

    cout << num.top() << '\n';
}

3. 单调栈

常见题型:

  1. 求每个数左边第一个比它小的数
  2. 求每个数左边第一个比它大的数
  3. 求每个数右边第一个比它小的数
  4. 求每个数右边第一个比它大的数

以求每个数左边第一个比它小的数为例。我们只要维护一个栈,每枚举一个数入栈之前,都把栈里不小于它的数弹出,这样每次求一个数左边第一个小于它的数,就只需要取栈顶元素。

题目链接

参考代码:

#include
using namespace std;

int main()
{
    int n; cin >> n;
    stack<int> stk;
    for(int i = 1; i <= n; i++)
    {
        int x; cin >> x;
        while(!stk.empty() && stk.top() >= x) stk.pop();
        if(stk.empty()) cout << -1 << ' ';
        else cout << stk.top() << ' ';
        stk.push(x);
    }
}

4. 单调队列

常见题型:滑动窗口求最值

首先,滑动窗口可以用一个队列来维护:滑动窗口每次向右走一步,队尾就插入一个数,由于滑动窗口的长度是定值,如果此时队头不合法就要弹出一个数。
暴力的做法是,滑动窗口每走一步,都扫描一遍滑动窗口的区间求最值。显然这种做法是 O ( N 2 ) O(N^2) O(N2) 的。
如何优化呢?以求最大值为例,每枚举一个数入队之前,都把队列里不大于它的数弹出,再将这个数入队,以此来维护一段单调递减的区间。这样滑动窗口每次求最大值,就只需要取队头元素。

题目链接

参考代码:

#include
using namespace std;

const int N = 1e6 + 10;
int a[N];

int main()
{
    int n, k; cin >> n >> k;
    for(int i = 1; i <= n; i++) cin >> a[i];

    deque<int> dq;//队列存下标,才能判断队头合法性
    
    //滑动窗口最小值
    for(int i = 1; i <= n; i++)
    {
        //判断队头合法性: 右端为i,长度为k的区间:[i - k + 1, i]
        if(!dq.empty() && dq.front() < i - k + 1) dq.pop_front();

        //维护队列单调递增
        while(!dq.empty() && a[dq.back()] >= a[i]) dq.pop_back();
        dq.push_back(i);
		
        //滑动窗口最小值取队头即可
        if(i >= k) cout << a[dq.front()] << ' ';
    }
    cout << '\n';

    //清空队列
    dq.clear();

    //滑动窗口最大值
    for(int i = 1; i <= n; i++)
    {
        //判断队头合法性: 右端为i,长度为k的区间:[i - k + 1, i]
        if(!dq.empty() && dq.front() < i - k + 1) dq.pop_front();

        //维护队列单调递减
        while(!dq.empty() && a[dq.back()] <= a[i]) dq.pop_back();
        dq.push_back(i);

        //滑动窗口最大值取队头即可
        if(i >= k) cout << a[dq.front()] << ' ';
    }
}

5. 并查集

题目链接

参考代码:

#include
using namespace std;

const int N = 1e5 + 10;
int p[N];
//p[x]是x的父亲
//p[x]==x表示x是根

//返回根
int find(int x)
{
    if(p[x] != x) p[x] = find(p[x]);//路径压缩
    return p[x];
}

int main()
{
    int n, m; cin >> n >> m;

    //初始化
    for(int i = 1; i <= n; i++) p[i] = i;

    while(m--)
    {
        string s;
        int a, b;
        cin >> s >> a >> b;
        if(s == "M") p[find(a)] = find(b);//合并
        else
        {
            if(find(a) == find(b))//查询
                cout << "Yes" << '\n';
            else
                cout << "No" << '\n';
        }
    }
}

你可能感兴趣的:(算法,算法,数据结构,c++,排序算法,leetcode)