【题解】洛谷P1309 瑞士轮(归并排序)

针对洛谷P1309瑞士轮问题,由于数据规模较大,快速排序会导致超时(TLE)。解决方案是采用归并排序算法,其时间复杂度为O(N)。归并排序通过合并两个有序数组来实现,使用两个指针p1和p2,依次将较大数值放入新数组,保持胜者组和败者组的有序性。在数组合并过程中,若某指针到达数组末尾,将剩余元素全部复制到合并数组中,确保最终结果的正确性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

看了一眼数据大小,我们可以知道用快排肯定会TLE,自己手写模拟试了试,果真只得了60分,因此我们需要改进排序算法。

这里我们可以用归并排序(时间复杂度O(N)),归并排序的思想就是对两个有序的数组进行操作,然后开另一个数组为两个数组大小之和。设两个指针p1,p2,开始初始化为1。当两个指针中的任何一个没有到最后时,就比较指针所指的数,将更大的数进入第三个数组,然后把该数所在的原数组的指针后移,反复进行上述操作。这可以节省快排的时间,但注意需要清零。在这道题里,对于这两个数组,我们把赢的放一起、输的放一起(都用结构体),由于在在过程中我们合并的数组排列是有序的,所以将两个人进行比较后,胜者放在胜者组,将胜者放一起也肯定是有序的(可以自己想想)。如果指针到头了,那我们就把没到头的那一个数组的后面的所有数都搬到合并数组的空位,然后就得到有序的合并数组了。注意对指针进行清零操作。

 

#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=200010;
int n,r,q;
struct player
{
	int s; //score
	int p; //power
	int num; //number
}a[maxn],win[maxn],lose[maxn],c[maxn];
bool cmp(const player &a,const player &b)
{
	if(a.s!=b.s) return a.s>b.s;
	else return a.num<b.num;
}
int p1,p2;
int main()
{
	scanf("%d%d%d",&n,&r,&q);
	for(int i=1;i<=2*n;i++)
	{
		scanf("%d",&a[i].s);
		a[i].num=i;
	}
	for(int i=1;i<=2*n;i++)
	{
		scanf(
<think>我们分析用户提供的代码,该代码用于解决洛谷P1309瑞士)问题。题目背景是2N名选手进行R比赛,每比赛前按分数(分数相同按id升序)排序,然后相邻两位选手(1和2,3和4...)比赛,每场比赛能力值高的选手得分+1。最后要求第Q名的选手id。 用户代码中存在一些逻辑错误,主要问题在于: 1. 在每比赛前,选手数组需要按照当前得分和id进行排序(用户使用了cmp函数,这是正确的)。 2. 比赛过程:用户试图通过一个循环来处理比赛,但循环变量使用混乱。具体来说,用户设置了一个外层循环变量k,然后内层使用i=2*k-1,但循环条件i<N不正确,且循环体中对member[i]和member[i+1]的处理有误(应该是每比赛后,能力值高的选手加分,而不是输的选手加分?实际上题目是能力值高的选手加分)。 3. 用户代码中,在比赛处理部分,循环条件为“i < N”,但i的初始值为2*k-1(k从1开始),这样会导致数组越界或者处理不全。 正确的做法应该是: 每比赛前先排序,然后将选手分成N对(共2N人,N对),每对中比较能力值,能力值高的选手得分+1。 另外,用户代码中在R循环的最后一次(R==1)又进行了一次排序,这个操作是不必要的,因为循环结束后还会再排一次(但题目要求R后输出第Q名,所以最后需要一次排序)。 但是,由于题目中R后需要输出第Q名,所以最后需要排序,但用户代码中是在R循环结束后输出,所以应该在循环结束后再排序一次。 具体修改建议: 1. 修改比赛处理部分的循环:循环变量k从0到N-1(共N组),每组两个选手的下标为2*k和2*k+1(数组从1开始,所以是member[2*k+1]和member[2*k+2]?注意数组下标从1开始,所以第一组是1和2,第二组是3和4,...,第k组是2*k-1和2*k。因此,循环k从1到N,然后取第一个选手下标i=2*k-1,第二个选手下标j=2*k。但注意数组大小为2*N,所以k从1到N,那么i从1到2*N-1(步长为2),j从2到2*N(步长为2)。 2. 在每比赛中,对于每一组,比较两个选手的shili(能力值),能力值高的选手的分数+1。 3. 由于每比赛前已经排好序,比赛后分数发生变化,所以下一比赛前需要重新排序。 4. 注意:题目要求进行R比赛,所以循环R次,每次循环内:先排序(按照当前分数和id),然后进行N场比赛(更新分数)。 5. 循环结束后,再进行一次排序,然后输出第Q名选手的id。 但是,由于R比赛,每比赛前都要排序,而排序的时间复杂度是O(N log N)(这里N是2*N,即总人数),而R最大为50,总人数最大为200000(2*100000),这样排序50次可能会超时(O(50 * 200000 * log200000) ≈ 50 * 200000 * 18 ≈ 180e6,在C++中可能勉强通过,但实际题目要求更高效率)。 因此,我们需要优化排序过程。注意到每比赛后,胜者和败者各自内部的相对顺序不变(因为胜者都加了1分,败者不加分,所以胜者组内部和败者组内部原本有序的,现在仍然有序,只是两个组之间需要合并)。因此,我们可以使用归并排序的思想,将胜者组和败者组合并。 优化思路: 1. 初始排序一次(按照分数降序,分数相同id升序)。 2. 每比赛后,将选手分成胜者组和败者组(每组N人),然后合并两个有序数组(胜者组和败者组都是有序的,合并成一个有序数组)。 具体步骤: - 比赛前,数组已经有序。 - 进行比赛:将相邻两位选手(1和2,3和4,...)比赛,胜者(能力值高的)加分(注意:这里加分后胜者组的分数都加了1,而败者组分数不变)。 - 但是,由于胜者组都加了1分,所以胜者组之间的相对顺序可能发生变化吗?实际上,因为原来有序,现在每个胜者都加了同样的分数(1分),所以胜者组内部仍然保持有序(因为原来分数高的加1分后还是比分数低的加1分高,如果原来分数相同,则id小的在前面,加同样的分数后,id顺序不变)。败者组同理。 - 因此,我们可以将胜者组和败者组分别存储在两个数组(win和lose)中,然后合并这两个有序数组(因为胜者组和败者组都是有序的,且胜者组的最低分可能高于败者组的最高分?不一定,因为胜者组中可能有人原本分数很低,但是因为赢了比赛加了1分,而败者组中可能有人原本分数很高,但是本输了没加分,所以胜者组的最低分可能低于败者组的最高分。因此不能简单比较两个组的最高分和最低分,需要归并合并。 归并合并方法: 使用两个指针,分别指向win和lose的头部,比较当前两个指针指向的选手的分数(若分数相同,则比较id),将较大的放入总数组,然后指针后移,直到合并完成。 修改后的代码步骤: 1. 读入数据,初始化。 2. 按照初始分数排序(使用sort,因为初始无序)。 3. 进行R比赛: a. 遍历数组,每两个选手一组(共N组),比较能力值,能力值高的选手分数+1,并将该选手加入胜者组(win),输的加入败者组(lose)。 b. 然后使用归并排序合并win和lose数组(因为win和lose都是有序的,合并后整个数组有序)。 4. 最后,数组已经有序,输出第Q个位置(下标为Q)的选手id。 注意:数组从1开始存储,所以总共有2*N个选手。 由于用户代码没有使用归并优化,可能会超时(因为题目中N最大为100000,R最大为50,每次排序O(N log N)的复杂度,50次排序就是50 * 200000*log2(200000)≈50*200000*18=180000000,在C++中可能勉强通过,但实际在洛谷上可能超时)。因此,我们采用归并排序优化。 修改后的代码框架: 定义结构体数组member(存储所有选手),再定义两个数组win和lose(大小均为N,因为胜者和败者各N人)。 伪代码: sort(member+1, member+2*N+1, cmp); // 初始排序 while (R--) { int idx_win = 0, idx_lose = 0; for (int i = 1; i <= 2*N; i+=2) { // 每次处理两个选手 if (member[i].shili > member[i+1].shili) { member[i].score++; win[++idx_win] = member[i]; lose[++idx_lose] = member[i+1]; } else { member[i+1].score++; win[++idx_win] = member[i+1]; lose[++idx_lose] = member[i]; } } // 现在win和lose数组已经生成,且两个数组内部都是有序的(因为原数组有序,且每个组内元素相对顺序不变) // 归并合并:将win和lose合并到member数组中 int i = 1, j = 1, k = 1; while (i <= idx_win && j <= idx_lose) { if (cmp(win[i], lose[j])) { // 注意:cmp是分数高的排在前面,如果win[i]的分数大于lose[j]的分数,或者分数相等且win[i]的id小于lose[j]的id,则返回true,那么应该先放win[i] member[k++] = win[i++]; } else { member[k++] = lose[j++]; } } while (i <= idx_win) member[k++] = win[i++]; while (j <= idx_lose) member[k++] = lose[j++]; } // 最后输出member[Q]的id 但是注意:上面的归并过程中,比较函数cmp是分数降序,id升序。所以我们在比较win[i]和lose[j]时,如果win[i]的分数大于lose[j]的分数,那么win[i]应该排在前面;如果分数相等,则比较id,id小的排在前面。 然而,在归并排序中,我们通常使用一个while循环来合并,并且需要比较两个元素的大小。这里我们可以直接使用cmp函数,但是要注意cmp函数是“大于”的含义(即win[i]应该排在lose[j]前面时,cmp(win[i], lose[j])返回true)。所以我们可以这样写: if (win[i].score > lose[j].score || (win[i].score == lose[j].score && win[i].id < lose[j].id)) { member[k] = win[i]; i++; k++; } else { member[k] = lose[j]; j++; k++; } 或者,我们可以写一个比较函数,但是为了效率,直接内联比较。 另外,注意数组下标从1开始,所以win和lose数组也从1开始存储(上面代码中win[++idx_win]和lose[++idx_lose])。 但是,用户原始代码中数组下标是从1开始的,所以这里我们保持从1开始。 修改后的完整代码: 注意:由于题目中N最大为100000,所以数组大小要开到200000(2*N)。win和lose数组也要开到100000以上。 由于用户原始代码中数组名为member,大小为1005,这显然不够。题目中N最大为100000,所以总人数为200000,因此数组要开到200005。 另外,我们定义两个数组win[100005]和lose[100005](因为每组最多100000人)。 代码修改如下:</think># 洛谷P1309 瑞士 C++代码调试与优化 您的代码实现瑞士比赛逻辑,但存在几个关键问题需要修正。我将分析问题并提供优化方案,使代码符合题目要求(N最大100000,R最大50)。 ## 问题分析 1. **数组越界风险**:`member`数组大小1005,但题目中N最大100000(总人数200000),需扩大数组 2. **比赛次逻辑错误**:循环条件`i < N`错误,应为`k <= N` 3. **比赛处理错误**:能力值比较后,应为胜者加分(您的代码给输者加分) 4. **性能问题**:每使用`sort`排序,时间复杂度O(R * N log N),在最大规模下会超时 ## 优化后代码 ```cpp #include<iostream> #include<algorithm> using namespace std; struct Player { int id; int score; int ability; }; const int MAX_N = 100000; Player players[2 * MAX_N + 5]; // 总人数2*N Player winners[MAX_N + 5]; // 胜者组 Player losers[MAX_N + 5]; // 败者组 // 排序比较函数:分数降序,分数相同则id升序 bool cmp(Player a, Player b) { if (a.score != b.score) return a.score > b.score; return a.id < b.id; } int main() { int N, R, Q; cin >> N >> R >> Q; int total = 2 * N; // 输入初始分数 for (int i = 1; i <= total; i++) { players[i].id = i; cin >> players[i].score; } // 输入能力值 for (int i = 1; i <= total; i++) { cin >> players[i].ability; } // 初始排序 sort(players + 1, players + total + 1, cmp); // 进行R比赛 while (R--) { int winCount = 0, loseCount = 0; // 处理每对选手 for (int i = 1; i <= total; i += 2) { // 能力值高的胜出,得分+1 if (players[i].ability > players[i+1].ability) { players[i].score++; winners[++winCount] = players[i]; losers[++loseCount] = players[i+1]; } else { players[i+1].score++; winners[++winCount] = players[i+1]; losers[++loseCount] = players[i]; } } // 归并排序合并胜者组和败者组 int i = 1, j = 1, k = 1; while (i <= winCount && j <= loseCount) { if (cmp(winners[i], losers[j])) { players[k++] = winners[i++]; } else { players[k++] = losers[j++]; } } // 处理剩余元素 while (i <= winCount) players[k++] = winners[i++]; while (j <= loseCount) players[k++] = losers[j++]; } // 输出第Q名选手的ID cout << players[Q].id; return 0; } ``` ## 关键优化点说明 1. **归并排序优化**: - 胜者组和败者组各自保持有序性 - 归并合并时间复杂度$O(N)$,远优于排序的$O(N \log N)$ - 总复杂度从$O(R \cdot N \log N)$优化为$O(R \cdot N)$ 2. **比赛处理逻辑修正**: - 正确为胜者加分:`players[i].score++` - 使用`winners`和`losers`数组分别存储胜败结果 3. **数据结构优化**: - 扩大数组容量至$2 \times \text{MAX\_N} + 5$,避免越界 - 使用独立数组存储胜败组,提高可读性 ## 算法复杂度分析 | 步骤 | 原始方案 | 优化方案 | |------|----------|----------| | 初始排序 | $O(N \log N)$ | $O(N \log N)$ | | 每比赛 | $O(N)$ | $O(N)$ | | 每排序 | $O(N \log N)$ | $O(N)$(归并) | | 总复杂度 | $O(R \cdot N \log N)$ | $O(R \cdot N + N \log N)$ | 当$R=50$,$N=100000$时: - 原始方案:$50 \times 200000 \times \log_2(200000) \approx 180,000,000$次操作 - 优化方案:$50 \times 200000 + 200000 \times \log_2(200000) \approx 10,000,000 + 3,600,000 = 13,600,000$次操作 ## 测试建议 1. **边界测试**: ```cpp // 最小规模测试 N=1, R=1, Q=1 初始分数: [1, 2] 能力值: [3, 2] // 预期输出: 1 (ID为1的选手获胜) ``` 2. **能力值相等测试**: ```cpp // 能力值相同,按初始分数排序 N=1, R=1, Q=1 初始分数: [2, 1] 能力值: [3, 3] // 预期输出: 2 (初始分数高的胜出) ``` 3. **大规模数据测试**: ```cpp // 使用N=100000, R=50生成随机数据 // 验证程序在最大规模下的性能 ``` ## 相关问题 1. 归并排序在算法竞赛中还有哪些典型应用场景?[^3] 2. 如何进一步优化瑞士算法的时间复杂度?[^2] 3. 除了归并排序,还有哪些方法可以处理有序序列的合并问题?[^3] 4. 在选手能力值动态变化的情况下,如何设计更高效的排名算法?[^1] [^1]: 参考洛谷P1035算法实现中的循环控制技巧 [^2]: 类似洛谷P1014题解中提到的牛顿法优化思想 [^3]: 基于算法竞赛备考冲刺必刷题中的高效算法设计原则
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值