5、数学问题
该部分不涉及很深的算法,却与数学息息相关,不需要特别的数学知识,只要掌握简单的数理逻辑即可。
最大公约数int gcd(int a,int b){return !b?a:gcd(b,a%b);}
这段代码十分简洁,并且实现了如果a<b
,先互换。辗转相除法也称欧几里得算法。
最小公倍数,在最大公约数的基础上计算,为ab/gcd(a,b)
5.2 常用math函数
pow(double r,double p)
返回r^p
log(double x)
返回double型变量的以自然对数为底的对数
sin(double x)
、cos(double x)
、tan(double x)
asin(double x)
、acos(double x)
、atan(double x)
round(double x)
四舍五入、返回类型也是double
5.3 日期处理
int mon[2][13]={0,31,28,31,30,31,30,31,31,30,31,30,31,\
0,31,29,31,30,31,30,31,31,30,31,30,31};
#define is_p(x) ((x%400==0||(x%100!=0&&x%4==0))?1:0)
struct today{
int year,month,day,week;
today(int y,int m,int d,int w):year(y),month(m),day(d),week(w){}
void operator++(){
day++;
week++;
if(week==8)week=1;
int flag=is_p(year);
if(day==mon[flag][month]+1){
day=1;
month++;
//cout<<month<<" "<<day<<endl;
if(month==13){
month=1;year++;
}
}
}
};
5.4 分数的四则运算
套路:实际做题过程中,还是套晴神模板最好,不需要单独设符号位,符号位在分子上即可,每次运算后化简。
建立分数结构类,实现加减乘除运算,符号位不用单独处理,计算时先不管分母正负,处理结果时如果分母为负,令分子分母同时取相反数即可,每次运算结束需要进行约分(注意分子不为0时除以gcd,为0时令分母为1)
int gcd(int a,int b){return b==0?a:gcd(b,a%b);}
struct Fraction{
int up,down;
Fraction(){}
Fraction(int a,int b):up(a),down(b){}
};
Fraction reduction(Fraction result){
if(result.down<0){result.up=-result.up;result.down=-result.down;}
if(result.up==0)result.down=1;
else{
int d=gcd(abs(result.up),result.down);
result.up/=d;
result.down/=d;
}
return result;
}
Fraction add(Fraction f1,Fraction f2){
Fraction result;
result.up=f1.up*f2.down+f2.up*f1.down;
result.down=f1.down*f2.down;
return reduction(result);
}
Fraction minu(Fraction f1,Fraction f2){
f2.up=-f2.up;
return add(f1,f2);
}
Fraction multi(Fraction f1,Fraction f2){
Fraction result;
result.up=f1.up*f2.up;
result.down=f1.down*f2.down;
return reduction(result);
}
Fraction divide(Fraction f1,Fraction f2){
swap(f2.up,f2.down);
return multi(f1,f2);
}
5.5 质数、质因子分解
套路:大量质数时素数筛法,少量时用is_prime函数
is_prime函数O(n^1/2)或者用数组记录过程结果的筛素数法O(nloglogn)。
对于质因子分解,首先看因子分解,如果一个数存在除1和它本身之外的其他因子,那么必定是在sqrt(n)两侧成对出现的。对于质因子而言,可以得到,要么所有的质因子都小于sqrt(n),要么只有一个质因子大于sqrt(n),所以,质因子分解可以先判断2-sqrt(n)之间的所有质数是否是其质因子,最后再判断除去这些质因子后是否为1,否的话说明这是一个比sqrt(n)大的质因子。
bool is_prime(int n){
if(n<=1)return false;
for(long long i=2;i*i<=n;i++){
if(n%i==0)return false;
}
return true;
}
5.6 高精度
实现大整数类,包括运算:高精度加减法、高精度与低精度的乘除法。
struct bign{
int d[1000],len;
bign():len(0){}
bign(char* str,int n):len(n){
for(int i=n-1;i>=0;i--)
d[n-i-1]=str[i]-'0';
}
};
int cmp(bign& f1,bign& f2){
if(f1.len!=f2.len){
if(f1.len>f2.len)return 1;
else return -1;
}else{
for(int i=f1.len-1;i>=0;i--){
if(f1[i]!=f2[i]){
if(f1[i]>f2[2])return 1;
else return -1;
}
}
return 0;
}
}
//加法
bign add(bign& f1,bign& f2){
bign result;
int carry=0;
for(int i=0;i<a.len||i<b.len;i++){
int tmp=a[i]+b[i]+carry;
result[result.len++]=tmp%10;
carry=tmp/10;
}
if(carry!=0)result[result.len++]=carry;
return result;
}
//减法a-b,从低位到高位,要借位
bign sub(bign a,bign b){
bign c;
for(int i=0;i<a.len || i<b.len;i++){
if(a.d[i]<b.d[i]){//如果不够减
a.d[i+1]--;
a.d[i]+=10;
}
c.d[c.len++]=a.d[i]-b.d[i];
}
while(c.len-1>=1&&c.d[c.len-1]==0){
c.len--;
}
return c;
}
//高精度与低精度的乘法
bign multi(bign a,int b){
bign c;
int carry=0;
for(int i=0;i<a.len;i++){
int temp=a.d[i] * b+carry;
c.d[c.len++]=temp%10;
carry=temp/10;
}
while(carry!=0){
c.d[c.len++]=carry%10;
carry/=10;
}
return c;
}
//高精度与低精度的除法
bign divide(bign a,int b,int& r){//r为余数,这里为引用
bign c;
c.len=a.len;
for(int i=a.len-1;i>=0;i--){
r=r*10+a.d[i];//和上一位遗留的余数组合
if(r<b)c.d[i]=0;//不够除,该位为0
else{
c.d[i]=r/b;//商
r=r%b;//获得新的余数
}
}
while(c.len-1>=1&&c.d[c.len-1]==0){
c.len--;
}
return c;
}
java大数
//创建大数类
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Scanner;
Scanner cin=new Scanner(System.in);
BigInteger num1=new BigInteger("12345");
BigInteger num2=cin.nextBigInteger();
BigDecimal num3=new BigDecimal("123.45");
BigDecimal num4=cin.nextBigDecimal();
//BigInterger整数
import java.math.BigInteger;
public class Main {
public static void main(String[] args) {
BigInteger num1=new BigInteger("12345");
BigInteger num2=new BigInteger("45"); //加法
System.out.println(num1.add(num2)); //减法
System.out.println(num1.subtract(num2)); //乘法
System.out.println(num1.multiply(num2)); //除法(相除取整)
System.out.println(num1.divide(num2)); //取余
System.out.println(num1.mod(num2)); //最大公约数GCD
System.out.println(num1.gcd(num2)); //取绝对值
System.out.println(num1.abs()); //取反
System.out.println(num1.negate()); //取最大值
System.out.println(num1.max(num2)); //取最小值
System.out.println(num1.min(num2)); //是否相等
System.out.println(num1.equals(num2));
}
}
//BigDecimal(浮点数)
import java.math.BigDecimal;
public class Main {
public static void main(String[] args) {
BigDecimal num1=new BigDecimal("123.45");
BigDecimal num2=new BigDecimal("4.5"); //加法
System.out.println(num1.add(num2)); //减法
System.out.println(num1.subtract(num2)); //乘法
System.out.println(num1.multiply(num2)); //除法(在divide的时候就设置好要精确的小数位数和舍入模式)
System.out.println(num1.divide(num2,10,BigDecimal.ROUND_HALF_DOWN)); //取绝对值
System.out.println(num1.abs()); //取反 System.out.println(num1.negate()); //取最大值
System.out.println(num1.max(num2)); //取最小值
System.out.println(num1.min(num2)); //是否相等
System.out.println(num1.equals(num2)); //判断大小( > 返回1, < 返回-1)
System.out.println(num2.compareTo(num1));
}
}
5.7 扩展欧几里得算法
ax+by=gcd(a,b).如何求得x,y。边界条件,b=0时x=1,y=0;根据辗转相除法递推可得递推公式x1=y2;y1=x2-(a/b) * y2;
int exGcd(int a,int b,int& x,int& y){
if(b==0){
x=1;y=0;
return a;
}
int g=exGcd(b,a%b,x,y);
int t=x;
x=y;
y=t-(a/b) * y;
return g;
}
方程ax+by=c的整数解,直接用扩展欧几里得算法,因为ax+by=gcd已知
同余式ax=c(mod m)求解,即ax+my=c求解,有解的条件是c%gcd(x,m)==0
逆元的求解。a,b,m>0,且ab=1(mod m),则称a,b互为模m的逆元
常用取模1e9+7
int32位的最大值为2147483647,所以对于int32位来说1000000007足够大
int64位的最大值为2^63-1,对于1000000007来说它的平方不会在int64中溢出
所以在大数相乘的时候,因为(a∗b)%c=((a%c)∗(b%c))%c,所以相乘时两边都对1000000007取模,再保存在int64里面不会溢出
5.8 组合数
比如10!中有2的质因子个数可表示为:有因子2的个数为10/2,有因子2^2的个数为10/2^2,有因子2^3的个数为10/2^3
可以递归求解,也可迭代求解。
即n!中有 $\frac{n}{p}+\frac{n}{p^2}+\frac{n}{p^3}+……$ 个质因子p,其中除法均为向下取整
//迭代
int cal(int n,int p){
int ans=0;
while(n>0){
ans+=n/p;
n/=p;
}
return ans;
}
//递归
int cal(int n,int p){
if(n<p)return 0;
else return n/p+cal(n/p,p);
}
第二个问题:组合数的计算
$C_m^n=\frac{m!}{n!(m-n)!}$
方法一就是通过定义直接计算,但是因为阶乘相当庞大,此种方法计算组合数能接受的数据范围很小。
方法二通过递归公式计算 $Cm^n=C{m-1}^n+C_{m-1}^{n-1}$
方法三通过定义变形来计算$C_m^n=\frac{(m-n+1)(m-n+2)……(m-n+n)}{n!}$,应注意到分子分母均为n项,先计算(m-n+1)/1,乘以(m-b+2)/2,不断乘以(m-n+i).
掌握第一种方法即可,在组合数的计算的递归公式法上改进
int res[1010][1010]={0};
//递归方法
int C(int n,int m,int p){
if(m==0||m==n)return 1;//C(n,0)=C(n,n)=1
if(res[n][m]!=0)return return res[n][m];
return res[n][m]=(C(n-1,m)+C(n-1,m-1))%p;
}
//递推方法
void CalC(){
for(int i=0;i<=n;i++){
res[i][0]=res[i][i]=1;//初始化边界条件
}
for(int i=2;i<=n;i++){
for(int j=0;j<=i/2;j++){
res[i][j]=(res[i-1][j]+res[i-1][j-1])%p;
res[i][i-j]=res[i][j];
}
}
}
5.9 错误重排问题
用A、B、C、D………表示写着n位友人名字的信封,a、b、c、d………表示n份相应的信,把n份信装错的总数记为D(n),那么n-1份信封装错的总数就是D(n-1)。 现在,假设这样一种情况,把a错装进B中,那么对于信b有以下两种分法: 1. b装入A中,这样剩下的(n-2)份信和信封A、B,和信a、b就没有任何关系了,所以这时候装错的可能性总共有D(n-2)。 2. b不一定装入A中,那么就有可能装入A、C、D等其余除B之外的信封了,这时总共就是(n-1)种装错的可能性了。 所以对于信b来说,总共有D(n-2)+D(n-1)种装错的可能性。所以最后除a之外还有(n-1)封信,所以最终的关系式如下: D(n)=(n-1)*[D(n-1)+D(n-2)]
5.10 斐波那契数列
想出递推公式最重要
5.11 约瑟夫问题
n个人围成一个圈,每个人分别标注为1、2、...、n,要求从1号从1开始报数,报到k的人出圈,接着下一个人又从1开始报数,如此循环,直到只剩最后一个人时,该人即为胜利者。例如当n=10,k=4时,依次出列的人分别为4、8、2、7、3、10,9、1、6、5,则5号位置的人为胜利者。给定n个人,请你编程计算出最后胜利者标号数。
约瑟夫问题,创新解法虽然分析复杂,但是代码却十分简洁明了,时间O(n),空间O(1),这就是数学的魅力。
主要思想:n个人第一个删除的人的秩为(k-1)%n
,假如我们已经知道了n-1个人时,最后胜利者的编号为x,利用映射关系逆推,就可以得出n个人时,胜利者的编号为(x+k)%n(要注意的是这里是按照映射后的序号进行的)。可以总结为: f(n,k)=(f(n−1,k)+k)%n
int LastRemaining_Solution(int n, int m){
if(n<1||m<1)return -1;
int last=0;
for(int i=2;i<=n;i++)
last=(last+m)%i;
return last;
}