君君演算法課堂-基礎演算法
君君演算法課堂
本節《君君演算法課堂》主要對於基本演算法進行講解
這些演算法雖然簡潔易懂,但卻是我們理解更加高深演算法的有力工具
我們也能在其中發現演算法世界的樂趣,培養我們對於演算法的興趣
下面我們話不多說,開啟本節《君君演算法課堂》
目錄位運算
位運算是對二進位制下每一位進行運算後得到一個新的二進位制數的運算
算數位運算子號
\(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\) 填充,高位越界後捨棄
\[1<<n=2^n,n<<1=2n \]算數右移:二進位制補碼意義下使數碼同時向右移動,高位以符號位填充,低位越界後捨棄
\[n>>1=\lfloor\frac{n}{2.0}\rfloor \]算數右移等於 除以 \(2\) 向下取整,\(-3>>1=-2,3>>1=1\)
但整數除以 \(2\) 在 \(C++\) 中運算為 除以 \(2\) 向零取整,\((-3)/2=-1,3/2=1\)
邏輯右移:二進位制補碼意義下使數碼同時向右移動,高位以 \(0\) 填充,低位越界後捨棄
注意:\(C++\)中對右移的實現沒有規定,由編譯器決定使用算數右移或邏輯右移
一般編譯器使用算數右移,我們預設右移操作採用算數右移的方式來實現
快速冪演算法
快速冪用於快速計算 \(x^y\%mod\)
計算的思路是:將十進位制的 \(y\) 看作二進位制,再通過二進位制下的一些運算統計答案
以下已預設為二進位制下的情況
過程:
對於求 \(x^y\)
所以 \(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]\)為其對應的字首和陣列
\[s[i]=\displaystyle\sum_{j=1}^{i}a[i] \]那麼對於求區間和的操作
可以進行相應的轉化
\[sum(l, r)=\displaystyle \sum_{i=l}^{r}a[i]=s[r]-s[l - 1] \]時間複雜度:預處理\(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]=\displaystyle\sum_{x=1}^{i}\displaystyle\sum_{y=1}^{j}a[x][y] \]可以推出 \(s[i][j]\) 的遞推式
\[s[i][j]=s[i-1][j]+s[i][j-1]-s[i-1][j-1]+a[i][j] \]那麼我們可以按如下方法(思想:容斥原理)求出矩陣所存變數的和
\[\displaystyle\sum_{i=x_1}^{x_2}\displaystyle\sum_{j=y_1}^{y_2}a[i][j]=s[x_2][y_2]-s[x_2][y_1-1]-s[x_1][y_2-1]+s[x_1-1][y_1-1] \]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]\)
\[b[1]=a[1],b[i]=a[i]-a[i-1](2\leq i\leq n) \]對於區間加法,假設對陣列 \(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; }
\[ax_1 + by_1 = gcd(a, b)
\]若我們已經知道以下的式子
\[bx_2 + (a\%b)y_2 = gcd(b, a \% b) \]則可以得出
\[ax_1 + by_1 = bx_2 + (a\%b)y_2 \]\[ax_1 + by_1 = bx_2 + (a - a / b * b)y_2 \]\[ax_1 + by_1 = ay_2 + b(x_2 - a/b*y_2) \]則有
\[x_1 = y_2,\ y_1 = x_2 - a / b * y_2 \]當 \(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\) 為最小正整數的一組解,可由以下公式推導
\[ax + by = 1 \]\[ax + by + k * ba - k * ba = 1 \]\[a(x + k*b) + b(y - k * a) = 1 \]則 \(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;
}