君君算法課堂
本節《君君算法課堂》主要對於基礎算法進行講解
這些算法雖然簡潔易懂,但卻是我們理解更加高深算法的有力工具
我們也能在其中發現算法世界的樂趣,培養我們對於算法的興趣
下面我們話不多說,開啟本節《君君算法課堂》
位運算
位運算是對二進制下每一位進行運算后得到一個新的二進制數的運算
算數位運算符號
\(and/\)&:按位與,\(1\&1=1,1\&0=0,0\&0=0\)
\(or/\)|: 按位或,\(1\ |\ 1=1,\ 1\ |\ 0=1,0\ |\ 0=0\)
\(xor/\)^: 按位異或,\(1\ xor\ 1=0,1\ xor\ 0=1,0\ xor\ 0=0\)
\(not/\)~: 按位非,將二進制(補碼意義下)每位都取反(0-->1, 1-->0),該運算滿足~\(x=-(x+1)\)
位移運算
左移: 二進制意義下使數碼同時向左移動,低位以 \(0\) 填充,高位越界后舍棄
算數右移:二進制補碼意義下使數碼同時向右移動,高位以符號位填充,低位越界后舍棄
算數右移等於 除以 \(2\) 向下取整,\(-3>>1=-2,3>>1=1\)
但整數除以 \(2\) 在 \(C++\) 中運算為 除以 \(2\) 向零取整,\((-3)/2=-1,3/2=1\)
邏輯右移:二進制補碼意義下使數碼同時向右移動,高位以 \(0\) 填充,低位越界后舍棄
注意:\(C++\)中對右移的實現沒有規定,由編譯器決定使用算數右移或邏輯右移
一般編譯器使用算數右移,我們默認右移操作采用算數右移的方式來實現
快速冪算法
快速冪用於快速計算 \(x^y\%mod\)
計算的思路是:將十進制的 \(y\) 看作二進制,再通過二進制下的一些運算統計答案
以下已默認為二進制下的情況
過程:
對於求 \(x^y\),將 \(y\) 表示為二進制,如:\(105_{(10)} = 1101001_{(2)}\)
所以 \(x^y = x^{105} = x^{2^6+2^5+2^3+2^0}=x^{2^6}x^{2^5}x^{2^3}x^{2^0}\)
用變量 \(ans\) 統計答案
x | ans |
---|---|
\(x\) | \(x^{2^0}\) |
\(x^2\) | \(x^{2^0}\) |
\(x^4\) | \(x^{2^0}\) |
\(x^8\) | \(x^{2^3}x^{2^0}\) |
\(x^{16}\) | \(x^{2^3}x^{2^0}\) |
\(x^{32}\) | \(x^{2^5}x^{2^3}x^{2^0}\) |
\(x^{64}\) | \(x^{2^6}x^{2^5}x^{2^3}x^{2^0}\) |
int ksm(int x, int y, int mod) {
int ans = 1;
for( ; y; y >>= 1, (x *= x) %= mod) if(y & 1) (ans *= x) %= mod;
return ans;
}
注:\((a *= b) \%= mod\)的寫法等價於 \(a = (a * b) \%mod\)
時間復雜度:\(O(log\ y)\)
前綴和
前綴和可以用於快速查詢 靜態數組區間和
一維前綴和
對於數組 \(a[i]\),設 \(s[i]\)為其對應的前綴和數組
那么對於求區間和的操作
可以進行相應的轉化
時間復雜度:預處理\(O(n)\),單次查詢\(O(1)\)
int a[N], s[N];
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; ++ i) scanf("%d", &a[i]);
for(int i = 1; i <= n; ++ i) s[i] = s[i - 1] + a[i];
for(int i = 1, l, r; i <= m; ++ i) {
scanf("%d%d", &l, &r);
printf("%d\n", s[r] - s[l - 1]);
}
二維前綴和
對於二維數組(矩陣),可以類似求出二位前綴和,通過計算得出二維部分和
對於數組 \(a[i][j]\),設 \(s[i][j]\)為其對應的前綴和數組
可以推出 \(s[i][j]\) 的遞推式
那么我們可以按如下方法(思想:容斥原理)求出矩陣所存變量的和
int a[N][N], s[N][N];
scanf("%d%d%d", &n, &m, &k);
for(int i = 1; i <= n; ++ i) {
for(int j = 1; j <= m; ++ j) scanf("%d", &a[i][j]);
}
for(int i = 1; i <= n; ++ i) {
for(int j = 1; j <= m; ++ j) {
s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];
}
}
for(int i = 1, x1, x2, y1, y2; i <= k; ++ i) {
scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
printf("%d\n", s[x2][y2] - s[x2][y1-1] - s[x1][y2-1] + s[x1-1][y1-1]);
}
時間復雜度:預處理\(O(n*m)\),單次查詢\(O(1)\)
差分
前綴和可以用於快速求解 區間加的離線單點查詢
注:在線詢問 與 離線詢問的區別
離線詢問時,每次詢問時的數據由系統輸入
而在線詢問時,每次詢問會以上次詢問的答案為基礎給出
因此,在線詢問需要在詢問后,立馬計算出答案,否則無法進行下一步
而離線詢問則可以在讀入所有數據后再進行運算
在線詢問的題目難度會比離線詢問的題目難度更大
對於數組 \(a[i]\),定義其差分數組 \(b[i]\)
對於區間加法,假設對數組 \(a[i]\) 區間 \([l,r]\)加上 \(d\)
可以轉化為對於數組 \(b[i]\) 進行操作:\(b[l]+=d,b[r+1]-=d\)
在進行詢問時,進行如下預處理操作
for(int i = 1; i <= n; ++ i) b[i] += b[i - 1];
則 \(b[i]\) 數組就可進行單點詢問了
int a[N], b[N];
scanf("%d%d%d", &n, &m);
for(int i = 1; i <= n; ++ i) scanf("%d", &a[i]);
for(int i = 1; i <= n; ++ i) b[i] = a[i] - a[i - 1];
for(int i = 1, l, r, d; i <= m; ++ i) {
scanf("%d%d%d", &l, &r, &d);
b[l] += d; b[r + 1] -= d;
}
for(int i = 1; i <= n; ++ i) b[i] += b[i - 1];
for(int i = 1; i <= n; ++ i) printf("%d\n", b[i]);
時間復雜度:操作預處理\(O(n)\),詢問預處理:\(O(m)\),單次詢問\(O(1)\)
ST算法
用於快速求解 靜態區間最值問題(RMQ問題)
利用倍增的思想,對於長度為 \(n\) 的數組 \(a[i]\)
設 \(s[i][j]\) 表示數組 \(a[i]\) 區間 \([j,j+2^i-1]\) 里的最大值,即從 \(j\) 開始的 \(2^i\) 個數的最大值
初始化:\(s[0][i]=a[i]\),即區間 \([i,i]\) 的最大值
在進行遞推時,分析如何得到 \(s[i][j]\)
我們考慮不斷倍增區間的長度
發現可以將長度為 \(2^i\) 的區間對半分開,求這兩個 區間最大值 的最大值
剛好子區間的最大值已經是被計算過的了
則得出遞推公式 \(s[j][i] = Max(s[j-1][i], s[j-1][i + (1 << (j-1))])\)
對於詢問區間 \([l,r]\) 的最大值,可以通過如下方法計算:
用兩個區間來將 \([l,r]\) 覆蓋
我們可以計算一個值 \(k\),滿足 \(2^k\leq r-l+1<2^{k+1}\)
則會有,以 \(l\) 起始的 \(2^k\) 個數和 以 \(r\) 結尾的 \(2^k\) 個數 覆蓋整個區間 \([l,r]\)
這兩段區間的最大值為 \(s[k][l]\) 和 \(s[k][r - (1<<k) + 1]\)
這兩個數取最大值即為詢問的答案
#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
typedef long long LL;
const int N = 1e6 + 5;
int read() {
int x = 0, f = 1; char ch;
while(! isdigit(ch = getchar())) (ch == '-') && (f = -f);
for(x = ch^48; isdigit(ch = getchar()); x = (x<<3) + (x<<1) + (ch^48));
return x * f;
}
template <class T> T Max(T a, T b) { return a > b ? a : b; }
template <class T> T Min(T a, T b) { return a < b ? a : b; }
LL ans;
int T, n, m, a[N], s[20][N];
inline void ST() {
for(int i = 1; i <= n; ++ i) s[0][i] = a[i];
for(int j = 1; j <= 18; ++ j) {
for(int i = 1; i + (1<<j) - 1 <= n; ++ i) {
s[j][i] = Max(s[j-1][i], s[j-1][i + (1 << (j-1))]);
}
}
}
inline int query(int l, int r) {
int k = log((double)r - l + 1) / log(2.0);
return Max(s[k][l], s[k][r - (1<<k) + 1]);
}
int main() {
n = read(); m = read();
for(int i = 1; i <= n; ++ i) a[i] = read();
ST();
for(int i = 1, l, r; i <= m; ++ i) {
l = read(); r = read();
printf("%d\n", query(l, r));
}
return 0;
}
時間復雜度:預處理\(O(n*log\ n)\),單次詢問\(O(1)\)
擴展歐幾里得算法
算法描述
用於求解關於 \(x,y\) 的方程 \(ax+by = gcd(a, b)\) 的整數解
方程 \(ax + by = m\) 有解的必要條件是 \(m\ mod\ gcd(a, b) = 0\)
證明:
由已知條件易得: \(a\ mod\ gcd(a, b) = 0,b\ mod\ gcd(a, b) = 0\)
則有 \((ax + by)\ mod\ gcd(a, b) = 0\)
即為 \(m\ mod\ gcd(a, b) = 0\)
前置知識:歐幾里得算法(輾轉相除法)
歐幾里得算法用於求解兩個數的最大公因數
int Gcd(int x, int y) { return y ? Gcd(y, x % y) : x; }
若我們已經知道以下的式子
則可以得出
則有
當 \(b = 0\) 時 \(ax = a\)
此時 \(y\) 最好取 \(0\),因為在回溯時,\(y\) 的增長較快,容易數值越界
int Ex_gcd(int a, int b, int &x, int &y) {
if(b == 0) return x = 1, y = 0, a;
int ans = Ex_gcd(b, a % b, x, y);
int tmp = x;
x = y; y = tmp - a / b * y;
return ans;
}
這樣能夠找到方程 \(ax+by=gcd(a, b)\) 的一組解
若要求解 \(x\) 為最小正整數的一組解,可由以下公式推導
則 \(x = (x \% b + b) \% b\)
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e6 + 5;
typedef long long LL;
int read() {
int x = 0, f = 1; char ch;
while(! isdigit(ch = getchar())) (ch == '-') && (f = -f);
for(x = ch ^ 48; isdigit(ch = getchar()); x = (x << 3) + (x << 1) + (ch ^ 48));
return x * f;
}
template <class T> T Max(T a, T b) { return a > b ? a : b; }
template <class T> T Min(T a, T b) { return a < b ? a : b; }
int Ex_gcd(int a, int b, int &x, int &y) {
if(b == 0) return x = 1, y = 0, a;
int ans = Ex_gcd(b, a % b, x, y);
int tmp = x;
x = y; y = tmp - a / b * y;
return ans;
}
int main() {
int a = read(), b = read(), x, y;
Ex_gcd(a, b, x, y);
x = (x % b + b) % b;
printf("%d\n", x);
return 0;
}