君君算法课堂
本节《君君算法课堂》主要对于基础算法进行讲解
这些算法虽然简洁易懂,但却是我们理解更加高深算法的有力工具
我们也能在其中发现算法世界的乐趣,培养我们对于算法的兴趣
下面我们话不多说,开启本节《君君算法课堂》
位运算
位运算是对二进制下每一位进行运算后得到一个新的二进制数的运算
算数位运算符号
\(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;
}