[旧题新解]HDU1005 Number Sequence

题意

f(1)=f(2)=1 f ( 1 ) = f ( 2 ) = 1

f(n)=(Af(n1)+Bf(n2))mod7 f ( n ) = ( A ∗ f ( n − 1 ) + B ∗ f ( n − 2 ) ) mod 7

1A,B1,000,1n108 1 ≤ A , B ≤ 1 , 000 , 1 ≤ n ≤ 10 8

f(n) f ( n )

思路

看到此题,一种比较平凡的思路就是找规律。很多人的直觉是,循环节的长度必然是49。想到这种想法,主要的理由就是对于一对(A, B)的取值, (f(n),f(n1)) ( f ( n ) , f ( n − 1 ) ) 的值对最多有49种,所以必然能在50步内找到循环节。由此形成如下代码:

#include 
using namespace std;
const int N = 50;
int s[N];
int main(){
    int a, b, n;
    while(scanf("%d%d%d", &a, &b, &n), a+b+n){
        s[0] = s[1] = 1;
        int T = 49;
        for(int i = 2; i < N; i++){
            s[i] = (a*s[i-1]+b*s[i-2]) % 7;
            if(s[i] == 1 && s[i-1] == 1){
                T = i-1;
                break;
            }
        }
        printf("%d\n", s[(n+T-1)%T]);
    }
    return 0;
}

提交后,我们发现这份代码是AC的。但是有一组样例,这样的写法是明显错误的:

A=7x,B=7y A = 7 x , B = 7 y

此时
f(x)={1x=0,10x2 f ( x ) = { 1 x = 0 , 1 0 x ≥ 2

这就不符合我们之上的理论了。

我们可以重新思考题意并建立模型,如果我们把任一对相邻的 f(x) f ( x ) 值看作一个点即 V={(f(i1),f(i))} V = { ( f ( i − 1 ) , f ( i ) ) } 那么我们可以知道任一 vi v i 有且仅有一条可能指向自身的边,即 f(a,b):vivj f ( a , b ) : v i → v j 。显然,这是一个49个点组成的图,且每个点有且仅有一条出边。

按直觉我们可以认定这个图必然有环,如果需要严谨证明的话,大概只能用到归纳法了:

若存在有n个点,每个点有一条出边的无环图,那么它必然是由n-1个点的具有同样性质的无环图产生的。(因为一个有环图不能通过增加一个点与一条点上的出边变得无环)

而已知当n=2时,不存在一个2个点,每个点均有一条出边的无环图。

所以有n个点,每个点有一条出边的图必有环。

那么重点即是,这个图中的环并不一定是从头开始,即(1,1)可能并不在循环节中:

[旧题新解]HDU1005 Number Sequence_第1张图片

知道以上结论后,我们可以将之上的理论进行代码实现:

#include 
using namespace std;
const int N = 10;
const int M = 50;
int fir[N][N], ans[M];
int main()
{
    int a, b, n;
    while(scanf("%d%d%d", &a, &b, &n), a+b+n){
        int x, y, cnt;
        x = y = cnt = 1;
        if(n == 1 || n == 2){
            printf("1\n");
            continue;
        }
        memset(fir, 0, sizeof(fir));
        memset(ans, 0, sizeof(ans));
        while(!fir[x][y]){
            fir[x][y] = cnt;
            ans[cnt++] = y;
            int v = (a*y + b*x)%7;
            x = y;
            y = v;
        }
        --n;
        if(n < fir[x][y])  printf("%d\n", ans[n]);
        else{
            int loop = cnt - fir[x][y];
            n -= fir[x][y];
            n %= loop;
            printf("%d\n", ans[n+fir[x][y]]);
        }
    }
    return 0;
}

即用fir数组来保存某一个值对第一次出现的计数,并为了我们方便地得到答案,使用 ansi a n s i 保存第i个答案。其中fir[x][y]保存的就是循环节的前导部分,loop为循环节长度:

[旧题新解]HDU1005 Number Sequence_第2张图片


此题还有另外一种解法,即 O(logn) O ( log ⁡ n ) 的矩阵快速幂的算法。

做过用矩阵快速幂求斐波那契数列的第n项的同学会知道,有下式:

(1110)(fn1fn2)=(fnfn1) ( 1 1 1 0 ) ∗ ( f n − 1 f n − 2 ) = ( f n f n − 1 )

即对于 An=Tn1Ai A n = T n − 1 ∗ A i ,其中
T=(1110) T = ( 1 1 1 0 )

对于二次递推 f(n)=Af(n1)+Bf(n2)+C f ( n ) = A ∗ f ( n − 1 ) + B ∗ f ( n − 2 ) + C ,有:

A10B00101fn1fn2C=fnfn1C ( A B 1 1 0 0 0 0 1 ) ∗ ( f n − 1 f n − 2 C ) = ( f n f n − 1 C )

由于此题中 C=0 C = 0 ,故本题中
T=(A1B0) T = ( A B 1 0 )

代码为:

#include
using namespace std;
const int N = 2;
struct Mat{
    int a[N][N];
    Mat(){memset(a, 0, sizeof(a));}
    Mat operator * (Mat& rh){
        Mat ret;
        for(int i = 0; i < N; i++)
            for(int j = 0; j < N; j++)
                for(int k = 0; k < N; k++){
                    ret.a[i][j] += a[i][k] * rh.a[k][j];
                    ret.a[i][j] %= 7;
                }
        return ret;
    }
};
Mat powMod(Mat a, int e){
    Mat ret;
    for(int i = 0; i < N; i++) ret.a[i][i] = 1;
    for(; e; e>>=1){
        if(e&1) ret = ret*a;
        a = a*a;
    }
    return ret;
}
int main()
{
    int a, b, n;
    while(scanf("%d%d%d", &a, &b, &n), a+b+n){
        Mat ans;
        ans.a[0][0] = a%7; ans.a[0][1] = b%7; ans.a[1][0] = 1;
        if(n<=2){
            printf("1\n");
            continue;
        }
        ans = powMod(ans, n-2);
        printf("%d\n", (ans.a[0][0]+ans.a[0][1])%7);
    }
    return 0;
}

你可能感兴趣的:(算法)