ZOJ 1222 超詳細(xì)題解

ZOJ 1222 解題報告

此題和HDU上的1066是一樣的

題目大意

求一個數(shù)階乘的最后一個非零位
樣例:

  • 0 ---> 1
  • 1 ---> 1
  • 2 ---> 2
  • 26 ---> 4
  • 125 ---> 8
  • 3125 ---> 2
  • 9999 ---> 8

初步分析

如果階乘的過程中沒有產(chǎn)生10的倍數(shù)(末尾也就不會出現(xiàn)0),那么整個階乘階段只需要記錄乘積的最后一位即可。進(jìn)一步分析可知,5是產(chǎn)生10的關(guān)鍵因子,因此,在計算的過程中需要將因子5提取出來單獨(dú)考慮。

具體分析

設(shè)f(n)表示n!的最后一非零位,由上述分析可知,5是產(chǎn)生10的關(guān)鍵因子,g(n)表示階乘過程中把所有5的倍數(shù)換成1的結(jié)果。例如:

h(n)表示g(n)的最后一位數(shù)字,很明顯這個數(shù)一定不為0。

下圖列出了從[0, 39]h(n)的值:

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
1 1 2 6 4 4 4 8 4 6 6 6 2 6 4 4 4 8 4 6 6 6 2 6 4 4 4 8 4 6 6 6 2 6 4 4 4 8 4 6

可以看出除了01以外(其實(shí)是一樣的,后面會討論)后面的都是按照一定規(guī)律的循環(huán)(每10個一循環(huán)),循環(huán)體是6 6 2 6 4 4 4 8 4 6

可知h(n)的取值范圍是2、4、6、8

n的階乘按照上述思路可以做以下的分解:

其中,表達(dá)式

是能夠除盡的,因為g(n)中因子2的數(shù)量比因子5的數(shù)量要多,這里只是除掉了因子5數(shù)量的因子2,由于g(n)是去掉因子5的階乘結(jié)果,其不會產(chǎn)生尾0,因此計算結(jié)果只需要保存最后一位即可。

f(n)顯然應(yīng)該等于

的最后一位數(shù)字,右半部分是可以遞歸求解的,左邊部分的最后一位數(shù)字記為l(n)

l(n)其實(shí)是下面表達(dá)式的循環(huán)結(jié)果(除以2的次數(shù)不一樣)

上述表達(dá)式的結(jié)果應(yīng)該是個偶數(shù)(左半部分因子2沒有除完),只能存在于2、4、6、8中

有如下對應(yīng)關(guān)系(除以2):

2 ---> 6 ---> 8 ---> 4 ---> 2
4 ---> 2 ---> 6 ---> 8 ---> 4
6 ---> 8 ---> 4 ---> 2 ---> 6
8 ---> 4 ---> 2 ---> 6 ---> 8

發(fā)現(xiàn)此對應(yīng)關(guān)系也是循環(huán)結(jié)構(gòu)(每4個一個循環(huán))

代碼

所有代碼直接復(fù)制可AC(需要去掉//注釋,不然會Compilation Error)

按照上面的分析寫出如下代碼:

遞歸代碼

#include <stdio.h>
#include <string.h>

#define MAX 1000

//此數(shù)組是g(n)的最后一位即h(n)從0-9的取值
int arr_no_5[] = {6, 6, 2, 6, 4, 4, 4, 8, 4, 6};

//此數(shù)組是考慮n屬于[0, 4]的情況,因為0!= 1、1!= 1,而不是上面的6,需要特殊考慮
int arr_small[] = {1, 1, 2, 6, 4};

//題目n的取值范圍很大,因此采用字符串的形式接受,后面的運(yùn)算也是大數(shù)形式的運(yùn)算
char n[MAX];

int f(char *, int);

//對應(yīng)上面的左半部分即l(n)
int l(char *, int);

//對應(yīng)上面的h(n),對應(yīng)于數(shù)組arr_no_5的值
int h(char *, int);

//除以5,只保留整數(shù)部分和C語言中的整數(shù)除法一樣
void div5(char *, int);

//對4取模運(yùn)算
int mod4(char *, int);

int main(void) {
    int len;
    memset(n, '\0', sizeof(char) * MAX);
    while (scanf("%s", n) != EOF) {
        len = strlen(n);
        printf("%d\n", f(n, len));
        memset(n, '\0', sizeof(char) * MAX);
    }
    return 0;
}

int f(char n[], int len) {
    char t[MAX];
    memcpy(t, n, sizeof(char) * MAX);
    if (len == 1 && n[0] - '0' >= 0 && n[0] - '0' <= 4) {
        return arr_small[n[0] - '0'];
    }
    //對應(yīng)上述遞歸公式
    div5(t, len);
    return (l(n, len) * f(t, strlen(t))) % 10;
}

int l(char n[], int len) {
    char t[MAX];
    int m = h(n, len);
    int i;
    int ct;
    memcpy(t, n, sizeof(char) * MAX);
    div5(t, len);
    //h(n)除以2的結(jié)果是每4個一循環(huán)
    ct = mod4(t, strlen(t));
    for (i = 0; i < ct; ++i) {
        //對應(yīng)于上面的轉(zhuǎn)換表
        switch (m) {
        case 6:
            m = 8;
            break;
        case 2:
            m = 6;
            break;
        case 4:
            m = 2;
            break;
        case 8:
            m = 4;
            break;
        }
    }
    return m;
}

int h(char n[], int len) { 
    return arr_no_5[n[len - 1] - '0']; 
}

void div5(char n[], int len) {
    char t[MAX];
    int res, flag = 0;
    int i;
    memset(t, '\0', sizeof(char) * MAX);
    for (i = 0; i < len; ++i) {
        t[i] = n[len - 1 - i];
    }
    //先乘以2
    for (i = 0; i < len; ++i) {
        res = (t[i] - '0') * 2 + flag;
        flag = res / 10;
        res %= 10;
        t[i] = res + '0';
    }
    if (flag) {
        t[len] = flag + '0';
        len++;
    }
    for (i = 1; i < len; ++i) {
        t[i - 1] = t[i];
    }
    t[--len] = '\0';
    memset(n, '\0', sizeof(char) * MAX);
    //再除以10,即去掉個位
    for (i = 0; i < len; ++i) {
        n[i] = t[len - 1 - i];
    }
    return ;
}

int mod4(char n[], int len) {
    int m;
    //模4只與后兩位有關(guān)系,其值等于后面二位模4的值
    m = n[len - 1] + n[len - 2] * 10;
    return m % 4;
}

上面的代碼有遞歸,其實(shí)非遞歸也很好寫,用循環(huán)即可代替:

非遞歸代碼

#include <stdio.h>
#include <string.h>

#define MAX 1000

//此數(shù)組是g(n)的最后一位即h(n)從0-9的取值
int arr_no_5[] = {6, 6, 2, 6, 4, 4, 4, 8, 4, 6};

//此數(shù)組是考慮n屬于[0, 4]的情況,因為0!= 1、1!= 1,而不是上面的6,需要特殊考慮
int arr_small[] = {1, 1, 2, 6, 4};

//題目n的取值范圍很大,因此采用字符串的形式接受,后面的運(yùn)算也是大數(shù)形式的運(yùn)算
char n[MAX];

int f(char *, int);

//對應(yīng)上面的左半部分即l(n)
int l(char *, int);

//對應(yīng)上面的h(n),對應(yīng)于數(shù)組arr_no_5的值
int h(char *, int);

//除以5,只保留整數(shù)部分和C語言中的整數(shù)除法一樣
void div5(char *, int);

//對4取模運(yùn)算
int mod4(char *, int);

int main(void) {
    int len;
    memset(n, '\0', sizeof(char) * MAX);
    while (scanf("%s", n) != EOF) {
        len = strlen(n);
        printf("%d\n", f(n, len));
        memset(n, '\0', sizeof(char) * MAX);
    }
    return 0;
}

int f(char n[], int len) {
    int res = 1;
    char t[MAX];
    memcpy(t, n, sizeof(char) * MAX);
    if (len == 1 && n[0] - '0' >= 0 && n[0] - '0' <= 4) {
        return arr_small[n[0] - '0'];
    }
    //對應(yīng)上述遞歸公式,其他都是一樣的,用循環(huán)代替遞歸
    while (strlen(t)) {
        res = (l(t, strlen(t)) * res) % 10;
        div5(t, strlen(t));
    }
    return res;
}

int l(char n[], int len) {
    char t[MAX];
    int m = h(n, len);
    int i;
    int ct;
    memcpy(t, n, sizeof(char) * MAX);
    div5(t, len);
    //h(n)除以2的結(jié)果是每4個一循環(huán)
    ct = mod4(t, strlen(t));
    for (i = 0; i < ct; ++i) {
        //對應(yīng)于上面的轉(zhuǎn)換表
        switch (m) {
        case 6:
            m = 8;
            break;
        case 2:
            m = 6;
            break;
        case 4:
            m = 2;
            break;
        case 8:
            m = 4;
            break;
        }
    }
    return m;
}

int h(char n[], int len) { 
    return arr_no_5[n[len - 1] - '0']; 
}

void div5(char n[], int len) {
    char t[MAX];
    int res, flag = 0;
    int i;
    memset(t, '\0', sizeof(char) * MAX);
    for (i = 0; i < len; ++i) {
        t[i] = n[len - 1 - i];
    }
    //先乘以2
    for (i = 0; i < len; ++i) {
        res = (t[i] - '0') * 2 + flag;
        flag = res / 10;
        res %= 10;
        t[i] = res + '0';
    }
    if (flag) {
        t[len] = flag + '0';
        len++;
    }
    for (i = 1; i < len; ++i) {
        t[i - 1] = t[i];
    }
    t[--len] = '\0';
    memset(n, '\0', sizeof(char) * MAX);
    //再除以10,即去掉個位
    for (i = 0; i < len; ++i) {
        n[i] = t[len - 1 - i];
    }
    return ;
}

int mod4(char n[], int len) {
    int m;
    //模4只與后兩位有關(guān)系,其值等于后面二位模4的值
    m = n[len - 1] + n[len - 2] * 10;
    return m % 4;
}

網(wǎng)上代碼

在網(wǎng)上看到最多的能夠AC的代碼,思路和上面是一樣的但是寫法相當(dāng)簡潔,特意分析學(xué)習(xí)(注釋是后加的)

代碼如下:

#include <stdio.h>
#include <string.h>

int mod[20] = {1, 1, 2, 6, 4, 2, 2, 4, 2, 8, 4, 4, 8, 4, 6, 8, 8, 6, 8, 2};

char n[1000];

//存放n的逆序,方便做計算
int a[1000];

int main(void) {
    int i, c, t, len;
    while (scanf("%s", n) != EOF) {
        t = 1;
        len = strlen(n);
        for (i = 0; i < len; i++)
            a[i] = n[len - 1 - i] - '0';
        while (len) {
            len -= !a[len - 1];
            //對應(yīng)于上面遞歸公式
            t = t * mod[a[1] % 2 * 10 + a[0]] % 10;
            //將n除以5
            for (c = 0, i = len - 1; i >= 0; i--) {
                c = c * 10 + a[i];
                a[i] = c / 5;
                c %= 5;
            }
        }
        printf("%d\n", t);
    }
    return 0;
}

前面分析了h(n)是個長度為10的循環(huán),而h(n)/2是長度為4的循環(huán),因此l(n)是長度為20(10和4的最小公倍數(shù))的循環(huán),其循環(huán)體是6 6 2 6 4 2 2 4 2 8 4 4 8 4 6 8 8 6 8 2

而0! = 1、1! = 1不是上面的6,因此需要特殊處理

#include <stdio.h>
#include <string.h>
//對于0、1的特殊處理
int small[] = {1, 1};
//l(n)的循環(huán)
int mod[20] = {6, 6, 2, 6, 4, 2, 2, 4, 2, 8, 4, 4, 8, 4, 6, 8, 8, 6, 8, 2};

char n[1000];

//存取n的逆序,方便后續(xù)計算
int a[1000];

int main() {
    int i, c, t, len;
    while (scanf("%s", n) != EOF) {
        t = 1;
        len = strlen(n);
        //特殊處理
        if (len == 1 && (n[0] - '0' == 0 || n[0] - '0' == 1)) {
            t = small[n[0] - '0'];
        } else {
            //逆序n方便處理
            for (i = 0; i < len; i++)
                a[i] = n[len - 1 - i] - '0';
            while (len) {
                //n除以5是0時退出循環(huán),即是(n/5)的階乘計算完了,相應(yīng)的長度也就是0
                len -= !a[len - 1];
                //對應(yīng)于上面的遞歸公式
                t = t * mod[a[1] % 2 * 10 + a[0]] % 10;
                //計算n除以5
                for (c = 0, i = len - 1; i >= 0; i--)
                    c = c * 10 + a[i], a[i] = c / 5, c %= 5;
            }
        }
        printf("%d\n", t);
    }
    return 0;
}

觀察到網(wǎng)上代碼的l(n)循環(huán)最前面的兩位是1、1,其實(shí)這里的1和6是沒有區(qū)別的,6和2、4、6、8相乘結(jié)果的最后一位還是原來本身,1也有這樣的效果,再考慮公式

若在n >= 10的情況下,左半部分是6或者是1,乘以右邊的結(jié)果都是一樣的,也就是右邊結(jié)果本身

階乘結(jié)果的最后一位非零數(shù)肯定是2、4、6、8中的一個

因此mod數(shù)組中6和1是可以互換的,考慮到0、1的特殊情況,采用網(wǎng)上的1 1 2 6 4 2 2 4 2 8 4 4 8 4 6 8 8 6 8 2是比較合理的,不需要額外的特殊處理,代碼簡潔

把mod數(shù)組中的6全部換成1:

#include <stdio.h>
#include <string.h>

//在n小于10的情況下的特殊處理,雖然0、1是一樣的,但是有不一樣的,比如3
//沒有做特殊處理在HDUOJ上是WA,但是在ZOJ是AC的。。。。。。
int small[] = {1, 1, 2, 6, 4, 2, 2, 4, 2, 8};

int mod[20] = {1, 1, 2, 1, 4, 2, 2, 4, 2, 8, 4, 4, 8, 4, 1, 8, 8, 1, 8, 2};

char n[1000];

int a[1000];

int main() {
    int i, c, t, len;
    while (scanf("%s", n) != EOF) {
        t = 1;
        len = strlen(n);
        if (len == 1 && n[0] - '0' >= 0 && n[0] - '0' <= 9) {
            t = small[n[0] - '0'];
        }else {
            for (i = 0; i < len; i++)
                a[i] = n[len - 1 - i] - '0';
            while (len) {
                len -= !a[len - 1];
                t = t * mod[a[1] % 2 * 10 + a[0]] % 10;
                for (c = 0, i = len - 1; i >= 0; i--)
                    c = c * 10 + a[i], a[i] = c / 5, c %= 5;
            }
        }
        printf("%d\n", t);
    }
    return 0;
}

把mod數(shù)組中的1全部換成6,代碼前面已經(jīng)有了

總結(jié)

這種類型的題先寫出數(shù)學(xué)公式,根據(jù)數(shù)學(xué)公式來寫代碼就簡單了,中間處理過程或者處理結(jié)果一般都會存在某種規(guī)律,最后注意大數(shù)的處理

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 第八章 遞歸(recursion) 8.1 導(dǎo)語 因為一些指導(dǎo)者傾向于先教遞歸作為第一個主要的控制結(jié)構(gòu),本章會以另...
    geoeee閱讀 1,539評論 0 5
  • 在C語言中,五種基本數(shù)據(jù)類型存儲空間長度的排列順序是: A)char B)char=int<=float C)ch...
    夏天再來閱讀 4,045評論 0 2
  • 前言 遞歸是算法中一種非常重要的思想,應(yīng)用也很廣,小到階乘,再在工作中用到的比如統(tǒng)計文件夾大小,大到 Google...
    謝kun閱讀 8,825評論 0 15
  • 什么是遞歸函數(shù) 一種計算過程,如果其中每一步都要用到前一步或前幾步的結(jié)果,稱為遞歸的。用遞歸過程定義的函數(shù),稱為遞...
    古月半半閱讀 5,054評論 0 1
  • 8月22日-----字符串相關(guān) 2-3 個性化消息: 將用戶的姓名存到一個變量中,并向該用戶顯示一條消息。顯示的消...
    future_d180閱讀 1,034評論 0 1

友情鏈接更多精彩內(nèi)容