排列组合主要应用在计数原理的问题上
加法原理:做一件事情,完成它有N类办法,在第一类办法中有M1种不同的方法,在第二类办法中有M2种不同的方法,……,在第N类办法中有M(N)种不同的方法,那么完成这件事情共有M1+M2+……+M(N)种不同的方法 (摘自百度百科)
乘法原理:做一件事,完成它需要分成n个步骤,做第一 步有m1种不同的方法,做第二步有m2不同的方法,……,做第n步有mn不同的方法。那么完成这件事共有 N=m1m2m3…mn 种不同的方法 (摘自百度百科)
排列的定义及其计算公式:从n个不同元素中,任取m(m≤n)个元素按照一定的顺序排成一列,叫做从n个不同元素中取出m个元素的一个排列;从n个不同元素中取出m(m≤n)个元素的所有排列的个数,叫做从n个不同元素中取出m个元素的排列数,用符号 A(n,m)表示 A(n,m)=n(n-1)(n-2)……(n-m+1)= n!/(n-m)! 此外规定0!=1
组合的定义及其计算公式:从n个不同元素中,任取m(m≤n)个元素并成一组,叫做从n个不同元素中取出m个元素的一个组合;从n个不同元素中取出m(m≤n)个元素的所有组合的个数,叫做从n个不同元素中取出m个元素的组合数。用符号 C(n,m) 表示 C(n,m)=A(n,m)/m!=n!/((n-m)!*m!);C(n,m)=C(n,n-m)。
环排列:从n个元素中取出m个元素的循环排列数=A(n,m)/m=n!/m(n-m)!.
多组组合:n个元素被分成k类,每类的个数分别是n1,n2,...nk这n个元素的全排列数为 n!/(n1!×n2!×...×nk!).
可重组合: k类元素,每类的个数无限,从中取出m个元素的组合数为C(m+k-1,m)。
该问题可引申到,k个非负整数xi{1<=i<=k}之和为m(m>=0),满足∑{1<=i<=k}xi=m的非负整数解有多少组
如果令yi=xi+1,n=m+k,则还可引申为,k个正整数yi{1<=i<=k}之和为n(n>=k),满足∑{1<=i<=k}yi=n的正整数解有多少组
范德蒙恒等式:对任意0<=k<=n,有C(n,m)=∑{0<=i<=m}C(k,i)C(n-k,m-i)
二项式定理:(x+y)^n =∑{0<=k<=n} C(n,k)x^(n-k)y^k
1.2 递推关系等差数列的通项公式为:an=a1+(n-1)d
前n项和公式为:Sn=n(a1+an)/2
等比数列(q不为0)的通项公式为:an=a1*q^(n-1)
前n项和公式为:
q不为1 Sn=a1*(1-q^n)/(1-q)
以及q=1 Sn=na1
线性常系数齐次递推关系:∑{0<=i<=k}CiAn-i=0,其中C0=1,并已知Ai(0<=i<k)
则特征方程为,∑{0<=i<=k}Cix^(k-i)=0
设其在复数域内共有m个不同的根,Ri(1<=i<=m),
且Ri是方程的Ji重根,满足∑{1<=i<=m}Ji=k
那么,原递推式通解为:An=∑{1<=i<=m}(Ri^n*∑{0<=j<Ji}Fij*n^j)
其中Fij为待定系数
如若,是非齐次,如∑{0<=i<=k}CiAn-i=G(n),而G(n)是多项式
那么可以将原式反复差分,从而齐次化,再依旧用待定系数法求解
求解一般递推数列中的不动点法
先得到原式的特征方程F(G(x))
然后求解F(G(x))=G(x)的根(简单起见许多时候可以就令G(x)=x)
设跟为a,b,c,d.....
那么很有可能方程可以化成形式是x-a,x-b,x-c,x-d.....等等的整数幂(包括负数幂,就是作分母)的乘积的和
如此再用迭代的方式求出来
1.3 素数
●知识精讲
质数,也称素数,是指因子只有1和其本身的数。对应的,还能被其他整数整除的数称为合数。0和1既不是素数也不是合数。素数在数论中有着举足轻重的地位,在ACM中也常常出现,常见的有判断一个数是否为素数,求某范围内的素数,分解质因数,求因子个数,因子和,以及与其他数论定理相结合的题目等等。
关于素数有许多的构造定理和猜想,详细的资料可自行查询,这里仅对题目中常见的有关素数的应用做简单介绍。
●例题分析
1.判断素数的方法
(1)直接按照定义判断
bool check(int n)
{
if (n<=1) return false;
if (n==2) return true;
for (int i=3; i<=sqrt(n); i+=2)
if (n%i==0)
return false;
return true;
}
(2)素数筛法
像“筛子”一样将一定范围内的素数筛选出来。
void Getprime()
{
memset(prime,0,sizeof(prime));
for (int i=3; i<=N; i++)
if (i%2==0) prime[i] = false;
else prime[i] = true;
prime[2] = true;
prime[1] = false;
for (int i=3; i<=sqrt(N); i+=2)
if (prime[i])
for (int j=i*i; j<N; j+=2*i)
prime[j] = false;
}
(3)费马小定理测试(定理)
费马小定理:如果p是一个素数,且0<a<p,则a^(p-1)%p=1. 利用费马小定理,对于给定的整数n,可以设计素数判定算法,通过 计算d=a^(n-1)%n来判断n的素性,当d!=1时,n肯定不是素数,当d=1时,n 很可能是素数.
(4)Miller-Rabbin素数测试法
二次探测定理:如果p是一个素数,且0<x<p,则方程x^2%p=1的解为:x=1或x=p-1. 利用二次探测定理,可以再利用费尔马小定理计算a^(n-1)%n的过程 中增加对整数n的二次探测,一旦发现违背二次探测条件,即得出n不是素数的结论.
如果n是大于2的素数,则(n-1)必是偶数,因此可令(n-1)=m*(2^q),其中m是正奇数( 若n是偶数,则上面的m*(2^q)一定可以分解成一个正奇数乘以2的k次方的形式 ),q是非负整数,考察下面的测试: 我们要选择一个随机的整数p (1<=p<=n-1),接下来判断 p^m=1 (mod n),如果成立,我们就说n通过了测试,但是有可能不是素数也能通过测试。所以我们通常要做多次这样的测试,以确保我们得到的是一个素数。Miller 测试进行k次,将合数当成素数处理的错误概率最多不会超过4^(-k).
bool Miller_rabin(INT n)
{
if (n==2) return true;
if (n<2 || !(n&1)) return false; //先计算出m、j,使得n-1=m*2^k,其中m是正奇数,k是非负整数
INT m = n-1,k = 0;
while (!(m&1))
{
k++;
m >>= 1;
}//得到m,k
for (int i=0; i<9; i++)
{//连续测试
if (p[i] >= n) return true;
INT u = power_mod(p[i],m,n); //随机取一个p计算 p^m % n
If (u == 1) continue; //一组测试完成通过
for (int j=0; j<k; j++){
INT buf = product_mod(u,u,n);//计算u*u%n
if (buf==1 && u!=1 && u!=n-1) return false; //二次检测非素数
u = buf;
}
if (u>1) return false;
}
return true;
}
2.欧拉函数
对正整数n,欧拉函数φ是少于或等于n的数中与n互质的数的数目。φ函数的值通式:φ(x)=x(1-1/p1)(1-1/p2)(1-1/p3)(1-1/p4)…..(1-1/pn),其中p1, p2……pn为x的所有质因数,x是不为0的整数。φ(1)=1(唯一和1互质的数就是1本身)。若m,n互质,φ(mn)=φ(m)φ(n)。
int Euler(int n)
{
int ans = n;
for (int i=2; i<=sqrt(n); i++)
{
if (n%i==0)
{
ans = ans-ans/i;
while (n%i==0) n /= i;
}
}
if (n>1)
ans = ans-ans/n;
return ans;
}
1.4 二进制
●知识精讲
进制也就是进位制,是人们规定的一种进位方法。 对于任何一种进制---X进制,就表示某一位置上的数运算时是逢X进一位。在这里主要讨论进制转换和位运算两个问题。
1.进制转换
所谓进制只是一个权重,在A进制下,数字实际值是各位数字的"权值*权重"的累加值。 而"权重"为A的n次方,n代表位数。
用公式来表示就是:
abcd = a * A^3 + b * A^2 + c * A^1 + d * A^0
举个直观的例子来说 在7进制下,数字 1234 的大小应该是 1 * 7^3 + 2 * 7^2 + 3 * 7^1 + 4 * 7^0 =1*343 + 2*49 + 3*7 + 4*1 =466
用这个方法可以将十进制转化为任意进制。
十进制转任意进制的方法一般有两种:
1.试减法 2.短除法
总的来说,方法1适合笔算,方法2适合计算机算。试减法就是通过估算反复减去不大于目标数字的权重的n次方来得到每一位的数字,事实上试探的过程。短除法通过反复短除目标数求余来得到每一位上的数字:
比如 1234 转 5 进制 1234 / 5 = 246余 4 246 / 5 = 49 余 1 49 / 5 = 9 余 4 9 / 5 = 1 余 4 1 / 5 = 0 余 1 可以看出,所有的余数就构成了转化的结果 14414。
通常的A 进制转 B 进制的做法是先将 A 进制转换为十进制,再将十进制的数字转化为B进制。
而对于大数的进制转换,则不能直接短除,我们改为按位除,但整个过程仍然是短除法的思想。比如将1001由十进制转化为二进制,第一轮从最高位1开始,1/2商0余1,计算0时,不是直接用0来除以2,而是由上一位的余数*基数10+此位数,即被除数为1*10+0=10,10/2商5余0,以此类推,得到商为0500,即500,最后一位的余数为1,第一轮完成,余数1即为转化后结果的最低位,由500开始下一轮,直到被除数为0。事实上,每一轮都是大数除法的过程,也就是模拟了手动计算除法的过程。
2.位运算总结
在计算机程序中,数据的位是可以操作的最小数据单位,理论上可以用“位运算”来完成所有的运算和操作。一般的位操作是用来控制硬件的,或者做数据变换使用,但是,灵活的位操作可以有效地提高程序运行的效率。C语言提供了位运算的功能, 这使得C语言也能像汇编语言一样用来编写系统程序。
C语言提供了六种位运算符:
& 按位与 | 按位或 ^ 按位异或 ~ 取反 << 左移 >> 右移
1. 按位与运算 按位与运算符"&"是双目运算符。其功能是参与运算的两数各对应的二进位相与。只有对应的两个二进位均为1时,结果位才为1 ,否则为0。参与运算的数以补码方式出现。
例如:9&5可写算式如下: 00001001 (9的二进制补码)&00000101 (5的二进制补码) 00000001 (1的二进制补码)可见9&5=1。 应用: a. 清零特定位 (mask中特定位置0,其它位为1,s=s&mask)(mash是掩码) b. 取某数中指定位 (mask中特定位置1,其它位为0,s=s&mask)
2. 按位或运算 按位或运算符“|”是双目运算符。其功能是参与运算的两数各对应的二进位相或。只要对应的二个二进位有一个为1时,结果位就为1。参与运算的两个数均以补码出现。
例如:9|5可写算式如下: 00001001|00000101 00001101 (十进制为13)可见9|5=13 应用: 常用来将源操作数某些位置1,其它位不变。 (mask中特定位置1,其它位为0 s=s|mask)
3. 按位异或运算 按位异或运算符“^”是双目运算符。其功能是参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。参与运算数仍以补码出现。
例如9^5可写成算式如下: 00001001^00000101 00001100 (十进制为12) 应用: a. 使特定位的值取反 (mask中特定位置1,其它位为0 s=s^mask) b. 不引入第三变量,交换两个变量的值 (设 a=a1,b=b1) 目 标 操 作 操作后状态 a=a1^b1 a=a^b a=a1^b1,b=b1 b=a1^b1^b1 b=a^b a=a1^b1,b=a1 a=b1^a1^a1 a=a^b a=b1,b=a1
4. 求反运算 求反运算符~为单目运算符,具有右结合性。 其功能是对参与运算的数的各二进位按位求反。
例如~9的运算为: ~(0000000000001001)结果为:1111111111110110
5. 左移运算 左移运算符“<<”是双目运算符。其功能把“<< ”左边的运算数的各二进位全部左移若干位,由“<<”右边的数指定移动的位数, 高位丢弃,低位补0。 其值相当于乘2。
例如: a<<4 指把a的各二进位向左移动4位。如a=00000011(十进制3),左移4位后为00110000(十进制48)。
6. 右移运算 右移运算符“>>”是双目运算符。其功能是把“>> ”左边的运算数的各二进位全部右移若干位,“>>”右边的数指定移动的位数。其值相当于除2。
●例题分析
在这里列举位运算的常见应用:
(1) 判断奇偶:a&1 = 0 偶数,a&1 = 1 奇数。
(2) 取int型变量a的第k位 (k=0,1,2……sizeof(int)),即a>>k&1
(3) 将int型变量a的第k位清0,即a=a&~(1 <<k)
(4) 将int型变量a的第k位置1, 即a=a |(1 <<k)
(5) int型变量循环左移k次,即a=a < <k |a>>16-k (设sizeof(int)=16)
(6) int型变量a循环右移k次,即a=a>>k |a < <16-k (设sizeof(int)=16)
(7)整数的平均值
对于两个整数x,y,如果用 (x+y)/2 求平均值,会产生溢出,因为 x+y 可能会大于INT_MAX,但是我们知道它们的平均值是肯定不会溢出的,我们用如下算法:
int average(int x, int y) //返回X,Y 的平均值 { return (x&y)+((x^y)>>1); }
(8)判断一个整数是不是2的幂:
boolean power2(int x) { return ((x&(x-1))==0)&&(x!=0); }
(9)不用temp交换两个整数
void swap(int x , int y) { x ^= y; y ^= x; x ^= y; }
(10)计算绝对值
int abs( int x ) { int y ; y = x >> 31 ; return (x^y)-y ; //or: (x+y)^y }
(11)取模运算转化成位运算 (在不产生溢出的情况下): a % (2^n) 等价于 a & (2^n - 1)
(12)乘法运算转化成位运算 (在不产生溢出的情况下):a * (2^n) 等价于 a << n
(13)除法运算转化成位运算 (在不产生溢出的情况下):a / (2^n) 等价于 a>> n
(14) a % 2 等价于 a & 1
(15) if (x == a) x= b; else x= a;等价于 x= a ^ b ^ x;
(16) x 的 相反数 表示为 (~x+1)
位运算在题目中经常用到,例如利用二进制枚举的题目,状态压缩DP等用到了位运算。
1.5 中国剩余定理
●知识精讲
这个问题源自于我国数学古书《孙子算经》中的一道问题:“今有物,不知其数,三三数之,剩二;五五数之,剩三;七七数之,剩二。问物几何?”意思是一个整数除以三余二,除以五余三,除以七余二,求这个整数(满足条件且最小)。
做法是:
n%3=2,n%5=3,n%7=2且3,5,7互质 找到使(5×7)的倍数模3得1的数,答案是70 找到使(3×7)的倍数模5得1的数,答案是21 找到使(3×5)的倍数模7得1的数,答案是15 那么(70×2+21×3+15×2) % (3×5×7) = 23,23就是最终答案了
理解如下:
70×2 = 140,当中的2是指n%3 = 2的2,因为70%3=1,乘2后使其能满足%3=2的条件,同理, 21×3使其能满足%5=3 15×2使其能满足%7=2 又70能整除5和7,21能整除3和7,15能整除3和5 因此70×2+21×3+15×2 = 233能满足%3=2,%5=3,%7=2的条件 将233对3×5×7=105取模,是为了取得最小的情况,因为105是能够同时整除3,5,7的数,如果减去,对该数满足%3=2,%5=3,%7=2没有影响。因此对105取模,得到的结果便是最小并满足条件的数
拓展开来问题就是有同余方程组 a = ai (mod ni), 求未知数a。 方法是:
定义 n=n1*n2...nk, 其中因子两两互质 mi = n1*n2*...nk / ni; ci = mi(mf mod ni); 其中 mi*mf mod ni = 1; 则 a = (a1*c1+a2*c2+...+ak*ck) (mod n)
中国剩余定理关键是mf的求法,如果理解了扩展欧几里得 ax+by=d, 就可以想到:
mi*mf mod ni = 1 => mi*mf+ni*y=1;
这里的中国剩余定理必须要求除数是互质的,但是有些题目的同余方程式除数并不互质,那么将不能使用传统的中国剩余定理
●例题分析
问题描述:POJ 1006
人自出生起就有体力,情感和智力三个生理周期,分别为23,28和33天。一个周期内有一天为峰值,在这一天,人在对应的方面(体力,情感或智力)表现最好。通常这三个周期的峰值不会是同一天。现在给出三个日期,分别对应于体力,情感,智力出现峰值的日期。然后再给出一个起始日期,要求从这一天开始,算出最少再过多少天后三个峰值同时出现。
问题分析:
这一题的做法可以类比算经中问题的方法
找到使(23×28)的公倍数模33得1的数,答案是1288
找到使(23×33)的公倍数模28得1的数,答案是14421|
找到使(28×33)的公倍数模23得1的数,答案是5544
(5544×p+14421×e+1288×i) % (23×28×33) = ans + d
若使用中国剩余定理的一般情况下的处理,则可按照一般情况的方法写出程序,参考核心程序为:
int mod[3],res[3];//除数,余数
void exGCD(int a,int b,int &x,int &y)//扩展欧几里得
{
if (b==0)
{
x = 1;
y = 0;
return;
}
exGCD(b,a%b,x,y);
int temp = x;
x = y;
y = temp-a/b*y;
}
int china_mod()//中国剩余定理
{
int mul=1,ans=0,mf,y,mi;
for (int i=0; i<3; i++)
mul *= mod[i];
for (int i=0; i<3; i++)
{
mi = mul/mod[i];
exGCD(mi,mod[i],mf,y);
ans += (mi*mf*res[i])%mul;
}
return (ans+mul)%mul;
}