算法竞赛知识点总结

归并排序 & 逆序对

算法步骤:

  • 如果序列长度为 1,结束排序。
  • 平分序列,递归排序子序列。
  • 合并两个有序子序列。

代码:

int buf[maxn]; // 辅助数组

void merge_sort(int *beg, int *end){
    int size= end-beg;
    if(size<=1) return;

    int *mid= beg+(size>>1);
    merge_sort(beg, mid);
    merge_sort(mid, end);

    int *left=beg, *right=mid, *ans=buf;
    bool has_left= true, has_right= true;
    while(has_left || has_right){
        if(has_left && (has_right==false || *left<*right){
            *ans++=*left++; has_left= left<mid;
        }
        else{
            *ans++=*right++; has_right= right<end;
            /* cnt+=mid-left; */   //可以顺便计数逆序对
        }
    }

    memcpy(beg, buf, sizeof(int)*size);
}

树状数组

用于解决单点修改,前缀和查询的问题。lowbit(x) 是它的核心函数。

lowbit(x)= x&-x,表示 x 的最右边的 1 及其右边所有的 0 组成的数,等价于能被 x 整除的最大的 2 的幂。

设原数组为 a,树状数组为 c,则 c[i] 存储了 a[i] 及其之前共 lowbit(i) 个元素的和。因为 lowbit(0)=0,所以树状数组不使用下标 0,这要求原数组的第一个元素下标为 1 。

具体的查询、修改过程画图便很容易明白,代码如下:

struct Binary_indexed_tree{
    int c[maxn], size;
    Binary_indexed_tree(int s):size(s){
        memset(c, 0, sizeof(int)*(size+1));
    }
    int lowbit(const int x){ return x&-x; }
    void add(int pos, int delta){ // 令 a[pos] 增加 delta
        while(pos<=size){ c[pos]+=delta; pos+=lowbit(pos); }
    }
    int sum(int pos){ // 求 a[1]~a[pos] 的闭区间的和。
        int ans=0;
        while(pos>0){ ans+=c[pos]; pos-=lowbit(pos); }
        return ans;
    }
};

快速幂 & 取模

取模的基本原理

a*b mod n = (a mod n)*(b mod n) mod n

大整数取模

abc mod n = (a*10+b)*10+c mod n
= ((a*10+b) mod n)*10+c mod n

由于各个位的值都不会超过 9,所以第一位乘 10 ,再加下一位的结果 ans 不会超过 99,再 mod n 后大小就不会超过 n-1 。

下一次 ans*10+c 的结果最大就 10(n-1)+9=10n-1,再 mod n 的结果又不会超过 n。以此类推,整个计算过程中最大的中间值永远不会超过 10n,基本避免了溢出情况。

代码:

LL big_mod(const string &s, LL n){
    LL ans=0;
    for(int i=0, end=s.size(); i<end; ++i){
        ans= (ans*10 + s[i]-'0') % n;
    }
    return ans;
}

快速幂

分治法的思路:

设 pow(a, n) 表示 an
则 n 为 0 时:pow(a, n)= 1;
n 为偶数时:pow(a, n)= (pow(a, n/2))2;
n 为奇数时:pow(a, n)= pow(a, n-1)*a;

这样只要 O(log2n) 的复杂度就能完成计算。

代码:

LL pow(LL a, LL n){
    if(n==0) return 1;
    if(n&1) return pow(a, n-1)*a;
    LL sqrt= pow(a, n>>1);
    return sqrt*sqrt;
}

还有一种按二进制位分解计算的方法,例如:

a13=a(1101)2=a(23+22+20)=a8+4+2=a8a4a1

对于任意 an 也可以像这样分解成任意 a2i 的积。

方法是将 n 的二进制位从低位到高位按 0、1、2……标号,
选择所有对应位是 1 的 i,将这些 a2i 相乘, 结果即为 an

处理时共需要遍历 log2n 个二进制位,判断每一个位是否为 1 只需常数时间,而计算当前为对应的 a2i 也只需将上一个位对应的 a 的幂平方即可。

例如第一个位用到的 a20就是 a,第二个位用到 a21 可由 a*a 得到,第三个位需要的 a22 可由上一位计算出的 a2 再平方获得。这样每一位对应的 a 的幂也可以在常数时间内获得。

因此总时间也是 O(log2n) ,而优于前一个分治法的是,这种方法很容易写成迭代代码,速度更快。

代码:

LL quick_pow(LL a, LL n){
    LL ans=1;
    while(n>0){  // 当 n 还有为 1 的二进制位时继续处理。
        if(n&1) ans*= a;   // 如果最低位是 1 的话,ans 乘上 a 的对应次幂。
        a*=a;  // 将 a 平方获得下一位对应的 a 的次幂。
        n>>=1;  // 将下一个二进制位移到最低位。
    }
    return ans;
}

不过即使用 long long 存储,乘方的结果还是很容易溢出。

快速幂取模

利用取模公式,将快速幂代码简单修改就是快速幂取模代码。

LL quick_pow_mod(LL a, LL n, LL m){ // 计算 a 的 n 次方 mod m
    LL ans=1;
    while(n){ 
        if(n&1) ans= ans*a%m;
        a= a*a%m;
        n>>=1;
    }
    return ans;
}

同样,每一步取模可以减少溢出的可能,但仍无法完全避免溢出。

C++ 使用技巧

无结束标志时的 cin 读取框架

假设数据是若干行 a b c 的格式,但没有结束标志,那么可以这样读取所有数据并正常结束:

while(cin>>a>>b>>c){
    // 处理 a b c ……
}

加速 cin、cout

在 mian 函数开头添加:

ios::sync_with_stdio(false); // 取消和 stdio 的绑定。
cin.tie(0); // 取消和 cout 的绑定。

string 的常用方法

int string.find(string key, int pos=0)

从下标为 pos 的位置开始查找 key,返回第一个 key 出现的下标。

没有找到则返回 string::npos 。

如果想枚举 string 中所有 key 的位置可以这样写代码:

for(int i= str.find(key); i!=string::npos; i=str.find(key, i+1)
    print(i);

void string.replace(int pos, int size, string key)

将 string 的从 pos 下标开始,长度为 size 的子串替换为 key 。

直接结束程序:exit(0)

如果在递归层数很深的搜索中找到了答案,使用 cstdlib 库中的 exit(0) 可以直接结束程序,节省回溯的时间。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值