HDU.1005 Number Sequence

本文深入解析HDU 1005 Number Sequence问题,探讨数列{an}

原题

HDU.1005 Number Sequence

分类

杂题

题意

给定一个数列 { a n } \left\{ a_n \right\} {an}的前两项 a 1 a_1 a1 a 2 a_2 a2,以及其递推公式,输出数列中某一项 a i a_i ai的值( i i i可能会很大)

输入/输出要求与格式
样例数的确定最后一个样例输入0 0 0表示结束
每个样例的输入每个样例一行输入三个数A B n
输出结果数列的第 n n n a n a_n an
输出格式每个案例结果独占一行

以下是数据范围:

数据数据范围
递推公式参数 1 ≤ A, B ≤ 1000 1 \leq \text{A, B} \leq 1000 1A, B1000
数列中元素位置 1 ≤ n ≤ 1 0 8 1 \leq n \leq 10^8 1n108

题解

总体思路

思路时间复杂度具体解释
穷举 O ( n ) O \left( n \right) O(n) a 3 a_3 a3开始,根据递推公式,一个一个地计算到 a n a_n an
找循环节 O ( 1 ) O \left( 1 \right) O(1)根据递推关系与范围限制,
矩阵快速幂学术不精,以后补上根据递推关系与范围限制,

注:本文用 a n a_n an代替原题中所说的 f ( n ) f \left( n \right) f(n).

思路①分析

如果不做任何优化,时间、空间都会不够。

空间的优化好办,每次只存三个元素,前两个推出第三个,要推第四个元素的时候用二、三两个元素覆盖一、二两个元素,重复前面的方法。

但是归根到底时间还是硬伤,只能舍弃这种纯暴力计算的方法了。

思路②分析

纯暴力的方法不行,于是我们猜想,是否存在着某种规律。

注意到公式里的 % 7 \% 7 %7,于是第一个想到的关键词是“有限”, a i a_i ai的取值范围只会在 0 ∼ 6 0 \sim 6 06而且还是整数。根据公式很容易可以知道,在 A \text{A} A B \text{B} B确定的情况下,某一项的值只由其前两项的值决定。

很快我们得到一个小结论:两个 0 ∼ 6 0 \sim 6 06的数的组合 ( a i − 1 , a i ) \left( a_{i-1}, a_i \right) (ai1,ai)决定下一个 0 ∼ 6 0 \sim 6 06的数 a i + 1 a_{i+1} ai+1

我们把这个小结论优化一下说法:一组 0 ∼ 6 0 \sim 6 06的数的组合 ( a i − 1 , a i ) \left( a_{i-1}, a_i \right) (ai1,ai)决定下一组 0 ∼ 6 0 \sim 6 06的数的组合 ( a i , a i + 1 ) \left( a_i, a_{i+1} \right) (ai,ai+1)

想到这点,我们思路总算是有点进展了,毕竟两个 0 ∼ 6 0 \sim 6 06的数的组合总共也就只有 7 × 7 = 49 7 \times 7 = 49 7×7=49种,那么从 ( a 1 , a 2 ) \left( a_1, a_2 \right) (a1,a2)对应的 ( 1 , 1 ) \left( 1, 1 \right) (1,1)开始往后到第 49 49 49组,也就是一直到 ( a 49 , a 50 ) \left( a_{49}, a_{50} \right) (a49,a50)。纵使 ( a 1 , a 2 ) ∼ ( a 49 , a 50 ) \left( a_1, a_2 \right) \sim \left( a_{49}, a_{50} \right) (a1,a2)(a49,a50) 49 49 49种组合个个都不相同,那么第 50 50 50个组合 ( a 50 , a 51 ) \left( a_{50}, a_{51} \right) (a50,a51)也必然是前面 49 49 49种组合之一了。

再结合之前的结论,我们很容易知道,在这种最极端的情况下,如果第 50 50 50个组合 ( a 50 , a 51 ) \left( a_{50}, a_{51} \right) (a50,a51)等于前面的第 i i i个组合 ( a i , a i + 1 ) 1 ≤ i ≤ 49 \left( a_i, a_{i+1} \right)_{1 \leq i \leq 49} (ai,ai+1)1i49,那么第 51 51 51个组合 ( a 50 , a 51 ) \left( a_{50}, a_{51} \right) (a50,a51)也会等于前面的第 i + 1 i+1 i+1个组合 ( a i + 1 , a i + 2 ) 1 ≤ i ≤ 49 \left( a_{i+1}, a_{i+2} \right)_{1 \leq i \leq 49} (ai+1,ai+2)1i49,那么我们就找到了一个循环结的起点 s t = i st = i st=i、周期 T = 50 − i T = 50 - i T=50i,我们很容易就能总结出 { a n } \left\{ a_n \right\} {an}的通项公式 a n = { a n n ≤ s t a s t + ( n − s t ) % T n > s t a_n = \begin{cases} a_n & {n \leq st}\\ a_{st + \left( n-st \right) \% T} & {n > st} \end{cases} an={anast+(nst)%Tnstn>st

但实际情况不会是前 49 49 49种组合都不同,往往很快就会进入循环节,我们需要找到不同情况下的一个循环节的起点。

有人认为 ( a 1 , a 2 ) = ( 1 , 1 ) \left( a_1, a_2 \right) = \left( 1, 1 \right) (a1,a2)=(1,1)一定会是循环节的起点,因此只要往后找第一个等于 ( 1 , 1 ) \left( 1, 1 \right) (1,1)的组合即可。
(1, 1)循环
实际上,这是未必的,就比如 A = B = 7 \text{A}=\text{B}=7 A=B=7,就会变成 ( 1 , 1 ) ⇒ ( 1 , 0 ) ⇒ ( 0 , 0 ) ⇒ ( 0 , 0 ) ⇒ ⋯ (1, 1) \Rightarrow (1, 0) \Rightarrow (0, 0) \Rightarrow (0, 0) \Rightarrow \cdots (1,1)(1,0)(0,0)(0,0),就会和 ( 1 , 1 ) (1, 1) (1,1)无关,变成 ( 0 , 0 ) (0, 0) (0,0)循环,这样的循环周期可以看成 T = 1 \text{T}=1 T=1.因此或许也会有人认为除了上面提到的 ( 1 , 1 ) (1, 1) (1,1)循环,最终会变成周期为1的循环。
(0, 0)循环
实际上,这也是未必的,就比如 A = 6 , B = 7 \text{A}=6,\text{B}=7 A=6,B=7,就会变成 ( 1 , 1 ) ⇒ ( 1 , 6 ) ⇒ ( 6 , 1 ) ⇒ ( 1 , 6 ) ⇒ ⋯ (1, 1) \Rightarrow (1, 6) \Rightarrow (6, 1) \Rightarrow (1, 6) \Rightarrow \cdots (1,1)(1,6)(6,1)(1,6),就会和 ( 1 , 1 ) (1, 1) (1,1)无关,变成 ( 1 , 6 ) − ( 6 , 1 ) (1, 6)-(6, 1) (1,6)(6,1)循环,这样的循环周期可以看成 T = 2 \text{T}=2 T=2.
(1, 6)-(6, 1)循环
多举几个例子,很容易可以知道,循环节的起点组合 ( a s t , a s t + 1 ) (a_{st}, a_{st+1}) (ast,ast+1)不是固定的,周期长度 T T T也不是固定的。

但是我们根据前面的结论可以知道,一定能找到落在范围 { 1 ≤ s t ≤ 49 1 ≤ T ≤ 49 \begin{cases} 1 \leq st \leq 49\\ 1 \leq T \leq 49 \end{cases} {1st491T49 s t st st T T T.

接下来就是如何去找循环节的 s t st st T T T

根据前面的分析,在知道 s t st st T T T的情况下,最多只有前 49 49 49项是对我们有用的,但是我们也需要去找 s t st st T T T。对前 49 49 49组的每一组 ( a i , a i + 1 ) (a_i, a_{i+1}) (ai,ai+1),我们一直往后寻找一组和其 ( a i + T , a i + T + 1 ) (a_{i+T}, a_{i+T+1}) (ai+T,ai+T+1),而根据 1 ≤ T ≤ 49 1 \leq T \leq 49 1T49这个条件,我们知道,最多只需要往后寻找 49 49 49次:

  • 如果一直不等,那肯定是卡在某个其他起点的循环节里面了,我们直接考虑下一组。
  • 如果找到相等,即找到 ( a i , a i + 1 ) = ( a j , a j + 1 ) (a_i, a_{i+1}) = (a_j, a_{j+1}) (ai,ai+1)=(aj,aj+1),我们就可以得出 { s t = i T = j − i \begin{cases} st = i\\ T = j - i \end{cases} {st=iT=ji然后根据上面的通项公式计算即可。

因此,我们需要先计算数列 { a n } \{ a_n \} {an}的前 100 100 100项,然后用上面的方法去寻找循环节的起点 s t st st、周期 T T T,最后带入通项公式。

思路③分析

其实,经过了思路②中的分析, ( a i − 1 , a i ) ⇒ ( a i , a i + 1 ) (a_{i-1}, a_i) \Rightarrow (a_i, a_{i+1}) (ai1,ai)(ai,ai+1),我们很容易感觉到这个递推公式中隐含着某种矩阵关系。

网上有很多大佬证明了这种方法是也是可以做出来的的,按理说快速幂的方法应该更适合ACM(毕竟快速幂的思想往往可以转移到其他题目上,而循环节的思想就很难了)

我个人对于快速幂掌握的也不是很熟练,以后有大把闲工夫的时候仔细思考思考或者拜读拜读大佬们的做法再更新这个思路的详细解法吧。😅😅😅

题解代码

HDU(C++/G++)AC代码如下:

#include <iostream>
#include <algorithm>
#include <functional>

using namespace std;

int a[105];

int main()
{
	ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);

	int A, B, n;
	int s, T, flag;

	//return (A * f(n - 1) + B * f(n - 2)) mod 7
	auto f2 = [](const int A, const int B)
	{
		return [=](int x)
		{
			return (A * a[x - 1] + B * a[x - 2]) % 7;
		};
	};
	
	// f(1) = 1, f(2) = 1
	a[1] = a[2] = 1;

	//循环输入每一组样例
	while (cin >> A >> B >> n, A || B || n)
	{
		//根据A、B生成计算函数
		auto f0 = f2(A, B);
		//先准备好前100个f(x)的数据
		for (int i = 3; i <= 100; ++i)
			a[i] = f0(i);
		//寻找循环前的数据准备
		flag = 0;
		//寻找循环,循环的起点、周期
		for (s = 1; s <= 49; ++s)
		{
			for (int j = s + 1; j <= s + 49; ++j)
				if (a[s] == a[j] && a[s + 1] == a[j + 1])
				{
					T = j - s;
					flag = 1;
					break;
				}
			if (flag)
				break;
		}
		//根据找到的循环体的起点、周期计算f(n)
		if (n <= s)
			cout << a[n] << endl;
		else
		{
			n -= s;
			n %= T;
			cout << a[s + n] << endl;
		}
	}

	return 0;
}

评价

这道题无疑是一道考验思维的题目,不能拘泥于一个一个往后推的暴力穷举。无论是循环节还是矩阵快速幂,都需要根据递推公式的特性转换一下思维。

补充

由于之前一个月学了一点Python,当时Python里面讲了一个高阶函数的概念。我就觉得,就算老版C++不支持,到了现在这种C++11、C++14、C++17新标准的时代,也总该有方法支持了吧。

稍微介绍一下高阶函数,高阶函数也是一种函数,要求至少满足下列条件之一:

  • 接收一个或多个函数作为参数
  • 返回一个函数

上面的题解代码中也用到了高阶函数的简单写法,返回一个lambda表达式构造的函数。

这里稍微解释一下这一小段代码:

//return (A * f(n - 1) + B * f(n - 2)) mod 7
auto f2 = [](const int A, const int B)
{
	return [=](int x)
	{
		return (A * a[x - 1] + B * a[x - 2]) % 7;
	};
};

函数f2接收两个参数A、B,将A、B代入表达式(A * a[x - 1] + B * a[x - 2]) % 7,生成函数f(x),x为生成的函数的参数。举几个例子:

  • f2(3, 3)将会生成一个参数为x、返回值为(3 * a[x - 1] + 3 * a[x- 2]) % 7的函数
  • f2(2, 3)将会生成一个参数为x、返回值为(2 * a[x - 1] + 3 * a[x- 2]) % 7的函数
  • f2(0, 3)将会生成一个参数为x、返回值为(3 * a[x- 2]) % 7的函数

下面是题解代码中用高阶函数f2生成函数的代码:

		//根据A、B生成计算函数
		auto f0 = f2(A, B);

看上去好像很高大上一样,其实用一个函数给三个参数直接返回也能搞定,勉强也算是一点C++新特性下的一点小花样吧。

平时练习刷刷小花样都没事,我觉得了解了解新特性也没少不好的,到了正式要竞赛之类的的时候,还是用常规写法,求稳一点吧。😄😄😄

开心一刻

当我看完这道题目的时候,我就知道,机会来了,我要创造一份最短代码来解决这个题目,而且要比别人短很多,不过能不能AC还是得看看运气,我相信只要我日复一日地提交代码,总有一天会AC的。

不说了,代码如下:

#include <iostream>
#include <ctime>

using namespace std;

int main()
{
	srand(time(NULL));
	int A, B, n;
	while (cin >> A >> B >> n, A || B || n)
		cout << (rand() % 7) << endl;

	return 0;
}

(hhh,开个小玩笑,到底是不是有一丝丝可能我也不确定)

后话

光是写这一篇博客就已经花了我4个小时时间,还有之前解题大概花了2小时时间。再加上双休日过去之后还有学校的网课加持,我想还是打一下脸吧,不要每日一更了,每周三更吧。😖😖😖

加油,疫情一定会很快挺过去的!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

God-Excious

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

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

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

打赏作者

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

抵扣说明:

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

余额充值