洛谷 P7792 KRIZA 题解
题目回顾
题目描述
这就是锁匠头子给了 Sisyphus \(n\) 把钥匙,放在一个大圆坠子上,蒙住他的眼睛并把他带进一个大房间的原因。这个房间里恰好有 \(n\) 扇锁着的门,用 \(1\) 到 \(n\) 的数字表示。吊坠上的每一把钥匙都正好能打开一扇门,其中第 \(i\) 把钥匙能打开第 \(v_i\) 扇门。Sisyphus 的工作是将每一扇门解锁并再次上锁。他这样做的方式是沿着墙壁移动,不改变方向,直到他到达一扇门。当他走到一扇门前时,他尝试用从当前左数第一把钥匙来解锁。如果钥匙没有开锁,Sisyphus 就把它丢到最右边,并再用当前最左边的那一把钥匙再试一次,重复这个过程,直到他找到正确的钥匙为止。当他所有的门解锁时,他的工作就完成了。Sisyphus 打开的第一扇门用 \(1\) 表示,下一扇用 \(2\) 表示,后一扇用 \(3\) 表示,以此类推......
Sisyphus 不知道的是,锁匠头子其实是在考验他的耐力,所以把他领到了一个圆形的房间。因此,Sisyphus 在解开并锁上最后一扇门后,会再次去解开并锁上第一扇门。因为他是一个勤奋而执着的家伙,Sisyphus 一直在做这项工作,几个小时都没有说一句话。只有在第 \(k\) 次成功开锁和锁门后,他才开口说话:"如果我知道到目前为止我把一把错误的钥匙放在门锁里有多少次就好了!" 你的任务是回答他的问题。
输入格式
输入共 \(n+1\) 行。
第一行输入两个整数 \(n,k\),分别表示 Sisyphus 拥有的的钥匙数量和他当前为止成功开锁和锁门的次数。
随后 \(n\) 行,每行一个整数 \(v_i\),表示第 \(i\) 把钥匙对应能开的门的编号。
输出格式
输出仅一行,表示 Sisyphus 把一把错误的钥匙放在门锁里的次数。
样例输入
4 6
4
2
1
3
样例输出
13
提示
【样例 2 解释】
以下是 Sisyphus 使用钥匙的情况,其中一个标红的数字表示他把错误的钥匙放在门锁里一次:
- 对第 \(1\) 扇门进行开锁和锁门操作:\(\color{Red}4~2~\color{black}1~3\)
- 对第 \(2\) 扇门进行开锁和锁门操作:\(\color{Red}1~3~4~\color{black}2\)
- 对第 \(3\) 扇门进行开锁和锁门操作:\(\color{Red}2~1~\color{black}3~4\)
- 对第 \(4\) 扇门进行开锁和锁门操作:\(\color{Red}3~\color{black}4~2~1\)
- 对第 \(1\) 扇门进行开锁和锁门操作:\(\color{Red}4~2~\color{black}1~3\)
- 对第 \(2\) 扇门进行开锁和锁门操作:\(\color{Red}1~3~4~\color{black}2\)
因此他把错误的钥匙放在门锁里的次数一共是 \(2+3+2+1+2+3=13\)。
【数据范围】
对于 \(40\%\) 的数据,保证 \(1\leqslant n,k\leqslant 1000\)
对于 \(60\%\) 的数据,保证 \(1\leqslant k\leqslant 5\times 10^4\)。
对于所有数据,\(1\leqslant v_i\leqslant n\leqslant 10^5\),\(1\leqslant k\leqslant 10^9\)。
思路
初步思路(24pts)
看到此题,第一反应肯定是暴力,模拟到每一扇门尝试每一把钥匙,这样的时间复杂度为 \(O(nk)\),肯定过不了,提交只有 \(24pts\)。
优化(48pts)
我们需要注意的是,当面对第 \(i\) 扇门,用最右边的钥匙解锁第 \(i - 1\) 扇门,并且在用循环变量 \(i\) 和 \(j\) 标记的钥匙之间有恒定数量的钥匙(即差一定)。
这样,找出 \(Sisyphus\) 插错了多少把钥匙的时间复杂度就变成了 \(O(1)\),于是我们得到了时间复杂度为 \(O(k)\) 的解,结合数据范围可得,可以拿到 \(60\%\) 的分数。
//48分比较简单,就不加注释了
#include<bits/stdc++.h>
using namespace std;
int v[1000001];
int n,k,ans;
int p=1;
int main()
{
cin>>n>>k;
for(int i=1;i<=n;i++)
{
int x;
cin>>x;
v[x]=i;
//为了方便处理,把输入的 $v_i$ 改成了表示第 $i$ 扇门的钥匙编号
}
for(int i=1;i<=k;i++)
{
if(p>n)p=(p-1)%n+1;
int t=(i-1)%n+1;
if(v[t]>=p)
{
ans+=v[t]-p;
p+=v[t]-p;
}
else
{
ans+=v[t]+n-p;
p+=v[t]+n-p;
}
}
cout<<ans;
return 0;
}
正解(80pts)
我们不难发现,对于尝试每一圈的 \(n\) 扇门,其实这是一个循环的过程,所以我们只需要遍历一圈即可。
我们可以用 \(a_i\) 表示从第 \(1\) 扇门到第 \(i-1\) 扇门尝试的次数,然后我们维护 \(p\) 表示当前已经选到的钥匙编号,然后从 \(2\) 到 \(n+1\) 遍历,对于每一扇门,更新 \(p\) 和 \(a_i\) 的值,最后可以计算出试完第 \(k\) 扇门的尝试次数,即:
\(a[n]\times(k\div n)+a[k\bmod n]\)
这是什么意思?很好理解,我们已经计算出了每圈的尝试次数,我们用次数乘上圈数就是整圈的尝试次数,然后我们再加上剩余不足一圈的尝试次数,就是答案了。
Q:为什么是从 \(2\) 到 \(n+1\) 遍历?
A:因为 \(a_i\) 表示的是从第 \(1\) 扇门到第 \(i-1\) 扇门尝试的次数。对于第 \(i-1\) 扇门,从第 \(1\) 扇门到这扇门的尝试次数等于上一次的尝试次数加上这一次的尝试次数。
注意两个坑
-
下一扇门的钥匙编号可能到下一圈,更新时要判断。
-
\(k\) 一定要减一,因为有 \(k\mod n = 0\) 的情况,最后可能会多算。
\(AC\) \(Code\)
#include<bits/stdc++.h>
using namespace std;
long long v[100001],a[100001];
int n,k;
int p,sum;
long long ans=0;//保险起见,开个long long
int main()
{
cin>>n>>k;
k--;
for(int i=1;i<=n;i++)
{
int x;
cin>>x;
v[x]=i;
//为了方便处理,把输入的 $v_i$ 改成了表示第 $i$ 扇门的钥匙编号
}
ans=v[1]-1;//这句很重要:ans的初始值
p=v[1];//p从1开始
v[n+1]=v[1];//第n+1扇门就是第1扇门
for(int i=2;i<=n+1;i++)
{
if(v[i]>=p)a[i-1]=a[i-2]+v[i]-p;
//钥匙的编号和当前的钥匙编号在一圈内:更新a数组:上一扇门加上这扇门需要尝试的次数
else a[i-1]=a[i-2]+v[i]+n-p;
//否则在下一圈
p=v[i];//更新p
}
ans+=a[n]*(k/n)+a[k%n];//算答案
cout<<ans;
return 0;
}
本文来自博客园,作者:FHenryh,转载请注明原文链接:https://www.cnblogs.com/FHenryh/p/solution-luogu-P7792.html