小 F 非常喜欢一种卡牌游戏,这种游戏需要 n 个人参加。 游戏开始时,每个人会领取到一张牌。之后每一轮游戏,第 i 个人会将手上的卡牌 交给第 pi 个人。为了保证每一轮游戏后,每个人手上都有一张卡牌,在设计游戏时,小 F 保证了 p1, p2, · · · , pn 是一个排. 列. 。 给出排列 p,小 F 想要知道,最少多少轮游戏后,所有人曾. 再次取得初始的卡牌。 【输入格式】 从文件 card.in 中读入数据。 输入的第一行为一个整数 n,代表参加游戏的人数。 输入的第二行为 n 个整数,第 i 个代表 pi,含义如题所示。 【输出格式】 输出到文件 card.out 中。 输出一行一个整数表示答案的c++代码
时间: 2025-07-10 08:37:46 浏览: 27
<think>嗯,这个问题看起来有点挑战性。让我仔细读题目。题目是说,有n个人参与卡牌游戏。每个人初始有一张卡牌。每一轮,第i个人会把卡牌传给第pi个人。p是一个排列,所以每一轮结束后每个人手里都有一张卡牌。现在需要找出最少多少轮之后,所有人都再次拿到自己初始的那张卡牌。也就是所有卡牌都回到初始位置的最少轮数。
首先,我得理解这个过程。每一轮交换的规则是,每个人i的卡牌传给p_i。那么,这个过程可以看作是一个置换的循环问题。置换的每个循环的长度,应该会影响最终的轮数。因为每个循环中的元素需要经过其长度次数的轮数才能回到原位。而整个系统的最小公倍数,就是所有循环长度的最小公倍数。这样,当所有循环都完成整数次数的循环时,所有卡牌才会回到初始状态。
举个例子,比如排列p是[2,3,1]。这个排列分解成循环的话,就是1→2→3→1,形成一个长度为3的循环。所以最少需要3轮,每个人才能回到初始状态。
那问题的解决步骤应该是:
1. 找出排列p中的所有循环的长度。
2. 计算这些长度的最小公倍数(LCM),这个就是答案。
那如何找出循环呢?可以用一个数组来记录每个位置是否被访问过。然后遍历每个未被访问的位置,进行深度优先搜索或者跟踪循环,直到回到起点,记录循环长度。
比如,对于输入n=3,p=[2,3,1]。初始时,所有位置未被访问。首先访问位置1,发现它指向2,然后2指向3,3指向1。这样循环长度是3。所以LCM是3。
另一个例子,比如排列p=[2,1,4,3]。分解的话,两个循环:(1→2→1)长度2,(3→4→3)长度2。LCM(2,2)=2。所以答案是2。
那现在问题转化为如何编写代码来分解循环,并计算它们长度的LCM。
那么,具体的步骤应该是:
- 输入n和数组p。注意数组p的下标可能从0开始还是1开始?题目中的描述是,输入的第二行为n个整数,第i个代表pi。比如样例中的排列是p1到pn。所以p数组的元素是1-based吗?比如,假设输入的p数组中的第一个数是p_1,所以当i=1的时候,传给p_1的位置。这个时候,在代码中,可能需要将数组视为1-based,或者调整为0-based。比如,在C++中,数组通常是0-based的,所以可能需要把输入中的每个数减一,得到对应的索引。
举个例子,假设输入的p数组是2 3 1。那么对于i=1(第一个元素),p_1是2,意味着原来的位置1的卡牌传给位置2。那么在代码中,如果数组是0-based的,那么索引i=0对应原来的1,所以p[0]应该是1(因为位置1传给位置2,即索引0传给索引1)。或者,可能题目中的排列是1-based的,所以输入的p数组中的每个元素都是1-based的,所以在处理的时候,需要将它们转换为0-based的索引。例如,输入的pi是第i个人传给pi的,而pi是1到n中的一个数。所以,在代码中,我们可能需要将每个pi减一,得到对应的数组下标。
这一步非常重要,否则循环分解会出错。例如,假设输入的p数组的元素是1-based的,那么在代码中处理的时候,应该转换成0-based的索引。比如,对于i=0(原第1个人),他传给p[0](原p1的值,假设输入是2的话,对应的索引是1)。
所以,在代码中,输入数组p的元素应该被处理为每个数减一,得到对应的下标。
接下来,如何找出循环?可以用一个visited数组来记录每个位置是否被访问过。遍历每个位置,如果未被访问过,则开始跟踪这个循环,直到回到起点,记录循环的长度。
例如,初始化visited数组全为false。然后循环每个i从0到n-1:
如果visited[i]为false,则开始一个新的循环。设置current=i,循环计数cnt=0。然后循环:current = p[current] -1?或者如果是已经处理过的数组的话,current = p[current]。这里可能需要根据输入的处理方式调整。
假设输入的p数组中的元素是1-based的,所以将每个元素减一得到正确的索引。例如,输入的pi是2,那么对应的索引是1。这样,在代码中,将数组p处理为每个元素减一后的值。
例如,输入样例中的排列是2 3 1,对应的处理后的数组是[1,2,0]。这样,当i=0时,p[i]是1,即原第1个人传给第2个人。i=1时,p[i]=2,即传给第三个人。i=2时,p[i]=0,即传回第一个人。所以循环是0→1→2→0,长度3。
所以,正确的处理方式是将输入的每个pi减一,得到正确的数组下标。
这一步处理正确的话,循环分解就能正确进行。
接下来,计算所有循环长度的LCM。例如,如果有两个循环长度是2和3,则LCM是6。如果有三个循环长度是4、3、6,则LCM是12。
如何计算多个数的LCM?可以先计算两个数的LCM,然后逐步合并。两个数的LCM等于它们的乘积除以GCD。所以,初始时lcm=1。然后对于每个循环长度len,计算lcm = LCM(lcm, len) = lcm * len / GCD(lcm, len)。
所以,在代码中,需要编写一个计算两个数的GCD的函数,可以用欧几里得算法。
现在,将整个思路转化为代码:
1. 读取n。
2. 读取p数组,并将每个元素减一,转换为0-based索引。
3. 初始化visited数组为false,长度n。
4. 遍历每个i从0到n-1:
a. 如果未访问,则进行循环检测。
b. 跟踪current,直到回到起始点,记录循环长度。
c. 将循环长度加入计算LCM。
5. 最后输出LCM。
那如何处理循环长度的计算?
例如,当i=0未被访问时,开始跟踪:
current = i
count =0
循环:
current = p[current]
count +=1
直到current等于i。
比如,初始current =0,count=0.
然后循环:
current = p[0] →假设p[0]是1。count=1。此时current不等于0,继续。
current = p[1] →假设是2。count=2.
current = p[2] →假设是0。count=3.此时等于i=0,循环结束。所以循环长度是3.
所以,在这个过程中,每次循环时,需要将current置为p[current],并标记为已访问。这样,就能正确分解每个循环。
所以,在代码中:
初始化visited数组为false。
遍历每个i从0到n-1:
如果 visited[i]为false:
len =0
current =i
while未访问:
如果visited[current]是false:
标记为true
current = p[current]
len +=1
否则:
退出循环
这样得到的len是否正确?
例如,假设循环是i→p[i]→p[p[i]]→…→i。那么,在循环过程中,每次移动current到p[current],直到回到i。但是,在这个过程中,必须确保每个节点都被访问过。所以,正确的做法是:
初始化len=0,current=i.
然后循环:
如果visited[current]为false:
标记为true,len +=1,然后current = p[current]
否则:
退出循环.
但这样的话,如果current回到i的时候,可能已经标记为true了。例如,假设i=0,p[0]=1,p[1]=2,p[2]=0。那么current的移动过程:
current=0,未访问,标记为true,len=1 →current=1.
current=1,未访问,标记为true,len=2 →current=2.
current=2,未访问,标记为true,len=3 →current=0.
此时,current=0已经被访问过,所以退出循环。此时len是3,正确。
所以,这个循环结构是正确的。
或者,可能应该用do-while结构,这样即使current初始被访问过,至少执行一次?
例如:
len=0
current =i.
do{
len ++;
visited[current] = true;
current = p[current];
} while(current !=i);
这样,可以确保循环次数是正确的。例如,当current是i的时候,进入循环,len加1,然后current移动到p[i],然后继续循环直到回到i。这样得到的len是正确的。
例如,假设循环长度是3:
i=0 →current=0.
进入循环,len=1,标记0为已访问,current=p[0]=1.
再次循环,current !=0 →继续。
len=2,标记1为已访问,current=p[1]=2.
current !=0 →继续.
len=3,标记2为已访问,current=p[2]=0.
此时current等于i,退出循环。得到len=3,正确。
所以,正确的做法是使用do-while循环,并且每次current都更新为p[current],同时标记为已访问。这样,不管循环长度如何,都能正确计算。
这可能比之前的while循环更准确,因为while循环可能在初始current未被访问时进入,但如果循环的结构导致current可能被多次访问,那么用do-while结构可能更可靠。
所以,在代码中,对于每个未被访问的i:
应该如何处理?
或者,不管i是否已经被访问,都要处理?但根据之前的逻辑,只有当i未被访问时才处理,因为如果i已经被访问,那么它属于某个循环已经被处理过了。
所以,正确的步骤是:
遍历每个i从0到n-1:
if (!visited[i]) {
开始处理这个循环:
len =0;
current =i;
do {
visited[current] = true;
len ++;
current = p[current];
} while(current !=i);
计算LCM,将len加入计算。
}
这样,这样处理的话,每个循环只会被处理一次。例如,当i=0时,处理整个循环,并将循环内的所有节点标记为已访问。之后,当遍历到这些节点时,会被跳过。
这样就能正确分解所有循环,并计算每个循环的长度。
现在,编写代码的结构:
读取n,然后读取数组p,每个元素减一。
初始化visited数组。
然后,计算各个循环的长度,并求它们的LCM。
然后输出LCM。
例如,对于n=3,输入p数组为2 3 1 →处理后变为 [1,2,0]。循环长度是3,LCM是3。
另一个例子,n=4,输入p数组是2 1 4 3 →处理后是 [1,0,3,2]. 此时循环是0→1→0(长度2),2→3→2(长度2)。LCM是2.
现在,编写计算LCM的函数。C++中可以用std::gcd函数(需要C++17或更高),或者自己实现。
对于两个数的LCM,可以用 a*b / gcd(a,b)。但是对于多个数,可以逐个计算。例如,初始lcm是1,然后对于每个循环长度len,计算lcm = lcm * len / gcd(lcm, len)。
例如,循环长度是3,则初始lcm=3。如果有另一个长度2,则gcd(3,2)=1 →lcm=6.
所以,代码中的LCM计算部分可以这样处理:
long long result =1;
for每个循环长度 len:
result = lcm(result, len) = result * len / gcd(result, len);
所以,在代码中,需要用long long类型来避免溢出,尤其是当n很大时,比如n是1e5,每个循环的长度可能很大,它们的LCM可能超过int的范围。
所以,在C++中,可以用:
#include <iostream>
using namespace std;
long long gcd(long long a, long long b) {
return b ==0 ? a : gcd(b, a%b);
}
然后,计算LCM的函数:
long long lcm(long long a, long long b) {
return a / gcd(a,b) * b; // 先除后乘,避免溢出
}
注意,这里顺序很重要,先除再乘可以减少中间结果的数值。例如,假设a和b都是很大的数,那么a*b可能会溢出,而先除则可以避免这种情况。所以,在实现的时候,应该先进行除法运算。
现在,将这些整合到代码中。
例如:
读取n:
int n;
cin >>n;
vector<int> p(n);
for(int i=0; i<n; i++){
int x;
cin >>x;
p[i] =x-1;
}
然后,初始化visited数组:
vector<bool> visited(n, false);
long long ans=1;
for(int i=0; i<n; i++){
if(!visited[i]){
int current =i;
int len=0;
do{
visited[current] =true;
len++;
current = p[current];
} while(current !=i);
ans = ans * len / gcd(ans, len);
}
}
cout << ans << endl;
这样,这个代码应该可以正确计算所需的最小轮数。
测试样例:
例如,输入:
3
2 3 1 →处理后p是[1,2,0]. 循环长度3 →输出3.
另一个测试案例:
4
2 1 4 3 →处理后p是[1,0,3,2]. 循环长度分别是2和2 →LCM(2,2)=2 →输出2.
另一个测试案例:
6
2 1 4 3 6 5 →处理后是 [1,0,3,2,5,4]. 三个循环:0→1→0(len=2),2→3→2(len=2),4→5→4(len=2) →LCM是2.
所以,代码应该返回2.
再比如,n=5,排列是3 4 5 1 2 →处理后是 [2,3,4,0,1]. 分解循环:
0→2→4→1→3→0 →长度5。所以输出5.
另一个例子,n=4,排列是4 3 2 1 →处理后p是 [3,2,1,0]. 循环分解:
0→3→0(len=2),1→2→1(len=2) →LCM(2,2)=2 →输出2.
综上,代码应该正确。
那么,最终的代码应该是这样的:
需要注意的是,输入输出需要从文件card.in读取,输出到card.out。所以,在C++中,需要将输入输出重定向到文件。
所以,完整的代码应该是:
#include <iostream>
#include <vector>
#include <fstream>
using namespace std;
long long gcd(long long a, long long b) {
return b == 0 ? a : gcd(b, a % b);
}
int main() {
ifstream fin("card.in");
ofstream fout("card.out");
int n;
fin >> n;
vector<int> p(n);
for (int i = 0; i < n; ++i) {
int x;
fin >> x;
p[i] = x - 1;
}
vector<bool> visited(n, false);
long long ans = 1;
for (int i = 0; i < n; ++i) {
if (!visited[i]) {
int current = i;
int len = 0;
do {
visited[current] = true;
len++;
current = p[current];
} while (current != i);
ans = ans / gcd(ans, len) * len; // 先除后乘,防止溢出
}
}
fout << ans << endl;
fin.close();
fout.close();
return 0;
}
注意,在计算ans时,顺序应该是先除以gcd,再乘以len,这样可以避免中间结果过大导致溢出。比如ans * len可能超出long long的范围,所以先进行除法,再乘法。
例如,ans是之前的LCM值,len是当前循环长度。计算新的ans时,ans和len的LCM等于 (ans * len) / gcd(ans, len)。但为了避免ans * len的溢出,可以先除以gcd(ans, len),再乘以len。这需要确保ans能被gcd(ans, len)整除,这当然成立,因为gcd(ans, len)是ans的因数。
所以,代码中的计算顺序是正确的。
综上,这个代码应该可以正确解决问题。</think>```cpp
#include <iostream>
#include <vector>
#include <fstream>
using namespace std;
long long gcd(long long a, long long b) {
return b == 0 ? a : gcd(b, a % b);
}
int main() {
ifstream fin("card.in");
ofstream fout("card.out");
int n;
fin >> n;
vector<int> p(n);
for (int i = 0; i < n; ++i) {
int x;
fin >> x;
p[i] = x - 1; // 将输入的1-based转换为0-based索引
}
vector<bool> visited(n, false);
long long ans = 1;
for (int i = 0; i < n; ++i) {
if (!visited[i]) {
int current = i;
int len = 0;
do { // 计算循环长度
visited[current] = true;
len++;
current = p[current];
} while (current != i);
ans = ans / gcd(ans, len) * len; // 更新最小公倍数
}
}
fout << ans << endl;
fin.close();
fout.close();
return 0;
}
```
### 解决思路
1. **循环分解**:将排列分解为若干个不相交的循环。例如排列`[2,3,1]`可分解为`1→2→3→1`的单一循环。
2. **计算循环长度**:每个循环中元素需要其长度次数的轮转才能回到初始位置。
3. **最小公倍数(LCM)**:所有循环长度的LCM即为所有卡牌回归初始状态所需的最少轮数。
### 步骤解析
1. **输入处理**:将输入的1-based索引转换为0-based以便数组操作。
2. **标记访问状态**:通过`visited`数组跟踪已处理的元素。
3. **循环检测**:对每个未访问的起始点进行深度遍历,计算循环长度。
4. **LCM计算**:通过逐个计算循环长度的LCM,最终得到所有循环长度的LCM作为答案。
该算法时间复杂度为$O(n)$,能高效处理大规模数据。
阅读全文
相关推荐


















