算法競賽知識點總結

歸併排序 & 逆序對

算法步驟:

  • 如果序列長度爲 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) 可以直接結束程序,節省回溯的時間。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章