数学问题

巧解数学问题

在这里放一个暴论:数学方法基本上就是编程类问题的最优解。所以做题的时候如果能够运用到数学的知识,会大大减少解题的复杂度。

公因数和公倍数

      使用辗转相除法可以求解两个数的最大公因数,从而根据两个数的最大公因数可以求出两个数的最小公约数。
      这里介绍一下辗转相除法的原理以及简单的证明过程:
      欧几里得算法又称辗转相除法,是指用于计算两个非负整数a,b的最大公约数。计算公式gcd(a,b) = gcd(b,a mod b)
      证明过程:
      a可以表示成a = kb + ra,b,k,r皆为正整数,且r<b),则r = a mod b
      假设da,b的一个公约数,记作d|a,d|b,即ab都可以被d整除。
      而r = a - kb,两边同时除以dr/d=a/d-kb/d=m,由等式右边可知m为整数,因此d|r
      因此d也是b,a mod b的公约数。因(a,b)(b,a mod b)的公约数相等,则其最大公约数也相等,得证。

int gcd(int a,int b)
{
  return b==0?a:gcd(b,a%b);
}

      而最小公约数则好求了,直接是两数相乘之后除以最大公因数即可。

int lcm(int a,int b)
{
  return a*b/gcd(a,b);
}

这里进一步引入扩展的欧几里得算法:

      扩展欧几里得算法是欧几里得算法(又叫辗转相除法)的扩展。除了计算a、b两个整数的最大公约数,此算法还能找到整数x、y(其中一个很可能是负数)。通常谈到最大公因子时, 我们都会提到一个非常基本的事实: 给予二整数 ab, 必存在有整数 x 与 y 使得ax + by = gcd(a,b)。有两个数a,b,对它们进行辗转相除法,可得它们的最大公约数——这是众所周知的。然后,收集辗转相除法中产生的式子,倒回去,可以得到ax+by=gcd(a,b)的整数解。

      求解的思路:假设当前我们要处理的是求出 ab的最大公约数,并求出 xy使得a*x + b*y= gcd,而我们已经求出了下一个状态:ba%b的最大公约数,并且求出了一组x1y1使得: b*x1 + (a%b)*y1 = gcd , 那么这两个相邻的状态之间是否存在一种关系呢?我们知道: a%b = a - (a/b)*b(这里的 “/” 指的是整除,例如 5/2=2 , 1/3=0),那么,我们可以进一步得到:

    gcd = b*x1 + (a-(a/b)*b)*y1

        = b*x1 + a*y1 – (a/b)*b*y1

        = a*y1 + b*(x1 – a/b*y1)

      对比之前我们的状态:求一组xy 使得:a*x + b*y = gcd ,可以知道:

    x = y1

    y = x1 – a/b*y1

代码:

int cGCD(int a,int b,int &x,int &y)
{
  if(b==0)//递归的基线情况
  {
    x = 1,y = 0;
    return a;
  }
  int x1, y1,gcd = xGCD(b,a%b,x1,y1);
  x = y1, y = x1 - (a/b)*y1;
  return gcd;
}

质数

      这里是介绍一种方法,他能够在判断一个整数n是否是质数的同时也能够判断小于n的质数的个数。其原理就是:从1到n进行遍历的时候。假设当前遍历到m,则把所有的小于n的,且是m的倍数的整数标为和数,遍历完成之后,没有被标为和数的数字即为质数。(埃氏筛法)
这里给出优化了的代码:

int countprimes(int n)
{
  if(n<=2) return 0;
  vector<bool> prime(n,true);
  int i = 3, sqrtn = sqrt(n), count = n/2;//偶数一定不是质数
  while(i<=sqrtn)
  {
    for(int j = i*i; j < n;j+= 2*i)
    {
      if(prime[j])
        {
           --count;
           prime[j] = false;
        }
    }
    do
    {
      i+=2;
    }while(i<=sqrtn&&!prime[i]);   
  } 
  return count;
}

进制转换

      进制类转换的题目一般分为常规手法和特殊手法。常规手法指的就是使用除法和取模的运算;而特殊手法则是使用之前我们提到的位运算的手法。同时当原始数据是字符串而不是普通的数字类型的时候,需要注意整数 int 是否会出现越界的问题,也就是是否会溢出,这个时候考虑使用 long 或者做溢出情况的特殊处理。对了,也要记得考虑负数和零的情况哦。
      例如给定一个整数,输出一个字符串,求相关的七进制的表示。

class Solution {
public:
    string convertToBase7(int num) {
    //这里需要考虑负数和零的特殊情况
    if(num==0) return "0";
    string ans;
    bool is_negative = num<0;
    num = abs(num);
    while(num)
    {
        int b = num%7;
        ans = to_string(b) + ans;
        num /= 7;
    }
    return is_negative?"-"+ans:ans;
    }
};

某数的次方

      这里主要也是可以使用两种思路,正常的除数和取余的操作或者位运算的操作。
      这里使用求一个整数是否是3(质数)的次方为案例来讲解一下此类问题的做法,当然其他数如法炮制(注意对于2的次方数直接使用位运算就可以判断)

第一种解法,使用换底公式来求解

   return fmod(log10(n)/log10(3),1) == 0;

第二种解法,使用的是这个数的次数在整数范围内的上限值

  return n>0&&1162261467%n==0;

      同时这里也引入对于求一个整数是否是4的次方数的二进制位运算方法

  return n>0&&!n&(n-1)&&(n&1431655765);

随机与采样

      这里分为两种情况,一个一般是针对于数组,我们知道这个数组的长度,这个时候可以采取Fisher-Yates洗牌办法;一个一般是针对于链表,一般这个链表的长度我们是不知道的,这个时候使用著名的书库采样方法。
      首先针对于Fisher-Yates洗牌办法,其原理主要是通过在一个范围之内随机交换位置一次,然后再缩小范围的过程,其实现有正向洗牌和反向洗牌两种。
      这里主要通过 leetcode 384的代码,对这个方法做一个展示:

class Solution {
public:
   vector<int>orgin;
   //随机与取样问题,其中很重要的一个就是如何使用函数来产生随机值
   //难点就在于说函数的调用次数比较多,怎么样更好的节约其复杂度呢
    Solution(vector<int>& nums):orgin(move(nums)) {

    }
    
    /** Resets the array to its original configuration and return it. */
    vector<int> reset() {
    return orgin;
    }
    
    /** Returns a random shuffling of the array. */
    vector<int> shuffle() {
    if(orgin.empty()) return {};
    vector<int> ans(orgin);
    //反向洗牌,也就是随机取出来的数字放到数组的末尾,在这里也就是
    //采用swap函数
    for(int i = ans.size()-1;i>=0;--i)
    {
        //srand((unsigned)time(NULL));
        swap(ans[i],ans[rand()%(i+1)]);
    }
    //正向洗牌
    //for(int i = 0;i<ans.size();++i)
    //{
     // int pos = rand%(n-i);
     // swap(ans[i],ans[i+pos]);
    //}
    return ans;
    }
};

      现在针对于我们事先不清楚链表的长度的时候,可以采用水库采样法(又称为蓄水池采样法),也就是遍历一遍链表,在遍历到m个节点的时候,有1/m的概率覆盖掉之前的节点选择。可以简单的证明,这样选取到的每一个节点在理论上被选取到的概率为1/n(这里我们不妨假设最终的这个链表的长度为n)
      这里我们采用 leetcode 382的代码来作为一个展示:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode*first;
    /** @param head The linked list's head.
        Note that the head is guaranteed to be not null, so it contains at least one node. */
    Solution(ListNode* head):first(head) {
    //水库采样 要把我笑死 看看这个采样方法如何吧
    }
    
    /** Returns a random node's value. */
    int getRandom() {
    int i = 2;
    ListNode*node = first,*tmp = node->next;
    int ans = node->val;
    while(tmp)
    {
        if(rand()%i==0)
        {
          ans = tmp->val;
        }
        ++i;
        tmp = tmp->next;
    }
    return ans;
    }
};

/**
 * Your Solution object will be instantiated and called as such:
 * Solution* obj = new Solution(head);
 * int param_1 = obj->getRandom();
 */
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值