行亦谦ACM自闭之旅第五周


一、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

Sample Output

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值