一、GCD/LCM
欧几里得算法
gcd的性质
1.互质素因数同倍数下的gcd
我们都知道,gcd能找出两个数a和b的最大公因数,但我们是否思考过,将a和b除以他们的最大公因数能得到什么?
答案很简单,设最大公因数为k:
gcd(a,b) / k = 1是必然的
而gcd(a,b) / k = 1实际上可以等价于gcd(a/k,b/k) = 1
那我们能够得到的,正好是a和b互质情况下最小的值
再反过来想一想,我们是否就能用这个想法得到一个结论
gcd(ak,bk) 只要a和b互质,那么gcd的值永远不会变化
本来对于这种东西是完全没有思考的,知道我做到了下面这个题目
HDU - 5584 LCM Walk(数论 + GCD + LCM)
题解,来自大佬
①我们可以发现当前位置是(x,y)时,如果x>y,那么当前位置一定是由(x1,y)走到的,如果x<y,当前位置一定是由(x,y1)走到的,由这点确定了路径的唯一性
②设gcd(x,y)=k,x=nk,y=mk,lcm(x,y)=nmk,那么下一步能走到(n(m+1)k,mk)或者(nk,m(n+1)k),并且由于n(m+1)与m互质,n与m(n+1)互质,所以下一步的gcd依然是k.
③根据以上两点,就可以用逆推找出答案,每次都假设x>y,x=nk,y=mk,当前点就是由(x/(y/k+1),y)走到的,如果x不再是(y+k)的倍数(即:(y/k+1)*k的倍数),则表示不能再逆推
摘自blog:https://blog.csdn.net/qq_31759205/article/details/52628889
下面是代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll gcd(ll a,ll b){
return !b ?a :gcd(b,a%b);
}
int main()
{
ll t,cnt = 0;
cin >> t;
while(t--){
ll x,y,ans = 1;
cin >> x >> y;
ll g = gcd(x,y);
if(x > y) swap(x,y);
while(!(y % (x + g))){
ans++;
x = x;
y = y / (g + x)*g;
if(x > y) swap(x,y);
}
printf("Case #%lld: %lld\n",++cnt,ans);
}
return 0;
}
扩展欧几里得
简单来说,扩欧就是用来找二元一次方程的特解,然后根据题目来确认是否找通解之类的
设方程ax+by = n gcd(a,b) = k;
a = ka',n = kb' ax + by = k(a'x + b'y) = n
如果所有数都为整数,那么n必须为gcd的倍数才有解
很多时候,为了简化问题,判断完是否有解后,直接ax+by = gcd(a,b)
算出特解后整数倍扩大即可
提示:root也是结点,只是没有父结点的结点
二、数学知识 组合数学
同余与逆
同余
整数a、b、m,满足∶a mod m = b mod m 称a、b对m同余,m是同余的模
● 同余符号∶a=b(modm)
● 性质∶ a-b是m的整数倍
例∶23-5是6的整数倍,23和5对模6同余。
逆(inverse)
● 给出a,m,求解ax =1(modm)
● 即ax除以m,余数是1. 称x为a模m的逆
求逆的代码
int mod_inverse(int a,int m)
{
int x, y;
extend_gcd(a,m,x,y);
return(m+x%m)%m; //x可能是负数
}
求逆的另一个方法∶费马小定理。
费马小定理(Fermat’s little theorem)是数论中的一个重要定理,在1636年提出。如果p是一个质数,而整数a不是p的倍数,则有a^(p-1)=1(modp)
素数统计-埃式筛法
求素数数量:埃式筛法
时间复杂度 O(nloglogn) 空间复杂度 O(n)
const int MAXN = 1e7;//定义空间大小,1e7约10M
int prime[MAXN+1];//存放素数,它记录visit【j】=false的项
bool visit[MAXN+1];//true表示被筛掉,不是素数
int Esieve(int n){//埃式筛法,计算【2,n】内的素数
int k=0;//统计素数个数
for(int i=2; i<=n; i++){
if(!visit[i]){
prime[k++]=i;//i是素数,存储到prime【】中
for(int j=i*i;j<=n;j+=i)//i的倍数,都不是素数。
visit[j]=true;//标记为非素数,筛掉
}
}
return k;//返回素数个数
}
杨辉三角
(1+x)n的展开就是杨辉三角 复杂度为O(1)
容斥原理
去重:先不考虑重叠的情况,把所有对象的 数目先计算出来,然后再减去重复计算的数目
母函数(Generating function,又译为生成函数)
普通型母函数
(1+x)(1+x2)(1+x³)
母函数的原理
母函数是一种幂级数,其中每一项的系数 反映了这个序列的信息。
母函数的原理:“把组合问题的加法与幂 级数的乘幂对应起来”。
指数型母函数
卡特兰数 Catalan数
Catalan数是一个数列,它的定义如下:
前几个Catalan数是:1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862, 16796, 58786, 208012, 742900, 2674440, 9694845, 35357670, …Catalan数的增长速度极快。
Catalan数看起来有点奇怪,但是却是许多计数问题的最终形式。观察它的公式,其中有组合计数。
二、计算公式
适用于棋盘问题
适用于二叉树问题
Catalan数 模板
#include<iostream>
#include<algorithm>
#include <cstdio> //功能和C中的stdio.h很接近,但有些许不同
#include <cmath>//<math.h>
#include <cstring>//<string.h>
#include <bits/stdc++.h>//万能头文件
#include <cstdlib>//<stdlib.h>
using namespace std;
typedef long long ll;
typedef __int128_t i128;//128位整数,第40个卡特兰数
i128 c[105] = {1};
ll comb(int n, int k);
ll comb(int n, int k){
ll res = 1;
for(int i = 1; i <= k; i++){
res = res * (n - i + 1)/i;
}
return res;
}
//卡特兰数计算公式
ll catalan0(int n){
return comb(2 * n, n) / ( n + 1);
}
//卡特兰数计算公式1:C(n)=comb(2n, n)-comb(2n, n - 1)
ll catalan1( int n){
if(n == 0){
return 1;
}
return comb(2 * n, n) - comb(2 * n, n - 1);
}
//卡特兰数计算公式2:
//C(n)=C(0)*C(n-1)+C(1)*C(n-2)+.....+C(n-1)*C(0)
ll catalan2(int n){
for(int i = 1; i <= n; i++){
c[i] = 0;
for(int j = 0; j < i; j++){
c[i] += c[j] * c[i - 1 -j];
}
}
return c[n];
}
//输出128位整数x
inline void write(i128 x)
{
if(x < 0){
putchar('-');
x = -x;
}
if( x > 9){
write(x / 10);
}
putchar(x % 10 + '0');
}
int main( )
{
freopen("input.txt", "r", stdin);
freopen("output.txt", "w", stdout);
ios:: sync_with_stdio(false);
cin.tie(0);//加快速度
cout.tie(0);
int n;
while( cin >> n){
cout << catalan0(n) << " " << catalan1(n) <<" " << catalan2(n);
}
}
三、 动态规划 Dynamic Programming
动态规划法设计算法一般分成三个阶段:
(1)分段:将原问题分解为若干个相互重叠的子问题;
(2)分析:分析问题是否满足最优性原理,找出动态规划函数的递推式;
(3)求解:利用递推式自底向上计算,实现动态规划过程。
动态规划法利用问题的最优性原理,以自底向上的方式从子问题的最优解逐步构造出整个问题的最优解
01背包问题模板
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8
//01背包问题
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main( )
{
freopen("input.txt", "r", stdin);
freopen("output.txt", "w", stdout);
ios:: sync_with_stdio(false);
cin.tie(0);//加快速度
cin >> n >> m;
for(int i =1; i <=n; i++){
cin >> v[i] >> w[i];
}
for(int i =1; i <=n; i++){
for(int j = m; j >= v[i]; j--){
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[m] << endl;
return 0;
}
完全背包问题模板
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
10
//完全背包问题
#include<iostream>
#include<algorithm>
using namespace std;
const int N =1010;
int n, m;
int v[N], w[N];
int f[N];
int main( )
{
//freopen("input.txt", "r", stdin);
//freopen("output.txt", "w", stdout);
ios:: sync_with_stdio(false);
cin.tie(0);//加快速度
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> v[i] >> w[i];
}
for(int i = 1; i <= n; i++){
for(int j = v[i]; j <= m; j++){
f[j] = max( f[j], f[j - v[i]] + w[i]);
}
}
cout << f[m] << endl;
return 0;
}
多重背包问题模板
有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤100
0<vi,wi,si≤100
输入样例
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例:
10
//多重背包问题 优化版
#include<iostream>
#include<algorithm>
using namespace std;
const int N =25000, M = 2010;
int n, m;
int v[N], w[N];
int f[N];
int main( )
{
//freopen("input.txt", "r", stdin);
//freopen("output.txt", "w", stdout);
ios:: sync_with_stdio(false);
cin.tie(0);//加快速度
cin >> n >> m;
int cnt = 0;
for(int i = 1; i <= n; i++)
{
int a, b, s;
cin >> a >> b >> s;
int k =1;
while ( k <= s)
{
cnt ++;
v[cnt] = a * k;
w[cnt] = b * k;
s -= k;
k *= 2;
}
if( s > 0)
{
cnt ++;
v[cnt] = a * s;
w[cnt] = b * s;
}
}
n = cnt;
for(int i = 1; i <= n; i ++){
for(int j = m; j >= v[i]; j--){
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[m] << endl;
return 0;
}
分组背包问题模板
有 N 组物品和一个容量是 V 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行有两个整数 N,V,用空格隔开,分别表示物品组数和背包容量。
接下来有 N 组数据:
每组数据第一行有一个整数 Si,表示第 i 个物品组的物品数量;
每组数据接下来有 Si 行,每行有两个整数 vij,wij,用空格隔开,分别表示第 i 个物品组的第 j 个物品的体积和价值;
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤100
0<Si≤100
0<vij,wij≤100
输入样例
3 5
2
1 2
2 4
1
3 4
1
4 5
输出样例:
8
//分组背包问题 优化版
#include<iostream>
#include<algorithm>
using namespace std;
const int N =110;
int n, m;
int v[N][N], w[N][N], s[N];
int f[N];
int main( )
{
freopen("input.txt", "r", stdin);
freopen("output.txt", "w", stdout);
ios:: sync_with_stdio(false);
cin.tie(0);//加快速度
cin >> n >> m;
for(int i = 1; i <= n; i++)
{
cin >> s[i];
for(int j = 0; j < s[i]; j ++){
cin >> v[i][j] >> w[i][j];
}
}
for(int i = 1; i <= n; i ++){
for(int j = m; j >= 0; j--){
for(int k = 0; k < s[i]; k ++){
if( v[i][k] <= j){
f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
}
}
}
}
cout << f[m] << endl;
return 0;
}
例题
青蛙的约会
Description
两只青蛙在网上相识了,它们聊得很开心,于是觉得很有必要见一面。它们很高兴地发现它们住在同一条纬度线上,于是它们约定各自朝西跳,直到碰面为止。可是它们出发之前忘记了一件很重要的事情,既没有问清楚对方的特征,也没有约定见面的具体位置。不过青蛙们都是很乐观的,它们觉得只要一直朝着某个方向跳下去,总能碰到对方的。但是除非这两只青蛙在同一时间跳到同一点上,不然是永远都不可能碰面的。为了帮助这两只乐观的青蛙,你被要求写一个程序来判断这两只青蛙是否能够碰面,会在什么时候碰面。
我们把这两只青蛙分别叫做青蛙A和青蛙B,并且规定纬度线上东经0度处为原点,由东往西为正方向,单位长度1米,这样我们就得到了一条首尾相接的数轴。设青蛙A的出发点坐标是x,青蛙B的出发点坐标是y。青蛙A一次能跳m米,青蛙B一次能跳n米,两只青蛙跳一次所花费的时间相同。纬度线总长L米。现在要你求出它们跳了几次以后才会碰面。
Input
输入只包括一行5个整数x,y,m,n,L,其中x≠y < 2000000000,0 < m、n < 2000000000,0 < L < 2100000000。
Output
输出碰面所需要的跳跃次数,如果永远不可能碰面则输出一行"Impossible"
Sample Input
1 2 3 4 5
Sample Output
4
AC代码
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
typedef long long ll;
ll extend_gcd(ll a,ll b,ll &x1,ll &y1,ll &ans){
if(!b){
x1 = 1;y1 = 0;
return a;
}
ans = extend_gcd(b,a%b,x1,y1,ans);
ll t = x1;
x1 = y1;
y1 = t - a/b*y1;
return ans;
}
int main()
{
ll x,y,m,n,l,x1,y1,ans;
cin >> x >> y >> m >> n >> l;
ll a = x-y,b = n-m;
if(b < 0){
a = -a;
b = -b;
}
ans = extend_gcd(b,l,x1,y1,ans);
if(a % ans != 0) cout << "Impossible" << endl;
else cout << ((x1*(a/ans)) % (l/ans) + (l/ans))%(l/ans) << endl;
return 0;
}
A/B
Description
要求(A/B)%9973,但由于A很大,我们只给出n(n=A%9973)(我们给定的A必能被B整除,且gcd(B,9973) = 1)。
Input
数据的第一行是一个T,表示有T组数据。
每组数据有两个数n(0 <= n < 9973)和B(1 <= B <= 10^9)。
Output
对应每组数据输出(A/B)%9973。
Sample Input
2
1000 53
87 123456789
Sample Output
7922
6060
AC代码
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
typedef long long ll;
const ll mod = 9973;
void extend_gcd(ll a,ll b,ll &x,ll &y){
if(!b){
x = 1;y = 0;
return;
}
extend_gcd(b,a%b,x,y);
ll temp = x;
x = y;
y = temp - (a/b)*y;
return;
}
int main()
{
ll t;
cin >> t;
while(t--){
ll n,b,x,y;
cin >> n >> b;
extend_gcd(b,mod,x,y);
x = (x + mod) % mod;
x = (x*n) % mod;
cout << x << endl;
}
return 0;
}
小兔子的棋盘
Description
小兔的叔叔从外面旅游回来给她带来了一个礼物,小兔高兴地跑回自己的房间,拆开一看是一个棋盘,小兔有所失望。不过没过几天发现了棋盘的好玩之处。从起点(0,0)走到终点(n,n)的最短路径数是C(2n,n),现在小兔又想如果不穿越对角线(但可接触对角线上的格点),这样的路径数有多少?小兔想了很长时间都没想出来,现在想请你帮助小兔解决这个问题,对于你来说应该不难吧!
Input
每次输入一个数n(1<=n<=35),当n等于-1时结束输入。
Output
对于每个输入数据输出路径数,具体格式看Sample。
Sample Input
1
3
12
-1
Sample Output
1 1 2
2 3 10
3 12 416024
AC代码02
#include<iostream>
using namespace std;
__int64 a[36];
int main()
{
//freopen("input.txt", "r", stdin);
//freopen("output.txt", "w", stdout);
ios:: sync_with_stdio(false);
cin.tie(0);//加快速度
cout.tie(0);
int i,j,n;
a[0]=1;
for(i=1;i<=35;i++)
{
a[i]=0;
for(j=0;j<=i;j++)
a[i]+=a[j]*a[i-j-1];
}
i=1;
while(cin>>n&&n!=-1)
{
printf("%d %d %i64d\n",i++,n,a[n]*2);
}
return 0;
}
总结
平生最恨蹉跎
Input
数据的第一行是一个T,表示有T组数据。
每组数据有两个数n(0 <= n < 9973)和B(1 <= B <= 10^9)。
Output
对应每组数据输出(A/B)%9973。
Sample Input
2
1000 53
87 123456789