【BIT2021程设】8. 军训日记:报数!——循环节、快速幂、欧拉扩展定理

本文作者分享了针对一道程序设计题目——关于高精度报数问题的思考与解决策略。题目要求计算特定规则下的报数结果,由于数据范围极大,常规方法无法适用。作者介绍了三种不同的解题思路:寻找循环节、快速幂运算和扩展欧拉定理,并给出了相应代码示例。文章适合对算法和程序设计感兴趣的读者,特别是对高精度计算和优化策略的学习者。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

写在前面:

本系列博客仅作为本人十一假期过于无聊的产物,对小学期的程序设计作业进行一个总结式的回顾,如果将来有BIT的学弟学妹们在百度搜思路时翻到了这一条博客,也希望它能对你产生一点帮助(当然,依经验来看,每年的题目也会有些许的不同,所以不能保证每一题都覆盖到,还请见谅)。

不过本人由于学艺不精,代码定有许多不足之处,欢迎各位一同来探讨。

同时请未来浏览这条博客的学弟学妹们注意,对于我给出完整代码的这些题,仅作帮助大家理解思路所用(当然,因为懒,所以大部分题我都只给一个伪代码)。Anyway,请勿直接复制黏贴代码,小学期的作业也是要查重的,一旦被查到代码重复会严厉扣分,最好的方法是浏览一遍代码并且掌握相关的要领后自己手打一遍,同时也要做好总结和回顾的工作,这样才能高效地提升自己的代码水平。

加油!


Description

成绩0开启时间2021年08月27日 星期五 12:00
折扣0.8折扣时间2021年10月1日 星期五 23:59
允许迟交关闭时间2021年10月10日 星期日 23:59

 小军的军训正在如火如荼的进行着,而他仍然继续着他写日记的伟大事业。
报数永远是军训中很无聊的环节,教官为了让报数不那么无聊,以至于有人真的去“抱树”,他规定了一个船新的报数方案,方案如下:
第一个人报数x的后三位,之后第i个人报第i-1人所报数的x倍的后三位,即第i个人需要报x^i(x的i次方)的后三位。
站在队列中第n位的小军很想知道自己需要报多少,这样他就可以放心走神了,请问小军需要报多少呢?

数据范围与提示:0x≤999,1n≤10^1000
输出格式:行末无空格,文末有回车。 注意,1的后三位是1,而1001的后三位是001。


测试用例 1以文本方式显示
  1. 5 1↵
以文本方式显示
  1. 5↵
1秒64M
测试用例 2以文本方式显示
  1. 5 3↵
以文本方式显示
  1. 125↵
1秒64M
测试用例 3以文本方式显示
  1. 5 5↵
以文本方式显示
  1. 125↵
1秒64M

题意分析:

        题目非常简单,讲白了就是让你算x^n (Mod 1000),看起来非常人畜无害的一道题却因为这个n的取值范围到了1e1000而变得腥风血雨,这一题也顺理成章地成为了各路神仙八仙过海的舞台。

        那么暴力算肯定不行了(废话)。得想想有什么快速的解决方法。在讨论具体的解决方法之前,先讲一讲普遍的优化策略吧。首先是取模运算的一个公式(a\times b) % c = (a%c \times b%c)%c,这个公式感兴趣的同学可以自行证明一下。利用这个公式我们可以证明一个范围更广的定理:对于式子(\prod a_i) % c,我们可以任意地在中间某个项或者某几个项的乘积后面%c,式子的值不会改变。对于题目中的x^n也是同理,我们可以在每一次的计算中只保留后三位,并不影响最后出来的后三位结果(除了前导零的情况可能会不同,但是这种特例我们可以留待后面解决,这里先把一般的规律归纳出来)。然而仅仅这么优化是远远不够的,我们只解决了溢出的问题,然而这个1e1000的恐怖数字并没有得到优化,时间上肯定不能接受,那么究竟要怎么处理呢?

        思路一,对于没有数论基础的同学(说的就是我自己),最容易想到的思路应该是“找循环节”。容易想到,x^n的后三位一共只有1000种可能(000到999,同样先不考虑前导0的问题),而一旦出现了重复则必然是一个循环(可以想想为什么,其实很好证明——如果x^i的后三位与x^j相等,那么由前面的取模运算公式,x^{i+1}的后三位必然与x^{j+1}的后三位相等,显而易见地会成为一个循环),而且由于总共只有1000种可能,则循环节必然小于等于1000。因此,我们就可以用一个循环链表的形式来存储x的n次幂,即若干个节点作为前导节点,设其长度为f,若干个节点作为循环节点,设其长度为l。这样我们就可以通过(n-f)%l+f就可以知道究竟答案对应的节点是哪一个。这里涉及到一个高精度减低精度和高精度对低精度取模的运算,都不是很难,大家可以自行研究一下(笔者亲测此做法是可以全绿的)。至于前导0,可以选择先暴力看一下结果是不是超出了三位,如果是的话就不管前导0了,如果不是那更好,直接输出就行了;也可以在存储循环节的时候做点手脚,容易想到,一旦出现了某个数达到三位数,后面出现的所有节点都必然是含有前导0的,所以可以额外维护一个hasForerunner变量来保存是否有前导0,默认置false,一旦中途算出一个节点超过了1000,此后置true就行。所以最关键的问题就是循环节要如何求,每次都遍历一遍链表看看是否已经存在肯定不合理,我这里采用的方法是用map来做一个伪链表,即map[a] = b代表a的下一个节点是b,这样既可以在很短的时间内查到a是否在map中,也能通过某个节点直接链接到下一个节点。这样我们可以在1000次运算内找到全部的前导节点和循环节点,这个链表就被我们打出来了。

        同时这里有一个思路一的变形,暂称为思路一点五,如果你打完表看看这个循环节长度,你会发现它只会是1,2,5,50,100其中之一,换句话说,都是100的约数,所以直接让n模100,之后暴力就可以算出答案了(当然,过程中依然只要保留三位)。但是为什么只会出现这些长度的循环节,我始终给不出严谨的数学证明,如果有兴趣的同学可以试证一下然后告诉我T^T。

        当然,思路一在我看来并不好,原因就是代码量太大了(笑),思路一点五我又给不出严谨的证明。我身边很多大佬写出了既简短又快的方法,我在这简单介绍几个:

        思路二:运用快速幂定理。(名词解释:快速幂)这个帖子已经讲的很详细了,我就简单讲两点。一个是高精度除以2的操作其实可以直接通过删去n的最后一位来完成,二是对有前导0的数、还有0和1的特殊情况要另外处理。具体的代码我没有写,但是大部分人都告诉我全绿是没问题的,因为O(log10^n)会骤降为O(nlog10),基本上不超过一万,肯定是没问题的。

        思路三:运用扩展欧拉定理。(名词解释:扩展欧拉定理及证明)简单来说,就是当b\geq \phi (p)时,a^b\equiv a^{b \,Mod\, \phi(p) + \phi (p)} (Mod \, p),其中\phi(p)是欧拉函数,值为小于p的各正整数中与p互质的个数。对此题而言,p\equiv 1000,同时我们可以预处理出来\phi(1000)=400,那么这个1e1000的恐怖数字现在连1000都不剩了,整个问题就很轻松地迎刃而解了;而b< \phi (p)时,甚至直接暴力就可以解开了。


贴代码:       

这里贴个笨比思路一的代码吧,肯定不是最好的,但是能过T^T。

不要妄想读懂我的代码,因为一个月过去我自己也读不太懂了QAQ。

#include <bits/stdc++.h>
using namespace std;  
typedef long long ll;
const int INF = 0x3f3f3f;
      
      
      
int main(){
    //ifstream infile("input.txt", ios::in);
    //ofstream outfile("output.txt", ios::out);
      
    int x;
    string n;
    cin >> x >> n;
      
    void generateMap(int x, map<int, int>& linkMap, int res[2]);
    map<int,int> linkMap;
    int loopParameter[2];
    generateMap(x, linkMap, loopParameter);
      
    bool isBigger(string n, int a);
    bool hasForerunner = false;
    int getRemainder(string n, int a);
    int rem;
    if(isBigger(n, loopParameter[0])){
        void reduce(string& num1, int num2);
        reduce(n, loopParameter[0]);
        rem = getRemainder(n, loopParameter[1]);
        rem += loopParameter[0];
    } else{
        rem = getRemainder(n, INF);
    }
      
      
    int search(map<int, int> linkMap, int count, int x, bool& hasForerunner);
    int res = search(linkMap, rem, x, hasForerunner);
      
    void output(int res, bool hasForerunner);
    output(res, hasForerunner);
      
    return 0;
}

//用于打表,得到一个用map表示的伪链表
void generateMap(int x, map<int, int>& linkMap, int res[2]){
    //res[0] stores the length of fore body, res[1] stores the length of loop body
    int getNext(int cur, int x);
    int cur = x;
    while(linkMap.find(cur) == linkMap.end()){
        int next = getNext(cur, x);
        linkMap.insert(pair<int, int> (cur, next));
        cur = next;
    }
    int pt = x;
    int count = 0;
    while(pt != cur){
        pt = linkMap[pt];
        count ++;
    }
    res[0] = count;
    res[1] = linkMap.size() - count;
}

//用于计算某节点的下一个节点
int getNext(int cur, int x){
    cur *= x;
    return cur % 1000;
}

//高精度减低精度,直接原地改变
void reduce(string& num1, int num2){
    string::iterator it;
    it = num1.end();
    it--;
    if(*(it) - '0' > num2)
        *(it) -= num2;
    else{
        *(it) += 10;
        *(it) -= num2;
        while(true){
            it--;
            if(*(it) - '0' > 0){
                *(it) -= 1;
                break;
            } else{
                *(it) = '9';
            }
        }
    }
}

//返回高精度对低精度求模
int getRemainder(string n, int a){
    int pt = 1;
    int cur = n[0] - '0';
    while(pt != n.size()){
        while(cur < a and pt != n.size()){
            cur *= 10;
            cur += n[pt++] - '0';
        }
        cur = cur % a;
    }
    if(cur >= a)
        cur = cur % a;
    if(cur == 0)
        return a;
    return cur;
}

//高精度与低精度的比大小
bool isBigger(string n, int a){
    int digit = 0;
    int ac = a;
    while(ac != 0){
        ac /= 10;
        digit ++;
    }
    if(n.size() > digit)
        return true;
    else if(n.size() < digit)
        return false;
    else{
        int rem = 0;
        for(auto ch: n){
            rem *= 10;
            rem += ch-'0';
        }
        return rem > a;
    }
    return -1;
}

//在map中查找某个位置的值
int search(map<int, int> linkMap, int count, int x, bool& hasForerunner){
    count --;
    int pt = x;
    if(pt >= 100)
        hasForerunner = true;
    while(count --){
        pt = linkMap[pt];
        if(!hasForerunner and pt >= 100)
            hasForerunner = true;
    }
    return pt;
}

//输出
void output(int res, bool hasForerunner){
    if(hasForerunner){
        int digit = 0;
        int rc = res;
        while(rc != 0){
            rc /= 10;
            digit++;
        }
        if(digit == 0)
            cout << "000" << endl;
        else if(digit == 1)
            cout << "00" << res << endl;
        else if(digit == 2)
            cout << "0" << res << endl;
        else
            cout << res << endl;
    } else{
        cout << res << endl;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

千里之码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值