ACM - 数学 - 基础(数论 / 高精度 / 组合 / 博弈论 / 其它)_7+47++#++[++///++/++7*##*%-:-程序员宅基地

技术标签: 数学  知识点  acm竞赛  

ACM 数学

一、数论

1、素数

线性筛模板

int primes[N], cnt;     // primes[]存储所有素数
bool st[N];         // st[x]存储x是否被筛掉

void get_primes(int n)
{
    
    for (int i = 2; i <= n; i ++ )
    {
    
        if (!st[i]) primes[cnt ++ ] = i;
        for (int j = 0; primes[j] <= n / i; j ++ )
        {
    
            st[primes[j] * i] = true;
            if (i % primes[j] == 0) break;
        }
    }
}

例题1、区间筛素数(线性筛+埃氏筛) :POJ 2689 Prime Distance

原题链接:http://poj.org/problem?id=2689
在这里插入图片描述
在这里插入图片描述

题目大意
对于任意询问的区间【l,r】,计算出两个相邻素数之间的最短和最长距离。
如果有距离相等的,就输出素数之和最小的。
(例如 2 和 3 之间没有其他素数,则称之为两个相邻素数,且此时距离最短)

思路
因为 l 和 r 的范围很大,所以也不可能把primes和st开很大,但是无论如何最终都是要落实到用素数把合数筛掉上。
题目上 r 的上限是 21 亿多,那么当我们求出 50000 以内的所有素数的时候,假如一个在【l,r】上的数是合数,那么它的质因子一定至少有一个落在【2,50000】上。

换句话说,这道题就是筛两次。
第一次用线性筛筛出【2,50000】之间的素数,第二次用记录在primes的素数数组把【l,r】上的合数筛掉()。
第二次筛具体来说,就是从头开始遍历primes数组,找到【l,r】区间上,第一个能被primes [ i ] 整除的数,然后往下筛。

需要注意的是,如果输入的 l 是 1,那么需要把 l ++,否则会把 1 当成第一个素数;
因为在第二次筛的时候,是用 0 号索引表示【l,r】区间上的第一个数,所以要注意在找到primes [ i ] 第一个能筛的数的时候,会不会这个数是 primes [ i ] 本身。

代码

#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>

using namespace std;

#define getlen(array) {
      return (sizeof(array) / sizeof(array[0]));}
#define ll long long 
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rit int t; scanf("%d", &t)
#define sc scanf
#define pr printf

const int INF = 0x3f3f3f3f;
const int N = 50000; 
const int NN = 1e6 + 20;

struct mynode {
      //表示最终答案的左区间和右区间 
	ll l, r;
};

int primes[N], cnt;
bool st[N];

bool he_nums[NN];  //第二次筛--he_nums[i] == true 表示数 l + i 是合数 


//线性筛--获得50000范围内的所有素数 
void make_primes() {
    
	for (int i = 2; i < N; ++ i) {
    
		if (!st[i]) primes[cnt ++] = i;
		for (int j = 0; primes[j] <= N / i; ++ j) {
    
			st[primes[j] * i] = true;
			if (i % primes[j] == 0) break;
		}
	}
}

int main() {
    
//	freopen("D:\\in.txt", "r", stdin); 
	//freopen("D:\\out.txt", "w", stdout);
	ll l, r;
	make_primes();
	
	while (sc("%lld%lld", &l, &r) != -1 ) {
    
		if (l == 1) ++ l;  // 特判,否则 l、r = [1,100]时,会把 1 当成素数 
		MEM(he_nums, false);  //重置
		 
		mynode minn = {
    0, 1e9}, maxx = {
    0, 0}; //初始化最终结果 
		ll length = r - l + 1;  //[l,r]区间上有length个数 
		for (int i = 0; i < cnt; ++ i) {
    
			int yu = l % primes[i];
			ll num;  //num表示索引从哪里开始 
			if (yu == 0) num = 0;
			else num = primes[i] - yu;
			//如果num+l在询问的区间范围内,需要跳过一个数,因为此时num+l当成素数 
			if (num + l == primes[i]) num += primes[i];  
			//二次筛素数 
			while (num < length) {
    
				he_nums[num] = true;
				num += primes[i];
			}
			
		}
		
		//遍历he_nums,获得最小最大素数距离
		//[last,i]表示最新获得的两个相邻素数 
		int last = -9;
		for (ll i = 0; i < length; ++ i) {
    
			if (!he_nums[i]) {
    
				if (last < 0) last = i;
				else {
    
					if (i - last < minn.r - minn.l) {
    
						minn.l = last;
						minn.r = i;
					}
					if (i - last > maxx.r - maxx.l) {
    
						maxx.l = last;
						maxx.r = i;
					}
					last = i;   //记得更新 
				}
			}
		}
		
		//输出 
		if (maxx.r == 0) {
    
			pr("There are no adjacent primes.\n");
		}
		else {
    
			pr("%lld,%lld are closest, %lld,%lld are most distant.\n", minn.l + l, minn.r + l, maxx.l + l, maxx.r + l);
		}
	}
	return 0;
}

例题2、前缀和 + 线性筛 :HDU 4548 美素数

原题链接:http://acm.hdu.edu.cn/showproblem.php?pid=4548
在这里插入图片描述
在这里插入图片描述

#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>

using namespace std;

#define getlen(array) {
      return (sizeof(array) / sizeof(array[0]));}
#define ll long long 
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rit int t; scanf("%d", &t)
#define sc scanf
#define pr printf

const int INF = 0x3f3f3f3f;
const int N = 1000010; 

//(val & 1) == 0偶, == 1奇。

int primes[N], cnt;
bool st[N], mei[N];
int ans[N];

//线性筛
void make_primes() {
    
	for (int i = 2; i < N; ++ i) {
    
		if (! st[i]) primes[cnt ++] = i;
		for (int j = 0; primes[j] <= N / i; ++ j) {
    
			st[primes[j] * i] = true;
			if (i % primes[j] == 0) break;
		}
	}
}

//判断一个素数是不是美数:各位之和是不是素数
void judge_mei() {
    
	for (int i = 0; i < N; ++ i) {
    
		if (st[i]) mei[i] = true;
	}
	for (int i = 0; i < cnt; ++ i) {
    
		int num = 0, a = primes[i];
		while(a > 0) {
    
			num += a % 10;
			a /= 10;
		}
		if (st[num]) mei[primes[i]] = true;
	} 
}

//前缀和求美数之和
void make_ans() {
    
	for (int i = 2; i < N; ++ i) {
    
		ans[i] = ans[i - 1];
		if (! mei[i]) ++ ans[i];
	}
}

int main() {
    
	//freopen("D:\\in.txt", "r", stdin); 
	//freopen("D:\\out.txt", "w", stdout);
	rit;
	make_primes();
	judge_mei();
	make_ans();
	int l, r;
	for (int i = 1; i <= t; ++ i) {
    
		sc("%d %d", &l, &r);
		pr("Case #%d: %d\n", i, ans[r] - ans[l - 1]);
	}
	return 0;
}

例题3、区间分解质因数 + 二分:HDU 6287 口算训练

原题链接:http://acm.hdu.edu.cn/showproblem.php?pid=6287

在这里插入图片描述
在这里插入图片描述
思路
观察数据范围,发现数的大小和数的数量都在 10 ^ 5 之内,由于后面还有 m 组区间查询,所以我们需要先预处理那 n 个数,得到 n 个数的所有质因子都在哪些数出现过,出现过几次, 再通过二分区间查找 d 的每一个质因子的所在区间的上限和下限,判断 下限 - 上限 是否大于等于该质因子的数量。

代码实现方面:开一个vector数组book(动态二维数组,防止爆内存),存下 n 个数的所有质因子的信息,比如说对于题目给的样例
1
5 4
6 4 7 2 5
1 2 24
1 3 18
2 5 17
3 5 35

我们对于 6 4 7 2 5 这 5 个数处理如下:
book [ 1 ] :
book [ 2 ] :1、2、2、4
book [ 3 ] :1
book [ 4 ] :
book [ 5 ] :5
book [ 6 ] :
book [ 7 ] :3
book [ 8 ] :

对于book [ 2 ] :1、2、2、4所表达的是:这 n 个数里面,第 1、2、4个数有质因子 2,且第 2 个数有两个。
同理,book [ 7 ] :3 所表达的是 n 个数里面,第 3 个数有质因子 7 ,其他以此类推。

当获得 book 后,我们对于每一组 l r d 的查询,先分解出 d 的质因子,然后对 book [质因子] 二分查 l 的下限、r 的上限,由此判断book里面有没有足够的质因子和 d 对应。

综上。

代码

#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>

using namespace std;

#define getlen(array) {
      return (sizeof(array) / sizeof(array[0]));}
#define ll long long 
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rit int t; scanf("%d", &t)
#define sc scanf
#define pr printf

const int INF = 0x3f3f3f3f;
const int N = 100010; 

//(val & 1) == 0偶, == 1奇。

vector<int> book[N];
int primes[N], cnt;
bool st[N];

void make_primes() {
    
	for (int i = 2; i < N; ++ i) {
    
		if (!st[i]) primes[cnt ++] = i;
		for (int j = 0; primes[j] <= N / i; ++ j) {
    
			st[primes[j] * i] = true;
			if (i % primes[j] == 0) break;
		}
	}
}

int main() {
    
	//freopen("D:\\in.txt", "r", stdin); 
	//freopen("D:\\out.txt", "w", stdout);
	rit;
	make_primes();
	while (t --) {
    
		int n, m;
		sc("%d %d", &n, &m);
		MEM(book, 0);
		int a;
		for (int i = 1; i <= n; ++ i) {
    
			sc("%d", &a);
			int k = 0;
			while (a > 1) {
    
				if (!st[a]) {
    
					book[a].push_back(i);
					break;
				}
				while (a % primes[k] == 0) {
    
					book[primes[k]].push_back(i);
					a /= primes[k];
				}
				++ k;
			}
		}
		int l, r, d;
		while (m --) {
    
			sc("%d%d%d", &l, &r, &d);
			int flag = true;
			int k= 0;
			while (d > 1) {
    
				int cnt = 0;
				if (!st[d]) {
    
					int x = lower_bound(book[d].begin(), book[d].end(), l) - book[d].begin();
					int y = upper_bound(book[d].begin(), book[d].end(), r) - book[d].begin();
					d = 1;
					if (y - x < 1) {
    
						flag = false;
						break;
					}
				}
				while (d % primes[k] == 0) {
    
					++ cnt;
					d /= primes[k];
				}
				int x = lower_bound(book[primes[k]].begin(), book[primes[k]].end(), l) - book[primes[k]].begin();
				int y = upper_bound(book[primes[k]].begin(), book[primes[k]].end(), r) - book[primes[k]].begin();
				if (y - x < cnt) {
    
					flag = false;
					break;
				}
				
				++ k;
			}
			if (flag) pr("Yes\n");
			else pr("No\n");
		}
	}
	return 0;
}

2、约数

求 N 的约数个数和约数之和:
在这里插入图片描述

例题1、约数个数定理:E - 解方程(牛客小白月赛31)

传送门:https://blog.csdn.net/CSDNWudanna/article/details/112727606

3、欧拉函数 / 定理+费马小定理

定理内容

(1)欧拉函数 φ(n):表示从1 到 n 一共有多少个和 n 互质。
(在数论,对正整数n,欧拉函数是小于n的正整数中与n互质的数的数目)

φ(n) = n (1 - 1 / p1) (1 - 1 / p2) (1 - 1 / p3)……(1 - 1 / pk),其中 pi 为 n 的质因子。

//筛法求欧拉函数
int primes[N], cnt;     // primes[]存储所有素数
int euler[N];           // 存储每个数的欧拉函数
bool st[N];         // st[x]存储x是否被筛掉

void get_eulers(int n)
{
    
    euler[1] = 1;
    for (int i = 2; i <= n; i ++ )
    {
    
        if (!st[i])
        {
    
            primes[cnt ++ ] = i;
            euler[i] = i - 1;
        }
        for (int j = 0; primes[j] <= n / i; j ++ )
        {
    
            int t = primes[j] * i;
            st[t] = true;
            if (i % primes[j] == 0)
            {
    
                euler[t] = euler[i] * primes[j];
                break;
            }
            euler[t] = euler[i] * (primes[j] - 1);
        }
    }
}

(2)欧拉定理:若 a 和 n 互质,那么 a ^ φ(n) ≡ 1 (mod n)

(3)费马小定理:若 n 为质数,那么φ(n) = n - 1,此时可得 a ^ (n - 1) ≡ 1 (mod n)。

例题1、隔板原理+求组合数+费马降幂+快速幂:HDU 4704 Sum

原题链接:http://acm.hdu.edu.cn/showproblem.php?pid=4704
在这里插入图片描述
题目大意
给定一个数n ,将其分解,S(k) 表示将 n 拆成 k 个数的方案数,
求 sum( S(k) ) ,其中1 <= k <= n;

思路

① 隔板原理

n 最多可以被拆成 n 个 1, 那么在这些 1 之间,最多有 n - 1 个位置可以放隔板,假如现在在这 n - 1个位置中放 2 个隔板,那么这 n 个 1 会被分成 3 个区间,此时能得到 3 个数,而 S(3) = ( n − 1 2 ) \tbinom{n - 1}{2} (2n1),依次类推可得出,S(1)、S(2)、S(3)……S(n) 分别为 ( n − 1 0 ) \tbinom{n - 1}{0} (0n1) ( n − 1 1 ) \tbinom{n - 1}{1} (1n1) ( n − 1 2 ) \tbinom{n - 1}{2} (2n1)…… ( n − 1 n − 1 ) \tbinom{n - 1}{n - 1} (n1n1)

②求组合数

故而我们可以发现 ans = ( n − 1 0 ) \tbinom{n - 1}{0} (0n1)+ ( n − 1 1 ) \tbinom{n - 1}{1} (1n1)+ ( n − 1 2 ) \tbinom{n - 1}{2} (2n1)+……+ ( n − 1 n − 1 ) \tbinom{n - 1}{n - 1} (n1n1) = 2 ^ (n - 1)
(n - 1 个隔板的位置,每个位置都有放隔板和不放隔板两种情况,所以所有取值总和就是 2 ^ (n - 1) )

③费马小定理降幂
n 的 范围很大,所以需要对 n - 1 降幂才能求 2 ^ (n - 1) 。
我们可以发现,2 和 1e9 + 7 都是质数(满足 p 是质数且 a、p互质),根据费马小定理有 2 ^ (1e9 + 6) ≡ 1(mod 1e9 + 7),而当 n - 1 > 1e9 + 7 时,2 ^ (n - 1) = 2 ^ (1e9 + 6) * 2 ^ (n - 1 - (1e9 + 6)),换句话说,2 ^ (n - 1) = 2 ^ ((n - 1) % (1e9 + 6))。

④快速幂求解
当把 n - 1 降幂后,求 2 的幂次方最高还是可以达到 1e9 + 5,所以不能直接暴力求,需要用快速幂边乘边求模。

代码

#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>

using namespace std;

#define getlen(array) {
      return (sizeof(array) / sizeof(array[0]));}
#define ll long long 
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rit int t; scanf("%d", &t)
#define sc scanf
#define pr printf

const int INF = 0x3f3f3f3f;
const int N = 1000010; 
const int mod = 1e9 + 7;

//(val & 1) == 0偶, == 1奇。
char str[N];

// 快速幂
ll qmi(ll a, ll k) {
    
	ll ans = 1;
	while (k > 0) {
    
		if (k & 1) ans *= a, ans %= mod;
		k >>= 1;
		a *= a;
		a %= mod;
	}
	return ans;
}

int main() {
    
	//freopen("D:\\in.txt", "r", stdin); 
	//freopen("D:\\out.txt", "w", stdout);
	while (cin >> str) {
    
		ll num = 0;
		for (int i = 0; str[i]; ++ i) {
    
			num = num * 10 + str[i] - '0';
			num %= (mod - 1);  //降幂
		}
		pr ("%lld\n", qmi(2, num - 1));
	}
	return 0;
}

4、快速幂、龟速乘、矩阵乘法

(1)快速幂

//求 m^k mod p,时间复杂度 O(logk)。
int qmi(int m, int k, int p)
{
    
    int res = 1 % p, t = m;
    while (k)
    {
    
        if (k&1) res = res * t % p;
        t = t * t % p;
        k >>= 1;
    }
    return res;
}

(2)龟速乘

当快速幂过程中会爆 ll 时,有关于乘法的部分需要用龟速乘解决。

//龟速乘:求(a * b)% p
ll qmul(ll a, ll b, ll p) {
    
    ll res = 0, t = a;
    while (b) {
    
        if (b & 1) res = (res + t) % p;
        b >>= 1;
        t = (t + t) % p;
    }
    return res;
}

//快速幂:求10的k次方mod c
ll qmi(ll a, ll k, ll p) {
    
    ll res = 1, t = a;
    while (k) {
    
        if (k & 1) res = qmul(res, t, p);
        k >>= 1;
        t = qmul(t, t, p);
    }
    return res == 1;
}

(3)矩阵乘法

AcWing 1303. 斐波那契前 n 项和

原题链接:https://www.acwing.com/problem/content/description/1305/
在这里插入图片描述

// 构造乘法矩阵+快速幂思想
#include<iostream>

using namespace std;

#define ll long long

ll n, m;

// f = f * a
void mul(ll f[], ll a[][3]) {
    
    ll temp[] = {
    0, 0, 0};
    for (int i = 0; i < 3; ++ i) {
    
        for (int j = 0; j < 3; ++ j) {
    
            temp[i] += f[j] * a[j][i];
            temp[i] %= m;
        }
    }
    for (int i = 0; i < 3; ++ i) f[i] = temp[i] % m;
}

// a = a * a
void mul(ll a[][3]) {
    
    ll temp[3][3] = {
    {
    0}};
    for (int i = 0; i < 3; ++ i) {
    
        for (int j = 0; j < 3; ++ j) {
    
            for (int k = 0; k < 3; ++ k) {
    
                temp[i][j] += a[i][k] * a[k][j];
                temp[i][j] %= m;
            }
        }
    }
    for (int i = 0; i < 3; ++ i) {
    
        for (int j = 0; j < 3; ++ j){
    
            a[i][j] = temp[i][j] % m;
        }
    }
}

int main() {
    
    cin >> n >> m;
    //构造矩阵,f[] = {fi, fi+1, Si}
    // a的意义是 {fi, fi+1, Si} * a = {fi+1, fi+2, Si+1}
    ll f[] = {
    1, 1, 1}, a[][3] = {
    {
    0, 1, 0}, {
    1, 1, 1}, {
    0, 0, 1}};
    -- n;  //减减是因为 f1 已经计算出来了
    //用快速幂的思想做矩阵乘法
    while (n) {
    
        if (n & 1) mul(f, a);
        mul(a);
        n >>= 1;
    }
    cout << f[2];
    return 0;
}

5、逆元

若 a / b ≡ a * x (mod p) ,则称 x 为 b 在模 p 下的逆元。
若 p 为质数,且 b、p 互质时,由费马小定理可得 x = b ^ (p - 2)。

例题1、逆元 + 快速幂 :HDU 1576 A / B

原题链接:http://acm.hdu.edu.cn/showproblem.php?pid=1576
在这里插入图片描述
思路

(A / B) % 9973 = (A * B-1) % 9973 = A % 9973 * B-1 % 9973 = n * B-1 % 9973。
B-1 = B ^ (9973 - 2) (B和9973互质,且9973是质数,所以用费马小定理可求逆元)
最后再用快速幂求一下结果即可。

代码

#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>

using namespace std;

#define getlen(array) {
      return (sizeof(array) / sizeof(array[0]));}
#define ll long long 
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rit int t; scanf("%d", &t)
#define sc scanf
#define pr printf

const int INF = 0x3f3f3f3f;
const int N = 10000; 
const int mod = 9973;

//(val & 1) == 0偶, == 1奇。

//快速幂
ll qmi(int a, int k) {
    
	ll ans = 1;
	a %= mod;
	while (k > 0) {
    
		if (k & 1) ans *= a, ans %= mod;
		k >>= 1;
		a *= a;
		a %= mod;
	}
	return ans;
}

int main() {
    
	//freopen("D:\\in.txt", "r", stdin); 
	//freopen("D:\\out.txt", "w", stdout);
	rit;
	ll n, b;
	while (t --) {
    
		sc("%lld %lld", &n, &b);
		pr("%lld\n", (n * qmi(b, 9971)) % mod);
	}
	return 0;
}

6、gcd + exgcd

int gcd(int a, int b)
{
    
    return b ? gcd(b, a % b) : a;
}
// 求x, y,使得ax + by = gcd(a, b)
int exgcd(int a, int b, int &x, int &y)
{
    
    if (!b)
    {
    
        x = 1; y = 0;
        return a;
    }
    int d = exgcd(b, a % b, y, x);
    y -= (a/b) * x;
    return d;
}

解线性同余方程

给定 a、b、m,求一个 x ,使得 ax ≡ b (mod m)。
易知,存在一个整数 y 使得 ax = my + b,则有 ax - my = b。
令 y ’ = - y,那么 ax + my ’ = b。
故当 gcd(a,m) | b 时 x 有解。

7、卢卡斯定理

若p是质数,则对于任意整数 1 <= m <= n,有:
C(n, m) = C(n % p, m % p) * C(n / p, m / p) (mod p)

例题1、卢卡斯+组合推论:HDU 5226 Tom and matrix

原题链接:http://acm.hdu.edu.cn/showproblem.php?pid=5226
在这里插入图片描述
思路
首先对于组合数有
( n m ) \tbinom{n}{m} (mn)

= ( n − 1 m − 1 ) \tbinom{n - 1}{m - 1} (m1n1) + ( n − 1 m ) \tbinom{n - 1}{m} (mn1)

= ( n − 1 m − 1 ) \tbinom{n - 1}{m - 1} (m1n1) + ( n − 2 m − 1 ) \tbinom{n - 2}{m - 1} (m1n2) + ( n − 2 m ) \tbinom{n - 2}{m} (mn2)

= ( n − 1 m − 1 ) \tbinom{n - 1}{m - 1} (m1n1) + ( n − 2 m − 1 ) \tbinom{n - 2}{m - 1} (m1n2) + ( n − 3 m − 1 ) \tbinom{n - 3}{m - 1 } (m1n3) + ( n − 3 m ) \tbinom{n - 3}{m} (mn3)

= ……

针对 x1 = 5,x2 = 8, y1 = 2,y2 = 4 这个样例,我们需要求:

( 5 2 ) \tbinom{5}{2} (25) ( 5 3 ) \tbinom{5}{3} (35) ( 5 4 ) \tbinom{5}{4} (45)

( 6 2 ) \tbinom{6}{2} (26) ( 6 3 ) \tbinom{6}{3} (36) ( 6 4 ) \tbinom{6}{4} (46)

( 7 2 ) \tbinom{7}{2} (27) ( 7 3 ) \tbinom{7}{3} (37) ( 7 4 ) \tbinom{7}{4} (47)

( 8 2 ) \tbinom{8}{2} (28) ( 8 3 ) \tbinom{8}{3} (38) ( 8 4 ) \tbinom{8}{4} (48)

依靠上面的推论我们可以得出: ( 9 5 ) \tbinom{9}{5} (59) = ( 8 4 ) \tbinom{8}{4} (48) + ( 7 4 ) \tbinom{7}{4} (47) + ( 6 4 ) \tbinom{6}{4} (46) + ( 5 4 ) \tbinom{5}{4} (45) + ( 5 5 ) \tbinom{5}{5} (55)

那么对于

( 8 4 ) \tbinom{8}{4} (48)

( 7 4 ) \tbinom{7}{4} (47)

( 6 4 ) \tbinom{6}{4} (46)

( 5 4 ) \tbinom{5}{4} (45)

这一列, ( 9 5 ) \tbinom{9}{5} (59) - ( 5 5 ) \tbinom{5}{5} (55) = ( 8 4 ) \tbinom{8}{4} (48) + ( 7 4 ) \tbinom{7}{4} (47) + ( 6 4 ) \tbinom{6}{4} (46) + ( 5 4 ) \tbinom{5}{4} (45)

这样就可以对问题的规模降一个维度,防止tle。

另外这道题需要对 ( n m ) \tbinom{n}{m} (mn)判断一下是否 n >= m。
(题目应该是默认输入的 x1、x2、y1、y2 合法)

代码

#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>

using namespace std;

#define getlen(array) {
      return (sizeof(array) / sizeof(array[0]));}
#define ll long long 
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rit int t; scanf("%d", &t)
#define sc scanf
#define pr printf

const int INF = 0x3f3f3f3f;
const int N = 100010; 

//(val & 1) == 0偶, == 1奇。

int fact[N], infact[N]; //阶乘、阶乘逆元

//快速幂
int qmi(int a, int k, int p) {
    
	int res = 1;
	while (k) {
    
		if (k & 1) res = (ll) res * a % p;
		k >>= 1;
		a = (ll) a * a % p;
	}
	return res;
}

//获得fact和infact数组
void make_arr(int p) {
    
	fact[0] = infact[0] = 1;
	for (int i = 1; i < N; ++ i) {
    
		fact[i] = (ll)fact[i - 1] * i % p;
		infact[i] = (ll)infact[i - 1] * qmi(i, p - 2, p) % p;
	}
}

//求组合数
int C(int i, int j, int p) {
    
	if (i < j) return 0;
	return (ll)fact[i] * infact[j] % p * infact[i - j] % p;
}

//卢卡斯定理
int lucas(int i, int j, int p) {
    
	if (i < p) return C(i, j, p);
	return ((ll)C(i % p, j % p, p) * lucas(i / p, j / p, p)) % p;
}

int main() {
    
	//freopen("D:\\in.txt", "r", stdin); 
	//freopen("D:\\out.txt", "w", stdout);
	
	int x1, y1, x2, y2, p; 
	while (sc("%d%d%d%d%d", &x1, &y1, &x2, &y2, &p) != -1) {
     
		make_arr(p);
		int ans = 0;
		for (int i = y1; i <= y2; ++ i) {
    
			ans = (((ll) ans + lucas(x2 + 1, i + 1, p)) % p - lucas(x1, i + 1, p) + p) % p;
		}
		pr("%d\n", ans);
	}
	return 0;
}

8、高斯消元

线性方程组–模板题

基本上,消元的顺序是:

  1. 将方程的所有系数存成矩阵的形式(二维数组)
  2. 枚举每一列
  3. 对当前列,找出 r ~ n 行中行首绝对值最大的那一行 t ,即找出 fabs(a [ i ] [ c ])
  4. 交换第 t 行和第 r 行,使得每一次枚举, r 行的行首都是绝对值最大(目的是为了提高精度)
  5. 将交换后的第 r 行的行首化为单位1
  6. 将第 r + 1 行到第 n 行的第 c 列全部化为 0
  7. ++ r,++ c 进入下一行下一列
  8. 【另】:假如获得的第 t 行的行首为 0 ,即:a [ t ] [ c ] < eps,那么说明该列全为0,该列不用处理,但是该行还是需要继续消元,所以只需要 ++ c,而不用 ++ r。

获得方程组的解
如果 r <= n,说明秩不为 n,假如出现 0 = !0 的情况,说明方程组无解,反之方程组有无穷多解;
而如果 r == n + 1,即秩为 n ,那么说明恰好有唯一解,此时还需要把每一个方程消成只剩下一项,才能得到最终解。(从倒数第二行开始消起)

例题:AcWing 883 高斯消元解线性方程组
在这里插入图片描述
在这里插入图片描述

#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>

using namespace std;

#define getlen(array) {
      return (sizeof(array) / sizeof(array[0]));}
#define ll long long 
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rit int t; scanf("%d", &t)
#define sc scanf
#define pr printf

const int INF = 0x3f3f3f3f;
const int N = 110;   //本题规模
const double eps = 1e-6;   //极小的数

//(val & 1) == 0偶, == 1奇。

double a[N][N];  //增广矩阵

int solve(int n) {
    
    int r, c;
    //枚举每一列
    for (r = 1, c = 1; c <= n; ++ c) {
    
        int t = r;
        //获得第c列里,哪一行的数的绝对值最大--精度会更高一些
        for (int i = r + 1; i <= n; ++ i) {
    
            if (fabs(a[i][c]) > fabs(a[t][c]))
                t = i;
        }
        if (fabs(a[t][c]) < eps) continue;  //说明当前列全为0
        //交换行
        if (t != r) {
    
            for (int i = c; i <= n + 1; ++ i) {
    
                swap(a[t][i], a[r][i]);
            }
        }
        //首位化为单位1
        for (int i = n + 1; i >= c; -- i) a[r][i] /= a[r][c];
        //把r行下面所有列消为0
        for (int i = r + 1; i <= n; ++ i) {
    
            for (int j = n + 1; j >= c; -- j) {
    
                a[i][j] -= a[i][c] * a[r][j];
            }
        }
        ++ r;
    }
    if (r <= n) {
      //秩不为n
        for (int i = r; i <= n; ++ i)
            if (fabs(a[i][n + 1]) > eps) 
                return 0;  //出现 0 = !0,所以无解
        return 2;   //有无穷多解
    }
    else {
     //有唯一解
        for (int i = n - 1; i >= 1; -- i) {
      //行
            for (int j = n; j > i; -- j) {
       //列
                a[i][n + 1] -= a[i][j] * a[j][n + 1];
            }
        }
        return 1;
    }
}

int main() {
    
	//freopen("D:\\in.txt", "r", stdin); 
	//freopen("D:\\out.txt", "w", stdout);
	rin;
	for (int i = 1; i <= n; ++ i) {
    
	    for (int j = 1; j <= n + 1; ++ j) {
    
	        sc("%lf", &a[i][j]);
	    }
	}
	
	int ans = solve(n);
	if (ans == 0) pr("No solution\n");
	else if (ans == 2) pr("Infinite group solutions\n");
	else {
    
	    for (int i = 1; i <= n; ++ i) {
    
	        pr("%.2lf\n", a[i][n + 1]);
	    }
	}
	
	return 0;
}

异或线性方程组:AcWing 884

传送门:AcWing 884 高斯消元解异或线性方程组
在这里插入图片描述
在这里插入图片描述
思路
假如现在有一个增广矩阵:
1 1 0 1 1
1 0 1 0 1
1 1 0 0 0
1 0 1 0 1

设对应的四个解分别为 a、b、c、d,我们先取出前两行:
1 1 0 1 1
1 0 1 0 1
我们可以发现,其实这两行表达的就是:
a ^ b ^ d = 1
a ^ c = 1
那么如果要消掉第二行的行首,我们只需要将第一行所有系数为 1 的数来异或第二行所有系数为 1 的数(当然同时最右边的常数也需要异或一下),即:
a ^ b ^ d ^ a ^ c = c ^ b ^ d = 1 ^ 1 = 0,故而这两行可以变换为:
1 1 0 1 1
0 1 1 1 0

再像解一般的线性方程组那样,逐行逐列地搞一遍,就可以得到这个矩阵的秩和最终的解。

#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>

using namespace std;

#define getlen(array) {
      return (sizeof(array) / sizeof(array[0]));}
#define ll long long 
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rit int t; scanf("%d", &t)
#define sc scanf
#define pr printf

const int INF = 0x3f3f3f3f;
const int N = 110;


//(val & 1) == 0偶, == 1奇。
int a[N][N];

int solve(int n) {
    
    int c,r;
    for (r = 1, c = 1; c <= n; ++ c) {
    
        int t = r;
        for (int i = t + 1; i <= n; ++ i) {
    
            if (a[i][c] > a[r][c]) t = i;
        }
        if (a[t][c] == 0) continue;  //说明该列全为0
        //交换
        if (a[r][c] != 1) {
      //如果当行行首是0,就得交换一下
            for (int i = c; i <= n + 1; ++ i) swap(a[t][i], a[r][i]);
        }
        //对第 r 行以下第 c 列的数消零
        for (int i = r + 1; i <= n; ++ i) {
    
            if (a[i][c] == 1) {
    
                for (int j = c; j <= n + 1; ++ j) {
    
                    a[i][j] ^= a[r][j];
                }
            }
        }
        ++ r;
    }
    if (r <= n) {
    
        for (int i = r; i <= n; ++ i) {
    
            if (a[i][n + 1] != 0) return 0;
        }
        return 2;
    }
    else {
    
        for (int i = n - 1; i >= 1; -- i) {
    
            for (int j = n; j > i; -- j) {
    
                if (a[i][j] != 0) {
    
                    a[i][n + 1] ^= a[j][n + 1];
                }
            }
        }
        return 1;
    }
}

int main() {
    
	//freopen("D:\\in.txt", "r", stdin); 
	//freopen("D:\\out.txt", "w", stdout);
	rin;
	for (int i = 1; i <= n; ++ i) {
    
	    for (int j = 1; j <= n + 1; ++ j) {
    
	        sc("%d", &a[i][j]);
	    }
	}
	int ans = solve(n);
	if (ans == 0) pr("No solution\n");
	else if (ans == 2) pr("Multiple sets of solutions\n");
	else {
    
	    for (int i = 1; i <= n; ++ i) {
    
	        pr("%d\n", a[i][n + 1]);
	    }
	}
	return 0;
}

同余方程组:HDU 5755 Gambler Bo

原题链接:http://acm.hdu.edu.cn/showproblem.php?pid=5755
在这里插入图片描述
在这里插入图片描述
题目大意
对于一个 n * m 的矩阵,若在位置(x,y)处操作一次,那么(x,y)自增2,而在(x,y)的上下左右四个位置自增1,问在所有矩阵的数值 mod 3 的前提下,输出一个能在 2 * n * m 次操作内将矩阵所有数值变为 0 的操作方案。

思路
(关于本题,x 和 y 的坐标从 1 开始)
首先这是一个开关灯问题,但是因为 n 和 m 数据范围在 30 以内,如果暴力枚举第一行,显然 2 ^ 30 会tle,所以这里是将矩阵的 n * m 个位置,看成 n * m 个变量,列出 n * m 个同余方程,然后解这个方程组求的每个位置的操作次数。

比如说,对于样例
1
2 3
2 1 2
0 2 0

首先设该情况下各个位置的操作方案如下:
x y z
a b c

那么要想将(1,1)处的 2 变为 0 ,那么在mod 3 的前提下,(1,1)需要操作自增 1 才能达到想要的效果。

而(1,1)这个位置想要增加 1 + 3p,那么 1 + 3p = 2 * x + y + a。
换句话说 2x + y + 0z + a + 0b + 0c ≡ 1 (mod 3)

依次类推,就可以得到 n * m 个同余方程,进而用高斯消元解这个方程组(记得先 + 3 再 mod 3 防止出现负数(合理性:比如说 -2x + 3x,这在mod 3 的情况下是不影响结果的,详见同余的性质))

ps. 最后的求变量的值用扩展欧几里得,列出最后两行就能模拟出来。

代码

#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>

using namespace std;

#define getlen(array) {
      return (sizeof(array) / sizeof(array[0]));}
#define ll long long 
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rit int t; scanf("%d", &t)
#define sc scanf
#define pr printf

const int INF = 0x3f3f3f3f;
const int N = 920; 

//(val & 1) == 0偶, == 1奇。

int n, m;
int fcz[N][N];

//扩展欧几里得解同余方程
int exgcd(int a, int b, int &x, int &y) {
    
	if (b == 0) {
    
		x = 1, y = 0;
		return a;
	}
	int d = exgcd(b, a % b, y, x);
	y -= a / b * x;
	return d;
}

//初始化方程组
void init() {
    
	MEM(fcz, 0);  //置零
	int length = n * m, a;
	for (int i = 1; i <= length; ++ i) {
    
		sc("%d", &a);
		fcz[i][length + 1] = (3 - a) % 3;  // 得到该位置还需要操作几次才能为0
		fcz[i][i] = 2;  //自增2
		//获得在原矩阵的行和列
		int row = i / m, col = i % m;
		if (col == 0) col = m;
		else ++ row;
		//上下左右有变化,该位置自增1
		if (row > 1) fcz[i][(row - 2) * m + col] = 1;
		if (row < n) fcz[i][row * m + col] = 1;  //这里我最傻逼 把n敲成m 多de了三天的bug
		if (col > 1) fcz[i][(row - 1) * m + col - 1] = 1;
		if (col < m) fcz[i][(row - 1) * m + col + 1] = 1;
	}
} 

//高斯消元 + 获得每个变量的解
void gauss() {
    
	int row, col, length = n * m;
	for (row = 1, col = 1; col <= length; ++ col) {
    
		//获得当前列中,还没处理的行的最大值
		int t = row;
		for (int i = row + 1; i <= length; ++ i) {
    
			if (fcz[i][col] > fcz[t][col]) t = i;
		}
		if (fcz[t][col] == 0) continue;
		// 交换行
		if (t != row) {
    
			for (int i = col; i <= length + 1; ++ i) {
    
				swap(fcz[t][i], fcz[row][i]);  //我是傻逼 把i敲成col
			}
		} 
		//消元 
		for (int i = row + 1; i <= length; ++ i) {
    
			if (fcz[i][col] != 0) {
    
				//因为fcz只取0、1、2,而且是同余方程组,所以不用double和lcm也能直接算
				int times = fcz[row][col] / fcz[i][col];
				for (int j = col; j <= length + 1; ++ j) {
    
					fcz[i][j] *= times;
					fcz[i][j] -= fcz[row][j];
					fcz[i][j] = (fcz[i][j] + 3) % 3;
				}
			}
		}
		++ row;
	}

	//获得每一个变量的解
	for (int i = row - 1; i >= 1; -- i) {
    
		int num = 0;
		 for (int j = length; j > i; -- j) {
    
		 	num += fcz[i][j] * fcz[j][length + 1]; 
		 } 
		 num = (num + 3) % 3;
		 int x, y;
		 int d = exgcd(fcz[i][i], 3, x, y);
		 x = ((x % 3) + 3) % 3;
		 fcz[i][length + 1] = x * (((fcz[i][length + 1] - num) + 3) % 3) / d;
		 fcz[i][length + 1] = (fcz[i][length + 1] + 3) % 3;
	} 
}

//输出结果
void print_ans() {
    
	int cnt = 0;
	int length = n * m;
	for (int i = 1; i <= length; ++ i) {
    
		cnt += fcz[i][length + 1];  //统计有多少次操作
	}
	pr("%d\n", cnt);
	//输出每一个操作
	for (int i = 1; i <= length; ++ i) {
    
		int row = i / m, col = i % m;
		if (col == 0) col = m;
		else ++ row;
		while (fcz[i][length + 1] --) {
    
			pr("%d %d\n", row, col); 
		}
	}
}

int main() {
    
	//freopen("D:\\in.txt", "r", stdin); 
	//freopen("D:\\out.txt", "w", stdout);
	rit;
	while (t --) {
    
		sc("%d%d", &n, &m);
		init(); //初始化
		gauss(); //高斯
		print_ans(); //输出
	}
	return 0;
}

9、斐波那契数列

若形如 F(1)=1,F(2)=1,F(n)=F(n-1)+F(n-2),n≥3,则称这些数构成斐波那契数列。

O(1)通项公式

在这里插入图片描述
【代码】

int fib(int n) {
    
    double sqrt5 = sqrt(5);
    double fibN = pow((1 + sqrt5) / 2, n) - pow((1 - sqrt5) / 2, n);
    return round(fibN / sqrt5);
}

例题1、循环节:HDU 1021 Fibonacci Again

在这里插入图片描述
思路
用程序跑出第 0 ~ 22 个该数列的数,发现第 2、6、10、14、18、22个数可以被 3 整除,所以直接利用规律就可以求。

代码

#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>

using namespace std;

#define getlen(array) {
      return (sizeof(array) / sizeof(array[0]));}
#define ll long long 
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rit int t; scanf("%d", &t)
#define sc scanf
#define pr printf

const int INF = 0x3f3f3f3f;
const int N = 10000; 

//(val & 1) == 0偶, == 1奇。

int main() {
    
	freopen("D:\\in.txt", "r", stdin); 
	//freopen("D:\\out.txt", "w", stdout);
	int n;
	while (sc("%d", &n) != -1) {
    
		n -= 2;
		if (n % 4 == 0) pr("yes\n");
		else pr("no\n");
	}
	return 0;
}

例题2、构造非法三角形边长

原题链接:2021牛客寒假算法基础集训营2 J 牛牛想要成为hacker
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
思路

如果三条边能构成三角形,那么最小的两条边之和必定大于第三边。
换句话说,要想构造出不合法的三边,那么最小的两条边之和必定小于等于第三边,但是又要尽可能地使得有更多的数可以凑,第三边 == 最小两边之和是最划算的,而边的长度最小为1,那么我们可以构造出:
1,1,2 , 3, 5, 8, 13, 21, 34 ……(明显的斐波那契数列)

但是因为斐波那契增长很快,而 n 也不小,所以当 Fi 超过 1e9 的时候,就应该拿别的数来凑。这里我们会发现无论拿什么数都不太理想,因为 1 太小了,小到除了 2 ,没有别的数可以和 1 、1凑,那么这里我们就可以想,既然是 1 碍事,那就把两个 1 放到后面去,也就是构造 2 , 3, 5, 8, 13, 21, 34,……,433494437,701408733,1,1,1,1,1……至此完毕。

代码

#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>

using namespace std;

#define getlen(array) {
      return (sizeof(array) / sizeof(array[0]));}
#define ll long long 
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rit int t; scanf("%d", &t)
#define sc scanf
#define pr printf

const int INF = 0x3f3f3f3f;
const int N = 10000; 

//(val & 1) == 0偶, == 1奇。

int main() {
    
	//freopen("D:\\in.txt", "r", stdin); 
	//freopen("D:\\out.txt", "w", stdout);
	rin;
	int book[100] = {
    2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040, 1346269, 2178309, 3524578, 5702887, 9227465, 14930352, 24157817, 39088169, 63245986, 102334155, 165580141, 267914296, 433494437, 701408733};
	pr("2 3 5");
 	for (int i = 3; i < n; ++ i) {
    
 		if (i <= 41) cout << ' ' << book[i];
 		else cout << ' ' << 1; 
	 }
	return 0;
}

10、中国剩余定理

(1)定理内容

在这里插入图片描述
模板题:AcWing 1298. 曹冲养猪
原题链接:https://www.acwing.com/problem/content/1300/
在这里插入图片描述
在这里插入图片描述

#include<iostream>

using namespace std;

#define ll long long

int n, A[20], B[20];  // Ai对应mi, Bi对应ai

//求逆元:a的逆元为x
void exgcd(ll a, ll b, ll &x, ll &y) {
    
    if (!b) {
    
        x = 1, y = 0;
        return;
    }
    exgcd(b, a % b, y, x);
    y -= a / b * x;
}

int main() {
    
    cin >> n;
    ll M = 1;
    for (int i = 1; i <= n; ++ i) {
    
        cin >> A[i] >> B[i];
        M *= A[i];
    }
    ll ans = 0;
    for (int i = 1; i <= n; ++ i) {
    
        ll Mi = M / A[i];
        ll x, y;
        exgcd(Mi, A[i], x, y);
        ans += B[i] * Mi * x;
    }
    //最后可能会是负数,而M是所有mi的乘积,对其取模保证了最小整数解
    cout << (ans % M + M) % M;
    return 0;
}

(2)拓展中国剩余定理

AcWing 204. 表达整数的奇怪方式

原题链接:https://www.acwing.com/problem/content/description/206/

在这里插入图片描述
思路

由 x ≡ mi(mod ai) 可得到前两条方程:
x = a1k1 + m1
x = a2k2 + m2
故而 a1k1 + m1 = a2k2 + m2(k1、k2未知)
移项,化简得:a1k1 - a2k2 = m2 - m1
用 exgcd 得到 k1 和 k2 以及 gcd 的值。

但是由于此时的 k1 是由 a1k1 - a2k2 = gcd 得到的,如果 (m2 - m1)% gcd != 0,说明方程无解,反之说明有解,但是 k1 需要乘上(m2 - m1)/ gcd 才是 a1k1 - a2k2 = m2 - m1 的其中一个解。

首先 k1 的通解 = k1 + (a2 / gcd)* k(其中k为任意整数)。
我们把通解代入 x = a1k1 + m1 可以得到:
x
= (k1 + (a2 / gcd)* k)* a1 + m1
= k1 * a1 + m1 + k * (a1 * a2) / gcd (其中k为任意整数)
很明显,前半部分的 k1 * a1 + m1 是常数,而最后面的 (a1 * a2) / gcd 也同样是常数,这和一开始的 x = a1k1 + m1 一样是 y = kx + b 这样的二元一次方程,由此可将起初的两条方程并成一条,重复 n - 1 次后,就只剩下一条方程,即可求解。

由于题目要求 x 最小,为了不爆 long long ,需要在过程中最小化 k1 的值(由前面可知,如果确定不会爆 ll 的话,其实 x 可以通过取模获得min,可以不用最小化k1)。
由 k1 的通解 = k1 + (a2 / gcd)* k,可知 k1 可以通过减少 abs(a2 / gcd)得到最小正整数解。

#include<bits/stdc++.h>

using namespace std;

#define ll long long

ll exgcd(ll a, ll b, ll &x, ll &y) {
    
    if (b == 0) {
    
        x = 1;
        y = 0;
        return a;
    }
    ll d = exgcd(b, a % b, y, x);
    y -= a / b * x;
    return d;
}

int main() {
    
    int n;
    cin >> n;
    ll a1, m1;
    cin >> a1 >> m1;
    bool ans = true;
    for (int i = 1; i < n; ++ i) {
    
        ll a2, m2;
        cin >> a2 >> m2;
        ll k1, k2, gcd;
        gcd = exgcd(a1, -a2, k1, k2);  //获得系数和gcd
        if ((m2 - m1) % gcd) ans = false;  //说明无解
        //因为方程右边是 m2 - m1,而此时的k1是在右边为gcd的前提下,所以需要换算同样的倍数
        k1 *= (m2 - m1) / gcd;   
        ll ab = abs(a2 / gcd);  //题目要求非负,所以k1和a1不能为负
        k1 = (k1 % ab + ab) % ab;  //k1可能会爆ll,求模后保证x的min属性
        m1 += k1 * a1;  //合并
        a1 *= abs(a2 / gcd);  //保证为正
    }
    if (ans) cout << (m1 % a1 + a1) % a1;
    else cout << "-1";
    return 0;
}

二、高精度

1、加减乘除模板

大数加大数

#include<iostream>
#include<vector>
using namespace std;

// 计算 A + B = C
vector<int> A, B, C;

// 高精度加
void add() {
    
    int t  = 0;
    for (int i = 0; i < A.size() || i < B.size(); ++ i) {
    
        if (i < A.size()) t += A[i];
        if (i < B.size()) t += B[i];
        C.push_back(t % 10);
        t /= 10;
    }
    if (t > 0) C.push_back(t);  //可能还有进位
}


int main(){
    
    string a, b;
    cin>> a >> b;
    for (int i = a.size() - 1; i >= 0; -- i) A.push_back(a[i] - '0');
    for (int i = b.size() - 1; i >= 0; -- i) B.push_back(b[i] - '0');
    
    add();
    
    for (int i = C.size() - 1; i >= 0; -- i) 
        cout << C[i];
        
    return 0;
}

大数减大数

#include<iostream>
#include<vector>
using namespace std;

// 计算 A * B = C
vector<int> A, B, C;

//判断 A 是否大于等于 B
bool judge() {
    
    if (A.size() != B.size()) return A.size() > B.size();
    else {
    
        for (int i = A.size() - 1; i >= 0; -- i) {
    
            if (A[i] != B[i])
                return A[i] > B[i];
        }
        return true;
    }
}

//高精度减
void sub (vector<int> &x, vector<int> &y) {
    
    int t = 0;
    for (int i = 0; i < x.size(); ++ i) {
    
        t += x[i];
        if (i < y.size()) t -= y[i];
         //重点:+10 并不影响 %10 的结果,却可以同时处理t为正负数的情况
        C.push_back((t + 10) % 10); 
        if (t >= 0) t = 0;
        else t = -1;
    }
    //处理前导 0
    while (C.size() > 1 && C.back() == 0) C.pop_back();
}


int main() {
    
    string a, b;
    cin >> a >> b;
    for (int i = a.size() - 1; i >= 0; -- i) A.push_back(a[i] - '0');
    for (int i = b.size() - 1; i >= 0; -- i) B.push_back(b[i] - '0');
    
    if (judge()) sub(A, B);
    else sub(B, A), cout << "-";
    
    for (int i = C.size() - 1; i >= 0; -- i) 
        cout << C[i];
    
    return 0;
    
}

大数乘小数

#include<iostream>
#include<vector>

using namespace std;

//计算 A * B = C
vector<int> A, C;
int B;

void mul() {
    
    int t = 0;
    for (int i = 0; i < A.size(); ++ i) {
    
        t += A[i] * B;
        C.push_back(t % 10);
        t /= 10;
    }
    //建议用while,因为如果需要乘多次,这样才能确保容器的每一位都是一个个位的数字
    while (t > 0) C.push_back(t % 10), t /= 10;
    //if (t > 0) C.push_back(t);  //还有进位
    while (C.size() > 1 && C.back() == 0) C.pop_back(); //处理前导0
}


int main() {
    
    string s;
    cin >> s >> B;
    
    for (int i = s.size() - 1; i >= 0; -- i) {
    
        A.push_back(s[i] - '0');
    }
    
    mul();
    
    for (int i = C.size() - 1; i >= 0; -- i) {
    
        cout << C[i];
    }
}

大数除小数

#include<iostream>
#include<vector>

using namespace std;

//计算 A / B = C 余 yushu
vector<int> A, C;
int B, yushu;  

//高精度除法
void mul() {
    
    int t = 0;
    for (int i = 0; i < A.size(); ++ i) {
    
        t = t * 10 + A[i];
        C.push_back(t / B);
        t %= B;
    }
    yushu = t;
}

int main() {
    
    string s;
    cin >> s >> B;
    for (int i = 0; i < s.size(); ++ i) {
    
        A.push_back(s[i] - '0');
    }
    
    mul();
    
    //处理前导 0
    int i = 0;
    while (i < C.size() - 1 && C[i] == 0) ++ i; 
    //输出商
    for (; i < C.size(); ++ i) {
    
        cout << C[i];
    }
    //输出余数
    cout << endl << yushu << endl;
    
    return 0;
}

例题1、求阶乘 : HDU 1042 N!

原题链接:http://acm.hdu.edu.cn/showproblem.php?pid=1042

在这里插入图片描述

#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>

using namespace std;

#define getlen(array) {
      return (sizeof(array) / sizeof(array[0]));}
#define ll long long 
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rit int t; scanf("%d", &t)
#define sc scanf
#define pr printf

const int INF = 0x3f3f3f3f;
const int N = 10000; 

//(val & 1) == 0偶, == 1奇。

void mul(vector<int> &A, int B) {
    
	int t = 0;
	for (int i = 0; i < A.size(); ++ i) {
    
		t += A[i] * B;
		A[i] = t % 10;
		t /= 10;
	}
	//这里剩余的进位 t 不能像模板那样直接 A.push_back(t)
	//否则后面计算 t += A[i] * B 时,A[i] * B 可能会爆掉 
	while (t > 0) {
    
		A.push_back(t % 10);
		t /= 10;
	}
}

int main() {
    
	//freopen("D:\\in.txt", "r", stdin); 
	//freopen("D:\\out.txt", "w", stdout);
	
	int n;
	while (sc("%d", &n) != -1) {
    
		if (n == 0) {
    
			pr("1\n");
			continue;
		}
		
		vector<int> A;
		A.push_back(1);
		for (int i = 2; i <= n; ++ i) {
    
			mul(A, i);
		}
		//输出 
		for (int i = A.size() - 1; i >= 0; -- i) {
    
			pr("%d", A[i]);
		}
		pr("\n");
	} 
	
	return 0;
}

例题2、高精度乘+除法求卡特兰数 :HDU 1134 Game of Connections

原题链接:http://acm.hdu.edu.cn/showproblem.php?pid=1134
在这里插入图片描述
题目大意
有一个由 1、2、3、……、2n - 1、2n 顺时针围成的圆圈,现在要求每个数只能被连接一次,所有数都有且仅有一条连线,并且所有线都不会有交叉,问针对这样的 n ,连接的方案有多少种。

思路
设 f(2n)为问题的解,那么 f(2n) = f(0) * f(2n - 2) + f(2)* f(2n - 4) + f(4)* f(2n - 6) + …… + f(2n - 2)* f(0)。
其中, f(0) * f(2n - 2)可以看成是先在 1 和 2n 之间连上一条线,那么 f(0)即这条线左上角所有数的连接方案数,而 f(2n - 2)即为这条线右下角所有数的连接方案数,其他依次类推。

有这个公式可知 f (2n) = C(n),C(n)即为卡特兰数。

所以
C(n)

= ( 2 n n ) \tbinom{2n}{n} (n2n) / (n + 1)

= ((2n)!/ (n!n!))/ (n + 1)

= ((n + 2)* (n + 3)* …… * (2n))/ (1 * 2 * 3 * …… * n)。

#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>

using namespace std;

#define getlen(array) {
      return (sizeof(array) / sizeof(array[0]));}
#define ll long long 
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rit int t; scanf("%d", &t)
#define sc scanf
#define pr printf

const int INF = 0x3f3f3f3f;
const int N = 10000; 

//(val & 1) == 0偶, == 1奇。

//高精度乘
void mymul(vector<int> &A, int b) {
    
	int t = 0;
	for (int i = 0; i < A.size(); ++ i) {
    
		t += A[i] * b;
		A[i] = t % 10;
		t /= 10;
	}
	//注意 t 可能会有多位,不能直接if(t > 0)……
	while (t > 0) A.push_back(t % 10), t /= 10;
}

//高精度除
void mydiv(vector<int> &A, int b) {
    
	int t = 0;
	vector<int> C;
	for (int i = 0; i < A.size(); ++ i) {
    
		t = t * 10 + A[i];
		C.push_back(t / b);
		t %= b;
	} 
	int i = 0;
	while (C[i] == 0) ++ i;   //处理前导零
	int k = 0;
	while (i < C.size()) {
      
		A[k ++] = C[i ++];
	}
	while (A.size() > k) A.pop_back();
}

int main() {
    
	//freopen("D:\\in.txt", "r", stdin); 
	//freopen("D:\\out.txt", "w", stdout);
	int n;
	while (sc("%d", &n) != -1) {
    
		if (n == -1) break;
		if (n == 1) {
    
			pr("1\n");
			continue;
		}
		vector<int> A;
		int a = n + 2, b = 2 * n;  //乘法的区间【a,b】
		
		int c = a;
		while (c > 0) {
      //注意第一个数可能不止一位
			A.push_back(c % 10);
			c /= 10;
		}

		++ a;
		while (a <= b) {
    
			mymul(A, a);
			++ a;
		}

		//反转A,方便后面高精除
		for (int i = 0, j = A.size() - 1; i < j; ++ i, -- j) {
    
			int temp = A[i];
			A[i] = A[j];
			A[j] = temp;
		}
		
		a = 2, b = n;  //除法的区间【a,b】
		while (a <= b) {
    
			mydiv(A, a);
			++ a;
		}
		
		//输出
		for (int i = 0; i < A.size(); ++ i) {
    
			pr("%d", A[i]);
		}
		pr("\n");
	} 
	return 0;
}

三、组合数学

1、求组合数

(1)AcWing 885. 求组合数 I

原题链接:https://www.acwing.com/problem/content/887/
在这里插入图片描述

// 用杨辉三角求
#include<iostream
using namespace std;

const int mod = 1e9 + 7;
const int N = 2020;
#define ll long long

int book[N][N];

void make_table() {
    
    for (int i = 0; i < N; ++ i) {
    
        for (int j = 0; j <= i; ++ j) {
    
            if (!j) book[i][j] = 1;
            else book[i][j] = ((ll)book[i - 1][j] + book[i - 1][j - 1]) % mod;
        }
    }
}

int main() {
    
    make_table();
    int n, a, b;
    scanf("%d", &n);
    while (n --) {
    
        scanf("%d%d", &a, &b);
        printf("%d\n", book[a][b]);
    }
}

(2)AcWing 886. 求组合数 II

原题链接:https://www.acwing.com/problem/content/888/
在这里插入图片描述

//用公式 C(n取m) = n ! / (m ! * (n - m) !)

#include<iostream>
using namespace std;

const int N = 100010;
const int mod = 1e9 + 7;
#define ll long long

//fact[i]表示i的阶乘
//infact[i]表示i的阶乘的逆元
ll fact[N], infact[N];

ll qmi(ll a, ll k) {
    
    ll res = 1;
    while (k > 0) {
    
        if (k & 1) res *= a, res %= mod;
        k >>= 1;
        a *= a;
        a %= mod;
    }
    return res;
}

void make_arr() {
    
    fact[0] = infact[0] = 1;
    for (int i = 1; i < N; ++ i) {
    
        fact[i] = fact[i - 1] * i % mod;
        infact[i] = infact[i - 1] * qmi(i, mod - 2) % mod;
    }
}

int main() {
    
    int n, a, b;
    scanf("%d", &n);
    make_arr();
    while (n --) {
    
        scanf("%d %d", &a, &b);
        ll ans = fact[a] * infact[b] % mod * infact[a - b] % mod;
        printf("%lld\n", ans);
    }
}

(3)AcWing 887. 求组合数 III

原题链接:https://www.acwing.com/problem/content/description/889/
在这里插入图片描述

//若p是质数,则对于任意整数 1 <= m <= n,有:
//   C(n, m) = C(n % p, m % p) * C(n / p, m / p) (mod p) ----- 卢卡斯定理
#include<iostream>

using namespace std;
#define ll long long

int qmi(int a, int k, int p) {
    
    int res = 1;
    while (k > 0) {
    
        if (k & 1) res = (ll)res * a % p;
        k >>= 1;
        a = (ll)a * a % p;
    }
    return res;
}

int C(int a, int b, int p) {
    
    int res = 1;
    for (int i = 1; i <= b; ++ i) {
    
        res = (ll)res * qmi(i, p - 2, p) % p;
        res = (ll)res * a % p;
        -- a;
    }
    return res;
}

int lucas(ll a, ll b, int p) {
    
    if (a < p) return C(a, b, p);
    return (ll)C(a % p, b % p, p) * lucas(a / p, b / p, p) % p;
}

int main() {
    
    int n;
    scanf("%d", &n);
    ll a, b;
    int p;
    while (n --) {
    
        scanf("%lld%lld%d", &a, &b, &p);
        printf("%d\n", lucas(a, b, p));
    }
}

(4)AcWing 888. 求组合数 IV

原题链接:https://www.acwing.com/problem/content/890/
在这里插入图片描述

//统计C(n取m)是由哪些质因子相乘得到的,再用高精度计算出最终结果
#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>

using namespace std;

#define getlen(array) {
      return (sizeof(array) / sizeof(array[0]));}
#define ll long long 
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rit int t; scanf("%d", &t)
#define sc scanf
#define pr printf

const int INF = 0x3f3f3f3f;
const int N = 5010; 

//(val & 1) == 0偶, == 1奇。

int primes[N], sum[N], cnt;
bool st[N];
vector<int> ans;

//线性筛素数
void make_primes() {
    
	for (int i = 2; i < N; ++ i) {
    
		if (!st[i]) primes[cnt ++] = i;
		for (int j = 0; primes[j] <= N / i; ++ j) {
    
			st[primes[j] * i] = true;
			if (i % primes[j] == 0) break;
		}
	}
}

// 求从 1 到 num 里面有几个质因数 p 
int get_cnt(int num, int p) {
    
	int res = 0;
	while (num >= p) {
    
		res += num / p;
		num /= p;
	}
	return res;
}

// 高精度乘法:ans * num = ans
void mul(int num) {
    
	int t = 0;
	for (int i = 0; i < ans.size(); ++ i) {
    
		t += num * ans[i];
		ans[i] = t % 10;
		t /= 10;
	}
	while (t > 0) {
    
		ans.push_back(t % 10);
		t /= 10;
	}
}

//将所有质因数相乘
void get_ans() {
    
	ans.push_back(1);
	for (int i = 0; i < cnt; ++ i) {
    
		while (sum[i] > 0) {
    
			mul(primes[i]);
			-- sum[i];
		}
	}
}

//输出答案
void printf_ans() {
    
	for (int i = ans.size() - 1; i >= 0; -- i) {
    
		pr("%d", ans[i]);
	}
}

int main() {
    
	//freopen("D:\\in.txt", "r", stdin); 
	//freopen("D:\\out.txt", "w", stdout);
	make_primes();  //预处理素数
	int a, b;
	scanf("%d %d", &a, &b);
	for (int i = 0; primes[i] <= a; ++ i) {
    
		sum[i] = get_cnt(a, primes[i]) - get_cnt(b, primes[i]) - get_cnt(a - b, primes[i]);
	}
	get_ans();
	printf_ans();
	return 0;
}

2、卡特兰数

基本定义和公式

给定 n 个 0 和 n 个 1,它们按照某种顺序排成一个长度为 2n 的序列,且这个序列需要满足任意前 i 个数中, 0 的个数都不少于 1 的个数。
而这样的序列的数量即满足卡特兰数:Cat(n)= ( 2 n n ) \tbinom{2n}{n} (n2n) - ( 2 n n − 1 ) \tbinom{2n}{n -1 } (n12n) = ( 2 n n ) \tbinom{2n}{n} (n2n) / (n + 1)

前几项为:1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862, 16796, 58786, 208012, 742900, 2674440, 9694845, 35357670, 129644790, 477638700, 1767263190, 6564120420, 24466267020, 91482563640, 343059613650, 1289904147324, 4861946401452, …

为什么满足卡特兰数——传送门

由于对于组合数而言有: ( n m ) \tbinom{n}{m} (mn) = n ! / (m ! * (n - m) !) ,故而有:

C(n)

= ( 2 n n ) \tbinom{2n}{n} (n2n) / (n + 1)

= ((2n)!/ (n!n!))/ (n + 1)

= ((n + 2)* (n + 3)* …… * (2n))/ (1 * 2 * 3 * …… * n)

常见题型

题型详解:https://blog.csdn.net/wuzhekai1985/article/details/6764858

  1. n对括号有多少种匹配方式?—— Cat(n)
  2. 矩阵链乘: P=a1×a2×a3×……×an,依据乘法结合律,不改变其顺序,只用括号表示成对的乘积,试问有几种括号化的方案?—— Cat(n - 1)
  3. 一个栈(无穷大)的进栈序列为1,2,3,…,n,有多少个不同的合法出栈序列? —— Cat(n)
  4. n个节点构成的二叉树,共有多少种情形?—— Cat(n)
  5. 在圆上选择2n个点,将这些点成对连接起来使得所得到的n条线段不相交的方法数?—— Cat(n)
  6. 求一个凸多边形区域划分成三角形区域的方法数?
  7. 在平面直角坐标系上,每一步只能往上走或往右走,从(0,0)走到(n,n)且除了两个端点外不接触直线 y = x 的路线数量?—— 2 * Cat(n - 1)

例题1、高精度乘+除求卡特兰数 HDU 1134 Game of Connections

(在本篇博客高精度例题2里有详解)

例题2、快速幂+逆元求卡特兰数 HDU 5673 Robot

原题链接:http://acm.hdu.edu.cn/showproblem.php?pid=5673
在这里插入图片描述

题目大意
一个机器人在坐标原点,它每秒钟可以向左或者向右移动 1 个单位,或者留在原地不动,但是任意时刻不能处于负半轴。问经历 n 秒后,机器人回到原点的方案数。

思路

首先要想回到原点,向右走的步数和向左走的步数必定是一样的,那么这就是很明显的0/1的个数问题,也就是卡特兰数。

但是因为这道题目多了一个可以停留的选择,所以需要对 0 到 n / 2 进行遍历求卡特兰数,并乘上对应的停留的方案数。

设 i 为向右走的次数,那么 ans = ( n n − 2 ∗ i ) \tbinom{n}{n - 2 * i} (n2in) * ( 2 ∗ i i ) \tbinom{2 * i}{i} (i2i) / ( i + 1 )

i ∈ [ 0 ,n / 2 ]

( n n − 2 ∗ i ) \tbinom{n}{n - 2 * i} (n2in) —— 空格方案数

( 2 ∗ i i ) \tbinom{2 * i}{i} (i2i) / ( i + 1 ) —— 卡特兰数

注意:题目时间限制是 6000 ms,需要对逆元预处理,不然会超时。

代码

#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>

using namespace std;

#define getlen(array) {
      return (sizeof(array) / sizeof(array[0]));}
#define ll long long 
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rit int t; scanf("%d", &t)
#define sc scanf
#define pr printf

const int INF = 0x3f3f3f3f;
const int N = 1000010; 
const int mod = 1e9 + 7;

//(val & 1) == 0偶, == 1奇。

int fact[N], infact[N], inv[N];

//快速幂求逆元
int qmi(int a, int k) {
    
	int res = 1;
	while (k) {
    
		if (k & 1) res = (ll)res * a % mod;
		k >>= 1;
		a = (ll) a * a % mod;
	}
	return res;
}

void make_arr() {
    
	//逆元打表
	for (int i = 1; i < N; ++ i) {
    
		inv[i] = qmi(i, mod - 2);
	}
	//阶乘和阶乘逆元打表
	fact[0] = infact[0] = 1;
	for (int i = 1; i < N; ++ i) {
    
		fact[i] = (ll)fact[i - 1] * i % mod;
		infact[i] = (ll)infact[i - 1] * inv[i] % mod;
	}
}

int main() {
    
	//freopen("D:\\in.txt", "r", stdin); 
	//freopen("D:\\out.txt", "w", stdout);
	make_arr();
	rit;
	while (t --) {
    
		rin;
		int ans = 0;
		for (int i = 0; i <= n / 2; ++ i) {
     
			ans += (ll)fact[n] * infact[n - 2 * i] % mod * infact[i] % mod *infact[i] % mod * inv[i + 1] % mod;
			ans %= mod;
		}
		pr("%d\n", ans);
	}
	return 0;
}

3、容斥原理

在这里插入图片描述
简单来说,就是在求加法的时候重叠部分被重复计算了,而在去掉重叠部分时,又去多了,所以需要再加回来二次重叠的部分循环往复。

模板题 AcWing 890. 能被整除的数

原题链接:https://www.acwing.com/problem/content/892/

在这里插入图片描述
思路
对于题目样例:
10 2
2 3
我们可以列出1 ~ 10 范围内能被 2 和 3 整除的数:
S2 = {2、4、6、8、10}
S3 = {3、6、9}
而我们要计算的就是 S2 + S3 - S2 ∩ S3
显然 S2 = n / 2,S3 = n / 3,而 S2 ∩ S3 = n / (2 * 3),其余同理。
(因为题目明确说明这 m 个数一定都是素数,所以直接相乘即可)

代码

#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>

using namespace std;

#define getlen(array) {
      return (sizeof(array) / sizeof(array[0]));}
#define ll long long 
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rit int t; scanf("%d", &t)
#define sc scanf
#define pr printf

const int INF = 0x3f3f3f3f;
const int N = 10000; 

//(val & 1) == 0偶, == 1奇。

ll book[20];

int main() {
    
	//freopen("D:\\in.txt", "r", stdin); 
	//freopen("D:\\out.txt", "w", stdout);
	int n, m;
	sc("%d%d", &n, &m);
	for (int i = 0; i < m; ++ i) {
    
		sc("%lld", &book[i]);
	}
	int length = pow(2, m);
	int ans = 0;
	for (int i = 1; i < length; ++ i) {
      //二进制枚举
		int cnt = 0;
		ll num = 1;
		for (int j = 0; j <= 16; ++ j) {
    
			if ((i >> j) & 1) {
      //判断 i 的二进制数的第 j 位是不是 1
				++ cnt;
				num *= book[j];
				if (num > n) break;
			}
		}
		if (cnt & 1) ans += n / num;
		else ans -= n / num;
	}
	pr("%d\n", ans);
	return 0;
}

四、博弈论

1、尼姆博弈

模板题:HDU - 2176 取(m堆)石子游戏

原题链接:http://acm.hdu.edu.cn/showproblem.php?pid=2176

在这里插入图片描述

思路

首先我们需要明确最终 n 堆石子必定会走到全部取完的状态,即 a1 ^ a2 ^ a3 …… ^ an = 0 ^ 0 ^ 0 …… ^ 0 = 0。
那么对于所有非终态,我们设 a1 ^ a2 ^ a3 …… ^ an = x :

① x == 0
无论在任意堆取任意数量,最终 a1 ^ a2 ^ a3 …… ^ an != 0

② x != 0
在最优策略下,我们完全可以在某一堆取出若干石子使得 a1 ^ a2 ^ a3 …… ^ an == 0
(例如,设 x 的二进制最高位 1 在第 k 位,则必然存在一个数 ai 的第 k 位是 1, 对 ai 堆取走 ai - (ai ^ x)颗石子,那么 ai 堆会剩下 ai ^ x 颗石子,此时 a1 ^ a2 ^ a3 ……^ ai ^ x ^ …… ^ an = x ^ x == 0)

换句话说,如果在初始状态下,a1 ^ a2 ^ a3 …… ^ an != 0,那么先手可以拿一定数目使得 xor 后变为 0,而如果此时石子还没取完,那么在后手操作后 xor 必定不等于 0,循环往复,直到某一次先手操作后 xor == 0,而且是 0 ^ 0 ^ 0 …… ^ 0 的 0,那么后手必败。
反之如果初始状态下,a1 ^ a2 ^ a3 …… ^ an == 0,那么先手必败,后手必胜。

结论:初始状态下,a1 ^ a2 ^ a3 …… ^ an != 0,先手胜,反之后手胜。

代码

#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>
#include <unordered_set>
#include <unordered_map>

using namespace std;

#define getlen(array) {
      return (sizeof(array) / sizeof(array[0]));}
#define ll long long 
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rit int t; scanf("%d", &t)
#define sc scanf
#define pr printf

const int INF = 0x3f3f3f3f;
const int N = 200010; 

//(val & 1) == 0偶, == 1奇。
int nums[N];

int main() {
    
	//freopen("D:\\in.txt", "r", stdin); 
	//freopen("D:\\out.txt", "w", stdout);
	int m;
	while (sc("%d", &m) != -1 && m != 0) {
    
		int ans = 0;  //存所有数异或后的值
		for (int i = 0; i < m; ++ i) {
    
			sc("%d", &nums[i]);
			ans ^= nums[i];
		} 
		if (ans) {
    
			pr("Yes\n");
			int k = -1, copy = ans;
			while (copy) {
      //计算ans最高位的 1 在第几位(从0数起)
				++ k;
				copy >>= 1;
			}
			//这道题不用hash判重也可以过
			unordered_set<int> hash;
			for (int i = 0; i < m; ++ i) {
    
				if ((nums[i] >> k) & 1) {
    
					if (hash.count(nums[i]) == 0) {
     //nums[i] 还没出现过
						hash.insert(nums[i]);
						pr("%d %d\n", nums[i], nums[i] ^ ans);
					}
				}
			}
		}
		else pr("No\n");
	}
	return 0;
}

例题1、AcWing 892. 台阶-Nim游戏

原题链接:https://www.acwing.com/problem/content/description/894/
在这里插入图片描述
思路

对于序号是偶数的台阶,假如后手在第 4 阶台阶拿了 a 个放在第 3 阶,那么先手只需要从第 3 阶也拿 a 个放在第 2 阶,即先手进行镜像操作,会使得最终胜负取决于序号是奇数的台阶(因为接近地面的是奇数台阶,所以综上,后手操作偶数台阶不影响最终结果)。

对于奇数台阶,设 xor = a1 ^ a3 ^ a5 ^……
由前面尼姆游戏的模板题可知,假如 xor == 0,后手胜,反之先手胜。

这道题主要是要抽象出在第 i 阶拿若干个放在 i - 1 阶的意义。

代码

#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>
#include <unordered_set>
#include <unordered_map>

using namespace std;

#define getlen(array) {
      return (sizeof(array) / sizeof(array[0]));}
#define ll long long 
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rit int t; scanf("%d", &t)
#define ria int a; scanf("%d", &a)
#define sc scanf
#define pr printf

const int INF = 0x3f3f3f3f;
const int N = 10000; 

//(val & 1) == 0偶, == 1奇。

int main() {
    
	//freopen("D:\\in.txt", "r", stdin); 
	//freopen("D:\\out.txt", "w", stdout);
	rin;
	int ans = 0;
	for (int i = 1; i <= n; ++ i) {
    
		ria;
		if (i & 1) ans ^= a;
	}
	if (ans) pr("Yes\n");
	else pr("No\n");
	return 0;
}

2、SG 函数

设集合 S = {0、2、3}
那么 mex(S)= 1,因为mex 是求不属于集合 S 的最小自然数
若由数 a 可以走向数 x、y、z,那么 SG(a)= mex{ SG(x) 、SG(y) 、SG(z) }

模板题 AcWing 893. 集合-Nim游戏

原题链接:https://www.acwing.com/problem/content/895/
在这里插入图片描述
在这里插入图片描述
思路

例如对于样例第三堆中,有 7 块石头,那么有两种取法 – 拿走 2 块 or 拿走 5 块,而 SG (7) = mex{ SG(2) 、SG(5) }。
在这里插入图片描述

若 SG(x) != 0,说明有一种取法使得下一步的 sg == 0,而 sg == 0,说明无论怎么取再下一步的 sg 都不会为 0,即不可能是取到无路可取的状态,此时 sg 又回到一开始的 SG( x ’ ) != 0的情况,循环往复。

换而言之,若初始时 SG(x) == 0,那么先手败,反之先手胜

但是呢,因为题目是有 n 堆石子,所以各堆的 sg 值应该看成独立的,此时就可以将 n 个 sg 值抽象成是一个 Nim游戏,对这 n 个 sg 值求 xor,若 xor == 0,先手败,反之先手胜。

这里简单类比一下:
xor == 0,没石子可取,先手败;
xor == 0,有石子可取,先手取后,xor 必定不为 0,换句话说一定有一堆石子的 sg 值不为 0,那么后手一定可以在操作后把这一堆石子的 sg 值变为 0,那么 sg == 0的局面一定是被先手遇到,换句话说先手必败;
xor != 0,类比前面可证先手必胜。

代码

#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>
#include <unordered_set>

using namespace std;

#define getlen(array) {
      return (sizeof(array) / sizeof(array[0]));}
#define ll long long 
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rit int t; scanf("%d", &t)
#define sc scanf
#define pr printf

const int INF = 0x3f3f3f3f;
const int N = 110, M = 10010; 

//(val & 1) == 0偶, == 1奇。
int S[N], sgs[M];
int k;

//求sg值
int sg(int num) {
    
	if (sgs[num] != -1) return sgs[num]; 
	unordered_set<int> book;
	for (int i = 0; i < k; ++ i) {
    
		if (num - S[i] >= 0) book.insert(sg(num - S[i])); 
	}
	// 求 mex
	for (int i = 0; ; ++ i) {
    
		if (book.count(i) == 0) {
    
			sgs[num] = i;
			break;
		}
	}
	return sgs[num];
}

int main() {
    
	//freopen("D:\\in.txt", "r", stdin); 
	//freopen("D:\\out.txt", "w", stdout);
	sc("%d", &k);
	for (int i = 0; i < k; ++ i) sc("%d", &S[i]);
	rin;
	int ans = 0;
	MEM(sgs, -1);
	while (n --) {
    
		int a;
		sc("%d", &a);
		ans ^= sg(a);
	}
	if (ans) pr("Yes\n");
	else pr("No\n");
	return 0;
}

例题1、AcWing 894. 拆分-Nim游戏

原题链接:https://www.acwing.com/problem/content/896/

在这里插入图片描述
思路
在这里插入图片描述
假设现在有 n 堆石头,其中有一堆石头数量只有 3 块,玩家如果选择了这一堆,需要将 3 块石头全部拿走,并且一共有 6 种可能的放回方式,其中绿色表示左边的 sg 值,蓝色表示右边的 sg 值。

那么很显然,这 6 种方式其实和上面 sg 函数的模板题一样,是 3 的可能分支,sg (3) = mex { 6 种取法的 sg 值 }

但问题就在于,每一种取法的 sg 值怎么求?应该怎么处理一堆变两堆?

这里我们可以类比一下,在模板题里面,我们是求出每一堆的 sg 值,然后对所有堆的 sg 值求异或值判断与否。而这里的取一堆放回两堆,其实可以抽象成:现在有 2 堆石子,操作后先手是必胜还是必败。故而我们就把取完一堆放回两堆的操作看成一个子问题去求解。

综上,sg (3) = mex { sg(0) ^ sg(0),sg(0) ^ sg(1),sg(0) ^ sg(2),sg(1) ^ sg(1),sg(1) ^ sg(2),sg(2) ^ sg(2) }

代码

#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>
#include <unordered_set>
#include <unordered_map>

using namespace std;

#define getlen(array) {
      return (sizeof(array) / sizeof(array[0]));}
#define ll long long 
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rit int t; scanf("%d", &t)
#define ria int a; scanf("%d", &a)
#define sc scanf
#define pr printf

const int INF = 0x3f3f3f3f;
const int N = 110; 

//(val & 1) == 0偶, == 1奇。
int sgs[N];

int sg(int num) {
    
	if (sgs[num] != -1) return sgs[num];
	unordered_set<int> hash;
	for (int i = 0; i < num; ++ i) {
    
		for (int j = 0; j < num; ++ j) {
    
			hash.insert(sg(i) ^ sg(j));
		}
	}
	for (int i = 0; ; ++ i) {
    
		if (! hash.count(i)) {
    
			sgs[num] = i;
			break;
		}
	}
	return sgs[num];
}


int main() {
    
	//freopen("D:\\in.txt", "r", stdin); 
	//freopen("D:\\out.txt", "w", stdout);
	rin;
	int ans = 0;
	MEM(sgs, -1);
	while (n --) {
    
		ria;
		ans ^= sg(a);
	}
	if (ans) pr("Yes\n");
	else pr("No\n");
	return 0;
}

五、常用结论

1、求两个数不能组合成的最大整数

在这里插入图片描述
在这里插入图片描述
思路
首先这是一个结论题,假如数据有解,那么不能组合成的最大数目是(n - 1)*(m - 1) - 1

有无解的分析:
设 a、b、x、y为整数,可正可负。
① 数据无解
如果 gcd(n,m)= d > 1,那么a * n + b * m = x * d,那么由 n 和 m 能凑成的数一定只能是 d 的整数倍,非整数倍的数可以无限大,所以此时的 n 和 m 无解。
② 数据有解
假如 gcd(n,m)= d == 1,那么根据裴蜀定理( gcd(n,m)= d,则必然存在 xn + ym = d),必然可以有以下变换:
xn + ym = 1
xpn + ypm = p
(xp - am)n + (yp + bn)m = p
而 xp - am 和 yp + bn 可以使得让一个大的数减少一些,一个小的数变大一些,使得系数为正,进而可得数据有解。

结论的证明: https://www.acwing.com/solution/acwing/content/3165/

代码

import java.util.Arrays;
import java.util.Comparator;
import java.util.Scanner;
public class Main {
    
	public static void main(String[] args){
    
		Scanner sc = new Scanner(System.in);
		int n = sc.nextInt();
		int m = sc.nextInt();
		System.out.println(((n - 1) * (m - 1) - 1));
	}
}

——————————————————————————
2021.03.22
陆陆续续学完一些 ACM 数学入门,主要是简单数论、组合、高精度、简单博弈论,后面如果有时间会接着补充。(现文 4.7 万字)

2021.05.03
增加一些常用结论

2021.07.20
补充(拓展)中国剩余定理

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/CSDNWudanna/article/details/114194533

智能推荐

使用JDBC连接数据库出现 The server time zone value ‘�й���׼ʱ��‘ is unrecognized or represents more than one解决方案_jdbc.properties timezone-程序员宅基地

文章浏览阅读553次。在 jdbc.properties 文件中的 url 后面加上 ?serverTimezone=UTC加入之前的jdbc.properties文件:user=rootpassword=12345678url=jdbc:mysql://localhost:3306/testdriverClass=com.mysql.cj.jdbc.Driver加入之后:user=rootpassword=12345678url=jdbc:mysql://localhost:3306/test?serv_jdbc.properties timezone

计算机图形学孔令德基础知识,计算机图形学基础教程孔令德答案-程序员宅基地

文章浏览阅读1.4k次。计算机图形学基础教程孔令德答案【篇一:大学计算机图形学课程设】息科学与工程学院课程设计任务书题目:小组成员:巴春华、焦国栋成员学号:专业班级:计算机科学与技术、2009级本2班课程:计算机图形学指导教师:燕孝飞职称:讲师完成时间: 2011年12 月----2011年 12 月枣庄学院信息科学与工程学院制2011年12 月20日课程设计任务书及成绩评定12【篇二:计算机动画】第一篇《计算机图形学》..._计算机图形学基础教程 孔令德 答案

python xlwings追加数据_大数据分析Python库xlwings提升Excel工作效率教程-程序员宅基地

文章浏览阅读1k次。原标题:大数据分析Python库xlwings提升Excel工作效率教程Excel在当今的企业中非常非常普遍。在AAA教育,我们通常建议出于很多原因使用代码,并且我们的许多数据科学课程旨在教授数据分析和数据科学的有效编码。但是,无论您偏爱使用大数据分析Python的程度如何,最终,有时都需要使用Excel来展示您的发现或共享数据。但这并不意味着仍然无法享受大数据分析Python的某些效率!实际上,..._xlwings通过索引添加数据

java8u211_jre864位u211-程序员宅基地

文章浏览阅读911次。iefans为用户提供的jre8 64位是针对64位windows平台而开发的java运行环境软件,全称为java se runtime environment 8,包括Java虚拟机、Java核心类库和支持文件,不包含开发工具--编译器、调试器和其它工具。jre需要辅助软件--JavaPlug-in--以便在浏览器中运行applet。本次小编带来的是jre8 64位官方版下载,版本小号u211版..._jre8是什么

kasp技术原理_KASP基因分型-程序员宅基地

文章浏览阅读5k次。KASP基因分型介绍KASP(Kompetitive Allele-Specific PCR),即竞争性等位基因特异性PCR,原理上与TaqMan检测法类似,都是基于终端荧光信号的读取判断,每孔反应都是采用双色荧光检测一个SNP位点的两种基因型,不同的SNP对应着不同的荧光信号。KASP技术与TaqMan法类似,它与TaqMan技术不同的是,它不需要每个SNP位点都合成特异的荧光引物,它基于独特的..._kasp是什么

华为p50预装鸿蒙系统,华为p50会不会预装鸿蒙系统_华为p50会预装鸿蒙系统吗-程序员宅基地

文章浏览阅读154次。华为现在比较火的还真就是新开发的鸿蒙系统了,那么在即将上市的华为p50手机上会不会预装鸿蒙系统呢?接下来我们就来一起了解一下华为官方发布的最新消息吧。1.华为p50最新消息相信大家都知道,随着华为鸿蒙OS系统转正日期临近,似乎全网的花粉们都在关注华为鸿蒙OS系统优化、生态建设等等,直接忽略了不断延期发布的华为P50手机,如今华为P50系列手机终于传来了最新的好消息,在经过一系列方案修改以后,终于被..._华为手机p50直接预装鸿蒙系统

随便推点

python用什么软件编程好-初学python编程,有哪些不错的软件值得一用?-程序员宅基地

文章浏览阅读2.1k次。Python编程的软件其实许多,作为一门面向大众的编程言语,许多修正器都有对应的Python插件,当然,也有特地的PythonIDE软件,下面我简单引见几个不错的Python编程软件,既有修正器,也有IDE,感兴味的朋友可以本人下载查验一下:1.VSCode:这是一个轻量级的代码修正器,由微软规划研发,免费、开源、跨途径,轻盈活络,界面精练,支撑常见的自动补全、语法提示、代码高亮、Git等功用,插..._python入门学什么好

pytorch一步一步在VGG16上训练自己的数据集_torch vgg训练自己的数据集-程序员宅基地

文章浏览阅读3.2w次,点赞30次,收藏307次。准备数据集及加载,ImageFolder在很多机器学习或者深度学习的任务中,往往我们要提供自己的图片。也就是说我们的数据集不是预先处理好的,像mnist,cifar10等它已经给你处理好了,更多的是原始的图片。比如我们以猫狗分类为例。在data文件下,有两个分别为train和val的文件夹。然后train下是cat和dog两个文件夹,里面存的是自己的图片数据,val文件夹同train。这样我们的..._torch vgg训练自己的数据集

毕业论文管理系统设计与实现(论文+源码)_kaic_论文系统设计法-程序员宅基地

文章浏览阅读968次。论文+系统+远程调试+重复率低+二次开发+毕业设计_论文系统设计法

在python2与python3中转义字符_Python 炫技操作:五种 Python 转义表示法-程序员宅基地

文章浏览阅读134次。1. 为什么要有转义?ASCII 表中一共有 128 个字符。这里面有我们非常熟悉的字母、数字、标点符号,这些都可以从我们的键盘中输出。除此之外,还有一些非常特殊的字符,这些字符,我通常很难用键盘上的找到,比如制表符、响铃这种。为了能将那些特殊字符都能写入到字符串变量中,就规定了一个用于转义的字符 \ ,有了这个字符,你在字符串中看的字符,print 出来后就不一定你原来看到的了。举个例子>..._pytyhon2、python3对%转义吗

java jar 文件 路径问题_「问答」解决jar包运行时相对路径问题-程序员宅基地

文章浏览阅读1.3k次。我这几天需要做一个Java程序,需要通过jar的形式运行,还要生成文件。最终这个程序是要给被人用的,可能那个用的人还不懂代码。于是我面临一个问题:生成的文件一定不能存绝对路径。刚开始我想得很简单,打绝对路径改成相对路径不就行了吗?于是有了这样的代码:String path = "../test.txt";File file = new File(path);……这个写法本身并没有问题,直接运行代码..._jar启动文件路径中存在!

微信读书vscode插件_曾经我以为 VSCode 是程序员专属的工具,直到发现了这些……...-程序员宅基地

文章浏览阅读598次。如果你知道 VSCode,一说起它,你可能第一个想到的就是把它当做一个代码编辑器,而它的界面应该可能大概率是这样的——如果你恰好又是个程序员,那你可能经常会用到它,不管是 Python、JS 还是 C++ 等各种语言对应的文件,都可以用它来进行简单的编辑和整理,甚至是运行和 debug......但是今天要讲的显然不是这些,经过小美的多方研究,发现了即使是对于大多数并不了解 VSCode,也完全不..._vscode weixin read

推荐文章

热门文章

相关标签