当前位置: 首页 > news >正文

第二十七天:游戏组队问题

每日一道C++题:游戏组队问题

题目详述

一天,zzq 主持一项游戏,有 (n) 位同学参与,要求两两同学为一组上台玩游戏。然而,如果两位同学的颜值差距大于等于 (m),他们就会互相嫌弃,影响游戏体验。因此,zzq 在游戏开始前调查了所有 (n) 个同学的颜值,现在需要确定最多能凑出多少组同学一起上台,且一人只能出现在一个组中。

输入描述

  • 多组输入。
  • 第一行输入两个正整数 (n) 和 (m)((n \leq 10^5),(m \leq 10^9)),其中 (n) 表示同学的数量,(m) 为颜值差距的阈值。
  • 第二行包含 (n) 个由空格分开的正整数 (x_i)((x_i \leq 10^9)),代表第 (i) 个同学的颜值。

输出描述

每一行输出一个数,表示最多能凑出的组数。

二、基础解法

(一)解题思路

  1. 排序:对所有同学的颜值进行排序。这是后续双指针法能够高效工作的基础,因为排序后,同学的颜值按照从小到大的顺序排列,方便比较相邻同学之间的颜值差距。
  2. 双指针法:使用两个指针,left 初始指向数组的第一个元素,right 初始指向数组的第二个元素。通过移动这两个指针,遍历整个数组,判断当前 leftright 所指向同学的颜值差距是否小于 (m)。如果小于 (m),则这两位同学可以组成一组,然后更新指针位置继续寻找下一组;如果大于等于 (m),则只移动 right 指针,尝试与下一个同学组成一组。

(二)代码实现(C++)

#include <iostream>
#include <vector>
#include <algorithm>using namespace std;int main() {int n, m;while (cin >> n >> m) {vector<int> scores(n);for (int i = 0; i < n; ++i) {cin >> scores[i];}sort(scores.begin(), scores.end());int pairs = 0;int left = 0, right = 1;while (right < n) {if (scores[right] - scores[left] < m) {pairs++;left = right + 1;right = left + 1;} else {right++;}}cout << pairs << endl;}return 0;
}

(三)代码详细解释

  1. 输入处理
    • while (cin >> n >> m) 实现多组输入,每次循环读取一组 (n) 和 (m) 的值。这里利用了 C++ 输入流 cin 的特性,cin 会从标准输入读取数据,>> 运算符用于提取输入的整数并赋值给相应变量。只要输入的数据格式正确(即能成功读取两个整数),循环就会持续执行,这在处理多个测试用例时非常方便,无需多次手动运行程序输入数据。
    • vector<int> scores(n) 创建一个大小为 (n) 的整数向量 scores,用于存储 (n) 个同学的颜值。vector 是 C++ 标准模板库(STL)中的动态数组容器,它可以根据需要自动调整大小。
    • for (int i = 0; i < n; ++i) 循环,通过 cin >> scores[i] 逐行读取每个同学的颜值,并存储到 scores 向量中。在输入时,需保证输入的整数之间用空格分隔,以便 cin 按顺序正确读取。
  2. 排序
    • sort(scores.begin(), scores.end()) 使用 C++ STL 中的 sort 函数对 scores 向量进行升序排序。sort 函数通常基于快速排序、归并排序或堆排序等高效算法实现。以快速排序为例,它的基本原理是选择一个基准元素,将数组分为两部分,使得左边部分的元素都小于等于基准元素,右边部分的元素都大于等于基准元素,然后分别对左右两部分进行递归排序,平均时间复杂度为 (O(n \log n))。排序后,数据按顺序排列,为后续双指针法高效判断同学能否成组提供了便利。若数据无序,双指针法在判断时会增加大量比较操作,显著提高时间复杂度。
  3. 双指针法计算组数
    • 初始化两个指针 left = 0right = 1,分别指向数组的第一个和第二个元素。双指针法是一种常用算法技巧,通过在数组或链表等数据结构上移动两个指针来遍历、搜索或解决特定问题。
    • while (right < n) 循环条件确保 right 指针没有越界,只要 right 指针在数组范围内,就持续执行循环体。
    • 在循环体中,if (scores[right] - scores[left] < m) 判断当前 right 指向的同学和 left 指向的同学的颜值差距是否小于 (m)。
      • 如果满足条件,说明这两个同学可以组成一组,将 pairs(记录组数的变量)加 (1)。然后更新指针,left = right + 1left 指针移动到 right 指针的下一个位置,right = left + 1 再将 right 指针移动到 left 指针的下一个位置,继续寻找下一组。这种指针移动方式能保证每个同学只参与一次分组判断,避免重复计算。
      • 如果不满足条件,即 scores[right] - scores[left] \geq m,则只将 right 指针向右移动一位(right++),尝试让 right 指向的同学与 left 指向的同学组成一组。通过这种方式,双指针在一次遍历中就能高效地找到所有满足条件的同学组,时间复杂度为 (O(n)),相较于暴力嵌套循环的 (O(n^2)) 时间复杂度,效率大幅提升。
  4. 输出结果
    • while 循环结束后,pairs 中存储的值就是最多能凑出的组数,通过 cout << pairs << endl 将结果输出到控制台。cout 是 C++ 的标准输出流,<< 是输出运算符,用于将数据输出到标准输出设备(通常是显示器)。

(四)时间复杂度和空间复杂度分析

  1. 时间复杂度:排序部分的时间复杂度为 (O(n \log n)),双指针遍历数组部分的时间复杂度为 (O(n))。总体时间复杂度主要由排序部分决定,所以整个算法的时间复杂度为 (O(n \log n))。
  2. 空间复杂度:代码中使用了一个大小为 (n) 的向量 scores 来存储同学的颜值,所以空间复杂度为 (O(n))。

三、拓展内容

(一)优化空间复杂度

  1. 优化思路:当前代码使用大小为 (n) 的数组存储所有同学的颜值,如果 (n) 非常大,可能会占用较多内存。优化方法是在读取输入时直接进行处理,而不存储所有值。具体来说,在读取每个颜值值后,立即尝试与之前已处理的合适值进行配对。
  2. 实现难点:这种方法实现起来相对复杂,需要更精细地控制指针和已处理值的状态。例如,需要额外的数据结构或标记来记录哪些值已经被考虑过,以及如何在不完整的数据存储情况下准确地移动指针和判断配对情况。同时,由于没有完整的数组,在某些情况下可能需要重新遍历之前处理过的值,这可能会增加时间复杂度。

(二)拓展为多维度条件

  1. 条件扩展:假设除了颜值差距,还需要考虑其他条件,比如身高差距、性格匹配度等。可以将每个同学表示为一个结构体,结构体中包含多个属性。例如:
struct Student {int appearance;int height;int personality;
};
  1. 算法调整:在比较时,需要同时满足所有条件才能组成一组。这就需要对当前的双指针法进行扩展,在判断条件中加入多个属性的比较逻辑。例如:
while (right < n) {if (abs(students[right].appearance - students[left].appearance) < appearanceThreshold &&abs(students[right].height - students[left].height) < heightThreshold &&abs(students[right].personality - students[left].personality) < personalityThreshold) {pairs++;left = right + 1;right = left + 1;} else {right++;}
}

这里假设 appearanceThresholdheightThresholdpersonalityThreshold 分别是颜值、身高和性格匹配度的差距阈值。

(三)动态变化的条件

  1. 条件变化场景:考虑如果 (m)(颜值差距阈值)在游戏过程中会动态变化,例如每轮游戏后根据玩家反馈进行调整。
  2. 程序调整:这就需要在程序中实时处理 (m) 的变化,并重新计算最多能凑出的组数。可以通过添加额外的输入处理逻辑,在每次 (m) 变化时重新调用计算组数的函数。例如:
int main() {int n;cin >> n;vector<int> scores(n);for (int i = 0; i < n; ++i) {cin >> scores[i];}sort(scores.begin(), scores.end());int m;while (cin >> m) {int pairs = 0;int left = 0, right = 1;while (right < n) {if (scores[right] - scores[left] < m) {pairs++;left = right + 1;right = left + 1;} else {right++;}}cout << pairs << endl;}return 0;
}

在这个修改后的代码中,通过外层的 while (cin >> m) 循环,每次输入新的 (m) 值时,重新计算并输出最多能凑出的组数。

(四)分组数量最大化且组间差异最小化

  1. 目标分析:不仅要使分组数量最多,还希望组与组之间的综合差异最小化,以保证游戏的公平性和趣味性。这就需要在分组过程中不仅考虑当前两个同学能否成组,还要考虑整体分组的平衡性。实现这个目标较为复杂,因为简单的贪心策略可能无法找到全局最优解,需要更高级的算法。
  2. 动态规划思想
    • 原理:动态规划是一种将复杂问题分解为若干个子问题,并保存子问题的解,避免重复计算,从而提高算法效率的方法。在分组数量最大化且组间差异最小化的拓展中,如果采用动态规划思想,我们可以定义一个状态,例如用 dp[i][j] 表示考虑前 (i) 个同学,组成 (j) 组时的最小组间差异。然后通过状态转移方程,根据已有的状态计算新的状态。例如,dp[i][j] 可能由 dp[i - 1][j](不将第 (i) 个同学加入当前组)和 dp[i - 2][j - 1](将第 (i) 个同学与另一个同学组成新的一组)等状态推导而来。
    • 应用:在本题拓展场景中,动态规划可以帮助我们在考虑所有可能的分组组合时,有效地利用已经计算过的子问题的解,避免重复计算,从而在合理的时间复杂度内找到最优的分组方案。但动态规划的难点在于如何定义合适的状态和状态转移方程,这需要对问题有深入的理解和分析。
  3. 启发式算法
    • 模拟退火算法
      • 原理:模拟退火算法源于对固体退火过程的模拟。在固体退火中,固体从高温开始,随着温度的降低,分子的热运动逐渐减弱,最终达到能量最低的稳定状态。模拟退火算法从一个初始的分组状态开始,计算当前分组的组数和组间差异(作为当前状态的能量)。然后通过一定的规则对分组进行随机调整,每次调整后计算新的组数和组间差异(新状态的能量)。如果新的状态更优(组数最多且组间差异最小,即能量更低),则接受新状态;否则,以一定概率接受较差的状态,这个概率随着时间(迭代次数)逐渐降低,通常使用一个降温函数来控制概率的下降速度。这样可以避免算法陷入局部最优解,从而更有可能找到全局最优的分组方案。
      • 应用:在解决分组问题时,模拟退火算法可以通过不断尝试不同的分组调整,在一定程度上遍历解空间,有机会找到较优的分组方案。例如,在每次迭代中,随机选择两个同学交换分组,然后计算新的组数和组间差异。如果新方案更优则直接采用;若较差,则根据当前温度对应的概率决定是否采用,随着温度降低,接受较差方案的概率逐渐减小。
    • 遗传算法
      • 原理:遗传算法是借鉴生物进化过程中的遗传、变异和自然选择机制的一种优化算法。将每个分组方案看作一个个体,用某种编码方式表示分组方案(例如,用一个数组表示每个同学所在的组)。通过选择、交叉和变异等遗传操作,不断生成新的个体(分组方案)。在每一代中,评估每个个体的适应度(可以定义为组数最多且组间差异最小的指标),选择适应度高的个体进行遗传操作,逐渐进化出更优的分组方案。选择操作通常基于个体的适应度比例进行,适应度高的个体有更大的概率被选中;交叉操作是将两个选中的个体的部分编码进行交换,生成新的个体;变异操作则是对个体的编码进行随机小幅度的改变,以引入新的基因特征,防止算法过早收敛到局部最优解。
      • 应用:在分组问题中,首先随机生成一组初始的分组方案作为第一代种群。然后对每个方案计算适应度,根据适应度进行选择、交叉和变异操作,产生下一代种群。不断重复这个过程,种群中的个体(分组方案)会逐渐向最优解进化,最终得到一个相对较优的分组方案,满足分组数量最大化且组间差异最小化的要求。
http://www.dtcms.com/a/340829.html

相关文章:

  • 【GPT入门】第49课 LlamaFacotory 训练千问
  • Mac电脑 Pixelmator Pro 专业图像处理【媲美PS】
  • UE5 InVideo插件打包报错
  • Linux 下实现“连 root 都无法查看和删除”的加密文件夹(附一键挂载 + 自动超时退出)
  • 【P7071 [CSP-J2020] 优秀的拆分 - 洛谷 https://www.luogu.com.cn/problem/P7071】
  • 织梦素材站网站源码 资源付费下载交易平台源码
  • 棒子出品,无须破解!
  • PyTorch API 6
  • 深度学习实战116-基于Qwen大模型与层次化对齐评分模型(HASM)的中学数学主观题自动批改系统
  • 常见开源协议详解:哪些行为被允许?哪些被限制?
  • AV1视频编码器2024-2025技术进展与行业应用分析
  • 本地部署的终极多面手:Qwen2.5-Omni-3B,视频剪、音频混、图像生、文本写全搞定
  • 第四章:大模型(LLM)】07.Prompt工程-(5)self-consistency prompt
  • PyTorch 深度学习常用函数总结
  • 使用 SSH 方式克隆 GitHub 仓库没有权限解决办法
  • [递归回溯]679. 24 点游戏
  • LINUX 820 shell:shift,expect
  • 第5.8节:awk自增自减运算
  • linux的内核符号表
  • 服装外贸系统软件怎么用才高效防风险?
  • 曲面的交线的切向量计算及其在坐标平面投影的几何分析
  • 有向图(Directed Graph)和有向无环图(Directed Acyclic Graph,DAG)代码实践
  • 反向Shell(Reverse Shell)
  • Meta 再次重组人工智能部门
  • Visual Studio 2010 简体中文旗舰版 安装全过程详解(附安装包下载)
  • 常见的学术文献数据库
  • 华为数通认证学习
  • 微服务网关中数据权限传递的那些坑:从 Feign 兼容性问题到解决方案
  • 【鸿蒙心迹】7×24小时极限求生:当Origin_null遇上鸿蒙,我如何用100杯咖啡换一条跨域活路?
  • IDM 下载失败排查全攻略