数学算法

5、数学问题

  1. 套路:熟悉常见题目

  2. 该部分不涉及很深的算法,却与数学息息相关,不需要特别的数学知识,只要掌握简单的数理逻辑即可。

  3. 最大公约数int gcd(int a,int b){return !b?a:gcd(b,a%b);}这段代码十分简洁,并且实现了如果a<b,先互换。辗转相除法也称欧几里得算法。

  4. 最小公倍数,在最大公约数的基础上计算,为ab/gcd(a,b)

5.2 常用math函数

  • fabs(double x)

  • floor(double x)向下取整

  • ceil(double x)向上取整

  • pow(double r,double p)返回r^p

  • sqrt(double x)

  • log(double x)返回double型变量的以自然对数为底的对数

  • sin(double x)cos(double x)tan(double x)

  • pi=acos(-1)

  • 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 分数的四则运算

  1. 套路:实际做题过程中,还是套晴神模板最好,不需要单独设符号位,符号位在分子上即可,每次运算后化简。

  2. 建立分数结构类,实现加减乘除运算,符号位不用单独处理,计算时先不管分母正负,处理结果时如果分母为负,令分子分母同时取相反数即可,每次运算结束需要进行约分(注意分子不为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 质数、质因子分解

  1. 套路:大量质数时素数筛法,少量时用is_prime函数

  2. is_prime函数O(n^1/2)或者用数组记录过程结果的筛素数法O(nloglogn)。

  3. 对于质因子分解,首先看因子分解,如果一个数存在除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 高精度

  1. 套路:写的多了,问题就少了

  2. 实现大整数类,包括运算:高精度加减法、高精度与低精度的乘除法。

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

      1. 1000000007是一个质数

    • int32位的最大值为2147483647,所以对于int32位来说1000000007足够大

    • int64位的最大值为2^63-1,对于1000000007来说它的平方不会在int64中溢出

    • 所以在大数相乘的时候,因为(a∗b)%c=((a%c)∗(b%c))%c,所以相乘时两边都对1000000007取模,再保存在int64里面不会溢出

5.8 组合数

  • 第一个问题:求n!有多少个质因子P

  • 比如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).

  • 第三个问题:如何计算$C_n^m\%P$

  • 掌握第一种方法即可,在组合数的计算的递归公式法上改进

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(n1,k)+k)%nf(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;
}

Last updated

Was this helpful?