ZHX 清北数学 Day 1 笔记
ZHX 清北数学 Day 1 笔记
一、模运算
除法的一些小性质
首先,我们看一下小学的除法:
$ a \div b = c \cdots \cdots d $
这里有一个显然的性质:
$ a = b \times c + d $
大小关系:$ b > d \ge 0 $
模运算的性质
$ (a + b) \bmod c = (a \bmod c + b \bmod c) \bmod c $
证明:
这里我们设:$ a = xc + a', b = yc + b' $
那么,$ (a + b) \bmod c = (xc + a' + yc + b') \bmod c $
我们把这个可以整除的部分提出去,$ (a + b) \bmod c = (a' + b') \bmod c = (a \bmod c + b \bmod c) \bmod c $
因此,得证
同样的,我们可以得出:
$ (a - b) \bmod c = (a \bmod c - b \bmod c) \bmod c $
$ (a \times b) \bmod c = \lbrack (a \bmod c) \times (b \bmod c) \bmod c \rbrack $
除法的话可以代个数进去试试,会出问题。
例题
求 $ (a + b) \bmod c $, $ (a - b) \bmod c $, $ (a \times b) \bmod c $
比较简单,代码如下:
# include <bits/stdc++.h>
using namespace std;
int a, b, p;
signed main () {
cin >> a >> b >> p;
cout << (a % p + b) % p << endl;
cout << (a % p - b) % p << endl;
cout << (a % p * b) % p << endl;
return 0;
}
负数取模
这里看一下 C++ 如何处理负数除法:
$ -3 \bmod 2 = -1 $
C++ 是忽略负号,然后计算,最后加上负号,这样的话会出问题。所以我们可以加上 模数,再次取模,即:
$ (-3 \bmod 2 + 2) \bmod 2 $
写出公式:$ (a \bmod b + b) \bmod b $
补充·int 转 long long
这里有两种方式:
-
(long long) a * b
-
1ll * a * b
例题
求 $ n ! \bmod p $。
比较简单,代码如下:
# include <bits/stdc++.h>
using namespace std;
signed main () {
int n, p, m;
cin >> n >> p;
int ans = 1;
for (register int i = 1; i <= p; ++ i) {
ans *= n;
ans %= m;
}
cout << ans % m << endl;
return 0;
}
二、gcd 和 lcm
这个地方也比较简单。主要讲证明。
定义
gcd:最大公因数,即两个数所有因数中,公共的因数里面最大的那一个
lcm:最小公倍数,即两个数所有倍数中,公共的倍数里面最小的那一个
证明辗转相除法
求 $ gcd (a, b) $,设为 $ g $
设 $ a = a'g $, $ b = b'g $
那么很显然,$ gcd (a', b') = 1 $,即 $ a' $ 和 $ b' $ 互质
接下来证明,$ gcd (a, b) = gcd (a - b, b) $
这里我们知道 $ gcd (a, b) = g $,所以我们只需要证明 $ gcd (a - b, b) = g $
然后 $ gcd (a'g - b'g, b'g) = g $
得证
同样的方式,我们可以发现 $ gcd (a - 2b, b) = g $, $ gcd (a - 3b, b) \dots $
所以化简 $ gcd (a, b) = gcd (a \bmod b, b) $
然后辗转相除即可。
代码如下:
inline int gcd (int x, int y) {
return y ? gcd (y, x % y) : x;
}
gcd 的复杂度
首先 $ gcd (a, b) = gcd (a \bmod b, b) (a \ge b) $
然后推出 $ a \bmod b < a $
如果 $ a \bmod b < \frac{1}{2}a $,那么复杂度显然是 $ log $ 的
考虑怎么证明
分类讨论:
- $ b \le \frac{1}{2}a $
推出 $ a \bmod b < b \le \frac{1}{2}a $
- $ \frac{1}{2}a < b < a $
推出 $ a \bmod b = a - b \times 1 $ (被除数 - 除数 × 商 = 余数)
所以 $ a \bmod b = a - b < \frac{1}{2}a $
所以得证
最小公倍数
有一个十分有趣的定理,很神奇,大家可以自己证明,比较简单:
$ x \times y = gcd (x, y) \times lcm (x, y) $
处理一下溢出,所以我们可以得到代码:
inline int lcm (int x, int y) {
return x / gcd (x, y) * y;
}
三、快速幂
这里我们讲一下简单的版本,首先举个例子,我们求 $ x^{37} $
然后我们发现,按照普通的乘法,我们需要做 $ 37 $ 次乘法,但是我们考虑化简
$ x^{37} = x^{18} \times x^{18} \times x $,按照这种做法,我们可以只需要做 $ 7 $ 次乘法
是不是很不错,下面是规则:
这样的话就快了很多,然后我们看一下代码:(加上取模)
inline int qpow (int x, int y, int z) {
if (y == 0) return 1;
if (y == 1) return x;
int tmp = qpow (x, y / 2, z);
if (y & 1) return tmp * tmp % z * x % z;
else return tmp * tmp % z;
}
例题
求 $ x \times y \bmod p $
追求快速的做法,我们考虑使用和 快速幂一样的思路,把幂运算改成乘法运算,然后就是一样的了,代码如下:
inline int qtimes (int x, int y, int z) {
if (y == 0) return 0;
if (y == 1) return x;
int tmp = qtimes (x, y / 2, z);
if (y & 1) return (tmp + tmp) % z * x % z;
else return (tmp + tmp) % z;
}
四、矩阵
定义
矩阵的话就是一组 $ n \times m $ 的数组,比较好理解
例如这是一个 $ 2 \times 3 $ 的矩阵:
矩阵加减法
矩阵加减法就是对应位置相加减,即:
矩阵数乘
用一个数去乘这个矩阵里面所有的数,即:
矩阵乘法
这个也比较简单,我们用两个矩阵来相乘,只有 $ n \times m $ 的矩阵和 $ m \times k $ 的矩阵才可以相乘。
比如:
运算法则是答案矩阵是 $ n \times k $ 的,对应答案矩阵中的第 $ l $ 行第 $ r $ 列的数等于第一个矩阵中的第 $ l $ 行,第二个矩阵中的第 $ r $ 列挨个相乘,即第一个乘第一个,第二个乘第二个。
接下来是代码时间:
struct Matrix {
int n, m;
int a[105][105];
Matrix () {
n = m = 0;
memset (a, 0, sizeof (a));
}
} ;
Matrix operator * (const Matrix &a, const Matrix &b) {
Matrix ans;
int x = a.n, y = b.m;
for (register int i = 1; i <= x; ++ i)
for (register int j = 1; j <= y; ++ j)
for (register int k = 1; k <= a.m; ++ k)
ans.a[i][j] += a.a[i][k] * b.a[k][j];
return ans;
}
这里我们交换 for 循环的顺序卡常,我们会发现,枚举顺序是 ikj 的时候运行速度最快, 所以代码如下:
struct Matrix {
int n, m;
int a[105][105];
Matrix () {
n = m = 0;
memset (a, 0, sizeof (a));
}
} ;
Matrix operator * (const Matrix &a, const Matrix &b) {
Matrix ans;
int x = a.n, y = b.m;
for (register int i = 1; i <= x; ++ i)
for (register int k = 1; k <= a.m; ++ k)
for (register int j = 1; j <= y; ++ j)
ans.a[i][j] += a.a[i][k] * b.a[k][j];
return ans;
}
矩阵快速幂
这个只需要把快速幂里的 int 类型改成 Matrix 类型就行
代码:
struct Matrix {
int n, m;
int a[105][105];
Matrix () {
n = m = 0;
memset (a, 0, sizeof (a));
}
} ;
Matrix operator * (const Matrix &a, const Matrix &b) {
Matrix ans;
int x = a.n, y = b.m;
for (register int i = 1; i <= x; ++ i)
for (register int k = 1; k <= a.m; ++ k)
for (register int j = 1; j <= y; ++ j)
ans.a[i][j] += a.a[i][k] * b.a[k][j];
return ans;
}
inline Matrix qpow (Matrix x, int y) {
if (y == 1) return x;
Matrix tmp = qpow (x, y / 2);
if (y & 1) return tmp * tmp * x;
else return tmp * tmp;
}
矩阵快速幂优化递推
P1962
考虑暴力会 TLE,我们使用矩阵快速幂优化
这里我们可以推出:
所以:
然后矩阵快速幂就行了
代码:
# include <bits/stdc++.h>
# define int long long
using namespace std;
int n;
int mod = 1000000000 + 7;
struct Matrix {
int a[15][15];
Matrix () { for (int i = 1; i <= 2; ++ i) for (int j = 1; j <= 2; ++ j) a[i][j] = 0; }
Matrix operator * (Matrix y) {
Matrix res;
for (int i = 1; i <= 2; ++ i) for (int j = 1; j <= 2; ++ j) for (int k = 1; k <= 2; ++ k) res.a[i][j] = (res.a[i][j] + a[i][k] * y.a[k][j] % mod) % mod;
return res;
}
} ;
inline int syzakioi (int n) {
Matrix res, base;
base.a[1][1] = base.a[1][2] = base.a[2][1] = 1;
res.a[1][1] = res.a[1][2] = 1;
while (n) {
if (n & 1) res = res * base;
base = base * base;
n >>= 1;
}
return res.a[1][1];
}
signed main () {
int n;
cin >> n;
if (n <= 2) { puts ("1"); return 0; }
printf ("%d", syzakioi (n - 2));
return 0;
}
P1939
使用同样的方式推出矩阵,然后解决,是一样的
最终的矩阵是:
代码:
# include <bits/stdc++.h>
# define int long long
using namespace std;
int n;
int mod = 1000000000 + 7;
struct Matrix {
int a[15][15];
Matrix () { for (int i = 1; i <= 3; ++ i) for (int j = 1; j <= 3; ++ j) a[i][j] = 0; }
Matrix operator * (Matrix y) {
Matrix res;
for (int i = 1; i <= 3; ++ i) for (int j = 1; j <= 3; ++ j) for (int k = 1; k <= 3; ++ k) res.a[i][j] = (res.a[i][j] + a[i][k] * y.a[k][j] % mod) % mod;
return res;
}
} ;
inline int syzakioi (int n) {
Matrix res, base;
base.a[1][1] = base.a[1][2] = base.a[2][3] = base.a[3][1] = 1;
res.a[1][1] = res.a[1][2] = res.a[1][3] = 1;
while (n) {
if (n & 1) res = res * base;
base = base * base;
n >>= 1;
}
return res.a[1][1];
}
signed main () {
int T;
cin >> T;
while (T --) {
int n;
cin >> n;
if (n <= 3) { puts ("1"); continue; }
printf ("%d\n", syzakioi (n - 3));
}
return 0;
}
口糊题
求 $ 1 $ 到 $ n $ 经过 $ k $ 条边的方案数
首先我们考虑 dp
$ dp_{i,j} $ 表示当前经过 $ i $ 条边,并且在 $ j $ 点上
然后转移是 $ dp_{i,j} = \sum^n_{x = 1}dp_{i - 1,x} \times z_{x,j} $
这里 $ z_{i,j} $ 表示点 $ i $ 和点 $ j $ 之间是否有边
然后添加一维,滚掉 $ i $ 那一维,变成 $ dp_{d,j} = \sum^n_{z = 1}dp_{d,x} \times z_{x,j} $
这不就是矩阵乘法吗?所以我们直接得出:
$ dp_x = dp_{x - 1} \times z $
然后就是:
$ dp_x = dp_0 \times z^{x} $
然后就切了
P4159
我们考虑拆边,比如边权是 $ 3 $,就拆成 $ 3 $ 条边
这样拆出的点太多,会爆空间
然后我们只需要把一个点延伸出的边中把重复的边合并,即可
然后转换成上面的题
这里我们需要注意一下拆边的方式,首先我们记 $ id(i, j) $ 表示点 $ i $ 拆出的第 $ j + 1 $ 个点,由于最多拆出 $ 9 $ 个点,所以我们考虑 $ id(i, j) = (i - 1) \times 9 + j $
然后我们考虑拆成一条链,也就是拆出的第 $ j + 1 $ 个点和第 $ j $ 个点的距离是 $ 1 $,所以我们就考虑初始化 $ F[id(i, j)][id(i, j - 1)] = 1 $
然后我们考虑增加点,如果点 $ i $ 到点 $ j $ 的距离是 $ x $,那么我们考虑连接点 $ i $ 和点 $ j $ 拆出来的第 $ x - 1 $ 个点,这样保证点 $ i $ 到点 $ j $ 的距离是 $ x $
代码:
# include <bits/stdc++.h>
# define mod 2009
using namespace std;
int n, t;
inline int id (int i, int j) {
return (i - 1) * 9 + j;
}
struct Matrix {
int a[105][105];
Matrix () {
memset (a, 0, sizeof (a));
}
inline void set () {
for (int i = 0; i < 9 * n; ++ i) a[i][i] = 1;
}
} F ;
Matrix operator * (const Matrix &a, const Matrix &b) {
Matrix ans;
for (register int i = 0; i < 9 * n; ++ i)
for (register int k = 0; k < 9 * n; ++ k)
for (register int j = 0; j < 9 * n; ++ j)
ans.a[i][j] = (ans.a[i][j] + a.a[i][k] * b.a[k][j] % mod) % mod;
return ans;
}
inline Matrix qpow (Matrix x, int y) {
Matrix ans;
ans.set ();
while (y) {
if (y & 1) ans = ans * x;
x = x * x;
y >>= 1;
}
return ans;
}
signed main () {
cin >> n >> t;
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= 8; ++ j) F.a[id (i, j)][id (i, j - 1)] = 1;
for (int j = 1; j <= n; ++ j) {
int x;
scanf ("%1d", &x);
if (x) F.a[id (i, 0)][id (j, x - 1)] = 1;
}
}
F = qpow (F, t);
cout << F.a[id (1, 0)][id (n, 0)] << endl;
return 0;
}
五、素数
素数就是只有 $ 1 $ 和它本身因数的数,所以可得,$ 0 $ 和 $ 1 $ 不是素数
判定
枚举因数,这里有个神奇的性质,也就是判断的时候只需要枚举到 $ \sqrt{x} $
inline bool Prime_checking (int x) {
if (x == 1 || x == 0) return 0;
for (register int i = 2; i * i <= x; ++ i) if (! (x % i)) return 0;
return 1;
}
这里讲一种错误写法,i <= sqrt (x)
,这样每循环一次都要计算 sqrt(x)
,所以我们可以提前算好
这有些类似 strlen
函数,这个函数是 $ O(n) $ 的
上述代码不需要考虑精度
分解质因数
直接枚举所有因数,如果发现当前数可以被整除,就知道一定是因数,然后就可以了,代码:
int prime[114514], num[114514], cnt = 0;
inline void Factorize (int x) {
for (register int i = 2; i * i <= x; ++ i) {
if (! (x % i)) {
++ cnt;
prime[cnt] = i;
while (! (x % i)) ++ num[cnt], x /= i;
}
}
if (x != 1) {
prime[++ cnt] = x;
num[cnt] = 1;
}
}
CF45G
这个题很简单,考虑三种情况:
-
所有的数之和是质数,只需要全输出 1
-
是大于 $ 4 $ 的偶数,只需要考虑如何分解成两个质数
-
如果是奇数,就考虑如果 $ - 2 $ 是质数,就分解,否则就 $ - 3 $,剩下的一定是偶数,考虑第二种情况
代码:
# include <bits/stdc++.h>
using namespace std;
int ans[114514];
inline bool Prime (int x) {
if (x == 1 || x == 0) return 0;
for (register int i = 2; i * i <= x; ++ i) if (! (x % i)) return 0;
return 1;
}
signed main () {
int m;
cin >> m;
int n = (m + 1) * m / 2;
if (Prime (n)) {
for (register int i = 1; i <= m; ++ i) cout << 1 << " ";
cout << endl;
return 0;
}
for (register int i = 1; i <= m; ++ i) ans[i] = 1;
if (n & 1 && ! Prime (n - 2)) ans[3] = 3, n -= 3;
for (register int i = 2; i <= m; ++ i) {
if (Prime (i) && Prime (n - i)) {
ans[i] = 2;
break;
}
}
for (register int i = 1; i <= m; ++ i) cout << ans[i] << " ";
cout << endl;
return 0;
}