acwing25数论

分解质因数

对于某一合数X,有且只有一个大于根号X的质因数。
反证:假如有两个大于根号X的质因数,相乘大于X矛盾。
所以i 只需要 枚举到根号 X,然后X不断除以 i 可以把所有i的倍数全部筛掉,那么最后如果剩下一个大于1 的X ,则X一定是大于根号X的那个质因数。

#include <iostream>
#include <algorithm>

using namespace std;

void divide(int x)
{
    for (int i = 2; i <= x / i; i ++ )
        if (x % i == 0)
        {
            int s = 0;
            while (x % i == 0) x /= i, s ++ ;
            cout << i << ' ' << s << endl;
        }
    if (x > 1) cout << x << ' ' << 1 << endl;
    cout << endl;
}

int main()
{
    int n;
    cin >> n;
    while (n -- )
    {
        int x;
        cin >> x;
        divide(x);
    }

    return 0;
}

试除法求质数

常用板子,不多说。

#include <iostream>
#include <algorithm>

using namespace std;

bool is_prime(int x)
{
    if (x < 2) return false;
    for (int i = 2; i <= x / i; i ++ )
        if (x % i == 0)
            return false;
    return true;
}

int main()
{
    int n;
    cin >> n;

    while (n -- )
    {
        int x;
        cin >> x;
        if (is_prime(x)) puts("Yes");
        else puts("No");
    }

    return 0;
}

埃氏筛

从小到大枚举所有的数,所有的倍数都会被删除,那么没有被标记已删除的数就是素数。

#include <iostream>
#include <algorithm>

using namespace std;

const int N= 1000010;

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

void get_primes(int n)
{
    for (int i = 2; i <= n; i ++ )
    {
        if (st[i]) continue;
        primes[cnt ++ ] = i;
        for (int j = i + i; j <= n; j += i)
            st[j] = true;
    }
}

int main()
{
    int n;
    cin >> n;

    get_primes(n);

    cout << cnt << endl;

    return 0;
}

线性筛

对于任意合数x,假设pj是x的最小质因子,当 i 枚举到 x/pj 的时候,这个合数就会被筛掉,枚举之后的 i 没有意义。
在这里插入图片描述
例如:如果primes[j] > n / i ,那么筛的时候一乘就大于n了,没有意义。

#include <iostream>
#include <algorithm>
using namespace std;
const int N= 1000010;
int primes[N], cnt;
bool st[N];
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;//如果下面i % primes[j] != 0,因为primes[j]
//从小到大枚举,所以primes[j]< i 的最小质因子,所以primes[j]是primes[j]* i 的最小值因子 
            if (i % primes[j] == 0) break;
            //如果i % primes[j] == 0,因为primes[j]是从小到大枚举的 
// 那么primes[j]一定是i的最小质因子,也是primes[j]* i 的最小值因子 
        }
    }
}
int main()
{
    int n;
    cin >> n;

    get_primes(n);

    cout << cnt << endl;

    return 0;
}

试除法求约数

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
vector<int> get_divisors(int x)
{
    vector<int> res;
    for (int i = 1; i <= x / i; i ++ )
        if (x % i == 0)
        {
            res.push_back(i);
            if (i != x / i) res.push_back(x / i);//防止x=i*i; 
        }
    sort(res.begin(), res.end());
    return res;
}

int main()
{
    int n;
    cin >> n;

    while (n -- )
    {
        int x;
        cin >> x;
        auto res = get_divisors(x);

        for (auto x : res) cout << x << ' ';
        cout << endl;
    }

    return 0;
}

约数个数

在这里插入图片描述
对于任意的一个整数 N N N,由算数基本定理得 N = p a 1 ⋅ p a 2 ⋅ . . . . . . p a k N=p^{a_1}·p^{a_2}·......p^{a_k} N=pa1pa2......pak,对于 N N N的每一个约数,对应着质因子次方的选法。就比如 2 ⋅ 6 ⋅ 8 = 2 5 ⋅ 3 1 2·6·8=2^5·3^1 268=2531,对于 2 2 2来说可以选 0 — — 5 0——5 05次,对于 3 3 3来说可以选 0 — — 1 0——1 01次 , 所以约数个数为 ( a 1 + 1 ) ⋅ ( a 2 + 1 ) . . . . . . ( a k + 1 ) 。 (a_1+1)·(a2+1)......(a_k+1)。 (a1+1)(a2+1)......(ak+1)

#include <iostream>
#include <algorithm>
#include <unordered_map>
#include <vector>

using namespace std;

typedef long long LL;

const int N = 110, mod = 1e9 + 7;

int main()
{
    int n;
    cin >> n;

    unordered_map<int, int> primes;
//primes存的是每一个质因数的指数
    while (n -- )
    {
        int x;
        cin >> x;

        for (int i = 2; i <= x / i; i ++ )
            while (x % i == 0)
            {
                x /= i;
                primes[i] ++ ;
            }

        if (x > 1) primes[x] ++ ;
    }
 //求质因数的指数
    LL res = 1;
    for (auto p : primes) res = res * (p.second + 1) % mod;//公式

    cout << res << endl;

    return 0;
}

这个地方要求熟练掌握使用auto遍历一个map:
p可以找到map中被赋值的数的下标。
在这里插入图片描述

在这里插入图片描述

约数之和

在这里插入图片描述
根据约数个数,很显然这道题就是把约数表达出来,依然用 2 ⋅ 6 ⋅ 8 = 2 5 ⋅ 3 1 2·6·8=2^5·3^1 268=2531,它的每一个约数对应某一种取法,所以 ( 2 0 + 2 1 + 2 2 + 2 3 + 2 4 + 2 5 ) ⋅ ( 3 0 + 3 1 ) (2^0+2^1+2^2+2^3+2^4+2^5)·(3^0+3^1) (20+21+22+23+24+25)(30+31) 就是把每一种约数表达出来并求和。
就相当于:
在这里插入图片描述
我们设对于任意的质数 p p p ,它的最高次为 a a a 次,那么我们可以让 t = 1 t=1 t=1 ,循环 a a a 次,每次让 t = p t + 1 t=pt+1 t=pt+1,这样循环完a次之后就可以得到图片中以 p i p_i pi为下标的某个括号里的和了。最后把每个质数的那个括号和累乘起来即可。

#include <iostream>
#include <algorithm>
#include <unordered_map>
#include <vector>

using namespace std;

typedef long long LL;

const int N = 110, mod = 1e9 + 7;

int main()
{
    int n;
    cin >> n;

    unordered_map<int, int> primes;

    while (n -- )
    {
        int x;
        cin >> x;

        for (int i = 2; i <= x / i; i ++ )
            while (x % i == 0)
            {
                x /= i;
                primes[i] ++ ;
            }

        if (x > 1) primes[x] ++ ;
    }

    LL res = 1;
    for (auto p : primes)
    {
        LL a = p.first, b = p.second;
        LL t = 1;
        while (b -- ) t = (t * a + 1) % mod;
        res = res * t % mod;
    }

    cout << res << endl;

    return 0;
}

欧几里得(辗转相除)

1.利用algorithm库函数
在这里插入图片描述
2.手写

int gcd(int a, int b)
{
    return b ? gcd(b, a % b) : a;
}

欧拉函数(分解质因数+公式)

在这里插入图片描述

互质是公约数只有1的两个整数,叫做互质整数。—— g c d ( a , b ) = 1 说 a , b 互 质 gcd(a,b)=1说a,b互质 gcd(a,b)=1a,b
如果N可以分解质因数:
在这里插入图片描述
那么对于N的欧拉函数为: 在这里插入图片描述

#include <iostream>

using namespace std;


int phi(int x)
{
    int res = x;//相当与先乘上公式里的N
    for (int i = 2; i <= x / i; i ++ )
        if (x % i == 0)
        { //res=res*(1 - 1/i) 
		 //不能让1/i出来个小数 所以用下边式子等价
            res = res / i * (i - 1);//公式 
            while (x % i == 0) x /= i;
        }
    if (x > 1) res = res / x * (x - 1);

    return res;
}


int main()
{
    int n;
    cin >> n;
    while (n -- )
    {
        int x;
        cin >> x;
        cout << phi(x) << endl;
    }

    return 0;
}

筛法求欧拉函数

给定一个正整数 n,求 1∼n 中每个数的欧拉函数之和。

输入格式
共一行,包含一个整数 n。

输出格式
共一行,包含一个整数,表示 1∼n 中每个数的欧拉函数之和。

数据范围
1≤n≤106
输入样例:

6

输出样例:

12

一个质数p,与其互质的数一定为p-1个,所以质数p的欧拉函数为p-1。
如果对pj是i的质因子和不是i的质因子讨论并套用公式得:
在这里插入图片描述

#include <iostream>

using namespace std;

typedef long long LL;

const int N = 1000010;


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


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


int main()
{
    int n;
    cin >> n;

    get_eulers(n);

    LL res = 0;
    for (int i = 1; i <= n; i ++ ) res += euler[i];

    cout << res << endl;

    return 0;
}

快速幂

在这里插入图片描述
快速幂就是把a的指数K分解成多个2的x次方相加(2进制)。
通过这步处理可以把快速幂的复杂度降为log (n).
举个例子:
在这里插入图片描述
具体见注释:

#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

//预处理a的2的x次方,相当于每次给a*a,就可以实现指数递增
LL qmi(LL a, int k, int p)
{  //第一次进来存的是a的2的0次方,就是a自己
    LL res = 1;
    while (k)
    {
        if (k & 1) res = res * a % p;//如果当前k的二进制个位是1的话,乘等a;
        a = a * a % p;//不管k的个位是1是0 a都平方一下。
        k >>= 1;//k的二进制进一位,删掉末位。
    }
    return res;//返回答案
}


int main()
{
    int n;
    scanf("%d", &n);
    while (n -- )
    {   LL a;
        int k, p;
        scanf("%lld%d%d", &a, &k, &p);
        printf("%lld\n", qmi(a, k, p));
    }
    return 0;
}

快速幂求逆元(mod为质数)

给定 n 组 ai,pi,其中 pi 是质数,求 ai 模 pi 的乘法逆元,若逆元不存在则输出 impossible。

注意:请返回在 0∼p−1 之间的逆元。
在这里插入图片描述
输入格式
第一行包含整数 n。

接下来 n 行,每行包含一个数组 ai,pi,数据保证 pi 是质数。

输出格式
输出共 n 行,每组数据输出一个结果,每个结果占一行。

若 ai 模 pi 的乘法逆元存在,则输出一个整数,表示逆元,否则输出 impossible。

数据范围
1≤n≤105,
1≤ai,pi≤2∗109
输入样例:

3
4 3
8 5
6 3

输出样例:

1
2
impossible

化简题目的表达式:
在这里插入图片描述
找逆元:
在这里插入图片描述
使用快速幂:

#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

//预处理a的2的x次方,相当于每次给a*a,就可以实现指数递增
LL qmi(int a, int k, int p)
{  //第一次进来存的是a的2的0次方,就是a自己
    LL res = 1;
    while (k)
    {
        if (k & 1) res = res * a % p;//如果当前k的二进制个位是1的话,乘等a;
        a = a * (LL)a % p;//不管k的个位是1是0 a都平方一下。
        k >>= 1;//k的二进制进一位,删掉末尾。
    }
    return res;//返回答案
}


int main()
{
    int n;
    scanf("%d", &n);
    while (n -- )
    {
        int a, k, p;
        scanf("%d%d", &a, &p);
        if(a%p!=0)
        printf("%lld\n", qmi(a, p-2, p));//求逆元
        else puts("impossible");
    }
    return 0;
}

博弈论

Nim游戏
给定 n 堆石子,两位玩家轮流操作,每次操作可以从任意一堆石子中拿走任意数量的石子(可以拿完,但不能不拿),最后无法进行操作的人视为失败。

问如果两人都采用最优策略,先手是否必胜。

输入格式
第一行包含整数 n。

第二行包含 n 个数字,其中第 i 个数字表示第 i 堆石子的数量。

输出格式
如果先手方必胜,则输出 Yes。

否则,输出 No。

数据范围
1≤n≤105,
1≤每堆石子数≤109
输入样例:

2
2 3

输出样例:

Yes

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int main()
{
    int n;
    scanf("%d", &n);
    int res = 0;
    while (n -- )
    {
        int x;
        scanf("%d", &x);
        res ^= x;
    }
    if (res) puts("Yes");
    else puts("No");
    return 0;
}

提前移除x堆

dp[1][1][0] = 1;
dp[1][0][a[1]] = 1;
for (int i = 2; i <= n; i++)
{
    for (int j = 0; j <= i; j++)
    {
        for (int k = 0; k < 异或最值; k++)
        {
            dp[i][j][k] = (dp[i-1][j-1][k] + dp[i-1][j][k^a[i]]) % mod;
        }
    }
}

台阶-Nim游戏
现在,有一个 n 级台阶的楼梯,每级台阶上都有若干个石子,其中第 i 级台阶上有 ai 个石子(i≥1)。

两位玩家轮流操作,每次操作可以从任意一级台阶上拿若干个石子放到下一级台阶中(不能不拿)。

已经拿到地面上的石子不能再拿,最后无法进行操作的人视为失败。

问如果两人都采用最优策略,先手是否必胜。

输入格式
第一行包含整数 n。

第二行包含 n 个整数,其中第 i 个整数表示第 i 级台阶上的石子数 ai。

输出格式
如果先手方必胜,则输出 Yes。

否则,输出 No。

数据范围
1≤n≤105,
1≤ai≤109
输入样例:

3
2 1 3

输出样例:

Yes

在这里插入图片描述
所以奇数阶不为0的先手必胜。

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

int main()
{
    int n;
    scanf("%d", &n);

    int res = 0;
    for (int i = 1; i <= n; i ++ )
    {
        int x;
        scanf("%d", &x);
        if (i % 2) res ^= x;
    }

    if (res) puts("Yes");
    else puts("No");

    return 0;
}


SG函数:求集合中不包含的最小自然数。
比如{1,3,2}这个集合的SG值为0;
集合-Nim游戏
给定 n 堆石子以及一个由 k 个不同正整数构成的数字集合 S。

现在有两位玩家轮流操作,每次操作可以从任意一堆石子中拿取石子,每次拿取的石子数量必须包含于集合 S,最后无法进行操作的人视为失败。

问如果两人都采用最优策略,先手是否必胜。

输入格式
第一行包含整数 k,表示数字集合 S 中数字的个数。

第二行包含 k 个整数,其中第 i 个整数表示数字集合 S 中的第 i 个数 si。

第三行包含整数 n。

第四行包含 n 个整数,其中第 i 个整数表示第 i 堆石子的数量 hi。

输出格式
如果先手方必胜,则输出 Yes。

否则,输出 No。

数据范围
1≤n,k≤100,
1≤si,hi≤10000
输入样例:

2
2 5
3
2 4 7

输出样例:

Yes

对于某一个点的SG函数,可以用一种“局面”来描述。
在这里插入图片描述
对于某一个点,如果它是终点,即无法操作,如果它不是终点,一定可以把局面变成必败态甩给对手。
同之前的NIm游戏证明。
在这里插入图片描述
最终把每一个点的SG异或起来,得0必败。
注释代码:

#include <cstring>
#include <iostream>
#include <algorithm>
#include <unordered_set>
using namespace std;
const int N = 110, M = 10010;
int n, m;
int s[N], f[M];
int sg(int x)
{
    if (f[x] != -1) return f[x];//记忆化搜索,确定SG的点不再搜了。
    unordered_set<int> S;//存每一个点的SG所形成的局面
    for (int i = 0; i < m; i ++ )
    {
        int sum = s[i];//遍历对某堆石子可以拿走的石子块数。
        if (x >= sum) S.insert(sg(x - sum));//能够形成哪些局面?递归下去
    }
    for (int i = 0; ; i ++ )
        if (!S.count(i))//从小到大枚举当前点形成的局面里没有的最小自然数
            return f[x] = i;//给这个点的SG赋值
}
int main()
{
    cin >> m;
    for (int i = 0; i < m; i ++ ) cin >> s[i];
    cin >> n;//s[i]是对某一堆石子,可以拿走其中的几块?
    memset(f, -1, sizeof f);
    int res = 0;
    for (int i = 0; i < n; i ++ )
    {
        int x;
        cin >> x;
        res ^= sg(x);
    }
    if (res) puts("Yes");//非0必胜
    else puts("No");
    return 0;
}

拆分-Nim游戏
给定 n 堆石子,两位玩家轮流操作,每次操作可以取走其中的一堆石子,然后放入两堆规模更小的石子(新堆规模可以为 0,且两个新堆的石子总数可以大于取走的那堆石子数),最后无法进行操作的人视为失败。

问如果两人都采用最优策略,先手是否必胜。

输入格式
第一行包含整数 n。

第二行包含 n 个整数,其中第 i 个整数表示第 i 堆石子的数量 ai。

输出格式
如果先手方必胜,则输出 Yes。

否则,输出 No。

数据范围
1≤n,ai≤100
输入样例:

2
2 3

输出样例:

Yes

在这里插入图片描述
拆分还是利用了SG函数的性质,对于某一个局面可以由两个局面异或求得,所以如果一堆石子被两堆石子替代,实际上就是求两堆石子异或的一个过程,其他的地方和集合_nim差不多。

#include <cstring>
#include <iostream>
#include <algorithm>
#include <unordered_set>
using namespace std;
const int N = 110;
int n;
int f[N];
int sg(int x)
{
    if (f[x] != -1) return f[x];
    unordered_set<int> S;
    for (int i = 0; i < x; i ++ )
        for (int j = 0; j <= i; j ++ )
            S.insert(sg(i) ^ sg(j));
    for (int i = 0;; i ++ )
        if (!S.count(i))
            return f[x] = i;
}
int main()
{
    cin >> n;
    memset(f, -1, sizeof f);
    int res = 0;
    while (n -- )
    {
        int x;
        cin >> x;
        res ^= sg(x);
    }
    if (res) puts("Yes");
    else puts("No");
    return 0;
}

高斯消元解线性方程组

输入一个包含 n 个方程 n 个未知数的线性方程组。

方程组中的系数为实数。

求解这个方程组。

下图为一个包含 m 个方程 n 个未知数的线性方程组示例:
在这里插入图片描述
输入格式
第一行包含整数 n。

接下来 n 行,每行包含 n+1 个实数,表示一个方程的 n 个系数以及等号右侧的常数。

输出格式
如果给定线性方程组存在唯一解,则输出共 n 行,其中第 i 行输出第 i 个未知数的解,结果保留两位小数。

如果给定线性方程组存在无数解,则输出 Infinite group solutions。

如果给定线性方程组无解,则输出 No solution。

数据范围
1≤n≤100,
所有输入系数以及常数均保留两位小数,绝对值均不超过 100。

输入样例:

3
1.00 2.00 -1.00 -6.00
2.00 1.00 -3.00 -9.00
-1.00 -1.00 2.00 7.00

输出样例:

1.00
-2.00
3.00

在这里插入图片描述

#include <iostream>
#include <algorithm>
#include <cmath>

using namespace std;

const int N = 110;
const double eps = 1e-6;

int n;
double a[N][N];


int gauss()
{
    int c, r;
    for (c = 0, r = 0; c < n; c ++ )
    {
        int t = r;
        for (int i = r; i < n; i ++ )
            if (fabs(a[i][c]) > fabs(a[t][c]))
                t = i;

        if (fabs(a[t][c]) < eps) continue;

        for (int i = c; i < n + 1; i ++ ) swap(a[t][i], a[r][i]);
        for (int i = n; i >= c; i -- ) a[r][i] /= a[r][c];

        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)
    {
        for (int i = r; i < n; i ++ )
            if (fabs(a[i][n]) > eps)
                return 2;
        return 1;
    }

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

    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 t = gauss();

    if (t == 0)
    {
        for (int i = 0; i < n; i ++ ) printf("%.2lf\n", a[i][n]);
    }
    else if (t == 1) puts("Infinite group solutions");
    else puts("No solution");

    return 0;
}

高斯消元解异或线性方程组
输入一个包含 n 个方程 n 个未知数的异或线性方程组。

方程组中的系数和常数为 0 或 1,每个未知数的取值也为 0 或 1。

求解这个方程组。

异或线性方程组示例如下:
在这里插入图片描述
其中 ^ 表示异或(XOR),M[i][j] 表示第 i 个式子中 x[j] 的系数,B[i] 是第 i 个方程右端的常数,取值均为 0 或 1。
输入格式
第一行包含整数 n。

接下来 n 行,每行包含 n+1 个整数 0 或 1,表示一个方程的 n 个系数以及等号右侧的常数。

输出格式
如果给定线性方程组存在唯一解,则输出共 n 行,其中第 i 行输出第 i 个未知数的解。

如果给定线性方程组存在多组解,则输出 Multiple sets of solutions。

如果给定线性方程组无解,则输出 No solution。

数据范围
1≤n≤100
输入样例:

3
1 1 0 1
0 1 1 0
1 0 0 1

输出样例:

1
0
0
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 110;


int n;
int a[N][N];


int gauss()
{
    int c, r;
    for (c = 0, r = 0; c < n; c ++ )
    {
        int t = r;
        for (int i = r; i < n; i ++ )
            if (a[i][c])
                t = i;

        if (!a[t][c]) continue;

        for (int i = c; i <= n; i ++ ) swap(a[r][i], a[t][i]);
        for (int i = r + 1; i < n; i ++ )
            if (a[i][c])
                for (int j = n; j >= c; j -- )
                    a[i][j] ^= a[r][j];

        r ++ ;
    }

    if (r < n)
    {
        for (int i = r; i < n; i ++ )
            if (a[i][n])
                return 2;
        return 1;
    }

    for (int i = n - 1; i >= 0; i -- )
        for (int j = i + 1; j < n; j ++ )
            a[i][n] ^= a[i][j] * a[j][n];

    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 t = gauss();

    if (t == 0)
    {
        for (int i = 0; i < n; i ++ ) cout << a[i][n] << endl;
    }
    else if (t == 1) puts("Multiple sets of solutions");
    else puts("No solution");

    return 0;
}

求组合数

求组合数1
给定 n 组询问,每组询问给定两个整数 a,b,请你输出 Cbamod(109+7) 的值。

输入格式
第一行包含整数 n。

接下来 n 行,每行包含一组 a 和 b。

输出格式
共 n 行,每行输出一个询问的解。

数据范围
1≤n≤10000,
1≤b≤a≤2000
输入样例:

3
3 1
5 3
2 2

输出样例:

3
10
1

不同的a,b最多可以形成2000的平方个组合数,可以直接递推:
在这里插入图片描述
两个值:第一个值为在a个苹果里选红苹果,则下一个值为从a-1个苹果里再选b-1个苹果,第一个值并没有选择红苹果,则两个值都是在剩下的a-1个苹果里选择的。

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 2010, mod = 1e9 + 7;
int c[N][N];
void init()
{
    for (int i = 0; i < N; i ++ )
        for (int j = 0; j <= i; j ++ )
            if (!j) c[i][j] = 1;
            else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
}
int main()
{
    int n;
    init();
    scanf("%d", &n);
    while (n -- )
    {
        int a, b;
        scanf("%d%d", &a, &b);

        printf("%d\n", c[a][b]);
    }
    return 0;
}

求组合数2
数据范围
1≤n≤10000,
1≤b≤a≤10^5
当a,b的范围达到1e5的级别的时候,需要预处理阶乘。
对于除法取模要用到逆元:
在这里插入图片描述
假如inv[b]是b的逆元:
在这里插入图片描述
在这里插入图片描述

#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010, mod = 1e9 + 7;
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;
        a = (LL)a * a % p;
        k >>= 1;
    }
    return res;
}
int main()
{
    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] * qmi(i, mod - 2, mod) % mod;
    }
    int n;
    scanf("%d", &n);
    while (n -- )
    {
        int a, b;
        scanf("%d%d", &a, &b);
        printf("%d\n", (LL)fact[a] * infact[b] % mod * infact[a - b] % mod);//及时取模防止一次乘爆
    }

    return 0;
}

求组合数3
数据范围
1≤n≤20,
1≤b≤a≤10^18,
1≤p≤10^5
a,b的范围很大,mod的值变小:
复杂度接近 O ( p ) O(p) Op
使用卢卡斯定理:
在这里插入图片描述

#include <algorithm>
#include <iostream>
using namespace std;
typedef long long LL;
int qmi(int a, int k, int p) //快速幂求逆元
{
    int res = 1;
    while (k) 
    {
        if (k & 1) res = (LL)res * a % p;
        a = (LL)a * a % p;
        k >>= 1;
    }
    return res;
}
//从(a-b+1)*....a/1*2...b   分子有b项
LL C(LL a, LL b, LL p) 
{
    if (b > a) return 0;
    LL res = 1;
    LL inv=1;
    for (LL i = 1, j = a; i <= b; i++, j--) 
    {
        res = (LL)res * j % p;
        inv = (LL)inv * i % p;
    }
    res = (LL)res * qmi(inv, p - 2, p) % p;
    return res;
}

int lucas(LL a, LL b, int p) 
{
    if (a < p && b < p) return C(a, b, p);//小于p就直接做
    return (LL)C(a % p, b % p, p) * lucas(a / p, b / p, p) % p;
}

int main() 
{
    int n;
    cin >> n;
    while (n--) 
    {
        LL a, b;
        int p;
        cin >> a >> b >> p;
        cout << lucas(a, b, p) << endl;
    }
    return 0;
}

求组合数 IV
数据范围
1≤b≤a≤5000
在这里插入图片描述
在这里插入图片描述
a的阶乘的个数等于a中所有质数p的倍数对p下取整的和,但是p^2 由于已经在求p的倍数的时候算过了,所以求p^2 的时候要再除p,依次类推。
最后a的阶乘就变成了这种形式:
在这里插入图片描述
在这里插入图片描述
最终的答案就是对每一个因子p,分子上的减分母上的,再把它们乘起来。

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
const int N = 5010;
int primes[N], cnt;
int sum[N];
bool st[N];
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;
        }
    }
}

int get(int n, int p) //n这个数字是p这个数的几倍
{
    int res = 0;
    while (n) 
    {
        res += n / p;
        n /= p;
    }
    return res;
}

vector<int> mul(vector<int> a, int b)//高精度乘法
{
    vector<int> c;
    int t = 0;
    for (int i = 0; i < a.size(); i++) 
    {
        t += a[i] * b;
        c.push_back(t % 10);
        t /= 10;
    }
    while (t) 
    {
        c.push_back(t % 10);
        t /= 10;
    }
    return c;
}

int main() 
{
    int a, b;
    cin >> a >> b;
    get_primes(a);
    for (int i = 0; i < cnt; i++) 
    {
        int p = primes[i];
        sum[i] = get(a, p) - get(a - b, p) - get(b, p);//每一个因子在总的式子中出现的次数
        //就是这个因子在分子的指数减分母的指数。
    }
    vector<int> res;
    res.push_back(1);
    for (int i = 0; i < cnt; i++)//枚举所有的质数
        for (int j = 0; j < sum[i]; j++)//枚举每一个质数的次数 
          res = mul(res, primes[i]);
    for (int i = res.size() - 1; i >= 0; i--) printf("%d", res[i]);
    puts("");
    return 0;
}

扩展欧几里得算法

给定 n 对正整数 ai,bi,对于每对数,求出一组 xi,yi,使其满足 ai×xi+bi×yi=gcd(ai,bi)。

输入格式
第一行包含整数 n。

接下来 n 行,每行包含两个整数 ai,bi。

输出格式
输出共 n 行,对于每组 ai,bi,求出一组满足条件的 xi,yi,每组结果占一行。

本题答案不唯一,输出任意满足条件的 xi,yi 均可。

数据范围
1≤n≤105,
1≤ai,bi≤2×109
输入样例:

2
4 6
8 18

输出样例:

-1 1
-2 1

在这里插入图片描述

#include <iostream>
#include <algorithm>

using namespace std;

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

int main()
{
    int n;
    scanf("%d", &n);

    while (n -- )
    {
        int a, b;
        scanf("%d%d", &a, &b);
        int x, y;
        exgcd(a, b, x, y);
        printf("%d %d\n", x, y);
    }

    return 0;
}

线性同余方程(扩欧的应用)

给定 n 组数据 a i , b i , m i ai,bi,mi ai,bi,mi,对于每组数求出一个 x i x_i xi,使其满足 a i × x i ≡ b i ( m o d m ) ai×xi≡bi(mod_m) ai×xibi(modm),如果无解则输出 impossible。

输入格式
第一行包含整数 n。

接下来 n 行,每行包含一组数据 ai,bi,mi。

输出格式
输出共 n 行,每组数据输出一个整数表示一个满足条件的 xi,如果无解则输出 impossible。

每组数据结果占一行,结果可能不唯一,输出任意一个满足条件的结果均可。

输出答案必须在 int 范围之内。

数据范围
1 ≤ n ≤ 1 0 5 , 1≤n≤10^5, 1n105,
1 ≤ a i , b i , m i ≤ 2 × 1 0 9 1≤ai,bi,mi≤2×10^9 1ai,bi,mi2×109
输入样例:

2
2 3 6
4 3 5

输出样例:

impossible
-3

在这里插入图片描述

#include<bits/stdc++.h>
using namespace std;
int n, x, y;
using LL = long long ;
int exgcd(int a, int b, int &x, int &y)
{
    if (!b)
    {
        x = 1, y = 0;
        return a;
    }
    int d = exgcd(b, a % b, x, y);
    int z = x;
    x = y;
    y = z - a / b * y;
    return d;
}

int main()
{
    cin >> n;
    while (n --)
    {
        int a, b, m;
        cin >> a >> b >> m;
        int d = exgcd(a, m, x, y);//次方程的x 为 gcd的解,y=-y 。
        if (b % d) puts("impossible");//如果b不是gcd的倍数就无解,看证明
        else 
        {   //如果b是gcd的倍数,那么解得的 x 是满足方程最小的根
            //因为满足扩欧方程的x,y一定是gcd的倍数,而此时解gcd得方程的x一定是最小的根
            x = (LL)x * b / d % m;//最小的x乘以倍数即为x的解
            cout << x << endl;
        }
    }
    return 0;
}

注意:对于这样一个式子:
a ⋅ x + b ⋅ y = d 得 到 的 x , y 并 不 是 唯 一 解 a·x+b·y = d得到的x,y并不是唯一解 ax+by=dx,y
显 然 a ⋅ ( x − b d ) + b ⋅ ( y + a d ) = d 也 是 成 立 的 显然a·(x-\dfrac{b}{d})+b·(y+\dfrac{a}{d})=d也是成立的 a(xdb)+b(y+da)=d
− a ⋅ b d + a ⋅ b d = 0 -\dfrac{a·b}{d}+\dfrac{a·b}{d}=0 dab+dab=0
所 以 通 解 x = x 0 + k b d 所以通解x=x_0+k\dfrac{b}{d} x=x0+kdb
所 以 通 解 y = y 0 − k a d 所以通解y=y_0-k\dfrac{a}{d} y=y0kda

求mod非质数p的逆元(扩欧的应用)

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

int main() 
{   int a;
    cin >> a >> b;    // b = mod;
    int x, y,;
    exgcd(a, b, x, y);
    if (x < 0) x += mod;//第一个非负的x即为a在mod b 意义下的逆元
    return x;
}

容斥原理(能被整除的数)

给定一个整数 n 和 m 个不同的质数 p1,p2,…,pm。

请你求出 1∼n 中能被 p1,p2,…,pm 中的至少一个数整除的整数有多少个。

输入格式
第一行包含整数 n 和 m。

第二行包含 m 个质数。

输出格式
输出一个整数,表示满足条件的整数的个数。

数据范围
1≤m≤16,
1≤n,pi≤109
输入样例:

10 2
2 3

输出样例:

7

样例解释:
n = 10, p1=2,p2=3, 求1-10中能满足能整除p1或p2的个数, 即2,3,4,6,8,9,10,共7个。
根据韦恩图得出的容斥原理:
在这里插入图片描述
以题目样例为例
S1={2,4,6,8,10},S2={3,6,9},S1⋂S2={6},故S1⋃S2={2,3,4,6,8,9,10}
实现:
(1)Si如何表示:|Si|=n/pi
这道题不关注最终答案的集合是谁,所以只需要求出最终集合里的元素个数:如果对于p1…pk,满足pk<n ,(如果pk>n没有意义), 如果n%2==0, n/pk 即为n中pk倍数的个数,如果 n n%2 !=0 n,那么下取整[n/pk]为最多的n中pk倍数的个数。
(2)|Sm⋂Sn|如何表示:|Sm⋂Sn|=n/(pm*pn)
因为p为质数,这些质数的乘积就是他们的最小公倍数,n除这个最小公倍数就是交集的大小:举个例子,|S1⋂S2|= n / ( p 1 ∗ p 2 ) = 10 / ( 2 ∗ 3 ) = 1 n/(p1∗p2)=10/(2∗3)=1 n/(p1p2)=10/(23)=1,所以样例答案为 5 + 3 - 1 =7
(3)表示集合的状态:二进制
假设存在一个质数p=4,它的二进制为1101。那么就可以表示选中集合为S1,S2,S4三个集合——即以某一位数的1与0代表选取与否。然后计数选取集合的个数,根据容斥原理的公式,我们通过个数的奇偶判断某一项的符号 ( − 1 ) 3 − 1 (−1)^{3-1} (1)31 = = = + 1 +1 +1,那么对于这一项对答案的贡献就是:res+=n/(p1∗p2∗p4)。我们可以枚举0000到1111的每一个状态来求得每一项对答案的贡献即为最终答案。

#include <iostream>
using namespace std;
typedef long long LL;

const int N = 20;
int p[N], n, m;

int main() 
{
    cin >> n >> m;
    for (int i = 0; i < m; i++) cin >> p[i];

    int res = 0;
    //枚举从1 到 1111...(m个1)-(2^m位) 的每一个集合状态
    for (int i = 1; i < 1 << m; i++) //i<1<<m就是枚举到2^m-1,
    {//容斥原理复杂度2^n,减去一个集合不选的情况2^n-1
        int t = 1;  //选中集合对应质数的乘积
        int s = 0;  //选中的集合数量

        //枚举当前状态的每一位
        for (int j = 0; j < m; j++) 
        {
            //选中一个集合
            if (i >> j & 1) //i的二进制的第j位是不是1
            {
                //乘积大于n, 则n/t = 0, 没有意义,跳出这轮循环
                if ((LL)t * p[j] > n) 
                {
                    t = -1;
                    break;
                }
                s++;  //每有一个1,集合数量+1
                t *= p[j];
            }
        }

        if (t == -1) continue;

        if (s % 2 != 0)
            res += n / t;  //选中奇数个集合, 则系数应该是1, n/t为当前这种状态的集合数量
        else
            res -= n / t;  //反之则为 -1
    }

    cout << res << endl;
    return 0;
}

中国剩余定理

在这里插入图片描述

表达整数的奇怪方式

#include<iostream>

using namespace std;

typedef long long LL;//数据范围比较大,所以用LL来存储

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

int main()
{
    int n;
    LL a1,m1;
    cin>>n>>a1>>m1;
    LL x=0;
    for(int i=1;i<n;i++)
    {
        LL a2,m2;
        cin>>a2>>m2;
        LL k1,k2;
        LL d=exgcd(a1,a2,k1,k2);
        if((m2-m1)%d)
        {
            x=-1;
            break;
        }
        k1*=(m2-m1)/d;
        //因为此时k1是k1*a1+k2*a2=d的解,所以要乘上(m2-m1)/d的倍数大小
        LL t=abs(a2/d);
        k1=(k1%t+t)%t;
        //数据比较极端,所以只求k的最小正整数解
        m1=k1*a1+m1;
        //m1在被赋值之后的值为当前"x"的值,此时赋值是为了方便下一轮的继续使用
        a1=abs(a1*a2/d);
        //循环结束时a1的值为当前所有的a1,a2,……an中的最小公倍数
    }
    if(x!=-1)
    x=(m1%a1+a1)%a1;
    //当循环结束时,此时的值应该与最小公倍数取模,以求得最小正整数解
    printf("%lld\n",x);
    return 0;
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值