数论汇总

目录​​​​​​​

一,质数

1,试除法判定质数    时间复杂度O(sqrt(N))

2.分解质因数    时间复杂度O(sqrt(N))

3,线性筛法求质数        时间复杂度几乎为O(N)

二,约数

1,试除法求约数     时间复杂度O(sqrt(N))

2,求约数个数

3,求约数之和

4,求最大公约数

三,欧拉函数

1,求每个数的欧拉函数

2,用筛法求欧拉函数

四,快速幂

1,快速幂

2,快速幂求逆元

五,扩展欧几里得算法

1,扩展欧几里得算法

2,线性同余方程  

六,高斯消元           

1,高斯消元求解线性方程组        时间复杂度O(N^3)

七,求组合数

题目描述

 1,第一种情况

数据范围与提示

2,第二种情况

数据范围与提示

3,第三种情况

数据范围与提示

4,第四种情况

数据范围与提示

八,卡特兰数

九,容斥原理

十,博弈论

1,nimi游戏

2,sg函数    集合-nimi游戏


一,质数

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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值