「PMOI-4」人赢 的题解
在解题之前提交题解的代码会导致封禁。
作者:
概述
给定数列的前两项 、
(
),从第三项起每一项都是前两项乘积的个位数:
再给定位置 (
),要求输出
。
直接按定义递推需要 的时间,而
最大可以到
,在 1 秒的时限内显然做不到。但每一项都只有
种取值,相邻两项组成的「状态」只有
种,所以数列必然在很短的距离内进入循环。本题的全部内容就是发现并利用这个循环。
分析
核心观察
把相邻两项作为一个整体来看:
- 每一项都由前两项完全确定,即
只取决于
;
- 每一项的取值范围都是
,所以有序二元组
最多只有
种。
从下标 开始考察状态序列
,它一共只有
种可能的取值。根据抽屉原理,第
个状态(即
)必定与它前面的某个状态相同;换句话说,只要模拟不超过
步,就一定能找到两个下标
满足
。
状态相同就意味着后续完全相同:从 与从
出发走的是同一个递推过程,因此对一切
都有
,即数列从第
项开始以
为周期循环。
一旦求出 ,任意大的
都能
回答:
下标 落在
内,都是模拟时已经算出来的位置。
思路
直接按定义模拟,同时记下每个状态第一次出现的位置:
- 若状态
第一次出现在下标
(即
),就记下这个
;
- 模拟到下标
时发现
已经出现过,设它第一次出现的位置是
,则循环节长度
,答案用上面的公式给出;
- 若还没发现循环就已经模拟到了
(
很小的情况),直接输出
即可。
另外题面只保证 ,而递推式只对
成立,因此
与
必须单独回答
和
,不能丢进模拟里。
由于 最大
,模拟步数必须与
无关;好在循环必然在前
步内出现,算法天然满足这一点。
具体示例
样例 1:。
逐项计算:,其后的每一项都是
,于是数列是
。
模拟过程:
- 状态
出现在下标
;
- 下标
:
,状态
第一次出现,记在下标
;
- 下标
:
,状态
已经出现过,第一次出现在下标
,于是
、
;
,答案为
。
样例 2:。
前几项每一项分别是前两项乘积的个位数,得到:
| 下标 | |||||||||
|---|---|---|---|---|---|---|---|---|---|
这里 ,模拟到第
项就直接得到答案
。
如果继续模拟下去,下标 的状态
会与下标
的状态
相同,于是
、
;用公式验证:
,与直接模拟一致。注意这个数列的循环并不是从第一项就开始的,开头的
并不参与循环——这正是取模定位时必须带上起点
的原因。
顺便一提,把全部 种
都模拟一遍可以发现:循环起点
总是
、
或
,循环节长度
总是
、
或
(其中
有
种组合)。这也说明
个状态足以保证循环出现得足够早。
算法步骤
- 读入三个整数
,其中
要按 64 位整数处理。
- 若
,输出
;若
,输出
;直接结束。
- 令
、
,把状态
第一次出现的位置记为
。
- 从
开始循环:
- 计算
;
- 若
,输出
并结束(
很小、还没进入循环的情况);
- 若状态
之前出现过,设第一次出现的位置为
,令
,输出
并结束;
- 否则记下状态
第一次出现的位置为
,进入下一个
。
- 计算
- 循环最多执行
次,就一定会因为状态重复而结束。
复杂度分析
- 时间复杂度:模拟步数不超过
步(
个状态里必有两个相同),每步都是常数次运算,所以总时间是
,即
,与
的大小无关。作为对比,直接递推到第
项是
,在
时无法通过。
- 空间复杂度:只需要存常数项数列(
个位置足够)和
个状态第一次出现的位置,即
。
实现注意事项
超出了 32 位整数的范围,C++ 里必须用
long long,读入用%lld;Python 的整数没有这个问题。与
必须特判:递推式从第
项才生效,下标
、
的答案由输入直接给出。
- 「状态还没出现过」的标记不要和合法下标混在一起。本题把初始状态记在下标
、其余状态记在下标
,所以用
表示未出现也是安全的,但显式写
更不容易出错。
或
是合法输入(此时从第三项起全是
),不要当作非法数据特判掉。
- 所有项都是非负的,
% 10就是题面所说的「个位数」,不需要处理负数取模。 - 数组开到
以上就够了(循环必然在前
步内出现),不要按
的大小开数组,更不要真的递推
步。
- 输出是单个数字加换行。
源代码
#include <bits/stdc++.h>
using namespace std;
int main() {
long long n, m, k;
if (scanf("%lld %lld %lld", &n, &m, &k) != 3) return 0;
// 递推只对 i > 2 成立,前两项由输入直接给出。
if (k == 1) { printf("%lld\n", n); return 0; }
if (k == 2) { printf("%lld\n", m); return 0; }
static int a[128];
static int firstAt[10][10]; // firstAt[x][y]:状态 (x, y) 第一次出现的位置,0 表示还没出现
memset(firstAt, 0, sizeof(firstAt));
a[1] = (int)n;
a[2] = (int)m;
firstAt[a[1]][a[2]] = 2; // 初始状态 (a1, a2) 出现在位置 2
for (int i = 3; ; ++i) {
a[i] = a[i - 1] * a[i - 2] % 10;
if (i == k) { printf("%d\n", a[i]); return 0; } // k 很小,没等到循环
int x = a[i - 1], y = a[i];
if (firstAt[x][y] != 0) {
int j = firstAt[x][y]; // 循环从位置 j 开始
int p = i - j; // 循环节长度
printf("%d\n", a[j + (int)((k - j) % p)]);
return 0;
}
firstAt[x][y] = i;
}
}
import sys
def main():
data = sys.stdin.read().split()
n, m, k = int(data[0]), int(data[1]), int(data[2])
if k == 1:
print(n)
return
if k == 2:
print(m)
return
a = [0, n, m] # a[1] = n, a[2] = m
first_at = {(n, m): 2} # 状态 (a[i-1], a[i]) 第一次出现的位置
i = 3
while True:
a.append(a[i - 1] * a[i - 2] % 10)
if i == k:
print(a[i])
return
key = (a[i - 1], a[i])
if key in first_at:
j = first_at[key]
p = i - j
print(a[j + (k - j) % p])
return
first_at[key] = i
i += 1
main()
评论