目录
一,质数
1,试除法判定质数 时间复杂度O(sqrt(N))
#include<iostream> //试除法判定质数 O(sqrt(N))
#include<algorithm>
using namespace std;
bool prime(long long n)
{
if (n < 2)
return false;
for (int i = 2; i <= n / i; i++)
{
if (n % i == 0)
return false;
}
return true;
}
int main()
{
int n;
long long a[110];
scanf("%d", &n);
for (int i = 0; i < n; i++)
scanf("%lld", &a[i]);
for (int i = 0; i < n; i++)
{
if (prime(a[i]))
printf("Yes\n");
else
printf("No\n");
}
return 0;
}
2.分解质因数 时间复杂度O(sqrt(N))
#include<iostream> //分解质因数 时间复杂度为O(sqrt(N))
#include<algorithm>
using namespace std;
void divide(int n)
{
//一个数只可能有一个大于sqrt(N)的质因子,因此只用枚举到sqrt(N),最后特判一下即可
for (int i = 2; i <= n / i; i++)
{
if (n % i == 0)
{
int s = 0;
while (n % i == 0)
{
n /= i;
s++;
}
printf("%d %d\n", i, s);//输出 底数和指数
}
}
//特判
if (n > 1)
printf("%d 1\n", n);
puts(" ");
}
int main()
{
int t;
scanf("%d", &t);
while (t--)
{
int n;
scanf("%d", &n);
divide(n);
}
return 0;
}
3,线性筛法求质数 时间复杂度几乎为O(N)
#include<iostream> //线性筛法 时间复杂度为几乎为O(N)
#include<algorithm>
using namespace std;
const int N = 1e6 + 10;
//prime存储质数,cnt为质数的个数
int cnt, prime[N];
//表示每个数是否被筛过
bool v[N];
//线性筛法的核心思想就是让每个数都被这个数的最小质因数筛掉
void xianxingshai(int n)
{
for (int i = 2; i <= n; i++)
{
if (!v[i])
prime[cnt++] = i;
for (int j = 0; prime[j] <= n/i; j++)
{
v[i * prime[j]] = true;
if (i % prime[j] == 0)
break;
}
}
}
int main()
{
int n;
scanf("%d", &n);
xianxingshai(n);
printf("%d", cnt);
return 0;
}
二,约数
1,试除法求约数 时间复杂度O(sqrt(N))
#include<iostream> //试除法求约数
#include<algorithm> //思路与试除法求质数一样
#include<vector>
using namespace std;
vector<int> get_yueshu(int n)
{
vector<int>v;
for (int i = 1; i <= n / i; i++)
{
if (n % i == 0)
{
v.push_back(i);
if (i != n / i)
v.push_back(n / i);
}
}
sort ( v.begin(), v.end());
return v;
}
int main()
{
int n;
scanf("%d", &n);
while (n--)
{
int x;
scanf("%d", &x);
auto res = get_yueshu(x);
for (auto i : res)
printf("%d ", i);
printf("\n");
}
return 0;
}
2,求约数个数
用唯一分解定理
图中的p表示质因数,a表示质因数的指数
//一个数的约数个数为 将他分解质因数后的每个质因数的指数加一的乘积
#include<iostream> //求约数个数
#include<algorithm>
#include<unordered_map>
using namespace std;
const int mod = 1e9 + 7;
typedef long long ll;
int main()
{
int t;
scanf("%d", &t);
unordered_map<int, int>primes;
while (t--)
{
int n;
scanf("%d", &n);
//将每个数分解质因数
for (int i = 2; i <= n / i; i++)
{
while (n % i == 0)
{
n /= i;
primes[i]++;//将每个数的质因数的指数存下来
}
}
if (n > 1)
primes[n]++;
}
ll res = 1;
for (auto i : primes)
res = res * (i.second + 1) % mod;
printf("%lld", res);
return 0;
}
3,求约数之和
#include<iostream> //求约数之和
#include<algorithm> //需要记住公式,公式可以百度寻找
#include<unordered_map> //求约数之和与求约数个数方法相同,只是最后的求法不同
using namespace std;
const int mod = 1e9 + 7;
typedef long long ll;
int main()
{
int t;
//存储每个数的质因数,以及对应的指数的和
unordered_map<int, int>primes;
scanf("%d", &t);
while (t--)
{
int n;
scanf("%d", &n);
//分解质因数
for (int i = 2; i <= n / i; i++)
{
while (n % i == 0)
{
n /= i;
primes[i]++;//存储质因数的指数
}
}
if (n > 1)
primes[n]++;
}
ll res = 1;
for (auto i : primes)
{
int p = i.first, a = i.second;
ll t = 1;
while (a--)
t = (t*p+1) % mod;
res = res * t % mod;
}
cout << res;
return 0;
}
4,求最大公约数
#include<iostream> //最大公约数 辗转相除法
#include<algorithm>
using namespace std;
int gcd(int a, int b)
{
return a % b == 0 ? b : gcd(b, a % b);
}
int main()
{
int t;
scanf("%d", &t);
while (t--)
{
int a, b;
scanf("%d%d", &a,&b);
int t = gcd(a, b);
printf("%d\n", t);
}
return 0;
}
三,欧拉函数
首先给出欧拉函数的定义
图中的p表示质因数,a表示质因数的指数,这个公式是由容斥原理推导出来的
1,求每个数的欧拉函数
#include<iostream> //欧拉函数 有容斥原理的思想
#include<algorithm>
using namespace std;
int main()
{
int t;
scanf("%d", &t);
while (t--)
{
int n;
scanf("%d", &n);
int res = n;
for (int i = 2; i <= n / i; i++)
{
if (n % i == 0)
{
//这里有个小技巧,如果直接用1/a的话会出现小数,为了避免出现小数可以先除后乘
res = res / i * (i - 1);
while (n % i == 0)
n /= i;
}
}
if (n > 1)
res = res / n * (n - 1);
cout << res << endl;
}
return 0;
}
2,用筛法求欧拉函数
筛法求欧拉函数就是用线性筛法求欧拉函数,在筛质数的同时将一个数的欧拉函数求出来.
代码如下:
#include<iostream> //筛法求1至n中的欧拉函数
#include<algorithm>
using namespace std;
const int N = 1e6 + 10;
typedef long long ll;
//phi为一个数与他互质的个数,v表示是否被筛过
//cnt记录素数个数,prime记录素数值
int prime[N], cnt, phi[N],v[N];
ll get_phi(int n)
{
phi[1] = 1;
for (int i = 2; i <= n; i++)
{
if (!v[i])
{
prime[cnt++] = i;
phi[i] = i - 1;//如果一个数n是质数,那么与他互质的数就是n-1
}
for (int j = 0; prime[j] <= n / i; j++)
{
v[prime[j] * i] = 1;
if (i % prime[j] == 0)
{
//如果prime[j]是i的质因子,那么i*prime[j]的欧拉函数就是phi[i]*prime[j]
phi[i * prime[j]] = phi[i] * prime[j];
break;
}
//如果prime[j]不是i的质因子,那么i*prime[j]的欧拉函数就是phi[i]*(prime[j]-1)
phi[i * prime[j]] = phi[i] * (prime[j] - 1);
}
}
ll res = 0;
for (int i = 1; i <= n; i++)
res += phi[i];
return res;
}
int main()
{
int n;
scanf("%d", &n);
ll res= get_phi(n);
cout << res;
return 0;
}
四,快速幂
1,快速幂
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
ll quick_mi(ll a, ll b, ll p)
{
ll res = 1;
while (b)
{
if (b & 1)
res = res * a % p;
b >>= 1;
a = a * a % p;
}
return res;
}
int main()
{
ll n;
scanf("%d", &n);
while (n--)
{
ll a, b, p;
scanf("%d%d%d", &a, &b, &p);
printf("%lld\n", quick_mi(a, b, p));
}
return 0;
}
2,快速幂求逆元
首先介绍一下什么是乘法逆元
再介绍一下欧拉定理和费马小定理。
欧拉定理就是b^φ(p)≡1(mod p),φ(p)为p的欧拉函数。
当p为质数时,那么φ(p)=p-1,即b^p-1≡1(mod p),即费马小定理。
由乘法逆元的定义可以得到b*b^-1≡1(mod m)当b与m互质且m为质数时,再结合费马小定理就可以得到b的乘法逆元为b^(m-2).
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
int quick_mi(int a, int b, int p)
{
int res = 1;
while (b)
{
if (b & 1)
res = (ll)res * a % p;
b >>= 1;
a = (ll)a * a % p;
}
return res;
}
int main()
{
int t;
scanf("%d", &t);
while (t--)
{
int a, b;
scanf("%d%d", &a, &b);
int c = quick_mi(a, b - 2, b);
if (a % b)
printf("%d\n", c);
else
printf("impossible\n");
}
return 0;
}
五,扩展欧几里得算法
1,扩展欧几里得算法
首先介绍一下裴属定理
裴属定理:对于任意的正整数a,b,一定存在整数x,y,使得ax+by=gcd(a,b)
由欧几里得算法可以知道gcd(a,b)=gcd(b,a%b),这样一直循环下去当a%b=0时,即当b=0时(这里看不懂的话可以对照着下面的代码一起看),a*x+0*y=gcd(a,0)=a,可以得到x=1,y=任意值的一组解,这里我们取y=0,那就是x=1,y=0的解,这是循环到b为0时的对应的x和y的一组解,那么我们如何递推回去求最初的a,b对应的x和y的解呢?
因为gcd(a,b)=ax+by
那么gcd(b,a%b)=b*x1+(a%b)*y1=b*x1+(a-a/b*b)*y1=a*y1+b*(x1-a/b*y1)
因此x=y1,y=x1-a/b*y1
代码如下:
#include<iostream> //扩展欧几里得算法
#include<algorithm>
using namespace std;
void exgcd(int a, int b, int& x, int& y)
{
if (b == 0)
{
x = 1;
y = 0;
return;
}
int x1, y1;
exgcd(b, a % b,x1,y1);
x = y1;
y = x1 - a / b * y1;
}
int main()
{
int t;
scanf("%d", &t);
while (t--)
{
int a, b,x,y;
scanf("%d%d", &a, &b);
exgcd(a, b, x, y);
printf("%d %d\n", x, y);
}
return 0;
}
2,线性同余方程
首先给出题目描述什么是线性同余方程
给定 n 组数据 ai , bi , mi ,对于每组数求出一个 xi ,使其满足 ai × xi ≡ bi (mod mi),如果无解则输出 impossible。
对于这样的方程,我们可以转换成扩展欧几里得算法,上述等式=a*x=m*y+b,即a*x+m*y=bi,
再扩展欧几里得算法中bi是a与b的最大公约数,但是在这里的bi是a与b的最大公约数的倍数,所以最后求得的x还要乘上b/gcd(a,b)
代码如下:
#include<iostream> //线性同余方程
#include<algorithm>
using namespace std;
typedef long long ll;
int exgcd(int a, int b, int& x, int& y)
{
if (!b)
{
x = 1;
y = 0;
return a;
}
int x1, y1;
int d=exgcd(b, a % b, x1, y1);
x = y1;
y = x1 - a / b * y1;
return d;
}
int main()
{
int t;
scanf("%d", &t);
while (t--)
{
int a, b, m,x,y;
scanf("%d%d%d", &a, &b, &m);
int d = exgcd(a, m, x, y);
if (b % d == 0)
printf("%d\n", (ll)x*(b/d)%m);
else
printf("impossible\n");
}
return 0;
}
六,高斯消元
1,高斯消元求解线性方程组 时间复杂度O(N^3)
高斯消元用于求解多元线性方程
根据上述的操作我们求解多元线性方程组主要是以下几步:
1,枚举每一列c
2,找到绝对值最大的那一行
3,将该行换到最上面
4,将该行的第一个数变成1
5,将下面所有行的第c列消成0
最后会求得一个上三角行列矩阵,从最后一行向前枚举倒推回去求得答案
代码如下:
#include <iostream> //oj4371: 高斯消元解线性方程组 线性代数,转换为上三角求解
#include <cmath>
#include <algorithm>
using namespace std;
const int maxn = 110;
const double eps = 1e-7;
int n;
double a[maxn][maxn];
int gauss()
{
int c, r;
for (c = 0, r = 0; c < n; c++)//遍历每一列
{
int t = r;//t中存放当前最大值所在那一行行标
for (int i = r; i < n; i++)
if (fabs(a[i][c]) > fabs(a[t][c]))
t = i;
if (fabs(a[t][c]) < eps)//最大值等于0
continue;//说明该列全为0,换下一列进行比较
//交换两行
for (int i = c; i < n + 1; i++)//共有n+1列
swap(a[t][i], a[r][i]);
//把那个最大值变为1
for (int i = n; i >= c; i--)//这里应该从后往前,因为那个最大值必须最后一个除,不然会变成都除1
a[r][i] /= a[r][c];//r是当前的最上层
//把该列下面的值都消成0
for (int i = r + 1; i < n; i++)
if (fabs(a[i][c]) > eps)
for (int j = n; j >= c; j--)
a[i][j] -= a[r][j] * a[i][c];
r++;
}
if (r < n)//有左部全为0的行
{
for (int i = r; i < n; i++)
if (fabs(a[i][n]) > eps)//右部不为0
return 2;//无解
return 1;//有0=0,无穷多组解
}
//有唯一解,从下往上消
for (int i = n - 1; i >= 0; i--)//处理每一行
for (int j = i + 1; j < n; j++)//用下面那层消上面那层
a[i][n] -= a[j][n] * a[i][j];//系数是上面那行要被消掉的值a[i][j]
return 0;
}
int main()
{
cin >> n;
for (int i = 0; i < n; i++)
for (int j = 0; j < n + 1; j++)
cin >> a[i][j];
int flag = gauss();
if (flag == 0)
{
for (int i = 0; i < n; i++)
printf("%.2lf\n", a[i][n]);
}
else if (flag == 1)
cout << "Infinite group solutions" << endl;
else
cout << "No solution" << endl;
return 0;
}
七,求组合数
求组合数,即
题目描述
给定 n 组询问,每组询问给定两个整数 a , b , 请你输出
的值。
1,第一种情况
数据范围与提示
1 <= n <= 10000,
1 <= b <= a <= 2000,
首先给出组合数的递推公式C (m,n)=c (m-1,n-1)+c (m-1,n),可以理解为从m个苹果里面选择n个苹果,已知一个苹果,那么所有的选法就可以分成选这个苹果与不选这个苹果,如果选这个苹果那么就是c(m-1,n-1),如果不选这个苹果,那么就是c(m-1,n),两种情况的总和就是所有的选法
接着我们看这个题,如果用暴力的做法那么就是10000*2000*2000的一个时间复杂度,会TLE,于是观察我们发现总共只有2000*2000=4000000对a和b,那么我们可以首先预处理处理处理所有的a和b的组合数,最后再查表得就可以求得答案了
代码如下:
#include<iostream> //oj4373: 求组合数 I 预处理
using namespace std;
const int N = 2010,mod=1e9+7;
int a[N][N];
void init()
{
int i, j;
for (i = 0; i < N; i++)
{
for (j = 0; j <= i; j++)
{
if (!j)a[i][j] = 1;
else a[i][j] = (a[i - 1][j] + a[i - 1][j - 1]) % mod;
}
}
}
int main()
{
int n;
init();
cin >> n;
while (n--)
{
int x, y;
cin >> x >> y;
cout << a[x][y]<<endl;
}
return 0;
}
2,第二种情况
数据范围与提示
1 ≤ n ≤ 10^5,
1 ≤ b ≤ a ≤ 10^5
首先给出求组合数得通项公式
对于这种情况,如果预处理a和b得所有组合数,那么就是10^10得一个时间复杂度,也是会TLE的,这时我们就要选取其他的解法,根据求组合数的通项公式,我们可以预处理出来所有数的阶乘,当一个数与模数互质时,除以这个数就等于乘这个数的逆元,就可以用前面提到的快速幂集合费马小定理求逆元,求所有数的阶乘以及逆元的阶乘
代码如下:
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10,mod=1e9+7;
//fact表示阶乘,infact表示逆元的阶乘
int fact[N], infact[N];
int quick_mi(int a, int b, int p)
{
int res = 1;
while (b)
{
if (b & 1)
res = (ll)res * a % p;
a = (ll)a * a % p;
b >>= 1;
}
return res;
}
int main()
{
int n;
scanf("%d", &n);
//预处理出来所有数的阶乘以及逆元的阶乘
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] * quick_mi(i, mod - 2, mod)%mod;
}
while (n--)
{
int a, b;
scanf("%d%d", &a, &b);
//这里要注意,一定要及时取模,因为两个数相乘不会溢出long long但是3个数相乘可能会溢出
printf("%d\n", (ll)fact[a] * infact[a - b] % mod * infact[b]%mod);
}
return 0;
}
3,第三种情况
数据范围与提示
1 ≤ n ≤ 20,
1 ≤ b ≤ a ≤ 10^18,
1 ≤ p ≤ 10^5,
对于这种情况,题中保证了p是质数,我们就要用到卢卡斯定理
Lucas 定理用于求解大组合数取模的问题,其中模数必须为素数。 正常的组合数运算可以通过递推公式求解(详见 排列组合 ),但当问题规模很大,而模数是一个不大的质数的时候,就不能简单地通过递推求解来得到答案,需要用到 Lucas 定理。即:
代码如下:
#include<iostream> //求组和数III 用卢卡斯定理将大数转换成小数再求组合数
#include<algorithm
using namespace std;
typedef long long ll;
ll quick_mi(ll a, ll b, ll p)
{
ll res = 1;
while (b)
{
if (b & 1)
res = res * a % p;
a = a * a % p;
b >>= 1;
}
return res;
}
ll C(ll a, ll b, ll p)
{
ll res = 1;
for (int i = 1, j = a; i <= b; i++, j--)
{
//用通项公式求解
res = res * j % p;
res = res * quick_mi(i, p - 2, p)%p;
}
return res;
}
ll lucas(ll a, ll b, ll p)
{
if (a < p && b < p)
return C(a, b, p);
else
return C(a % p, b % p, p) * lucas(a / p, b / p, p)%p;
}
int main()
{
int n;
scanf("%d", &n);
while (n--)
{
ll a, b,p;
cin >> a >> b >> p;
cout << lucas(a, b, p) << endl;
}
return 0;
}
4,第四种情况
输入 a,b,求 Cba 的值。
注意结果可能很大,需要使用高精度计算。
数据范围与提示
1 ≤ b ≤ a ≤ 5000
对于这种没有没有模数,并且数据范围很大的情况下,我们要用到高精度乘法和除法做,但是那么做太复杂了,我们可以先将a的阶乘和b的阶乘以及(a-b)的阶乘分解成质因数,最后分子分母消去以后就只用一个高精度乘法就可以求出来答案了
代码如下:
#include<iostream> //求组和数IV 运用高精度乘法和分解质因数
#include<algorithm>
#include<vector>
using namespace std;
typedef vector<int> ver;
const int N = 5010;
int prime[N], v[N], cnt;
int sum[N];
//线性筛质数
void getprime(int n)
{
for (int i = 2; i <= n; i++)
{
if (!v[i])
prime[cnt++] = i;
for (int j = 0; prime[j]<=n/ i; j++)
{
v[prime[j] * i] = 1;
if (i % prime[j] == 0)
break;
}
}
}
//求出n的阶乘中质因数p的个数
int get(int n, int p)
{
int res = 0;
while (n)
{
res += n / p;
n /= p;
}
return res;
}
//高精度乘法
ver mul(ver v, int x)
{
reverse(v.begin(), v.end());
for (int i = 0; i < v.size(); i++)
v[i] *= x;
for (int i = 0; i < v.size(); i++)
{
if (v[i] < 10)
continue;
int t = v[i] / 10;
v[i] %= 10;
if (i < v.size() - 1)
v[i + 1] += t;
else
v.push_back(t);
}
reverse(v.begin(), v.end());
return v;
}
int main()
{
int a, b;
ver v;
cin >> a >> b;
//先筛质数
getprime(a);
//求出a的阶乘-b的阶乘-(a-b)的阶乘中质因数的个数
for (int i = 0; i < cnt; i++)
{
int p = prime[i];
sum[i] = get(a, p) - get(b, p) - get(a - b, p);
}
//高精度乘法求每个质因数的指数的乘积
v.push_back(1);
for (int i = 0; i < cnt; i++)
{
for (int j = 0; j < sum[i]; j++)
{
v = mul(v, prime[i]);
}
}
for (int i = 0; i < v.size(); i++)
cout << v[i];
return 0;
}
八,卡特兰数
卡特兰数是一个非常重要的知识点,由于本人还没有那个实力,暂时还讲不清楚卡特兰数的证明过程以及由来,所以有需要的可以进行其他的相关搜索,
这里我们首先只用记住卡特兰数就是
卡特兰数可以解决一系列类似于
给定 n 个 0 和 n 个 1,它们将按照某种顺序排成长度为 2n 的序列,
求它们能排列成的所有序列中,能够满足任意前缀序列中 0 的个数都不少于 1 的个数的序列有多少个。
这种问题
对于这种问题他的答案就是一个卡特兰数,这样我们就可以将他们转换成求组合数了
代码如下:
#include<iostream> //卡特兰数 求满足条件的01序列
#include<algorithm>
using namespace std;
typedef long long ll;
const int mod = 1e9 + 7;
//快速幂求逆元
int quick_mi(int a, int b, int p)
{
ll res = 1;
while (b)
{
if (b & 1)
res = (ll)res * a % mod;
a = (ll)a * a % mod;
b >>= 1;
}
return res;
}
int main()
{
int n,res=1;
scanf("%d", &n);
//求组合数
for (int i = 1, j = 2 * n; i <= n; i++, j--)
{
res = (ll)res * j % mod;
res = (ll)res * quick_mi(i, mod - 2, mod)%mod;
}
//最后再除以n+1;
cout << (ll)res * quick_mi(n + 1, mod - 2, mod)%mod;
return 0;
}
九,容斥原理
首先介绍什么是容斥原理,例如
这样一个图,我们想要求他的面积,那么就等于
这就是容斥原理
接下来我们看下面这个题目如何用容斥原理来做
给定一个整数 n 和 m 个不同的质数 p1 , p2 , … , pm。
请你求出 1∼n 中能被 p1 , p2 , … , pm 中的至少一个数整除的整数有多少个。
假定给定n为10,m为2,p1为2,p2为3
我们把这个问题看成求2的倍数的集合与3的倍数的集合的并集,就是上面的容斥原理,再观察上面的图,我们可以发现,如果一个集合中的元素个数是奇数,我们就会加上这个集合,反之如果是偶数,我们就会减去这个集合,那么这个题就是2的倍数的集合个数+3的倍数的集合个数-同时为2和3的倍数的集合,2的倍数的集合个数的倍数就是n/2,3的倍数的集合个数就是n/3,同时为2和3的倍数的集合个数就是n/(2*3),那么如何模拟这个过程呢?
我们可以用一个2进制长度为m的数,从1遍历到这个数,就会包含所有的选取情况,如果遍历到的数的二进制表示中第k位为1,我们就表示选择了第k个数,也就是pk,再记录一下1的数量,如果为奇数就减去,如果是偶数就加上
代码如下:
#include<iostream> //容斥原理 能被整除的数
#include<algorithm>
using namespace std;
typedef long long ll;
int main()
{
int n, m;
ll p[20];
ll res = 0;
scanf("%d%d", &n, &m);
for (int i = 0; i < m; i++)
cin >> p[i];
for (int i = 1; i < 1 << m; i++)//遍历选取数的所有的选法
{
ll t = 1,cnt=0;
for (int j = 0; j < m; j++)
{
if (i >> j & 1)//如果这个数的二进制该位为1的话表示这一位数被选了
{
cnt++;
t *= p[j];//如果选了第j位数,就要将这个数乘起来,用于后面求包括这个数的倍数的集合个数
}
}
if (cnt & 1)//如果选了奇数个数就加上这个集合
res += n / t;
else
res -= n / t;//如果选了偶数个数就减去这集合
}
cout << res;
return 0;
}
十,博弈论
1,nimi游戏
先说结论,如果x1^x2^x3^x4……xn=0说明先手必败,不为0则先手必胜
简单证明一下,如果说x1^x2^x3^x4……xn=P异或最终值不为0,为p,假设p的二进制表示中的最高位1为第k位,那么再x1,x2,x3,……xn中一定存在一个x的二进制表示中的第k位为1,将x^P后一定小于x,那么我们拿一些石子使得x变成x^p,则原式就变成了x1^x2^x3^x4…x^P…xn=P^P=0,说明,我们一定能进行某种操作让其异或的最终值变成0,说明异或值如果不为0,则先手必胜
接下来用反证法证明,如果说x1^x2^x3^x4……xn=0,异或的最终值为0,假设我们可以让其中的一个数x拿走一些石子后变成x^q异或值变成不为0,那即x1^x2^x3^x4… x^q…xn=0,得到0^q=0,所以q为0,说明没有拿走着石子,则说明如果异或值为0,我们不可能拿走石子使得异或值变成非零
代码如下:
#include<iostream> //博弈论 nimi游戏,通过异或判断先手是否必胜
#include<algorithm>
using namespace std;
int n;
int main()
{
scanf("%d", &n);
int res = 0;
while (n--)
{
int x;
scanf("%d", &x);
res ^= x;
}
if (res == 0)
printf("No");
else
printf("Yes");
return 0;
}
2,sg函数 集合-nimi游戏
sg函数可以用于求多个集合的nimi游戏类型的题目,每个点的sg值都是跟与他连接的点的sg值不同且是自然数中最小的数,例如
最后的点的sg值一定是0,从后往前推得每个集合sg(x)得值,如果只有一个集合x,他的sg(x)不为0得话说明先手必胜,否则先手必败
对于多个集合,将每个集合得sg(x)异或起来,得到得值如果不为0,则先手必胜,否则先手必败
代码如下:
#include<iostream> //sg函数 拆分—nimi游戏 用set集合来存储每个点连接的点的sg值
#include<algorithm>
#include<unordered_set>
#include<cstring>
using namespace std;
const int N = 110, M = 1e4 + 10;
int n, k;
int s[N], f[M];
int sg(int x)
{
if (f[x] != -1)
return f[x];//记忆化搜索,如果这个点搜过了直接返回值
unordered_set<int>S;
for (int i = 0; i < k; i++)
{
int sum = s[i];
if (x >= sum)
S.insert(sg(x - sum));//存储连接x的点的sg值
}
for (int i = 0;; i++)
{
if (!S.count(i))
return f[x] = i;//找到x的sg值
}
}
int main()
{
memset(f, -1, sizeof f);//初始化
scanf("%d", &k);
for (int i = 0; i < k; i++)
scanf("%d", &s[i]);
scanf("%d", &n);
int res = 0;
for (int i = 0; i < n; i++)
{
int x;
scanf("%d", &x);
res ^= sg(x);
}
if (res)//如果多个集合的异或值不为0则先手必胜
cout << "Yes" << endl;
else
cout << "No" << endl;
return 0;
}