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
补充(拓展)中国剩余定理

评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值