OJ 简单数学问题

最大公约数gcd和最小公倍数lcm

  • gcd(greatest common divisor)
    根据欧几里得算法,a、b的最大公约数与b、a%b的最大公约数相同;
    任何数与0的最大公约数为它本身
    因此有
int gcd(int a,int b)
{
    return !b?a:gcd(b,a%b);
}
  • lcm(lowest common multiple)
    最小公倍数可以根据最大公约数求出,为 a*b/gcd
int lcm(int a,int b)
{
    return a/gcd(a,b)*b;
}

分数运算

实现一个精简的分数类,根据题目要求实现一些必要的接口
有以下几点需要注意:

  1. 分子、分母用long long保存,防止运算时int溢出
  2. 分数化简有以下几点约定
    • 保持分母为正,如果分数是负数则令分子为负
    • 如果分子为0,令分母为1
    • 约分:分子分母同时除以gcd
  3. 除了基本+-*/操作外,还需要重载输出操作符

接口及部分实现


class fraction {

private:
    long long numerator; //定义分子  
    long long denominator;  //定义分母  


public:
    fraction(int numerator, int denominator = 1);  //构造函数  
    ~fraction();


    fraction operator+(const fraction& rv) const;  //重载 +  
    fraction operator-(const fraction& rv) const;  //重载 -  
    fraction operator*(const fraction& rv) const;  //重载*  
    fraction operator/(const fraction& rv) const;  //重载 /  
    fraction operator-();                          //重载单目运算符-  
    void reciprocal();                             //倒数  
    fraction& operator=(const fraction& rv);       //重载=  
    bool operator>(const fraction& rv) const;      //重载>  
    bool operator>=(const fraction& rv) const;     //重载>=  
    bool operator<(const fraction& rv) const;      //重载<  
    bool operator<=(const fraction& rv) const;     //重载<=  
    bool operator==(const fraction& rv) const;     //重载==  
    bool operator!=(const fraction& rv) const;     //重载!=  
private:
    void fractionReduction();                      //约分     
    int gcd(int nr, int dr);                       //求最大公约数 

};

//处理当 int 类型在双目运算符左边时的情形,在右边可由编译器自动代入构造函数  
const fraction operator+(int, const fraction&);
const fraction operator-(int, const fraction&);
const fraction operator*(int, const fraction&);
const fraction operator/(int, const fraction&);
const bool operator>(int, const fraction&);
const bool operator>=(int, const fraction&);
const bool operator<(int, const fraction&);
const bool operator<=(int, const fraction&);
const bool operator==(int, const fraction&);
const bool operator!=(int, const fraction&);
//重载输出操作符
ostream& operator<<(ostream& os, const fraction& fr);

//===============部分实现======================

long long  gcd(long long a, long long b)
{
    return !b ? a : gcd(b, a%b);
}

void fraction::fractionReduction()
{
    if (denominator < 0)
    {
        numerator *= -1;
        denominator *= -1;
    }
    if (numerator == 0)
    {
        denominator = 1;
    }
    else
    {
        long long  d = gcd(abs(numerator), denominator);
        numerator /= d;
        denominator /= d;
    }
}

ostream& operator<<(ostream& os, const fraction& fr)
{
    if (fr.denominator == 1)                        //分母为1
    {
        cout << fr.numerator;
    }
    else if (abs(fr.numerator) > fr.denominator)    //假分数
    {

        cout << fr.numerator / fr.denominator << " " << abs(fr.numerator) % fr.denominator << "/" << fr.denominator;
    }
    else                                            //真分数
    {
        cout << fr.numerator << "/" << fr.fractionReduction;
    }
    return os;
}

素数

  • 判断素数

质数又称素数。一个大于1的自然数,除了1和它自身外,不能被其他自然数整除的数叫做质数
只需判断 2,3,……,floor(sqrt(n)) [包含] 之间有没有n的约数,就可以判断n是否为素数。

bool isPrime(int n)
{
    int sqrt = floor(sqrt(n));   //不必在循环的每次都计算这个值
    for(int i = 2;i <= sqrt ; ++i)
    {
        if(n%i == 0){
            return false;
        }
    }
    return true;
}
  • 素数制表
    有些题目会用到素数表,我们可以先生成一个素数表储存起来供算法使用
const int maxn = 10000;  //需要的素数的个数
int Prime[maxn];
void GetPrime()
{
    int count = 0;
    int i = 2;            //最小的素数
    while(count < maxn){  //填满maxn个素数
        if(isPrime(i)){
            Prime[count++] = i;
        }
        ++i;
    }
}

质因子分解

将一个整数写成一个或多个质数相乘的形式,形如6 = 2*3 、180 = 2^2*3^2*5,称为质因子分解。
对于质因子分解,有一条重要的理论

对于一个正整数N,如果在[2,N]存在质因子,则只有以下两种情况:

1. 所有的质因子都在 [2,sqrt(N)]
2. 有且只有一个最大的质因子大于sqrt(N),其余质因子都在[2,sqrt(N)]中


大数运算

首先了解int和long long的大致取值范围

int            约 -2x10^9 ~ 2x10^9
long long        约 -9x10^18 ~ 9x10^18

超过这个范围的称为大数

  • 储存方式
struct bigNum
{
    int length;
    int N[1000];
    bigNum(const char* s = "") :
        length(strlen(s))
    {
        memset(N, 0, 1000);
        for (size_t i = 0; i < length; i++)
        {
            N[i] = s[length - i - 1] - '0';             //倒序储存
        }
    }
};
  • 比较
    比较两个大数的大小
int compare(const bigNum& lhs, const bigNum& rhs)
{
    if (lhs.length != rhs.length)
    {
        return lhs.length > rhs.length ? 1 : -1;
    }
    else
    {
        for (size_t i = 0; i < lhs.length; i++)
        {
            if (lhs.N[lhs.length - i - 1] > rhs.N[rhs.length - i - 1])
            {
                return 1;
            }
            else if (lhs.N[lhs.length - i - 1] < rhs.N[rhs.length - i - 1])
            {
                return -1;
            }

        }
        return 0;
    }
}
  • 四则运算


    • 两个大数相加
    bigNum add(const bigNum& lhs, const bigNum& rhs)
    {
        //进位
        int carry = 0;
        bigNum res;
        for (size_t i = 0; i < lhs.length || i < rhs.length; i++)
        {
            int temp = lhs.N[i] + rhs.N[i] + carry;
            res.N[res.length++] = temp % 10;
            carry = temp / 10;
        }
        while (carry)          //处理剩下的进位值
        {
            res.N[res.length++] = carry % 10;
            carry /= 10;
        }
        return res;
    }

    • 两个大数相减,为使算法简单,保证第一个参数大于第二个参数(compare返回1),如果lhs小于rhs,则交换他们的位置,并在结果前输出负号
    //保证lhs大于rhs
    bigNum sub(bigNum lhs, const bigNum& rhs)
    {
        bigNum res;
        for (size_t i = 0; i < lhs.length || i < rhs.length; i++)
        {
            if (lhs.N[i] < rhs.N[i])
            {
                --lhs.N[i + 1];
                lhs.N[i] += 10;
            }
            res.N[res.length++] = lhs.N[i] - rhs.N[i];
        }
        while (res.length > 1 && res.N[res.length - 1] == 0)
        {
            --res.length;
        }
        return res;
    }

    • 大数乘以一个int整型数,用大数的每一位(从低位到高位)乘以整型数,然后加起来
    bigNum mul(const bigNum& ths, int small)
    {
        bigNum res;
        int carry = 0;
        int temp;
        for (size_t i = 0; i < ths.length; i++)
        {
            temp = ths.N[i] * small + carry;
            res.N[res.length++] = temp % 10;
            carry = temp / 10;
        }
        while (carry)
        {
            res.N[res.length++] = carry % 10;
            carry /= 10;
        }
        return res;
    }
    

    • 大数除以int整型数,保证除数不为0,返回 pair<商,余数>
    pair<bigNum,int> divide(const bigNum& ths, int small)
    {
        bigNum res;         //余数
        int rem = 0;
        res.length = ths.length;            //除法先生成高位(被除数也先从高位开始计算)
        for (int i = ths.length - 1; i >= 0; i--)
        {
            rem = 10 * rem + ths.N[i];
            if (rem < small)
            {
                res.N[i] = 0;
            }
            else
            {
                res.N[i] = rem / small;
                rem = rem % small;
            }
        }
        while (res.length > 1 && res.N[res.length - 1] == 0)
        {
            --res.length;
        }
        return make_pair(res, rem);
    }
    
  • 输出
    由于大数在数组中逆序(从低位到高位)排列,因此输出结果时要倒序输出

    ostream& print(ostream& os,const bigNum& out)
    {
        for(int i = out.length - 1; i >= 0; --i)
        {
            cout<<out.N[i];
        }
        return os;
    }

组合数

  • 直接计算:最大数据规模 n=20

这里写图片描述
通过公式计算组合数时特别容易溢出,例如C(21,10) = 352716,但由于 21!已结超过long long的上界而无法计算

  • 组合数递推公式计算:最大数据规模 n=67

    C(n,m)=C(n-1,m)+C(n-1,m-1)

    通过这个公式可以不断减小n,m的规模,而C(n,0)=C(n,n)=1,可以使用递归方法来求

    long long C(long long n,long long  m)
    {
        if(m==0 || m==n) return 1;
        return C(n-1,m) + C(n-1,m-1);
    }

    但这样会造成大量的重复计算
    改进:
    用一个数组将计算过的值保存起来,下次直接返回

/*该算法所能计算的范围 n不超过67*/
long long res[70][70] = {0};
long long C(long long n,long long m)
{
    if(m==0 || m==n)  return 1;
    if(res[n][m] != 0)  return res[n][m];
    return res[n][m] = C(n-1,m) + C(n-1,m-1);
}
  • 取模运算:最大数据规模 n=1000
    很多题目会要求输出组合数结果的模,mod是int型(<2*10^9)
//该算法可以计算n 1000以内的组合数
const int mod = 100000007;
int C(int n,int m)
{
    if(m==0 || m==n)  return 1;
    if(res[n][m] != 0)  return res[n][m];
    return res[n][m] = (C(n-1,m) + C(n-1,m-1)) % mod;
}

排名问题

需要注意的是:

  • 有多个人获得相同的分数
  • 相同的分数获得相同的排名

例如 100、100、95、95、90、88、85、80
排名是 1、1、3、3、5、6、7、8

### 圆桌问题及其在OJ系统中的实现 #### 1. 圆桌问题的概念 圆桌问题是经典的组合数学问题之一,通常涉及如何安排一组人在圆形桌子周围就座,使得某些特定条件得到满足。这类问题可以扩展到计算机科学领域,尤其是在数据结构和算法的设计中具有重要意义[^1]。 #### 2. 数据结构的选择——循环链表 为了模拟圆桌上的座位排列,循环链表是一种非常合适的数据结构。它能够自然地表示环形结构,并支持高效的节点插入和删除操作。通过定义一个通用的模板类来封装循环链表的操作,可以使其实现更加灵活和可重用。 以下是基于C++模板设计的循环链表基本框架: ```cpp template<typename T> class CircularLinkedList { private: struct Node { T data; Node* next; Node(const T& val) : data(val), next(nullptr) {} }; Node* head; public: CircularLinkedList() : head(nullptr) {} ~CircularLinkedList(); void insert(T value); bool remove(T key); void displayList() const; }; ``` 上述代码片段展示了如何创建一个简单的模板化循环链表类 `CircularLinkedList` 的部分成员函数声明。 #### 3. 算法设计思路 针对具体的“圆桌问题”,可以通过以下方式构建解决方案: - **初始化阶段**: 将所有参与者加入到循环链表中作为初始状态。 - **处理逻辑**: 根据题目给定规则逐一轮流移除符合条件的人直到剩下最后一个人或者达到目标人数为止。 - **终止条件判断**: 当只剩下一个节点时结束程序执行流程;也可以设定其他退出准则依据实际需求而定。 下面是一个简化版的例子演示了如何利用前面提到过的循环链表来进行人员淘汰过程: ```cpp void solveJosephusProblem(int n, int k){ CircularLinkedList<int> circle; for (int i=1;i<=n;++i){ circle.insert(i); } auto current=circle.getHead(); // 假设存在getHead方法返回头指针 while(circle.size()>1){ for(int count=1;count<k; ++count){ current=current->next; } cout << "Removed person number:"<<current->data<<endl; current=circle.removeAfter(current); // 删除当前指向位置后的那个人 } } ``` 此段伪代码实现了著名的约瑟夫环问题解决办法,其中k代表每隔多少个人会被剔除出去。 #### 4. 在线判题系统的应用价值 像USACO这样的在线评测平台提供了丰富的练习资源供学习者提升自己的编程技巧以及解决问题的能力[^2]。对于初学者来说,这些平台上关于基础数据结构如数组、栈队列等的应用案例可以帮助他们更好地理解理论知识的实际运用场景。而对于更高级别的参赛者,则能接触到更多复杂度较高的挑战项目从而进一步锻炼思维能力和编码效率。 另外值得注意的是,在参与此类竞赛活动过程中除了要注重正确解答之外还需要关注时间空间性能指标等方面的要求以确保最终提交版本能够在规定时限内顺利完成全部测试样例验证工作。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值