算法复杂度

时间复杂度: 常对幂指阶

时间复杂度
时间复杂度指输入数据大小为 N 时,算法运行所需花费的时间。统计的是算法的「计算操作数量」,而不是「运行的绝对时间」。
时间复杂度具有「最差」、「平均」、「最佳」三种情况,分别使用 O ,Θ ,Ω 三种符号表
常见种类: O(1)<O(logN)<O(N)<O(N*logN)<O(N2)<O(2n)<O(N!)

N2 :如冒泡排序
指数阶常出现于递归
阶乘阶对应数学上常见的 “全排列”
对数阶与指数阶相反,指数阶为 “每轮分裂出两倍的情况” ,而对数阶是 “每轮排除一半的情况” 。对数阶常出现于「二分法」、「分治」等算法中,体现着 “一分为二” 或 “一分为多” 的算法思想。
线性对数阶(N*logN)常出现于排序算法,例如「快速排序」、「归并排序」、「堆排序」等

空间复杂度

通常情况下,空间复杂度指在输入数据大小为 N 时,算法运行所使用的「暂存空间」+「输出空间」的总体大小。
而根据不同来源,算法使用的内存空间分为三类:
指令空间:编译后,程序指令所使用的内存空间。
数据空间:算法中的各项变量使用的空间,包括:声明的常量、变量、动态数组、动态对象等使用的内存空间。
栈帧空间:程序调用函数是基于栈实现的,函数在调用期间,占用常量大小的栈帧空间,直至返回后释放。栈帧空间的累计常出现于递归调用
常见种类:O(1)<O(logN)<O(N)<O(N2)<O(2N)

acwing模板

基础算法

排序

快速排序

快速排序是不稳定的
关键:在于如何划分为两个序列
时间复杂度:O(n logn)
img

const int N = 1e6 + 10;
int q[N];
int n;
void quick_sort(int q[], int l, int r){//l是左边界,r是右边界
    if(l >= r) return;//当l和r相遇时,交换l和r位置

    int x = q[l + r >> 1];//定义一个中间值,用于比较,把q[]分成大于和小于两部分,更新了数据,不能用最左边和最右边了
    int i = l-1, j = 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]);//如果两个数满足在q[]的分别在另一边,交换位置
    }
    quick_sort(q,l, j);
    quick_sort(q, j + 1, r);//两侧迭代,直到有序
}

归并排序

归并排序是稳定的
关键:在于如何合并连个有序序列,双路归并,合二为一
时间复杂度:O(n logn)
img


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

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++];
    
    //把tmp临时数组再赋值回去
    for(int i = l, k = 0; i <= r; i++){
        q[i] = tmp[k++];
    }
 }

快速排序和归并排序区别

1.速排序是原地排序,原地排序指的是空间复杂度为O(1);
2.归并排序不是原地排序,因为两个有序数组的合并需要额外的空间协助才能合并;
3.快速排序是不稳定的,时间复杂度在O(nlogn)~O(n2)之间 。归并排序是稳定的,时间复杂度是O(nlogn)。
4.快速排序是二叉树模型
5.归并排序是到二叉树模型

二分

有序元素(有某种性质)+查找 == 二分

整数二分

二分的本质不是单调性,是二段性,将区间一分为二,一半满足,一半不满足
img
img
找分界点位置,分为两种,
第一种,找红色边界点,l = mid,而且mid = l+r+1 >>1

第二种,找绿色边界点,r = mid,mid = l + r >> 1

int l = 0, r = n-1;
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, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
    while (l < r)
    {
        int mid = l + r + 1 >> 1;
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    return l;
}

有点乱,看到还有一种方法,试试看
模板
img
img

const int N = 1e6 + 10;
int q[N];
bool check(int x){
    //要求数据的补集
    if() return true;
    return false;
}
int l = -1, r = n ;
while(l + 1 != r){
    int  mid = l + r >> 1;
    if(check(q[mid])){
        l = m;
    }
    else r = m;
}

这个方法,更容易出错,返回l,r也有区别

浮点数二分

这个比较简单,一般用于开方

int l = -100, r = 100;
while(r - l > 1e-6){//根据题目要求,一般情况下比题目保留位数大两位
    int mid = ( l + r ) / 2;
    if(mid * mid >= x) r = m;//x为带求开方数
    else l = m; 
}

高精度

高精度一般考四种
存储方式用数组,倒序存,从个位存到高位

a + b 位数一般106

img

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
//加法
vector<int>add(vector<int>&A, vector<int> &B){//这里用引用避免了复制,节省了时间
    vector<int>c;
    int t = 0;//t表示进位
    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 != 0)
    c.push_back(t);
    return c;
}
int main()
{
    string a,b;
    vector<int>A,B;
    cin>>a>>b;
    //倒序存放到数组的过程
    for(int i = a.size() - 1; i >= 0; i --) A.push_back(a[i] - '0');
    for(int i = b.size() - 1; i >= 0; i --) B.push_back(b[i] - '0');
    //加法
    auto c = add(A,B);
    //倒序取出
    for(int i = c.size() - 1 ; i >= 0; i --){
        printf("%d",c[i]);
    }
    return 0;
}

a - b 位数一般106

t表示借位
img

#include <iostream>
#include <string>
#include <vector>
using namespace std;
const int N = 1e5 + 10;
//判断a >= b
bool cmp(vector<int> &a, vector<int> &b){
    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;
}
//c = a - b
vector<int> sub(vector<int> &a, vector<int> &b){
    vector<int> c;
    for(int i = 0, t = 0; i < a.size(); i++){
        //t表示借位
        t = a[i] - t;
        //判断b还有数字
        if(i < b.size()) t -= b[i];
        //原本要分为两种情况,t > 0 和 t < 0, 这样处理直接不需要分
        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;
}
int main()
{
    string n,m;
    cin>>n>>m;
    vector<int> a, b;
    for(int i = n.size() - 1; i >= 0; i--) a.push_back(n[i] - '0');
    for(int j = m.size() - 1; j >= 0; j--) b.push_back(m[j] - '0');

    vector<int>c; // 存结果
    //如果a>b
    if(cmp(a,b)) {
        c = sub(a,b);
    }

    else c = sub(b, a), cout<<"-";
    //输出
    for(int i = c.size() - 1; i >= 0; i--){
        cout<<c[i];
    }
    cout<<endl;
    return 0;
} 


a * b 大整数乘以一个小整数len(a) ≤ 106 且b ≤ 106

把小的数字当做一个整体

#include <iostream>
#include <vector>
using namespace std;
// c =  a * b
vector<int> mul(vector<int> &a, int b)
{
    vector<int> c;
    int t = 0;                               // 进位
    for (int i = 0; i < a.size() || t; i++) // 当i没有处理完,或者t不为0
    {
        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;
}
int main()
{
    string a;
    int b;
    cin >> a >> b;
    vector<int> c;
    for (int i = a.size() - 1; i >= 0; i--)
    {
        c.push_back(a[i] - '0');
    }
    auto s = mul(c, b);

    for (int i = s.size() - 1; i >= 0; i--)
    {
        cout << s[i];
    }
    return 0;
}

a / b 求商和余数 大整数除以一个小整数len(a) ≤ 106 且b ≤ 106

注意倒着存,正着算
img

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// a/b,商是c,余数是r
vector<int> div(vector<int> &a, int b, int &r)
{
    vector<int> c;
    r = 0;
    // 注意这里是正着的算的,但是a是倒着存的,所以如下
    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());
    // 去掉前导0
    while (c.size() > 1 && c.back() == 0)
        c.pop_back();
    return c;
}
int main()
{
    string a;
    int b;
    cin >> a >> b;
    vector<int> c;
    for (int i = a.size() - 1; i >= 0; i--)
    {
        c.push_back(a[i] - '0');
    }
    int r;
    auto s = div(c, b, r);

    for (int i = s.size() - 1; i >= 0; i--)
    {
        cout << s[i];
    }
    cout << endl<< r;
    return 0;
}

前缀和

一维前缀和

前缀和一定要下标从1开始,快速求出数组中一段数的和,sum[0] = 0;

const int N = 100001;
int sum[N],a[N];
for(int i = 1; i <= n; i++){
    sum[i] = sum[i-1] + a[i];
}
int ans = sum[r] - sum[l-1];

询问是O(1),预处理过程中是O(n)

对于二维矩阵的前缀和

const int N = 1010;
int sum[N][N],a[N][N];
for(int i = 1; i <= n; i++){
    for(int j = 1; j <= m; j++){
        cin>>a[i][j];
        sum[i][j] = sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1] + a[i][j];
    }
}
//(x2,y2) - (x1,y1);
int ans = sum[x2][y2] - sum[x2][y1-1] - sum[x1-1][y2] + sum[x1-1][y1-1];

差分

一维差分

构造一个b数组使得a是b的前n项和

#include<iostream>
using namespace std;
const int N = 100010;
int n,m;
int A[N],B[N];//b数组是a数组的差分,a数组是b数组的前缀和
void insert(int l, int r, int c){
    B[l] += c;
    B[r+1] -= c;
}
int main(){
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n;i++){//注意从一开始
        scanf("%d",&A[i]);
    }
    for(int i = 1; i <= n; i++) insert(i,i, A[i]);
    while(m--){
        int l,r,c;
        scanf("%d%d%d",&l,&r,&c);
        insert(l,r,c);
    }
    for(int i = 1; i<= n; i++){
        B[i] += B[i-1];
        printf("%d ",B[i]);
    }
    return 0;
}

二维差分

img

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 1010;
int A[N][N],B[N][N];
void insert(int x1, int y1, int x2, int y2, int c){
    B[x1][y1] += c;
    B[x2 + 1][y1] -= c;
    B[x1][y2+1] -= c;
    B[x2 + 1][ y2 + 1] += c;
    
}
int main(){
    int n,m,q;
    cin>>n>>m>>q;
    for (int i = 1; i <= n; i ++ ){
        for (int j = 1; j <= m; j ++ ){
            cin>>A[i][j];
            insert(i,j,i,j,A[i][j]);
        }
    }
    while(q--){
        int x1,y1,x2,y2,c;
        cin>>x1>>y1>>x2>>y2>>c;
        insert(x1,y1,x2,y2,c);
    }
    for(int i= 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
                B[i][j] +=  B[i][j-1]+ B[i-1][j] - B[i-1][j-1];
                printf("%d ",B[i][j]);
        }
    
        cout<<endl;
    }
    
}

双指针算法

找到某种单调性
可以把O(n2) 的算法优化成O(n)
img
双指针算法将暴力的优化一下

位运算

操作符 数字a 数字b 结果
& 0101 0001 0001
0101 0001 0101
~ 0101 1010
^ 0101 0001 0100
>>(右移) /=2
<<(左移) *=2

n的二进制表示中,第k位是几
先把k右移到最后一位 n >> k (n>>1 == n/=2)
在进行与运算 n & 1;
组合起来就是 n >> k & 1;

lowbit(x)是返回x的最后一位1是多少,可以用来统计x中1的个数
img
实现方式
img

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
int lowbit(int x){
    return x & -x;
}
int main(){
    int n;
    cin>>n;
    while(n--){
        int x;
        cin>>x;
        int res = 0;
        while(x) {
            x -= lowbit(x);//减去x中最后一位1
            res++;
        }
        cout<<res <<' ';
    }
    return 0;
}

原码:原来的 x
反码:每一位取反 ~x
补码:取反在加一 ~x+1

离散化

这里特指有序、饱序的数字的离散化
img
模板

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 = 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, 2, ...n
}

unique实现

img

//a[j] 中存储的就是不重复元素
vector<int>::iterator unique(vector<int> &a){
    int j = 0; 
    for(int i = 0; i < a.size(); i++){
        if(!a || a[i]!=a[i-1]){
            a[j++] = a[i];
        }
    }
    return a.begin() + j;
}

这是典例,区间和

#include <iostream>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 300010;
typedef pair<int, int> PII;
vector<int> alls;//存储要离散化的位置
vector<PII> add,query;//插入操作,查询
int a[N],s[N];//a是输入的数,s是前缀和
//求x离散化后的结果,这里用到二分
int find(int x){
    int l = 0; 
    int r = alls.size() - 1;
    while(l < r){
        int mid = l + r >> 1;
        if(alls[mid] >= x) r = mid;//找大于等于x的最小值
        else l = mid + 1;
    }
    return r + 1;//方便进行前缀和
}
int main(){
    int n,m;
    cin >> n >> m;
    for (int i = 0; i < n; i++){
        int x,c;
        cin>>x>>c;
        //在x的位置插入c
        add.push_back({x,c});
        
        alls.push_back(x);
    }
    for (int j = 0; j < m; j++){
        int l,r;
        cin>>l>>r;
        query.push_back({l,r});
        
        alls.push_back(l);
        alls.push_back(r);
    }
    //去重alls
    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 = 0; 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;
}

区间合并

img
按照左端点进行排序

void merge(vector<PII> &segs)
{
    vector<PII> res;

    sort(segs.begin(), segs.end());

    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;
        }
        //有关系,更新ed
        else ed = max(ed, seg.second);
    //把最后一个加入到区间里
    if (st != -2e9) res.push_back({st, ed});

    segs = res;
}

数据结构

链表与邻接表:树与图的存储

数组模拟单链表

主要是避免反复new结点,造成缓慢
用于存储树和图

//head 表示头指针
//e[i] 表示下标为i的结点的值
//next[i] 表示i的下一个结点
//idx 表示当前指导那个位置
int e[N],next[N],idx,head;
//初始化
void init(){
    head = -1;
    idx = 0;
}
//插入到头结点
void add_to_head(int x){
    e[idx] = x;
    next[idx] = head;
    head = idx;
    idx ++; 
}
//把x插入到下标为k的后面
void add(int k, int x){
    e[idx] = x;
    ne[idx] = ne[k];
    ne[k] = idx;
    idx++;
}
//单链表删除k结点下一个元素
void remove(int k){
    ne[k] = ne[ne[k]];
}
//双向链表
int e[N],l[N],r[N],idx; //l[N]代表左侧,r[N]代表右侧

void init(){
    idx = 2;
    //两个边界
    r[0] = 1;
    l[1] = 0;
}

//在k右边插入x
void add(int k, int x){
    e[idx] = x;
    l[idx] = k;
    r[idx] = r[k];
    l[r[k]] = idx;
    r[k] = idx;
    idx++;
}
//删除第k个点
void remove(int k){
    l[r[k]] = l[k];
    r[l[k]] = r[k];
}

邻接表是多个单列表

详见第三章

栈与队列:单调队列、单调栈

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

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

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

// 栈顶的值
stk[tt];

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

}

单调栈

找某个数最左边或者右边距离他最近的大于或者小于他的数

常见模型:找出每个数左边离它最近的比它大/小的数
int tt = 0;
for (int i = 1; i <= n; i ++ )
{
    while (tt && check(stk[tt], i)) tt -- ;
    stk[ ++ tt] = i;
}

时间复杂度O(n)

普通队列

// 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)
{

}

单调队列,滑动窗口

//常见模型:找出滑动窗口中的最大值/最小值
int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{
    while (hh <= tt && check_out(q[hh])) hh ++ ;  // 判断队头是否滑出窗口
    while (hh <= tt && check(q[tt], i)) tt -- ;
    q[ ++ tt] = i;
}

原理差不多,等二刷

#include<iostream>
using namespace std;
const int N = 1000001;
int a[N], q[N];//q[N]存储的是滑动窗口中的下标
int main(){
    int n,k;
    scanf("%d%d", &n,&k);
    for(int i = 0; i < n; i++){
        scanf("%d",&a[i]);
    }
    int hh = 0, tt = -1;
    for(int i = 0; i < n; i++){
        //判断队头元素是否已经滑出窗口
        if (hh <= tt && q[hh] < i - k + 1) hh++;
        while(hh <= tt && a[q[tt]] >= a[i]) tt--;
        q[++tt] = i;
        if(i >= k -1) printf("%d ",a[q[hh]]);
    }
    puts("");
    
    hh = 0, tt = -1;
    for(int i = 0; i < n; i++){
        if(hh <= tt && q[hh] < i - k + 1) hh++;
        while(hh <= tt && a[q[tt]] <= a[i]) tt--;
        q[++tt] = i;
        if(i >= k - 1) printf("%d ", a[q[hh]]);
    }
    puts("");
    return 0;
}

kmp

// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度
求模式串的Next数组:
for (int i = 2, j = 0; i <= m; i ++ )
{
    while (j && p[i] != p[j + 1]) j = ne[j];
    if (p[i] == p[j + 1]) j ++ ;
    ne[i] = j;
}

// 匹配
for (int i = 1, j = 0; i <= n; i ++ )
{
    while (j && s[i] != p[j + 1]) j = ne[j];
    if (s[i] == p[j + 1]) j ++ ;
    if (j == m)
    {
        j = ne[j];
        // 匹配成功后的逻辑
    }
}

现在还不太懂代码,但是原理懂了

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010, M = 1000010;
int n,m;
char p[N], s[M];
int nex[N];
int main(){
    cin>>n>>p + 1>>m >> s+1;
    for(int i = 2, j = 0; i <= n; i++){
        while(j && p[i] != p[j + 1]) j = nex[j];
        if(p[i] == p[j + 1]) j++;
        nex[i] = j;
    }
    for(int i = 1, j = 0; i <= m; i++){
        while(j && s[i] != p[j + 1]) j = nex[j];
        if(s[i] == p[j + 1]) j++;
        if(j == n){
            printf("%d ", i- n);
            j = nex[j];
        }
    }
    return 0;
}

Trie

高效的存储和查找字符串集合的数据结构
img
将所有字符串集合的结尾进行标记

int son[N][26], cnt[N], idx;
// 0号点既是根节点,又是空节点
// son[][]存储树中每个节点的子节点
// cnt[]存储以每个节点结尾的单词数量
// idx 存储当前用到那个下标
// 插入一个字符串
void insert(char *str)
{
    int p = 0;//从根节点开始遍历
    for (int i = 0; str[i]; i ++ )
    {
        int u = str[i] - 'a';//将a-z映射成0-25
        if (!son[p][u]) son[p][u] = ++ idx;//如果不存在,就创建出来
        p = son[p][u];//结束时对应的结尾位置为p
    }
    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];
}

并查集

两个操作:(近乎O(1))

1.将两个集合合并
2.询问两个元素是否在一个集合中
3.用树的形式维护一个集合,根节点是他的代表元素,根节点的编号就是当前集合的编号,每个节点存储他的父节点,p[x]表示x的父节点
4.判断树根p[x] = x
5.如何求x的集合编号:while(p[x] != x) x = p[x]
6.如何合并两个集合:将一个集合当成另外一个集合的子节点p[x]是x的集合编号,p[y]是y的集合编号,p[x] = y
7.优化:
路径压缩一旦找到根节点,就将该路径上的所有点都指向根节点,基本就可以看成O(1)
按秩合并:先留下,后面在补

(1)朴素并查集:

    int p[N]; //存储每个点的祖宗节点

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ ) p[i] = i;

    // 合并a和b所在的两个集合:
    p[find(a)] = find(b);

    // 查找是否在同一个集合
    find(a) == find(b);


(2)维护size的并查集:

    int p[N], size[N];
    //p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ )
    {
        p[i] = i;
        size[i] = 1;
    }

    // 合并a和b所在的两个集合:这个有顺序,不能错
    size[find(b)] += size[find(a)];
    p[find(a)] = find(b);


(3)维护到祖宗节点距离的并查集:

    int p[N], d[N];
    //p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x)
        {
            int u = find(p[x]);
            d[x] += d[p[x]];
            p[x] = u;
        }
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ )
    {
        p[i] = i;
        d[i] = 0;
    }

    // 合并a和b所在的两个集合:
    p[find(a)] = find(b);
    d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量

堆是维护一个数据集合,是用一维数组实现的完全二叉树(常常用来构建优先队列,支持堆排序,快速找出一个集合中的最小值或者最大值)
img

一维数组存贮
x的左儿子是2x
x的右儿子是2x+1
img
堆排序
img
这些操作都可以用down(){}和up(){}操作表示
down和up操作是O(logn),求最小值是O(1),插入和删除都时O(logn)
将创建堆得操作从O(nlogn) 下降到 O(n),先插入n/2接下来的都down

Hash表

搜索和图论

DFS与BFS

DFS

img

数据结构 空间 特点
DFS stack O(h) 不具有最短性 最重要的顺序
BFS queue O(2n) 最短路
剪枝:
最优性剪枝:不是最优解,剪枝
可行性剪枝:不可行时候剪枝

BFS

img

/*
 做这种题目的步骤(最短路问题)
 1.创建两个存储,一个存储值,一个存储距离
 2.然后首先将第一个点的位置存储的距离提前标注出来
 3.然后弄两个方向变量用于上下左右前进int[] dx = {-1,0,1,0}, dy = {0,1,0,-1};
 4.然后如果四个方向上的点没有超过边界,在结合实际情况有没有用过的点,判断能不能够进行前进,如果可以就进行前进存储其内容g跟距离d+1
 5.最后返回想要的最短值d;
*/

树与图的遍历:拓扑排序

树是无环联通的特殊的图
图:无向图,有向图

最短路

最小生成树

二分图:染色法、匈牙利算法

数学知识

质数

线性筛素数

const int N = 200000;
int primes[N];
int cnt = 0;
int heshu[N];
void get_primes(int n){
    for(int i = 2; i < n; i++){
        if(!heshu[i]) primes[cnt++] = i;
        for(int j = 0; primes[j] * i <= n; j++){
            heshu[primes[j] * i] = true;
            if(i % primes[j] == 0) break;
        }
    }
}

1 ≤ n ≤ 106
时间复杂度O(n)

约数

int gcd(int a, int b)
{
    return b ? gcd(b,a%b):a;
}

正常

int gcd(int a, int b){
    if(a%b == 0) return b;
    else return gcd(b,a%b);
}

欧拉函数

快速幂

快速幂迭代版
求ab mod p

typedef long long LL;
int quickmi(int a, int b, int p){//
    int res = 1 %p;
    while(b){//对b进行二进制化,从低位到高位
        if(b & 1) res = (LL) res * a%p;
        //更新a,a依次为a^{2^0},a^{2^1},a^{2^2},....,a^{2^logb}
        a = (LL)a * a%p;
        //b二进制右移一位
        b >>= 1;
    } 
    return res;
}

b&1就是判断b的二进制表示中第0位上的数是否为1,若为1,b&1=true,反之b&1=false

b&1也可以用来判断奇数和偶数,b&1=true时为奇数,反之b&1=false时为偶数

判断二进制第k位的数字为1:n>>k&1 == true;

扩展欧几里得算法

中国剩余定理

高斯消元

组合计数

Cbn = Cbn-1 + Cb-1n-1

const int N = 2001, MOD = 1e9+7;
typedef long long LL;
int C[N][N];
void init(int n){
    for(int i = 0; i <= n; i++)
    {
        for(int j = 0; j <= i; j++){
            if(j == 0){C[i][j] = 1;}
            else C[i][j] = ((LL)C[i-1][j] + C[i-1][j-1])%MOD;
        }
    }
}

O(n2)

容斥原理

img
从n个数中选择任意多个数的方案数,共有2n项,时间复杂度2n
img
img

for (int i = 1; i < 1 << m; i ++ )
    //  i<1<<m   组合数  2^m-1  

    {
        //t表示所有质数的乘积,cnt表示i里面有几个一
        int t = 1, s = 0;


        for (int j = 0; j < m; j ++ )//遍历二进制的每一位

            if (i >> j & 1)//判断二进制第j位是否存在
            {
                if ((LL)t * p[j] > n)
                {
                    t = -1;
                    break;
                }
                t *= p[j];
                s ++ ;
            }

        if (t != -1)
        {

            if (s % 2) res += n / t;
            else res -= n / t;

        }
    }



简单博弈论

动态规划

背包问题

线性DP

区间DP

计数类DP

数位统计DP

状态压缩DP

树形DP

记忆化搜索

贪心

posted on 2022-07-04 14:43  运甓  阅读(110)  评论(0)    收藏  举报