NOIP模拟赛 数论专题 扩展欧几里得 + 组合数 + 容斥原理

来源:互联网 发布:seo中代码优化 编辑:程序博客网 时间:2024/05/20 06:08

Problem 1. pay

Input file: pay.in
Output file: pay.out
Time limit: 1 second
Memory limit: 256 MB
Mr.Hu 开了个饭店,来了两位客人:Alice 和Bob,他们吃完饭要结账时,发现他们需要支付c 元钱,但
是Alice 只有面值为a 的钱,Bob 只有面值为b 的钱(他们每个人的钱的和都大于c, 即可以认为他们有
无数张对应面值的钱)。现在,Mr.Hu 想知道,他们可能刚好支付完饭钱吗?如果可能,那么有多少种方
式?你还需要计算出他们所有可能的支付方式的支付的钱的张数的和。
Input
第1 行包含1 个整数:T opt,其中T 表示数据组数,opt 为数据类型。
接下来T 行,每行3 个整数:a b c。
Output
对于每组数据:
• 如果opt = 1,输出一行,包含一个整数:A,其中A 表示刚好支付的方案数。
• 如果opt = 2,输出一行,包含两个整数:A B,其中A 表示刚好支付的方案数,B 表示所有可能
支付方式的张数和。
Sample
pay.in pay.out
2 2
3 4 21
2 4 12
2 13
4 18
样例解释:
对于3 4 21,一共有两种可能的支付方式,分别是:(3; 3); (7; 0)1,所以A 为2,B 为3+3+7+0 = 13。
对于2 4 12,一共有四种可能的支付方式,分别是:(6; 0); (4; 1); (2; 2); (0; 3),所以A 为4,B 为
6 + 0 + 4 + 1 + 2 + 2 + 0 + 3 = 18。
pay.in pay.out
2 1
3 4 21
2 4 12
24
Note
• 对于20% 的数据,1  a; b; c  10000,1  T  1000;
• 对于另外40% 的数据,1  a; b; c  109,其中opt = 1;
• 对于另外40% 的数据,1  a; b; c  109,其中opt = 2;
• 对于100% 的数据,1  T  105,1  opt  2。

题解

就是求ax + by = c.
要求x和y都非负.
我们通过扩展欧几里得获得一组可行解. 那么由于我们知道x变化的最小正周期是b/d, 那么(x%mod + mod) % mod就能获得最小x非负解. 此时y最大. 这样我们就可以求出x最大最小解, y最大最小解, 用x最大解-最小解之差除以x最小正周期 + 1就是方案数.

再来看求和. 我们发现x每增加b/d, y就减少a/d, 那么x每变化b/d就会使和+ b/d - a/d. 那么这就是一个方案数为项数, 首项为xmin + ymax的等差数列, O(1)求和.

虽然是签到题, 但是好久没写扩欧的我还重新推了一遍….

#include<stdio.h>typedef long long dnt;int T, opt;dnt gcd(dnt a, dnt b){    return (!b) ? a : gcd(b, a % b);}void exgcd(dnt a, dnt b, dnt &x, dnt &y){    if(!b){        x = 1;        y = 0;        return;    }    dnt xx_1, yy_1;    exgcd(b, a % b, xx_1, yy_1);    x = yy_1, y = xx_1 - a / b * yy_1;}inline void gozero(){    if(opt == 1) puts("0");    else puts("0 0");}inline void melodyy(dnt a, dnt b, dnt c){    dnt d = gcd(a, b);    if(c % d){        gozero();        return;    }    a /= d, b /= d, c /= d;    dnt x, y;    exgcd(a, b, x, y);    x *= c, y *= c, a *= d, b *= d, c *= d;    dnt xL = b / d, xmin = (x % xL + xL) % xL;    dnt ymax = (c - xmin * a) / b;    if(ymax < 0) {gozero(); return;}    dnt yL = a / d, ymin = (y % yL + yL) % yL;    dnt xmax = (c - ymin * b) / a;    if(xmax < 0) {gozero(); return;}    dnt num = (xmax - xmin) / xL + 1;    dnt base = xmin + ymax, ch = xL - yL;    dnt sum = (base + base + ch * (num - 1)) * num / 2;    if(opt == 1) printf("%I64d\n", num);    if(opt == 2) printf("%I64d %I64d\n", num ,sum);}int main(){    freopen("pay.in", "r", stdin);    freopen("pay.out", "w", stdout);    scanf("%d%d", &T, &opt);    dnt a, b, c;    while(T--){        scanf("%I64d%I64d%I64d", &a, &b, &c);        melodyy(a, b, c);    }    return 0;}

Problem 2. sumcomb

Input file: sumcomb.in
Output file: sumcomb.out
Time limit: 1 second
Memory limit: 256 MB
给你杨辉三角.
现在,Mr.Hu 站在(n;m) 这个位置,他想知道,他向上或向左上方45 度望去,看到的数的和是多少。
从(n;m) 向上望去,他会看到(n;m); (n �� 1;m); (n �� 2;m);    ; (0;m) 这些位置。
从(n;m) 向左上方45 度望去,他会看到(n;m); (n �� 1;m �� 1);    ,直到某一维的下标变为0.
这个数可能很大,你只需将答案对109 + 7 取模即可。
Input
第1 行一个整数:T,表示数据组数。
接下来T 行,每行格式为:dir n m,其中dir 为1 表示向上看,2 表示向左上方看,(n;m) 为Mr.Hu
现在的位置。
Output
对于每组数据,输出一行表示答案。
Sample
sumcomb.in sumcomb.out
21
3 2
2 3 2
46
表格左上角长成这样(行列都是0 base 的):
1 0 0 0
1 1 0 0
1 2 1 0
1 3 3 1
这样从(3; 2) 向上看,会看到:3 1 0 0,和为4。
向左上角看,会看到:3 2 1,和为6。
Note
• 对于30% 的数据,1  n;m  5000,1  T  1000;
• 对于100% 的数据,1  n;m  106,1  T  50000。

题解

大水题一道, 稍微看看就能发现操作1答案就是C(n +1, m + 1). 另外一个就是C(n +1,m).

三种发现方法:
1. 推公式. C(n , m) +C(n-1, m) …. C(m, m)实际上只要在末尾加一个C(m, m + 1)即0就可以往前不断组合成C(n +1, m + 1). 另一个同理.
2.移项: 最上方或者最左上方其实都是1, 把那个1变换位置一下, 就可以在杨辉三角上不断组合得答案.
3.打表.

#include<stdio.h>typedef long long dnt;const int mod = 1e9 + 7;const int maxn = 1e6 + 5;int T, opt;dnt calc[maxn + 5];inline dnt inv(dnt a){    dnt b = mod - 2;    dnt ans = 1;    while(b){        if(b & 1) ans = ans * a % mod;        a = a * a % mod, b >>= 1;    }    return ans;}inline dnt C(int n, int m){    dnt vv = calc[m] * calc[n - m] % mod;    return calc[n] * inv(vv) % mod;}int main(){    freopen("sumcomb.in", "r", stdin);    freopen("sumcomb.out", "w", stdout);    scanf("%d", &T);    calc[0] = 1;    for(int i = 1; i <= maxn; ++i) calc[i] = i * calc[i - 1] % mod;    while(T--){        int n, m;        scanf("%d%d%d", &opt, &n, &m);        if(m > n){            puts("0"); continue;        }        if(opt == 1) printf("%I64d\n", C(n + 1, m + 1));        else printf("%I64d\n", C(n + 1, m));    }    return 0;}/*21 3 22 3 2*/

Problem 3. kor

Input file: kor.in
Output file: kor.out
Time limit: 1 second
Memory limit: 256 MB
Mr.Hu 觉得在学习过程中,需要举一反三,做一题要理解透,然后遇到相似的问题时能类似地转化。所
以想了一道和以前类似的题目,相信聪明如你,肯定能轻而易举地解决。
Mr.Hu 会给你n 个非负整数,然后从中选k 个出来,然后把这k 个数按位或起来,Mr.Hu 想知道有多
少种选法,使得或起来的结果为r。
Input
第1 行一个整数T,表示测试组数。
接下来T 组数据,对于每组数据:
第1 行两个整数n k r。
接下来1 行包含n 个非负整数:a1 a2 : : : an。
Output
对于每组数据,输出一行,包含一个整数,即方案数,因为结果可能很大,只需要对109 + 7 取模即可。

Sample
kor.in kor.out
24
2 3
1 2 3 4
4 1 1
1 2 3 4
31
对于第一组数据,一共有3 种选法:(1; 2); (1; 3); (2; 3)。
对于第二组数据,一共有1 种选法:(1)。
Note
• 对于10% 的数据,1  n  10,0  ai < 210;
• 对于30% 的数据,1  n  100,0  ai < 210;
• 对于50% 的数据,1  n  105,0  ai < 215;
• 对于100% 的数据,1  n  105,0  ai < 220,1  k  n,1  T  5。

题解

肯定是容斥.
首先考虑如果不是或是与的话怎么做.

2.1 10%
暴力dfs 所有k 组合即可.
复杂度O(C(n; k)).
2.2 30%
考虑动态规划, 状态dp[i][j][S] 表示前i 个数选了j 个出来, 其按位与为S 的方
案数, 每次O(1) 转移.
复杂度O(n22k), 其中k 为最大位数.
2.3 60%
考虑容斥原理.
我们用cnt1[S] 表示值为S 的数的个数, 通过它可以计算出cnt2[S], 表示值”
包含”S 的数的个数(a 包含b 当且仅当a&b = b).
这一步可以先枚举一个s, 然后枚举U ��s 的子集ss, 将所有cnt1[sjss] 加到
cnt2[s] 中即可, 复杂度(3k),k 为最大位数.
然后通过cnt2[S] 可以计算出cnt3[S], 表示从原来的n 个数中选择k 个并
取交后, 该值包含S 的方案数. 显然有cnt3[s] = comb(cnt2[s]; k).
然后计算cnt4[S], 表示取k 个数取交后结果为S 的方案数. 和第二步类似,
只是每次从加变成减(从大到小枚举).
以上第一步和第三步复杂度为O(3k), 第二步为O(2k)
总的复杂度为O(3k + n)
2.4 100%
总的思想和上面一致, 只是我们可以将第一步和第三步复杂度优化到O(k2^k), 枚举子集来优化容斥, 下面有讲.

再考虑或.
或我们只需要求出cnt[S]表示能组成S的子集的方案有多少.
那么或起来刚好为S的只需要减去这个cnt[S的子集]. 但是我们会发现要删重, 那么这里就是容斥原理.

说到容斥原理的优化的话. 恩其实我的理解方法比较抽象. 所以这里抱歉的写几句方便自己以后能复习看懂的话, 如果是其他读者的话只能期望看到这几句话有所启发了.

我们需要所有cnt子集都被枚举到且只被算到一次. 其实我们只需要一位一位的传递就可以了, 把小的传递给大的, 这个看代码就知道. 但是怎么保证只贡献一次呢. 其实我们会发现i是单调递增的. 比如0000贡献给了0001, 0010, 0100, 1000(只差一位), 那么我们就要求这4个数贡献给别人的时候不会重复. 但是0010再去贡献的时候, 由于i是单增的素以永远第0位不会为1. 但是0001贡献出去的由于是或所以第0为一定有1, 这就说明了他们没有交集. 其他的类似证明. 对于容斥里的加减在这里只需要-. 因为贡献每传递一次符号就取反. -+ = -. – = +.

#include<stdio.h>#include<cstring>#define clear(a) memset(a, 0, sizeof(a))typedef long long dnt;const int P = 20;const int mod = 1e9 + 7;const int maxn = 1e5 + 10;dnt calc[maxn + 5], inv[maxn + 5];int n, k, r, T, pw[22], cnt[1 << P], a[maxn];inline dnt mpow(dnt a){    dnt b = mod - 2;    dnt ans = 1;    while(b){        if(b & 1) ans = ans * a % mod;        a = a * a % mod, b >>= 1;    }    return ans;}inline dnt C(int n, int m){    if(m > n) return 0;    return calc[n] * inv[m] % mod * inv[n - m] % mod;}inline void fix(int &x){    if(x >= mod) x -= mod;    if(x < 0) x += mod;}inline void getcnt2(){    for(int i = 0; i < P; ++i){        int s = (pw[P] - 1) ^ pw[i];        for(int ss = s; ss > 0; ss = (ss - 1) & s){            cnt[ss | pw[i]] += cnt[ss];            fix(cnt[ss | pw[i]]);        }        cnt[pw[i]] += cnt[0];        fix(cnt[pw[i]]);    }}inline void getcnt4(){    for(int i = 0; i < P; ++i){        int s = (pw[P] - 1) ^ pw[i];        for(int ss = s; ss > 0; ss = (ss - 1) & s){            cnt[ss | pw[i]] -= cnt[ss];            fix(cnt[ss | pw[i]]);        }        cnt[pw[i]] -= cnt[0];        fix(cnt[pw[i]]);    }}int main(){    freopen("kor.in", "r", stdin);    freopen("kor.out", "w", stdout);    scanf("%d", &T);    pw[0] = 1, calc[0] = 1;    for(int i = 1; i <= P; ++i) pw[i] = pw[i - 1] << 1;    for(int i = 1; i <= maxn; ++i) calc[i] = i * calc[i - 1] % mod;    inv[maxn] = mpow(calc[maxn]);    for(int i = maxn - 1; ~i; --i)        inv[i] = inv[i + 1] * (i + 1) % mod;    while(T--){        clear(cnt);        scanf("%d%d%d", &n, &k, &r);        for(int i = 1; i <= n; ++i){            scanf("%d", &a[i]);            cnt[a[i]]++;        }        getcnt2();        for(int i = 0; i < pw[P]; ++i)            cnt[i] = C(cnt[i], k);        getcnt4();        printf("%d\n", cnt[r]);    }}/*24 2 31 2 3 44 1 11 2 3 4*/
阅读全文
0 0