刷题日志(3)——最大公约数与同余原理


一、最大公约数

方法:辗转相除法
设 a=50 ,b = 30 ,求(a,b)的最大公约数
过程:
1. 若b==0,则直接返回 a, 显然b !=0
2. 若b!=0,则 求(b,a%b)=(30,20)的最大公约数
3. 求(a%b,b%(a%b))=(20,10)的最大公约数
4. 同理,即求(10,0)的最大公约数,显然b=0,返回a,即10为最大公约数。

显然以上为一个递归过程。

1.求最大公约数
long gcd(long a,long b)
{
	return b==0?a:gcd(b:a%b);
}
//求gcd(a,b) ,其中a>b,时间复杂度为O((log a)^3)

2.推得: 最小公倍数
long lcm(long a,long b)
{
	return (long) a / gcd(a, b) * b;
}

3.原理证明:
// 证明辗转相除法就是证明如下关系:
	 gcd(a, b) = gcd(b, a % b)
	 假设a % b = r,即需要证明的关系为:gcd(a, b) = gcd(b, r)
	 证明过程:
	 因为a % b = r,所以如下两个等式必然成立
	 1) a = b * q + r,q为0123....中的一个整数
	 2) r = a − b * q,q为0123....中的一个整数
	   假设u是a和b的公因子,则有: a = s * u, b = t * u
	   把a和b带入2)得到,r = s * u - t * u * q = (s - t * q) * u
	   这说明 : u如果是a和b的公因子,那么u也是r的因子
	   假设v是b和r的公因子,则有: b = x * v, r = y * v
	   把b和r带入1)得到,a = x * v * q + y * v = (x * q + y) * v
	   这说明 : v如果是b和r的公因子,那么v也是a的公因子
	   综上,a和b的每一个公因子 也是 b和r的一个公因子,反之亦然
	   所以,a和b的全体公因子集合 = b和r的全体公因子集合
	   即gcd(a, b) = gcd(b, r)
	   证明结束

二、第 N 个神奇数字——最大公约数的经典应用

在这里插入图片描述
思路:
为了方便说明,假设 a=2 ,b =3 , 则从1开始逐渐增多,显然第一个神奇的数字为2 ,第二个神奇数字为3,第三个为4 。。。。。。
那么若要求第N个神奇的数字,

  1. 首先考虑能不能对N进行范围确定,使它不会超过这个范围呢?
    可以,左区间很容易定,只需要为1即可,而右区间=N*a

  2. 为什么右区间为N*a?
    假设我们要求第100个神奇数,那么很明显1~200范围内肯定包含了第100个神奇数,因为可以被a=2整除的显然就是一半,更何况还有可以被b=3整除的,那么我们可以进一步得出: 越小的数,其越可能在某区间内存在越多的神奇的数。也就是
    =》 右区间=N * min(a,b)

  3. 如何确定第N个神奇数字的具体位置?
    假设有函数 F(x),该函数可以求得 1~X范围内(包括X)有多少个神奇数字。那么很显然就是二分查找数字的过程
    =》从一个升序数组中,找到一个值为 F(x)=100 的数。

  4. 如何设计F(x)?
    假设 x=200, 则在 1~200上有多少个1数可以被2和3整除?
    显然 可以被 2整除数:200/2
            可以被3整除数:200/3
    显然,这之中重复计算了既可以被2整除也可以被3整除的数 :200/(23)
    故F(200)=200/2+200/3-200/(2
    3)
    更一般的得到=》F(X)=X/a+X/b-X/(lcm(a,b))

long gcd(long a, long b) 
{
	return b == 0 ? a : gcd(b, a % b);
}
long lcm(long a, long b) 
{
	return (long)a / gcd(a, b) * b;
}
int getmin(int x,int y)
{
    return x>y?y:x;
}
int nthMagicalNumber(int n, int a, int b) 
{
	long lcm_value = lcm(a, b);
	long ans = 0;
	// l = 0,二分左区间
	// r = (long) n * getmin(a, b)  ,二分右区间
	// l......r
	for (long l = 0, r = (long) n * getmin(a, b), m = 0; l <= r;) 
    {
		m = (l + r) / 2;    //得到中间位置
		if (m / a + m / b - m / lcm_value >= n) 
        {
			ans = m;
			r = m - 1;
		} else 
        {
			l = m + 1;
		}
	}
	return (int) (ans % 1000000007);
}

三. 同余原理

      

     1. 背景:假设有一个所以元素都很大的数组int nums,要求将所以元素均相乘,并且返回真实答案%m的结果。更进一步的即当计算某个中间结果时,它的值远超过 int/long等类型的范围,如何进行这些大数的运算?
     2.同余原理的用处:1、如何算对 真实值%m 2.多位 数运算
     3.对于第二点的解释:
       首先存在的误区:
       对于多位数的数运算我用字符串拼凑最后直接取模不就行了,比如JAVA中的biginteger?
       像Python中的int类型可以表示任意大小的整数,那么我直接用这个结果取模不就行了?

       答案是:不可以!!!,因为如果位数真的过长,那么很可能算不完
     4.分析:首先 一个 int (+)/(-)/(*)/(/) int,我们可以认为其运算的时间复杂度为O(1)。即使是long类型,我们也同样如此认为。其计算非常之快,为什么?因为这是固定位数。 但若在计算过程中将每一个中间结果均用真实值来保存并计算,显然这时候位数就不在固定,这就导致计算会越来越慢。实际上,当我们用K位的整数进行加/减,其时间复杂度为O(K),我们当然可以认为O(32),O(64)为O(1)的时间,但乘/除?时间复杂度为O(K^2),故当位数很大时,即使是一个很简单的运算,都可能导致超时!!!
       5.解答:
在这里插入图片描述

在这里插入图片描述

[注意] 该过程需要保证不发生溢出,比如int*int,应该使用long或更大的类型来接住该数,之后在取模转化为整型。

在这里插入图片描述

[注意]该过程需要保证(a-b)%m=非负,因此要+m
在数学上,模是一个非负数的标量,它表示实数到零的距离。因此,模不会是负数。

除法同余需要用到求逆元,暂时跳过!!!

// 为了测试
	public static long random() {
		return (long) (Math.random() * Long.MAX_VALUE);
	}

	// 计算 ((a + b) * (c - d) + (a * c - b * d)) % mod 的非负结果
	public static int f1(long a, long b, long c, long d, int mod) {
		BigInteger o1 = new BigInteger(String.valueOf(a)); // a
		BigInteger o2 = new BigInteger(String.valueOf(b)); // b
		BigInteger o3 = new BigInteger(String.valueOf(c)); // c
		BigInteger o4 = new BigInteger(String.valueOf(d)); // d
		BigInteger o5 = o1.add(o2); // a + b
		BigInteger o6 = o3.subtract(o4); // c - d
		BigInteger o7 = o1.multiply(o3); // a * c
		BigInteger o8 = o2.multiply(o4); // b * d
		BigInteger o9 = o5.multiply(o6); // (a + b) * (c - d)
		BigInteger o10 = o7.subtract(o8); // (a * c - b * d)
		BigInteger o11 = o9.add(o10); // ((a + b) * (c - d) + (a * c - b * d))
		// ((a + b) * (c - d) + (a * c - b * d)) % mod
		BigInteger o12 = o11.mod(new BigInteger(String.valueOf(mod)));
		if (o12.signum() == -1) {
			// 如果是负数那么+mod返回
			return o12.add(new BigInteger(String.valueOf(mod))).intValue();
		} else {
			// 如果不是负数直接返回
			return o12.intValue();
		}
	}

	// 计算 ((a + b) * (c - d) + (a * c - b * d)) % mod 的非负结果
	public static int f2(long a, long b, long c, long d, int mod) {
		int o1 = (int) (a % mod); // a
		int o2 = (int) (b % mod); // b
		int o3 = (int) (c % mod); // c
		int o4 = (int) (d % mod); // d
		int o5 = (o1 + o2) % mod; // a + b
		int o6 = (o3 - o4 + mod) % mod; // c - d
		int o7 = (int) (((long) o1 * o3) % mod); // a * c
		int o8 = (int) (((long) o2 * o4) % mod); // b * d
		int o9 = (int) (((long) o5 * o6) % mod); // (a + b) * (c - d)
		int o10 = (o7 - o8 + mod) % mod; // (a * c - b * d)
		int ans = (o9 + o10) % mod; // ((a + b) * (c - d) + (a * c - b * d)) % mod
		return ans;
	}

	public static void main(String[] args) {
		System.out.println("测试开始");
		int testTime = 100000;
		int mod = 1000000007;
		for (int i = 0; i < testTime; i++) {
			long a = random();
			long b = random();
			long c = random();
			long d = random();
			if (f1(a, b, c, d, mod) != f2(a, b, c, d, mod)) {
				System.out.println("出错了!");
			}
		}
		System.out.println("测试结束");

		System.out.println("===");
		long a = random();
		long b = random();
		long c = random();
		long d = random();
		System.out.println("a : " + a);
		System.out.println("b : " + b);
		System.out.println("c : " + c);
		System.out.println("d : " + d);
		System.out.println("===");
		System.out.println("f1 : " + f1(a, b, c, d, mod));
		System.out.println("f2 : " + f2(a, b, c, d, mod));
	}
  • 20
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值