CSP历年复赛题-P1050 [NOIP2005 普及组] 循环
原题链接:https://www.luogu.com.cn/problem/P1050
题意解读:对于一个长达100为的整数n,计算在n的t次幂后,其后k位出现和初始情况一样,求最小的t,也就是循环长度。
解题思路:
首先,看到数据规模,此题肯定是高精度;
其次,如果直接采用模拟法,进行n*n,取后k位,高精度乘法的复杂度是100*100,判断后k位是否相等复杂度100,如果循环长度大于100,则会超时;
最后,一定要找到某种规律,不能直接模拟法,才能解得此题。
考试时,关键在于得分,我们一步一步来,先看看直接模拟法如何得分!
1、模拟法-非高精度
直接反复乘n,对后k位进行比较,一旦与初始相等,输出循环长度,否则超过一定次数还找不到,输出-1。
20分代码:
#include <bits/stdc++.h>
using namespace std;
int n, k;
int main()
{
cin >> n >> k;
int cnt = 0;
int pow10k = 1; //10^k
for(int i = 1; i <= k; i++) pow10k *= 10;
int st = n % pow10k, ed = n; //st是初始后k位,ed记录n^t的结果后k位
int times = 10000000;
while(times--)
{
cnt++;
ed = (ed * n) % pow10k;
if(ed == st)
{
cout << cnt;
return 0;
}
}
cout << -1;
return 0;
}
2、模拟法-高精度
加上高精度,会不会多得一些分呢?
事实证明,在调整循环次数的情况下,艰难的多得了一分,没有比第一种好多少!
30分代码:
#include <bits/stdc++.h>
using namespace std;
vector<int> n;
int k;
vector<int> mul(vector<int> a, vector<int> b)
{
vector<int> res(a.size() + b.size());
for(int i = 0; i < k; i++) //高精度乘法只取后k位即可
{
for(int j = 0; j < k; j++) //高精度乘法只取后k位即可
{
if(i + j < k) //高精度乘法只取后k位即可
{
res[i + j] += a[i] * b[j];
res[i + j + 1] += res[i + j] / 10;
res[i + j] %= 10;
}
}
}
return res;
}
int main()
{
string str;
cin >> str >> k;
for(int i = 0; i < str.size(); i++)
n.push_back(str[str.size() - 1 - i] - '0'); //高精度,个位在前
int cnt = 0;
int pow10k = 1; //10^k
for(int i = 1; i <= k; i++) pow10k *= 10;
vector<int> st(n), ed(n); //st是初始后k位,ed记录n^t的结果后k位
int times = 10000;
while(times--)
{
cnt++;
ed = mul(ed, n); //不断乘n
bool yes = true;
for(int i = 0; i < k; i++) //判断后k位是否和初始相等
{
if(ed[i] != st[i])
{
yes = false;
break;
}
}
if(yes)
{
cout << cnt;
return 0;
}
}
cout << -1;
return 0;
}
3、递推-高精度
第二种方法的唯一问题是超时,主要原因就是要枚举n的每一次幂才行,有没有更简洁的方式呢?
可以假设后k-1位的循环长度已经计算出来,设为t,即n * n^t % 10^(k-1)和n % (k-1)相同
对于后k位如果要相同,必须有后k-1位相同,因此后k位的循环长度一定是t的倍数,有可能是1/2/3...s倍
即,n * n^t,n * n^2t, n * n^3t......某一个的后k位与n的后k位相同,又因为后k-1已经相同,所以只需要判断第k位是否相同
而第k位要相同,最多只需要10次枚举,因为整数只有0~9,如果超过10次还没有相同,则不存在循环。
根据以上分析,可以从最后1位、2位、3位的循环长度一直推出k位的循环长度。
举例:
数据 198123 4
,因为要求只取后 4 位,所以将其截取成 8123
。
我们逐位进行处理:
- 先处理最后一位的循环长度:最后一位是
3
,在乘了4次8123后出现了循环,循环长度为 4。所以后两位的循环节长度一定为 4 的倍数,为了加快计算,我们可以将乘数变为8123^4
,取后 4 位变成0641
。 - 再处理后两位:后两位是
23
,在乘了 5 次0641
后出现了循环,循环节长度为 4*5=20。同样为了加快计算,乘数变为8123^20=0641^5
,取后 4 位变成9201
。之后就按照这样的方法处理即可。 - 后三位:后三位是
123
,乘了 5 次9201
后出现循环,循环节长度为 20*5=100 ,乘数变为9201^5%(10^4)=6001
- 后四位:后四位是
8123
,乘了 5 次6001
后出现循环,循环节长度为 100*5=500,500 就是最终的答案。
100分代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 105;
vector<int> n;
int t[N]; //t[k]表示后k位的循环长度:后k-1位循环长度,t[0]=1
int k;
//高精度 * 高精度
vector<int> mul1(vector<int> a, vector<int> b)
{
a.resize(k); //只取k位,提速
b.resize(k); //只取k位,提速
vector<int> res(a.size() + b.size());
for(int i = 0; i < k; i++) //高精度乘法只取后k位即可
{
for(int j = 0; j < k; j++) //高精度乘法只取后k位即可
{
if(i + j < k) //高精度乘法只取后k位即可
{
res[i + j] += a[i] * b[j];
res[i + j + 1] += res[i + j] / 10;
res[i + j] %= 10;
}
}
}
res.resize(k); //只取k位,提速
return res; //不需要去除前导0,因为只取后k位即可
}
//高精度 * 低精度
vector<int> mul2(vector<int> a, int b)
{
vector<int> result;
int t = 0; // 进位
for(int i = 0; i < a.size() || t; i++) //这里把对最后一个t不为0的后续操作合并到一起
{
if(i < a.size()) t += a[i] * b; //合并之后需要考虑i超过a的范围
result.push_back(t % 10);
t /= 10;
}
return result;
}
int main()
{
string str;
cin >> str >> k;
for(int i = 0; i < str.size(); i++)
n.push_back(str[str.size() - 1 - i] - '0'); //高精度,个位在前
t[0] = 1; //最终结果是所有t[i]之积
vector<int> p(n); //乘数,初始是n
for(int i = 1; i <= k; i++) //依次计算后1~k位的循环长度
{
//乘数变成p^t[i-1],存入q
vector<int> q(1, 1); //累计乘p的结果
for(int j = 1; j <= t[i-1]; j++) q = mul1(q, p);
p = q;
//计算后i位循环长度与后i-1位循环长度的比值t[i]
vector<int> res(n); //n累计乘q,记录循环次数,使得第i位与n第i位相等
for(int j = 1; j <= 10; j++)
{
res = mul1(res, q);
//判断第i位是否相等,因为前i-1位已经相等
if(res[i-1] == n[i-1])
{
t[i] = j; //后i位的循环长度是j倍的i-1位的循环长度
break;
}
}
if(t[i] == 0) //没有找到后i位的循环长度
{
cout << -1;
return 0;
}
}
vector<int> ans(1,1);
for(int i = 1; i <= k; i++) ans = mul2(ans, t[i]); //结果是所有t[i]乘积
for(int i = ans.size() - 1; i >= 0; i--) cout << ans[i];
return 0;
}