板子

102 阅读3分钟

基础算法

[TOC]

基础杂项

排序

快排

快排板子

核心思想: 分治

const int N = 1e5+5;
int g[N];
int quick_sort(int l, int r)
{
	int p=l, q=r, x=g[l+r >> 1];
	while(p<=q)
	{
		while(g[p] < x) p++;
		while(g[q] > x) q--;
		if(p<=q) swap(g[p], g[q]), p++, q--;
	}
	if(q<r) quick_sort(q, r);
	if(p>l) quick_sort(l, p);
}

复杂度为

O(nlgn)O(nlgn)
第k大数
const int N = 1e5+5;
int g[N];
int kth_num(int l, int r, int k)
{
	int p=l, q=r, x=g[l+r >>1];
	while(p<=q)
	{
		while(g[p] < x) p++;
		while(g[q] < x) q--;
		if(p<=q) swap(g[p], g[q]), p++, q--;
	}
	if(p<=k) return kth_num(p, r); //停在了第p个位置,比第k个要小,结果在右边
	else if(q>=k) return kth_num(l, q); //不在右边结果就在左边
	else return g[l]; //前二者都不是,在中间.
}

复杂度为

O(lgn)O(lgn)
对顶堆

对于多次询问第k大数,我们可以选择调用刚才的第k大数板子,复杂度为

O(nlgn)O(nlgn)

但是也可以用两个堆维护,一个大顶堆一个小顶堆,被称作对顶堆

//查询第k大时,此时保证元素一定足够
//声明
priority_queue<int> bh;//大顶堆
priority_queue<int, vector<int>, greater<int>> sh;//小顶堆
//插入操作
if(!bh.size() || x > bh.top()) sh.push(x);
else bh.push(x);
//查询操作,当前要查询第k大
while(bh.size() > k)
{
	int p = bh.top(); bh.pop();
	sh.push(p);
}
while(bh.size() < k)
{
	int p = sh.top(); sh.pop();
	bh.push(p);
}

对顶堆的复杂度也可近似看作

O(nlgn)O(nlgn)

对顶堆在没有删除、每次要查询的第K大数递增时时间常数要优一些.

归并

归并板子
const int N = 1e5+5;
int g[N], b[N]; //b用于备份
void MergeSort(int l, int r)
{
	if(l==r) return; //边界 只有一个元素 无需排序
	int mid = l+r >> 1;
	MergeSort(l, mid); MergeSort(mid+1, r);
	int p=l, q=mid+1, cur=p;
	while(p<=mid && q<=r)
	{
		if(g[p] <= g[q]) b[cur++] = g[p++];
		else b[cur++] = g[q++];
	}
	while(p<=mid) b[cur++] = g[p++];
	while(q<=r) b[cur++] = g[q++];
	for(int i=l; i<=r; i++) g[i] = b[i];
}
逆序对的个数

首先证明逆序对的个数就是归并排序中交换的次数 将[1, n]的问题分解为[1, mid]与[mid+1, n]的问题 如果我们知道两个子问题的解,剩下的部分解就是在左边并且比右边大的元素对数了,这用归并排序很好实现.

const int N = 1e5+5;
int g[N], b[N];
void MergeSort(int l, int r)
{
	int ans = 0;
	if(l==r) return;
	int mid = l+r >> 1;
	MergeSort(l, mid); MergeSort(mid+1, r);
	int p=l, q=mid+1, cur=l;
	//记录右区间的方式
	while(p<=mid && q<=r)
	{
		if(g[p] <= g[q]) b[cur++] = g[p++]; //此处是小于等于
		//是因为小于等于的边界条件,如果是小于的话当两个元素相等坐标
		//q向前移动,此时答案更新,但p这个点也被算了进去.
		else b[cur++] = g[q++], ans += mid - p + 1;
	}
	while(p<=mid) b[cur++] = g[p++];
	while(q<=r) b[cur++] = g[q++];
	//记录左区间的方式
	while(p<=mid && q<=r)
	{
		if(g[p] <= g[q]) b[cur++] = g[p++], ans += q - (mid + 1); 
		//小于等于同理.
		//如果是小于那么会加入q. 下次更新时答案会多出来
		else b[cur++] = g[q++];
	}
	while(p<=mid) b[cur++] = g[p++], ans += q - (mid + 1); //即 r - mid
	while(q<=r) b[cur++] = g[q++];
	for(int i=l; i<=r; i++) g[i] = b[i];
	/*有两种计数方式,更优秀的一种是记录当右区间加入时的
	  这样的计数方式只需记录一次,在后面的循环中无需记录
	  而记录左区间加入时的计数方式需要记录两次.
	*/
}
最大子序列和

四种做法

  1. 暴力

  2. 优化的暴力

  3. 分治

  4. dp 暴力和优化的暴力见视频 子序列最值

这里分析一下这个题目为什么能用分治

问题[1, n]可以分解为[1,mid]与[mid+1]. 随后的横跨问题也很好解决,所以是天然的分治模板

二分

整数二分

大于等于模板
const int N = 1e5+5;
int g[N];
int l=1, r=n;
int x; //query
while(l<r)
{
	int mid = l + r >> 1;
	if(g[mid] >= x) r = mid;
	else l = mid + 1;
}
小于等于模板
const int N = 1e5+5;
int g[N];
int l=1, r=n;
int x; //query
while(l<r)
{
	int mid = l+r+1 >> 1;
	if(g[mid] <= x) l=mid;
	else r = mid-1;
}

注意小于等于时候要加1防止死循环

浮点数二分

例: 给定一个浮点数n,求它的三次方根。数的三次方根

10000n10000−10000≤n≤10000

double l=-100, r=100;
double n; cin>>n;
while(fabs(l-r) <= 1e-7)
{
    double mid = (l+r)/2;
    if(mid*mid*mid<=n) l=mid;
    else r=mid;
}

浮点数不需要考虑小于等于板子加一的问题,因为有fabs做保证。

高精度

高精度加法

// C = A + B, A >= 0, B >= 0
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;
}

高精度减法

// C = A - B, 满足A >= B, A >= 0, B >= 0
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;
        if (i < B.size()) t -= B[i];
        C.push_back((t + 10) % 10);
        if (t < 0) t = 1;
        else t = 0;
    }

    while (C.size() > 1 && C.back() == 0) C.pop_back();
    return C;
}

高精度乘法

//高精度乘以低精度
// C = A * b, A >= 0, b > 0
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;
}
//高精度乘以高精度O(n2),必要时用FFT优化
vector<int> mul(vector<int> A, vector<int> B)
{
    vector<int> C(A.size() + B.size());

    for (int i = 0; i < A.size(); i ++ )
        for (int j = 0; j < B.size(); j ++ )
            C[i + j] += A[i] * B[j];

    for (int i = 0, t = 0; i < C.size() || t; i ++ )
    {
        t += C[i];
        if (i >= C.size()) C.push_back(t % 10);
        else C[i] = t % 10;
        t /= 10;
    }

    while (C.size() > 1 && !C.back()) C.pop_back();

    return C;
}

高精度除法

// A / b = C ... r, A >= 0, b > 0
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);
        r %= b;
    }
    reverse(C.begin(), C.end());
    while (C.size() > 1 && C.back() == 0) C.pop_back();
    return C;
}

高精度比大小

vector<int> max_vec(vector<int> a, vector<int> b)
{
	if(a.size() > b.size()) return a;
	if(a.size() < b.size()) return b;
	if(vector<int>(a.rbegin(), a.rend()) > 
	vector<int>(b.rbegin(), b.rend())) return a;
	return b;
}

高精度输出

void output(vector<int> a)
{
	for(int i=a.size()-1; i>=0; i--) cout<<a[i]; 
}

前缀差分

一维

一维前缀和
一维差分

二维

二维前缀和
二维差分

双指针

双指针与其说是一种算法,不如说是一种思维,将问题无用的部份遗弃掉。 这里给出双指针的几个简单例子,更多的例子请见双指针专题。

位运算

位运算牵扯到的问题也很多,这里给出一个简单的lowbit运算的例子,更多的例子请见位运算专题。

区间问题

离散化

区间合并

数据结构

链表

单链表

双链表

手写模拟

单调栈

队列

手写模拟

单调队列

KMP

Trie

并查集

朴素并查集

带路径压缩的并查集

维护一些特殊属性的并查集

手写堆

堆排序

哈希表

模拟散列表

字符串哈希

搜索与图论

DFS

概述

基本策略

BFS

概述

基本策略

图的两种存储方式

图的DFS

树的重心

图的BFS

拓扑排序

拓扑排序的实现

利用拓扑排序判断环

最短路

Dijkstra

朴素
堆优化

bellman-ford

板子
应用: 求边数限制的最短路

spfa

板子
应用:求负权最短路
应用: 判断负环

Floyd

板子

最小生成树

Prim

朴素
堆优化

Kruskal

朴素
排序优化

二分图

染色法判定二分图

匈牙利算法--二分图的最大匹配

dp

背包

01背包

完全背包

多重背包

多重背包二进制优化

分组背包

线性dp