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} (2n−1),依次类推可得出,S(1)、S(2)、S(3)……S(n) 分别为 ( n − 1 0 ) \tbinom{n - 1}{0} (0n−1)、 ( n − 1 1 ) \tbinom{n - 1}{1} (1n−1)、 ( n − 1 2 ) \tbinom{n - 1}{2} (2n−1)…… ( n − 1 n − 1 ) \tbinom{n - 1}{n - 1} (n−1n−1)。
②求组合数
故而我们可以发现 ans =
(
n
−
1
0
)
\tbinom{n - 1}{0}
(0n−1)+
(
n
−
1
1
)
\tbinom{n - 1}{1}
(1n−1)+
(
n
−
1
2
)
\tbinom{n - 1}{2}
(2n−1)+……+
(
n
−
1
n
−
1
)
\tbinom{n - 1}{n - 1}
(n−1n−1) = 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} (m−1n−1) + ( n − 1 m ) \tbinom{n - 1}{m} (mn−1)
= ( n − 1 m − 1 ) \tbinom{n - 1}{m - 1} (m−1n−1) + ( n − 2 m − 1 ) \tbinom{n - 2}{m - 1} (m−1n−2) + ( n − 2 m ) \tbinom{n - 2}{m} (mn−2)
= ( n − 1 m − 1 ) \tbinom{n - 1}{m - 1} (m−1n−1) + ( n − 2 m − 1 ) \tbinom{n - 2}{m - 1} (m−1n−2) + ( n − 3 m − 1 ) \tbinom{n - 3}{m - 1 } (m−1n−3) + ( n − 3 m ) \tbinom{n - 3}{m} (mn−3)
= ……
针对 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、高斯消元
线性方程组–模板题
基本上,消元的顺序是:
- 将方程的所有系数存成矩阵的形式(二维数组)
- 枚举每一列
- 对当前列,找出 r ~ n 行中行首绝对值最大的那一行 t ,即找出 fabs(a [ i ] [ c ])
- 交换第 t 行和第 r 行,使得每一次枚举, r 行的行首都是绝对值最大(目的是为了提高精度)
- 将交换后的第 r 行的行首化为单位1
- 将第 r + 1 行到第 n 行的第 c 列全部化为 0
- ++ r,++ c 进入下一行下一列
- 【另】:假如获得的第 t 行的行首为 0 ,即:a [ t ] [ c ] < eps,那么说明该列全为0,该列不用处理,但是该行还是需要继续消元,所以只需要 ++ c,而不用 ++ r。
获得方程组的解:
如果 r <= n,说明秩不为 n,假如出现 0 = !0 的情况,说明方程组无解,反之方程组有无穷多解;
而如果 r == n + 1,即秩为 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 = 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 }
(n−12n) =
(
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
- n对括号有多少种匹配方式?—— Cat(n)
- 矩阵链乘: P=a1×a2×a3×……×an,依据乘法结合律,不改变其顺序,只用括号表示成对的乘积,试问有几种括号化的方案?—— Cat(n - 1)
- 一个栈(无穷大)的进栈序列为1,2,3,…,n,有多少个不同的合法出栈序列? —— Cat(n)
- n个节点构成的二叉树,共有多少种情形?—— Cat(n)
- 在圆上选择2n个点,将这些点成对连接起来使得所得到的n条线段不相交的方法数?—— Cat(n)
- 求一个凸多边形区域划分成三角形区域的方法数?
- 在平面直角坐标系上,每一步只能往上走或往右走,从(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} (n−2∗in) * ( 2 ∗ i i ) \tbinom{2 * i}{i} (i2∗i) / ( i + 1 )
i ∈ [ 0 ,n / 2 ]
( n n − 2 ∗ i ) \tbinom{n}{n - 2 * i} (n−2∗in) —— 空格方案数
( 2 ∗ i i ) \tbinom{2 * i}{i} (i2∗i) / ( 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
补充(拓展)中国剩余定理