第三届集训队第一次训练赛(第五次作业)

TiOzolo 2023-11-27 15:17:07

第三届集训队第一次训练赛(第五次作业)


1 题 —— 求一维数组中最小元素下标

题目描述

编写程序,求一维数组中最小元素下标。

输入格式:

在一行中输入数组元素的个数,在第二行中输入对应的元素

输出格式:

在一行中输出最小元素对应的下标值。

输入样例:

在这里给出一组输入。例如:

5
234 23 45 135 3

输出样例:

在这里给出相应的输出。例如:

数组中最小元素的下标为:4
  • 代码长度限制16 KB
  • 时间限制400 ms
  • 内存限制64 MB

试题解析文档

1. 题目理解与分析

1.1 题目简述

本题要求编写程序,找出一维数组中最小元素的下标。输入包含数组的大小和具体的元素值,输出为最小元素的下标。

1.2 可能的困难与注意事项

  • 数组下标的理解:C++中数组下标从0开始。
  • 数据范围:应考虑数组中元素的范围和数量,以及最小值可能是多个元素中的任何一个。
  • 初始最小值的设置:应将最小值初始化为一个足够大的数。

    2. 解题思路与算法设计

    2.1 解题思路

  1. 读取数组大小。
  2. 创建相应大小的数组。
  3. 初始化最小值为最大整数,并设置下标为0。
  4. 遍历数组,更新最小值及其下标。
  5. 输出最小值的下标。

    2.2 流程图设计

  6. 输入数组大小。
  7. 对于数组中的每一个元素:
    • 输入元素值。
    • 如果当前元素值小于最小值,则更新最小值及其下标。
  8. 输出最小值的下标。

    3. 代码实现与测试

    3.1 代码方案1

    #include <bits/stdc++.h>
    using namespace std;
    int main() {
     int n;
     cin >> n;
     int arr[n];
     int min = INT_MAX, index = 0;
     for (int i = 0; i < n; i++) {
         cin >> arr[i];
         if (min > arr[i]) {
             min = arr[i];
             index = i;
         }
     }
     cout << "数组中最小元素的下标为:" << index << endl;
    }
    

3.2 代码方案2

  • C++ STL(Standard Template Library,标准模板库)提供了一系列操作算法,其中 max_element 可以用来找出容器中的最大值。与之对应,min_element 函数可以用来找出容器中的最小元素。min_element 返回一个迭代器,指向找到的最小元素,如果有多个相同的最小元素,则返回第一个最小元素的迭代器。
  • 对于本题,您可以使用 min_element 函数来简化解决方案,代码如下:
#include <iostream>
#include <vector>
#include <algorithm> // !引入算法库
using namespace std;

int main() {
    int t; cin >> t; //丢失第一行数据
    vector<int> arr;
    while(cin >> t) { // 如果cin无法继续读取数据则停止
        arr.push_back(t);//添加到数组
    }
    // 使用min_element找到最小元素的迭代器
    auto min_i = min_element(arr.begin(), arr.end());
    //计算第一个元素到最小元素之间的距离
    cout << "数组中最小元素的下标为:" <<distance(arr.begin(), min_i) << endl;
}
  • 在这段代码中,我们使用了 std::vector 而不是数组,因为 min_elementmax_element 函数需要传入迭代器参数。std::distance 函数用于计算两个迭代器之间的距离,即下标。
  • 这种方法的效率较高,因为它避免了手动遍历数组的需要,并且代码也更加简洁。

3.3 测试数据

  • 测试数组中只有一个元素的情况。
  • 测试数组中有多个元素且最小值唯一的情况。
  • 测试数组中有多个元素且最小值不唯一的情况。
  • 测试极端情况,如数组中所有元素均为最大整数。

    4. 知识点总结

  • C++数组的基本使用。
  • 循环结构的应用。
  • 条件判断的实现。
  • C++基本输入输出。

    5. 问题拓展与延伸

  • 如果要求找出所有最小元素的下标怎么办?
  • 如果除了最小元素,还要求找出最大元素的下标,应如何修改程序?

    6. 总结

    本题主要考察了数组的遍历、条件判断和基本输入输出操作,是一个适合初学者的入门级题目。通过这道题目,可以加深对数组和循环结构的理解。

2 题 —— 十佳校园歌手决赛

题目描述

  • 高校团委组织校园歌手比赛,进入决赛的校园歌手有10位,歌手编号从1到10进行编号。组委会随机抽取方式产生了决赛次序为:3,1,9,10,2,7,5,8,4,6。
  • 比赛现场有5个评委为参赛的10位决赛歌手打分,分数为0~10分。歌手最后得分为去掉1个最高分和1个最低分后,剩余的3个分数的平均值。
  • 计算输出10位歌手的得分。

输入格式:

  • 输入5个评委的评分

输出格式:

  • 输出10位歌手的得分(保留小数点后两位)
  • printf("%2d号歌手得分:%.2f\n",/_输出列表_/);

输入样例:

9.0  9.1  9.5  9.6  8.8
9.3  9.2  9.6  9.4  9.0
8.9  9.4  9.7  9.3  9.0
9.4  9.2  9.6  9.9  9.8
9.6  9.0  9.5  9.3  9.6
8.8  9.4  9.2  9.7  9.2
9.1  9.5  9.5  9.3  9.3
9.3  9.2  9.6  9.4  9.0
9.7  9.1  9.5  9.4  9.5
9.5  9.2  9.6  9.2  9.5

输出样例:

 1号歌手得分:9.30
 2号歌手得分:9.47
 3号歌手得分:9.20
 4号歌手得分:9.47
 5号歌手得分:9.37
 6号歌手得分:9.40
 7号歌手得分:9.27
 8号歌手得分:9.30
 9号歌手得分:9.23
10号歌手得分:9.60
  • 代码长度限制16 KB
  • 时间限制400 ms
  • 内存限制64 MB

试题解析文档

1. 题目理解与分析

1.1 题目简述

本题目要求编写程序,计算10位歌手在5位评委的打分下的平均成绩。其中要求去除一个最高分和一个最低分,然后计算剩余三个分数的平均值。

1.2 可能的困难与注意事项

  • 数组的使用:需要正确使用二维数组来存储每位歌手的打分。
  • 分数的计算:在计算平均分时,要正确去除最高分和最低分。
  • 函数的定义和使用:利用函数来简化代码和提高可读性。

    2. 解题思路与算法设计

    2.1 解题思路

  1. 使用二维数组存储每位歌手的打分。
  2. 遍历每位歌手,对其打分调用计算平均分的函数。
  3. 定义一个函数来计算去除最高分和最低分后的平均分。

    2.2 流程图设计

  4. 输入每位歌手的打分。
  5. 对每位歌手的打分进行处理:
    • 调用计算平均分的函数。
    • 输出平均分。
  6. 结束。

    3. 代码实现与测试

    3.1 方案1

#include <bits/stdc++.h>  // 引入了所有标准库
using namespace std;     // 使用标准命名空间
int main() {
    double arr[11][6];  // 声明一个二维数组,用于存储11位歌手的6项评分(数组大小略大于实际需要)
    int flag[10] = {3,1,9,10,2,7,5,8,4,6}; // 定义一个数组,存储歌手的出场顺序
    // 遍历flag数组,按照歌手的出场顺序输入每位歌手的评分
    for (int num : flag) { // 使用范围for循环遍历flag数组
        for (int i = 1; i <= 5; i++) { // 对于每位歌手,遍历5个评分
            cin >> arr[num][i]; // 读入每个评分到arr数组中
        }
    }
    // 计算每位歌手的平均分
    for (int i = 1; i < 11; i++) { // 遍历10位歌手
        double max = 0.0; // 初始化最高分为0.0
        double min = 99999.0; // 初始化最低分为一个非常大的数
        double sum = 0; // 初始化总分为0
        // 找出每位歌手的最高分和最低分,并计算总分
        for (int j = 1; j < 6; j++) { // 遍历每位歌手的5个评分
            if (max < arr[i][j]) max = arr[i][j]; // 更新最高分
            if (min > arr[i][j]) min = arr[i][j]; // 更新最低分
            sum += arr[i][j]; // 累加总分
        }
        // 计算平均分
        sum = sum - (max + min); // 总分中去除最高分和最低分
        sum = sum / 3; // 计算剩余分数的平均值(去掉一个最高分和一个最低分后剩下3个分数)
        // 输出每位歌手的平均分
        printf("%2d号歌手得分:%.2f\n", i, sum); // 格式化输出歌手编号和平均分,保留两位小数
    }
    return 0; // 程序正常结束
}
  • 这段代码的核心功能是计算每位参赛歌手的平均分数,其中涉及到数组的使用、循环结构、条件判断和格式化输出。
  • 代码首先根据歌手的出场顺序录入评分,然后计算每位歌手的最高分、最低分和平均分,最后输出每位歌手的平均分数。

去除注释

#include <bits/stdc++.h>
using namespace std;

int main() {
    double arr[11][6];
    int flag[10] = {3,1,9,10,2,7,5,8,4,6};
    
    for (int num : flag) {
        for (int i = 1; i <= 5; i++) {
            cin >> arr[num][i];
        }
    }
    
    for (int i = 1; i < 11; i++) {
        double max = 0.0;
        double min = 99999.0;
        double sum = 0;
        for (int j = 1; j < 6; j++) {
            if (max < arr[i][j]) max = arr[i][j];
            if (min > arr[i][j]) min = arr[i][j];
            sum += arr[i][j];
        }
        sum = sum - (max + min);
        sum = sum / 3;
        printf("%2d号歌手得分:%.2f\n",i, sum);
    }
    return 0;
}

3.2 方案2

#include <bits/stdc++.h>
using namespace std;

// 定义计算平均分的函数
double calculateAverage(double scores[], int size) {
  double maxScore = scores[0];
  double minScore = scores[0];
  double sum = 0.0;

  for (int i = 0; i < size; i++) {
    sum += scores[i];
    if (scores[i] > maxScore) maxScore = scores[i];
    if (scores[i] < minScore) minScore = scores[i];
  }

  // 去掉一个最高分和最低分
  sum = sum - maxScore - minScore;
  // 返回平均分
  return sum / (size - 2);
}

int main() {
  double arr[11][6];
  int flag[10] = {3, 1, 9, 10, 2, 7, 5, 8, 4, 6};

  // 输入每个歌手的成绩
  for (int num : flag) {
    for (int i = 0; i < 5; i++) {
      cin >> arr[num][i];
    }
  }

  // 计算并输出每个歌手的平均成绩
  for (int i = 1; i < 11; i++) {
    double avg = calculateAverage(arr[i], 5);
    printf("%2d号歌手得分:%.2f\n", i, avg);
  }

  return 0;
}

3.2 方案3

#include <bits/stdc++.h>
using namespace std;

int main() {
    int flag[] = {3,1,9,10,2,7,5,8,4,6};
    double grade[11];
    
    for (int i = 0; i < 10; i++) {
        double sum = 0, max_n = 0, min_n = 99, temp;
        for (int j = 0; j < 5; j++) {
            cin >> temp;
            sum += temp;
            if (max_n < temp) max_n = temp;
            if (min_n > temp) min_n = temp;
        }
        grade[flag[i]] = (sum - max_n - min_n) / 3;
    }
    
    for (int i = 1; i <= 10; i++) {
        printf("%2d号歌手得分:%.2f\n",i,grade[i]);
    }
    
    return 0;
}
  • 这份代码同样是用于计算10位歌手的平均评分,但它采用了更加简化的方式。以下是对代码的逐行解释:
    #include <bits/stdc++.h>  // 引入所有标准库
    using namespace std;     // 使用标准命名空间
    int main() {
      int flag[] = {3,1,9,10,2,7,5,8,4,6}; // 定义一个数组,存储歌手的出场顺序
      double grade[11]; // 定义一个数组,用来存储每位歌手的平均成绩
      // 遍历10位歌手,计算他们的平均成绩
      for (int i = 0; i < 10; i++) {
          double sum = 0, max_n = 0, min_n = 99, temp; // 初始化总分、最高分、最低分和临时变量
          // 读取并处理每位歌手的5个评分
          for (int j = 0; j < 5; j++) {
              cin >> temp; // 读取评分
              sum += temp; // 累加总分
              if (max_n < temp) max_n = temp; // 更新最高分
              if (min_n > temp) min_n = temp; // 更新最低分
          }
          grade[flag[i]] = (sum - max_n - min_n) / 3; // 计算并存储平均分,去除一个最高分和一个最低分
      }
      // 输出每位歌手的平均成绩
      for (int i = 1; i <= 10; i++) {
          printf("%2d号歌手得分:%.2f\n", i, grade[i]); // 格式化输出歌手编号和平均分,保留两位小数
      }
      return 0; // 程序正常结束
    }
    
  • 这份代码的主要特点是直接在输入评分的循环中就计算了每位歌手的最高分、最低分和总分,从而避免了使用额外的循环来处理这些计算。
  • 这使得代码更加简洁高效。在计算平均分之后,它将结果存储在 grade 数组中,最后通过一个循环来输出每位歌手的平均分数。

3.3 测试数据

  • 测试输入符合要求的正常数据。
  • 测试极端情况,如所有评分都相同。
  • 测试评分中包含小数的情况。

    4. 知识点总结

  • 二维数组的使用。
  • 函数的定义和调用。
  • 循环和条件判断。
  • 浮点数的处理和格式化输出。

    5. 问题拓展与延伸

  • 如何修改程序以适应更多的歌手或评委?
  • 如果需要记录并输出每位歌手的最高分和最低分,应如何修改程序?

    6. 总结

    本题主要考查了数组的使用、函数的定义与调用,以及基本的算法设计。通过本题,可以加深对数组操作、函数模块化以及算法实现的理解,是一个适合中级程序员的实践题目。

3 题 —— 缺牌

  • 诺诺喜欢把一副扑克牌从牌盒里拿出来,又放回去,有时会拿着几张牌到处去藏起来,有时又会从其他副牌拿几张放到某副牌盒中。
  • 当诺妈核对某副牌时,会发现某种花色的牌多了或少了若干张。现在给出诺妈收集到的某副牌的某种花色的所有牌,请你确定缺了哪些牌,并按面值A,2,3……,10,J,Q,K的顺序输出所缺的牌。

输入格式:

  • 输入数据首先包含一个整数T,表示测试实例的个数,然后是T组测试数据。(1<=T<=30)
  • 每组数据有一行输入,首先是1个正整数n(1<=n<=20),然后是n张牌的面值(取2~10,A,J,Q,K的某个值,可能有重复),面值之间以一个空格分隔。

输出格式:

对于每一组数据,有一行输出;若某种花色的牌一张不缺,输出字符串“HAHA”;否则先输出所缺牌的张数,然后按面值A,2,3……,10,J,Q,K的顺序输出所缺的牌的面值(之间以一个空格分隔)。

输入样例:

2
4 J Q 10 10
13 A 2 3 4 5 6 7 8 9 10 J Q K

输出样例:

10 A 2 3 4 5 6 7 8 9 K
HAHA
  • 代码长度限制16 KB
  • 时间限制400 ms
  • 内存限制64 MB

1. 题目理解与分析

1.1 题目简述

本题目要求编写一个程序,用于检测并输出一组扑克牌中缺失的牌。给定一组扑克牌,程序需要判断哪些牌缺失,并按顺序输出缺失的牌。

1.2 可能的困难与注意事项

  • 牌的表示方法需要特别注意,例如将 AJQK 分别表示为 1, 11, 12, 13。
  • 考虑非法输入的处理,例如输入的牌超出了正常范围。
  • 保证代码的简洁性和高效性。

    2. 解题思路与算法设计

    2.1 解题思路

  1. 使用 map 来存储扑克牌与对应数值的映射关系。
  2. 读取输入的牌数,对每张牌进行处理,若在 map 中存在,则将其对应数值插入到 set 中。
  3. 计算缺失牌的数量并输出。如果 set 中的元素数量等于13,则表示没有缺失的牌;否则,输出缺失的牌。

    2.2 流程图设计

  4. 初始化扑克牌映射。
  5. 读取牌数。
  6. 对每张牌进行处理。
  7. 计算并输出缺失的牌。

    3. 代码实现与测试

    3.1 代码方案1

#include<bits/stdc++.h> // 引入所有标准库
using namespace std;

int main() {
  // 创建一个映射表,用于将扑克牌的字符表示映射到相应的整数值
  map<string, int> card = {
     {"A", 1}, {"2", 2}, {"3", 3}, {"4", 4}, {"5", 5},
     {"6", 6},{"7", 7}, {"8", 8}, {"9", 9}, {"10", 10}, 
     {"J", 11}, {"Q", 12}, {"K", 13}
  };
  // c++中,一行太长可以直接换行写

  int n; // 存储测试案例的数量
  cin >> n; // 读取测试案例的数量
  while(n--) { // 对每个测试案例进行操作
    int m; // 存储每个测试案例中的牌的数量
    cin >> m; // 读取牌的数量
    set<int> s; // 创建一个集合,用于存储当前已经读取的牌的值
    while(m--) { // 对每张牌进行操作
      string t; // 用于存储读取的牌
      cin >> t; // 读取牌
      // 如果读取的牌在映射表中,则将其值加入集合
      if(card.count(t)) {s.insert(card[t]);}
    }
    
    // 静态数组,用于将整数值转换回扑克牌的字符表示
    static string name[] = {"A", "2", "3", "4", "5", "6",
     "7", "8", "9", "10", "J", "Q", "K"};
    // 如果集合大小为13,表示没有缺失的牌
    if(s.size() == 13) cout << "HAHA";
    else {
      // 否则,计算并输出缺失的牌的数量和具体是哪些牌
      cout << 13 - s.size();
      for(int i = 1; i <= 13; ++i)
        if(!s.count(i)) cout << ' ' << name[i - 1];
    }
    cout << '\n'; // 每个测试案例后输出换行
  }
  return 0; // 程序结束
}

3.2 去除注释版

#include<bits/stdc++.h>
using namespace std;
int main() {
    map<string, int> card = {{"A", 1}, {"2", 2},\
    {"3", 3}, {"4", 4}, {"5", 5}, {"6", 6},\
    {"7", 7}, {"8", 8}, {"9", 9}, {"10", 10},\
    {"J", 11}, {"Q", 12}, {"K", 13}};
    
    int n;
    cin >> n;
    while(n--) {
        int m;
        cin >> m;
        set<int> s;
        while(m--) {
            string t;
            cin >> t;
            if(card.count(t)) s.insert(card[t]);
        }
        static string name[] = {"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"};
        if(s.size() == 13) cout << "HAHA";
        else {
            cout << 13 - s.size();
            for(int i = 1; i <= 13; ++i)
                if(!s.count(i)) {
                    cout << ' ' << name[i - 1];
                } 
        }
        cout << '\n';
    }
    return 0;
}

3.3 优化格式

#include <bits/stdc++.h>
#include <set>
using namespace std;

void func() {
    map<string, int> card;
    card["A"] = 1;card["J"] = 11;
    card["Q"] = 12;card["K"] = 13;
    for (int i = 2; i <= 10; i++) {
        card[to_string(i)] = i;
    }

    int n; cin >> n;
    set<int> s;
    while(n--) {
        string temp;
        cin >> temp;
        if (card.count(temp)) {
            s.insert(card[temp]);
        }
    }

    static string name[] = {"A", "2", "3", "4", "5", "6",
     "7", "8", "9", "10", "J", "Q", "K"};
    
    if (s.size() == 13) {
        cout << "HAHA";
    }else{
        cout << 13 - s.size();
        for (int i = 1; i <= 13; i++) {
            if (!s.count(i)) {
                cout << ' ' << name[i-1];
            }
        }
    }
    cout << endl;
}

int main () {
    int n; cin >> n;
    while(n--) {
        func();
    }
}

3.4 方案2

#include <iostream>
#include <unordered_set>
#include <vector>
#include <algorithm>

using namespace std;

int main() {
  int T;
  cin >> T;

  for (int t = 0; t < T; ++t) {
    int n;
    cin >> n;
    
    unordered_set<string> cards_present;
    vector<string> all_cards = {"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"};
    
    for (int i = 0; i < n; ++i) {
      string card;
      cin >> card;
      cards_present.insert(card);
    }
    
    vector<string> missing_cards;
    for (const string &card : all_cards) {
      if (cards_present.find(card) == cards_present.end()) {
        missing_cards.push_back(card);
      }
    }
    
    if (missing_cards.empty()) {
      cout << "HAHA" << endl;
    } else {
      cout << missing_cards.size();
      for (const string &missing_card : missing_cards) {
        cout << " " << missing_card;
      }
      cout << endl;
    }
  }

  return 0;
}

方案2逐行代码解释

代码头部和主函数
#include <iostream>            // 引入输入输出流库
#include <unordered_set>       // 引入无序集合库,用于存储元素并快速检查存在性
#include <vector>              // 引入向量库,用于存储和操作动态数组
#include <algorithm>           // 引入算法库,可能用于操作容器等
using namespace std;           // 使用标准命名空间
int main() {                   // 主函数的开始
初始化和读取输入
    int T;                     // 声明整型变量T,用于存储测试案例的数量
    cin >> T;                  // 从标准输入读取测试案例的数量
    for (int t = 0; t < T; ++t) { // 对每个测试案例进行遍历
处理每个测试案例
int n;                 // 声明整型变量n,用于存储每个测试案例中的牌的数量
cin >> n;              // 从标准输入读取牌的数量
unordered_set<string> cards_present; // 创建一个无序集合,用于存储已存在的牌
vector<string> all_cards = {"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"}; // 创建并初始化一个包含所有牌的向量
for (int i = 0; i < n; ++i) { // 遍历每张牌
    string card;           // 声明字符串变量card,用于存储读取的牌
    cin >> card;           // 从标准输入读取牌
    cards_present.insert(card); // 将读取的牌插入到无序集合中
}
找出缺失的牌
vector<string> missing_cards; // 创建一个向量,用于存储缺失的牌
for (const string &card : all_cards) { // 遍历所有可能的牌
    if (cards_present.find(card) == cards_present.end()) { // 如果当前牌不在已存在的牌集合中
        missing_cards.push_back(card); // 将这张缺失的牌添加到缺失牌的向量中
    }
}
输出结果
    if (missing_cards.empty()) { // 如果没有缺失的牌
        cout << "HAHA" << endl;  // 输出"HAHA"
    } else {                     // 如果有缺失的牌
        cout << missing_cards.size(); // 输出缺失的牌的数量
        for (const string &missing_card : missing_cards) { // 遍历所有缺失的牌
            cout << " " << missing_card; // 输出每张缺失的牌
        }
        cout << endl;           // 输出换行
    }
}
return 0;                      // 程序正常结束
}
  • 整体来看,这段代码逻辑清晰,结构紧凑,通过使用unordered_setvector来处理和查找缺失的牌,既简洁又高效。

3.5 测试数据

  • 测试数据1:4 J Q 10 10 应输出缺失的牌。
  • 测试数据2:13 A 2 3 4 5 6 7 8 9 10 J Q K 应输出“HAHA”。
  • 测试数据3:非法输入,如 1 14 应能正确处理。

    4. 逐行代码解释

  • map<string, int> card:初始化一个映射扑克牌与对应数值的映射。
  • while(n--):处理每一组输入的牌。
  • while(m--):读取每张牌的输入。
  • if(card.count(t)) s.insert(card[t]):如果牌有效,则插入到集合中。
  • if(s.size() == 13):判断是否缺失牌。
  • cout << name[i - 1] << ' ':输出缺失的牌。

    5. 知识点总结

  • 使用 mapset 来简化数据处理。
  • 理解并利用 STL 容器的特性,如 sizecount 方法。
  • 如何处理和优化字符串和集合的输入输出。

    6. 问题拓展与延伸

  • 变种问题:如果除了输出缺失的牌,还需要输出多余的牌,可以如何修改代码?
  • 类似应用:这种映射和集合处理方法可以应用于类似的问题,如检测重复元素或统计不同元素的数量。

    7. 总结

    在这个问题中,我们通过精简代码并利用C++的STL容器特性,实现了一个高效且简洁的解决方案。关键在于理解如何使用映射(map)和集合(set)来简化数据处理。此外,这个问题也展示了如何优化输入输出操作,尤其是在需要处理大量数据时的效率考虑。通过这个问题,我们可以学习到数据结构的有效运用,以及如何在保持代码简洁的同时确保其功能性和健壮性。

Map的使用与介绍

c++中map详解_c++ map_不会编程的小猿的博客-CSDN博客

  • 头文件#include<map>

map 是 C++ 标准模板库(STL)中的一种关联容器,它存储的是键值对(key-value pairs),其中每个键都是唯一的。它的主要作用是基于键快速查找、访问和修改对应的值。map 在内部通常是通过平衡二叉树(如红黑树)实现的,这使得它在插入、删除和查找操作上都能保持对数级的时间复杂度。

基本使用

  1. 声明和初始化
    std::map<key_type, value_type> myMap;
    
    其中 key_typevalue_type 分别代表键和值的数据类型。
  2. 插入元素
    myMap[key] = value;
    
    或者使用 insert 方法:
    myMap.insert(std::make_pair(key, value));
    
  3. 访问元素
    value_type value = myMap[key];
    
    如果 key 不存在,map 会自动创建一个新元素并初始化其值。
  4. 检查元素存在
    if (myMap.find(key) != myMap.end()) {
        // 存在
    }
    

std::map 也有 count 方法,这个方法用于检查指定的键(key)在 map 中是否存在。在 std::map 中,count 方法返回的是键存在于 map 中的次数,由于 map 中的键是唯一的,因此 count 返回的值要么是 0(表示键不存在),要么是 1(表示键存在)。

std::setstd::map 都是 C++ 标准模板库(STL)中的关联容器,它们都基于红黑树实现。这种树结构使得这两种容器在元素查找上都非常高效。因此,count 方法在这两种容器中的作用是类似的,都是用于快速检查某个元素(在 set 中是值,在 map 中是键)是否存在于容器中。

这个共有的 count 方法反映了 STL 关联容器的设计哲学,即提供一致的接口以方便程序员在不同的容器类型之间进行操作,同时保持每种容器的独特功能和优化。这种设计允许程序员在不同的容器间切换而不需要大幅度改变代码逻辑,从而提高了代码的可重用性和可维护性。

  1. 遍历 map
    for (auto &pair : myMap) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }
    
    其中 pair.first 是键,pair.second 是值。

    特点和使用场景

  • 有序性map 中的元素按键的顺序存储,这通常是字典序。
  • 唯一键:每个键在 map 中只能出现一次。
  • 动态大小map 可以根据需要动态增长或缩减。
  • 查找效率:对于大量数据的快速查找非常有效。
    map 适用于需要快速查找、插入和删除键值对的场景,尤其是当元素数量较大且频繁进行这些操作时。它常用于需要根据某个键(如字符串、数字等)快速检索相关信息的情况,比如字典、数据库索引模拟、各种映射关系等。

myMap.insert(std::make_pair(key, value)); 这里为什么要使用make_pair呢?insert不能直接把键和值传给map吗?

for (auto &pair : myMap) {
std::cout << pair.first << ": " << pair.second << std::endl;
}

这里的 & 的作用是什么呢?
firstsecond又是什么呢?


在 C++ 中,std::mapinsert 方法和 make_pair 函数通常一起使用来插入新元素,这是因为 map 存储的是键值对,而 make_pair 正好用于创建这种对应关系。

  1. 为什么使用 make_pair:
    • std::mapinsert 方法接受一个键值对作为参数。这个键值对在 C++ 中通常表示为 std::pair<key_type, value_type> 类型。
    • std::make_pair 函数就是用来快速创建这样的 pair 对象。它接受两个参数(一个键和一个值),并返回一个由这两个参数构成的 pair 对象。
    • 你也可以不使用 make_pair,而是直接构造一个 pair 对象,但 make_pair 让代码更简洁、易读。
  2. 直接使用 insert:
    • 实际上,你也可以直接向 map 插入一个 pair 对象而不使用 make_pair,如下所示:
      myMap.insert(std::pair<key_type, value_type>(key, value));
      
    • make_pair 更为简洁,因此更常用。
  3. 关于 & 的作用:
    • 在这个循环中,auto &pair 表示我们正在通过引用来访问 map 中的每个元素。这意味着我们不是在拷贝每个元素(这可能很耗费资源),而是直接在原始元素上操作。
    • 使用引用可以提高效率,尤其是在元素包含大型数据结构时,同时还允许你修改 map 中的元素。
  4. firstsecond:
    • std::pair 对象中,firstsecond 分别是用来访问键值对中的键和值。
    • 因此,在遍历 map 时,pair.first 访问的是键,而 pair.second 访问的是与键对应的值。

4 题 —— 三花牌 贵人相助

  • 三花牌是只有3个花色的牌,花色分别为r,g,b。每个花色有n张牌,编号为1到n。
  • 赌神提前拿走了一些牌,赌圣为了让你少输点,又放回了一些牌。
  • 请你按照花色r、g、b,数字从小打大的顺序输出所有剩余的牌面。

输入格式:

  • 每个花色牌的数量。
  • 赌神提前拿走的牌面。
  • 赌圣放回的牌面。

输出格式:

  • 所有剩余的牌面,每个牌面在一行输出。

输入样例:

  • 在这里给出一组输入。例如:
3
r2 b2 g2
b2 g2

输出样例:

  • 在这里给出相应的输出。例如:
r1
r3
g1
g2
g3
b1
b2
b3
  • 代码长度限制16 KB
  • 时间限制400 ms
  • 内存限制64 MB

1. 题目理解与分析

1.1 题目简述

本题是一个有关牌类游戏的编程问题。游戏使用一种特殊的牌,总共有三种花色(红、绿、蓝,分别用 r、g、b 表示),每种花色有 n 张牌,编号从 1 到 n。在游戏开始前,一些牌被提前拿走,然后又有一些牌被放回。任务是输出所有剩余牌面,按照 r、g、b 的顺序,每种花色内部按数字顺序排列。

1.2 可能的困难与注意事项

  • 需要注意牌的花色和数字的正确处理。
  • 牌面的排序,需要保证输出顺序为 r、g、b,且每种颜色内部编号顺序。
  • 输入格式的处理,尤其是字符串到数字的转换。

    2. 解题思路与算法设计

    2.1 解题思路

  1. 初始化牌堆:对每种颜色的牌,从 1 到 n 编号,将它们加入到对应的集合中。
  2. 处理赌神拿走的牌:读取赌神拿走的牌面信息,从集合中删除这些牌。
  3. 处理赌圣放回的牌:读取赌圣放回的牌面信息,将这些牌加回到集合中。
  4. 输出结果:按照 r、g、b 的顺序,依次输出每种颜色的牌面,确保每种颜色内部按数字顺序排列。

    2.2 流程图设计

  5. 初始化牌堆:创建三个集合,分别表示 r、g、b 花色的牌。
  6. 输入并处理赌神拿走的牌。
  7. 输入并处理赌圣放回的牌。
  8. 按照顺序输出所有剩余的牌。

    3. 代码实现与测试

    3.1 代码方案

    #include <bits/stdc++.h>
    using namespace std;
    int main() {
     int n;
     cin >> n;
     cin.ignore(); // 忽略换行符,如果没有忽略,后续的getline可能无法正常获取
     map<string, set<int>> cards;
     string colors[3] = {"r", "g", "b"}; //保证输出也是r,g,b
     // 初始化所有牌
     for (const auto &color : colors) {
         for (int i = 1; i <= n; i++) {
             cards[color].insert(i);
         }
     }
     // 赌神拿走的牌
     string line;
     getline(cin, line);
     istringstream iss(line);//把line字符串以空格分隔每次读取一个
     string card;
     while (iss >> card) {
         cards[card.substr(0, 1)].erase(stoi(card.substr(1)));
         //删除卡牌.erase
     }
     // 赌圣放回的牌
     getline(cin, line);
     istringstream iss2(line);
     while (iss2 >> card) {
         cards[card.substr(0, 1)].insert(stoi(card.substr(1)));
         //插入卡牌.insert
     }
     // 按照r, g, b的顺序输出剩余的牌面
     for (const auto &color : colors) {
         for (const auto &num : cards[color]) {
             cout << color << num << endl;
         }
     }
     return 0;
    }
    

3.2 测试数据

  • 测试1:n = 3,赌神拿走 r2 b2 g2,赌圣放回 b2 g2。
  • 测试2:n = 5,赌神拿走 r1 r3 g4 b5,赌圣放回 r1 g2。
  • 测试3:边界测试,n = 1,无牌被拿走和放回。

4. 逐行代码解释

这段代码的主要任务是模拟赌神和赌圣玩牌的过程。首先,生成一副从1到n的红、绿、蓝三种颜色的牌,然后赌神拿走一些牌,赌圣放回一些牌,最后按照红、绿、蓝的顺序输出剩余的牌面。

  • cin >> n; cin.ignore();:首先输入牌的数量 n,然后忽略输入流中的换行符,以便于后续的行输入。
  • map<string, set<int>> cards;:定义一个映射,键是花色字符串("r", "g", "b"),值是一个整数集合,存储每种颜色的牌号。
  • 初始化牌堆的循环:对每种颜色(r, g, b),将编号从 1 到 n 的牌添加到对应花色的集合中。
  • 读取并处理赌神拿走的牌:使用 istringstream 分割输入行,然后从相应的花色集合中删除指定编号的牌。
  • 读取并处理赌圣放回的牌:同样使用 istringstream 分割输入行,并将指定编号的牌添加回相应的花色集合。
  • 输出剩余的牌面:按照 r, g, b 的顺序,遍历每种颜色的牌号集合,并输出每张牌。

下面逐行解析这段代码:

  1. #include <bits/stdc++.h>:这是一个非标准的头文件,但在许多编译器中都被接受,它包含了大多数常用的C++库,使用它可以避免分别引入这些库。
  2. using namespace std;:这是使用命名空间 std 的声明,这样你就可以直接使用 std 命名空间中的名字(例如 cout, cin, map, set 等),而不需要每次都加上 std:: 前缀。
  3. int main() {...}:这是主函数,程序的执行从这里开始。
  4. int n; cin >> n; cin.ignore();:首先定义一个整型变量 n,然后从标准输入读取一个整数赋给 n,最后调用 cin.ignore() 忽略接下来的换行符。n 表示每种颜色的牌的数量。
  5. map<string, set<int>> cards;:定义一个 map,它的键是 string 类型,值是 set<int> 类型。这个 map 用于存储每种颜色的牌的集合。
  6. string colors[3] = {"r", "g", "b"};:定义一个字符串数组 colors,包含三种颜色 "r", "g" 和 "b"。
  7. for (const auto &color : colors) {...}:这是一个基于范围的 for 循环,它会遍历 colors 数组中的每一个元素。auto 是 C++11 中引入的自动类型推断关键字,编译器会根据 color 的实际类型自动确定它的类型。& 表示引用,const 表示这个引用是常量,不能用来修改 color 的值。
  8. for (int i = 1; i <= n; i++) {...}:这是一个普通的 for 循环,用于生成每种颜色的牌。
  9. cards[color].insert(i);:向 cards 中对应颜色的 set 插入一个新的牌(即数值 i)。
  10. string line; getline(cin, line);:定义一个字符串 line,然后从标准输入读取一行赋给 line。这行将包含所有赌神拿走的牌。
  11. istringstream iss(line);:定义一个[[第三届集训队第一次训练赛(第五次作业)#字符串流(std istringstream)|字符串流]] iss,并初始化为 line。字符串流可以用于处理字符串中的单词,就像处理文件中的单词一样。
  12. string card; while (iss >> card) {...}:定义一个字符串 card,然后在 while 循环中使用 iss >> cardiss 中读取一个单词赋给 card。这个 while 循环会一直进行,直到 iss 中的所有单词都被读取完。
  13. cards[card.substr(0, 1)].erase(stoi(card.substr(1)));:截取 card 的第一个字符作为颜色,然后将剩余的部分转换为整数作为牌的数值,并从对应颜色的 set 中删除这张牌。[[第三届集训队第一次训练赛(第五次作业)#字符串中提取子串(string.substr(0,1))|substr成员]]
  14. 后面的几行与前面的类似,只是改为了赌圣放回的牌。
  15. 最后的两个嵌套 for 循环用于按照红、绿、蓝的顺序输出剩余的牌面。
    关于 >><<,这两个符号在 C++ 中有多种用途。它们最常见的用途是作为输入和输出运算符,用于从输入流读取数据和向输出流写入数据。除此之外,它们也可以用作位移运算符,用于将整数的二进制表示向左或向右移动。在这段代码中,>> 被用作输入运算符,从字符串流中读取单词。

5. 知识点总结

  • STL(标准模板库)的使用,特别是 map 和 set。
  • 字符串处理,包括使用 istringstream 分割字符串,以及 substrstoi 函数。
  • 逻辑思维和编程技巧,尤其是在处理多步骤输入和保持输出顺序方面。

    6. 问题拓展与延伸

  • 变种题目:如果牌的花色增加或减少,或者每种花色的牌数量不同,如何修改程序以适应这些变化?
  • 性能考虑:如果牌的总数量非常大,是否需要考虑优化数据结构和算法以提高性能?

    7. 总结

    本题是一个典型的数据结构和算法问题,涉及到字符串处理、集合操作和逻辑思维。通过合理的数据结构选择和对输入输出的精确控制,可以有效地解决问题。这种类型的题目对于锻炼编程能力和逻辑思维非常有帮助。

auto关键字

auto是C++11及其后续版本提供的一个关键字,它让编译器自动推导变量类型。所以可以用auto来代替具体的类型名。在你给出的例子中,auto cards = map<string, set<int>>();就让编译器自动推导出cards的类型应为map<string, set<int>>
这种写法的一个优点是当变量的类型很复杂或者很难记住的时候,可以让代码更简洁。另一个优点是当你改变了变量的初始类型(例如,将map<string, set<int>>改为unordered_map<string, set<int>>)时,你不需要去修改所有的类型声明。
对于vectorset以及其他STL容器,你同样可以使用auto关键字。例如:

auto v = vector<int>();
auto s = set<string>();

需要注意的是,auto关键字只能用于初始化变量,因为编译器需要依靠初始化的值来推导变量的类型。所以以下的写法是不正确的:

auto v;  // 错误:没有初始化值,编译器无法推导类型
v = vector<int>();

字符串流(std::istringstream

  • 在 C++ 中,istringstream 是标准库中的一个类,它定义在 <sstream> 头文件中。你可以将 istringstream 理解为同时具有两种用途:
  1. 类型声明:就像其他任何类型一样,你可以使用 istringstream 来声明变量。这与使用 int, std::string, std::vector 等声明变量的方式相同。例如:
    std::istringstream iss;  // 声明一个 std::istringstream 类型的变量,但未初始化
    
  2. 创建字符串流对象istringstream 同时也是一个构造函数,可以用来创建并初始化一个字符串流对象。当你声明并初始化一个 istringstream 对象时,你实际上是调用了它的构造函数。例如:
    std::istringstream iss(line);  // 声明并初始化一个 std::istringstream 对象,使用字符串 line
    
  • 当你使用 iss = std::istringstream(line); 这样的表达式时,你实际上是在创建一个临时的 istringstream 对象,并将它赋值给已经声明的 iss 对象。这里发生了两步操作:构造一个新的 istringstream 对象,并调用 iss 的赋值操作符。

    所以,istringstream 既可以用作类型声明,也可以用作创建和初始化字符串流对象的构造函数。


  • 在 C++ 中,字符串流(std::istringstream)是一个非常有用的工具,它可以把一个字符串当作流来处理。这意味着我们可以使用流的操作(比如 >> 操作符)来从字符串中读取数据。
  • 假设 line 是字符串 "I Love You",当我们创建一个字符串流 std::istringstream iss(line); 时,iss 就像一个包含了 line 的流。然后,我们可以使用 iss >> card 来从 iss 中读取一个单词到 card 中。这个操作会自动忽略单词之间的空格。
  • 在这个例子中,第一次执行 iss >> card 时,card 会被赋值为 "I",第二次执行 iss >> card 时,card 会被赋值为 "Love",第三次执行 iss >> card 时,card 会被赋值为 "You"。这就像我们从一个文件或 std::cin 中读取数据一样。
  • 这种特性使得字符串流在处理包含多个数据项的字符串时非常有用,比如从一行中读取多个单词,或者从一行中读取多个由逗号或其他分隔符分隔的数据。

  • 在 C++ 中,std::istringstream>> 操作符默认使用空白字符作为分隔符,包括空格、制表符和换行符。如果你想使用其他分隔符,例如逗号或冒号,你需要自行编写代码来处理。
  • 一个常见的方法是使用 std::getline 函数,它允许你指定一个分隔符。例如,如果你有一个逗号分隔的字符串,你可以使用以下代码来处理:
    std::istringstream iss(line);
    std::string item;
    while (std::getline(iss, item, ',')) {
      // 处理每个由逗号分隔的项目
      // 'item' 是当前项目的字符串
    }
    
  • 在这个代码片段中,std::getline 函数会从 iss 中读取字符,直到遇到逗号或者到达字符串的末尾。读取的字符(不包括逗号)会被赋值给 item
  • 需要注意的是,使用 std::getline 时,分隔符(在这个例子中是逗号)会被读取并丢弃,而不会被包含在 item 中。同时,std::getline 也会读取分隔符之间的所有字符,包括空白字符。
  • 如果你的数据项之间有额外的空白字符,你可能需要额外的步骤来删除它们。

为什么 ==while (iss >> card)== iss >> card 也可以作为退出条件? 这个也会返回true和false吗?

  • 在 C++ 中,iss >> card 这个表达式不仅执行了从输入流 iss 中读取数据到字符串 card 的操作,而且还会返回一个输入流的引用。在 C++ 中,流对象(比如 istringstream)可以被用作==布尔表达式==。
  • 当流==成功地==执行了操作时(比如成功地从流中读取了数据),这个表达式的值为 true;如果遇到流的末尾(EOF)或发生错误,表达式的值为 false
  • 因此,while (iss >> card) 这个循环会==一直执行==,直到 iss 中没有更多的数据可以读取。每次循环,iss >> card 尝试从 iss 中读取下一个单词到 card。如果成功,循环继续;如果失败(比如因为已经到达了 iss 的末尾),循环停止。
  • 这是一种常见的模式,用于从流中连续读取数据,直到没有更多的数据可读。在我们的例子中,它用于从字符串流 iss 中读取一系列由空格分隔的单词(在这种情况下是卡牌信息),直到读取完所有的单词。

对于之前的这段代码(部分)

    string line;
    getline(cin, line);
    istringstream iss(line);
    string card;
    while (iss >> card) {
        cards[card.substr(0, 1)].erase(stoi(card.substr(1)));
    }
    
    //为什么 iss 变量不可以直接覆盖而需要创建一个 iss2 呢?
    getline(cin, line);
    istringstream iss2(line);
    while (iss2 >> card) {
        cards[card.substr(0, 1)].insert(stoi(card.substr(1)));
    }

为什么==line==变量可以重复使用,但是 ==iss==就不可以了呢?为什么再一次覆盖==iss==就会出错呢? 如果我想==再一次使用iss变量==,重新生成一个==字符串流==该怎么做呢?

在 C++ 中,变量 lineiss 的使用方式不同,这是为什么 line 可以重复使用而 iss 通常不这样做的原因。

  1. 关于 line 的重复使用
    • line 是一个 std::string 对象,它用于存储从标准输入读取的一行文本。
    • 当你使用 getline(cin, line) 时,line 中原有的内容会被新读取的内容覆盖。这是因为 getline 函数会清除 line 中的当前内容,然后将新内容填充进去。
    • 因此,你可以多次使用 getline 来读取不同的行到同一个 line 变量中,每次 line 都会被新的行内容覆盖。
  2. 关于 iss 的重复使用
    • iss 是一个 std::istringstream 对象,它是一个流对象,用于从一个字符串读取数据。
    • 一旦 iss 读取完它所包含的字符串,它就会到达末尾(EOF),这时如果你尝试从中读取更多的数据,操作会失败。
    • 你可以重新初始化 iss 来使用相同的变量名,但这实际上是创建了一个新的 istringstream 对象,而不是重用原来的对象。
  3. 如何重新使用 iss 变量
    • 如果你想要重新使用 iss 变量名来创建一个新的字符串流,你可以通过赋值操作符 = 来实现,如下所示:
      iss = istringstream(line);
      
    • 这样的操作实际上是销毁了原有的 iss 对象,并创建了一个新的 istringstream 对象,然后将它赋值给 iss 变量。
      综上所述,line 可以重复使用是因为每次调用 getline 都会替换其内容,而 iss 需要通过重新赋值来“重用”,其实质是创建了一个新的对象。

  • 是不是只有先使用了istringstream iss(line); 这条语句
  • 我才可以使用iss = istringstream(line);这条语句呢?

    我直接在程序的开始就使用iss = istringstream(line);是否会出错呢?

在 C++ 中,如果你直接在程序开始处使用 iss = istringstream(line); 而没有先声明 iss,这会导致编译错误,因为 iss 在这时尚未定义。
要正确使用 istringstream,你需要先声明一个 istringstream 类型的变量,然后可以通过赋值操作符 = 来对其进行初始化或重新赋值。下面是两种情况的示例:

  1. 首次声明并初始化 istringstream:
    std::string line;
    // 假设 line 已经被赋值
    std::istringstream iss(line);  // 首次声明并初始化
    
  2. 已经声明过的 istringstream 变量的重新赋值:
    std::string line;
    // 假设 line 已经被赋值
    std::istringstream iss;  // 首次声明
    iss = std::istringstream(line);  // 重新赋值
    
    如果你想在程序的一开始就使用 iss = std::istringstream(line); 这样的表达式,你需要先声明 iss。例如:
    std::string line;
    std::istringstream iss;  // 首次声明 iss
    // 稍后的代码中
    iss = std::istringstream(line);  // 使用这种方式重新赋值
    
    如果没有先声明 iss,直接使用 iss = std::istringstream(line); 会导致编译错误,因为编译器不知道 iss 是什么类型的变量。

字符串中提取子串(string.substr(0,1))

    while (iss >> card) {
        cards[card.substr(0, 1)].erase(stoi(card.substr(1)));
    }

这行代码的作用是从特定颜色的牌堆(set)中删除一张指定数值的牌。它的具体操作如下:

  • card.substr(0, 1):这是一个字符串的子串操作。substr 函数用于从字符串中提取子串。它的第一个参数是子串开始的位置(从 0 开始计数),第二个参数是子串的长度。因此,card.substr(0, 1) 就是取 card 的第一个字符,这个字符代表牌的颜色。
  • cards[card.substr(0, 1)]:这是一个 map 的索引操作。它用于获取 cards 中键为 card.substr(0, 1)(即牌的颜色)的值,这个值是一个 set,代表那种颜色的所有牌。
  • card.substr(1):这是另一个字符串的子串操作。由于 substr 函数的第二个参数是可选的,如果省略,那么子串就会一直到字符串的末尾。因此,card.substr(1) 就是取 card 从第二个字符开始到最后的所有字符,这些字符代表牌的数值,但仍然是字符串形式。
  • stoi(card.substr(1))stoi 函数用于将字符串转换为整数。因此,stoi(card.substr(1)) 就是将牌的数值从字符串形式转换为整数形式。
  • cards[card.substr(0, 1)].erase(stoi(card.substr(1))):这是一个 set 的删除操作。它删除 cards[card.substr(0, 1)](即特定颜色的牌堆)中的 stoi(card.substr(1))(即特定数值的牌)。
    总的来说,这行代码的作用就是从特定颜色的牌堆中删除一张特定数值的牌。

5 题 —— 排序#2

输入格式:

  • 输入一个名单,每行包括一个姓名和他/她的存款金额(==int范围内==),==姓名==和==金额==以空格隔开,以end结束

输出格式:

  • 输出按金额大小从==小到大==排序后的名单,如果==金额相同==则按输入顺序排序。

输入样例:

  • 在这里给出一组输入。例如:
马芸 666
范小勤 888
马化疼 123
end

输出样例:

在这里给出相应的输出。例如:

马化疼 123
马芸 666
范小勤 888

说明

  • 数据范围 $0<n<1000$
  • 代码长度限制16 KB
  • 时间限制400 ms
  • 内存限制64 MB

试题解析文档

1. 题目理解与分析

1.1 题目简述

本题要求使用C++编写一个程序,用于对一个包含姓名和存款金额的列表进行排序。排序需按照存款金额从小到大的顺序,如果存款金额相同,则按照输入顺序排序。

1.2 可能的困难与注意事项

  • 需要理解如何使用结构体来存储不同类型的数据。
  • 掌握排序算法和比较函数的使用。
  • 注意处理相同金额的排序顺序。

    2. 解题思路与算法设计

    2.1 解题思路

  1. 定义结构体:创建一个包含姓名(字符串)和存款金额(整数)的结构体。
  2. 读取输入:循环读取姓名和金额,直到遇到 "end"。
  3. 排序处理:使用排序算法根据金额进行排序,如果金额相同,则保持输入顺序。
  4. 输出结果:按排序后的顺序输出名单。

    2.2 流程图设计

  5. 定义结构体 Person 来存储姓名和金额。
  6. compare 函数用于比较两个 Person 对象的金额。
  7. main 函数中读取输入并存储到 vector 中。
  8. 使用 sort 函数和 compare 进行排序。
  9. 遍历排序后的 vector 并输出结果。

    3. 代码实现与测试

    3.1 代码方案

    #include <iostream>
    #include <vector>
    #include <algorithm>
    using namespace std;
    // 定义结构体
    struct Person {
     string name;
     int amount;
    };
    // 排序函数
    bool compare(const Person &a, const Person &b) {
     return a.amount < b.amount;
    }
    int main() {
     vector<Person> people;
     string name;
     int amount;
     // 读取数据
     while (cin >> name && name != "end") {
         cin >> amount;
         people.push_back({name, amount});
     }
     // 对人员进行排序
     sort(people.begin(), people.end(), compare);
     // 输出结果
     for (const auto &person : people) {
         cout << person.name << " " << person.amount << endl;
     }
     return 0;
    }
    

    3.2 测试数据

  • 测试空输入。
  • 测试存款金额相同的情况。
  • 测试仅一个输入的情况。

    4. 逐行代码解释

  • 这个问题需要我们使用C++来实现一个简单的排序程序,使用结构体来存储每个人的姓名和存款金额。下面是解决这个问题的步骤和代码解释:
  1. 定义结构体: 我们首先定义一个结构体 Person,它包含两个成员:一个字符串 name 存储姓名,和一个整数 amount 存储存款金额。
  2. 读取数据: 使用循环从标准输入读取姓名和存款金额,直到遇到 "end"。
  3. 排序: 根据存款金额对结构体数组进行排序,如果存款金额相同,则按照输入顺序排序。
  4. 输出结果: 输出排序后的名单。

代码解释

  • struct Person 定义了一个名为 Person 的结构体,其中包含 nameamount 两个成员。
  • bool compare(const Person &a, const Person &b) 是一个比较函数,用于排序时比较两个 Person 对象的存款金额。
  • main 函数中首先声明了一个 Person 类型的向量 people
  • 使用一个循环从输入中读取姓名和金额,直到输入 "end" 为止。
  • 使用 sort 函数和 compare 函数对 people 向量进行排序。
  • 最后,使用一个循环打印排序后的结果。
    这段代码完全符合题目要求,使用结构体来存储数据并按照指定的排序规则进行排序。

5. 知识点总结

  • 结构体struct的定义和使用。
  • vector 的使用和动态数组的概念。
  • 排序算法的应用及自定义比较函数的编写。
  • C++ I/O 操作。

    6. 问题拓展与延伸

  • 修改程序以支持降序排序。
  • 扩展程序以支持更多的数据类型和排序条件。

    7. 总结

    本题不仅考察了基本的C++编程技能,如结构体的使用和标准模板库的应用,还涉及了排序算法的理解和实现,是对C++编程基础的良好练习。

    8. 拓展的知识点

8.1 结构体的应用_C++

  1. 什么是结构体呢?
  2. 它是如何使用的呢?
  3. vector<Person> people; 这个要怎么理解呢? 与int与string类型的vector有什么区别呢?,这个结构体里的vector要怎么理解呢?
  4. ```cpp
    // 排序函数
    bool compare(const Person &a, const Person &b) {
    return a.amount < b.amount;

// 对人员进行排序
sort(people.begin(), people.end(), compare);

 - 这个排序函数是怎么实现的呢?`.amount` 又是什么意思呢?
5. `people.push_back({name, amount});` 这个代码是什么意思呢?
6. 最后的输出语句是怎么把答案输出的呢?

```cpp
    for (const auto &person : people) {
        cout << person.name << " " << person.amount << endl;
    }

  1. 什么是结构体(Struct)?

    • 结构体(struct)是 C++ 中的一种复合数据类型,它可以包含多个不同类型的数据成员。它是一种自定义的数据类型,可以用来组织和存储相关联的数据,使得代码更加清晰和易于理解。
    • 在本题中,我们使用结构体 Person 来存储每个人的名字和存款金额。这样做的优点是,我们可以将名字和金额紧密地联系在一起,使得代码更加直观和有意义。
  2. 结构体的使用:

    • 结构体的使用分为几个步骤:首先定义结构体,声明它包含的数据类型;然后创建该结构体的实例(变量);最后通过这些实例访问结构体内的数据。
    • 定义结构体的基本语法如下:
      struct StructName {
         TypeName1 memberName1;
         TypeName2 memberName2;
         // more members...
      };
      
      StructName 是结构体的名字,TypeName1, TypeName2 等是数据成员的类型,memberName1, memberName2 等是数据成员的名字。
  3. 理解 vector<Person> people;
    vector 是C++标准模板库中的一个动态数组,可以存储任何类型的数据。当我们声明 vector<Person> people; 时,我们创建了一个能够存储 Person 结构体实例的 vector。这意味着这个 vector 将包含多个 Person 实例,每个实例包含一个人的姓名和存款金额。

  4. 排序函数和 .amount 的意思:
    排序函数 compare 是一个自定义的比较函数,用于确定两个 Person 实例间的排序顺序。这里,compare 函数通过比较两个 Person 实例的 amount 成员来决定顺序。.amount 是结构体 Person 中定义的一个整数类型的成员变量,表示存款金额。sort 函数使用 compare 函数来确定排序顺序。

  5. 理解 people.push_back({name, amount});
    这行代码用于向 vector 中添加新的 Person 实例。push_backvector 类的一个方法,用于在 vector 的末尾添加一个新元素。这里,我们创建了一个新的 Person 实例,并设置其 nameamount,然后添加到 people vector 中。

  6. 最后的输出语句解释:
    这段代码使用了一个范围基于的 for 循环来遍历 people vector 中的每个 Person 实例。const auto &person 表示我们正在遍历的当前 Person 实例的引用,保证在循环中不会复制整个结构体,从而提高效率。cout << person.name << " " << person.amount << endl; 用于输出当前 Person 实例的姓名和存款金额。


我们回到这个片段:

struct Person {
    string name;
    int amount;
};
bool compare(const Person &a, const Person &b) {
    return a.amount < b.amount;
}
  • 同样是声明,为什么==结构体==需要再{}后面加上; 呢? 而声明compare函数却不需要在{}后面添加

  • 在 C++ 中,结构体(struct)的定义实际上是在声明一个新的数据类型,因此在结构体的定义末尾需要加分号(;),来标识一个数据类型定义的结束。这是 C++ 语言的==语法要求==。这样做也允许在结构体定义后立即定义该类型的变量,如:

    struct Person {
      string name;
      int amount;
    } person1, person2;
    
  • 这里定义了一个名为 Person 的新类型,并且在同一语句中声明了两个该类型的变量 person1person2

  • 另一方面,函数的定义包含了函数的整个主体,因此不需要在函数体结束的大括号(})后加分号。函数的定义结束于最后一个大括号的闭合,这足以让编译器理解函数定义的结束,无需额外的分号。

  • 这些规则是 C++ 语言的基本组成部分,理解这些细节对于编写正确和规范的 C++ 代码非常重要。

8.2 sort函数的使用_C++

  • std::sort 是 C++ 标准模板库(STL)中的一个函数,用于对序列(如数组或 vector)进行排序。
    std::sort 的==基本语法==如下:
    std::sort(first, last);
    
  • firstlast 是要排序序列的==起始==和==结束==迭代器。在这之后的序列将被按照升序排序。
  • 此外,std::sort 还可以接受一个自定义的比较函数作为第三个参数,如下:
    std::sort(first, last, compareFunction);
    
  • compareFunction 是一个比较函数,它接受两个参数,如果第一个参数应该排在第二个参数前面,那么它应该返回 true,否则返回 false
  • 在本题中,我们使用 std::sort 和自定义的比较函数 compare 来对 Person 对象进行排序。这样,我们可以按照自己的需求(即按照存款金额的升序)进行排序,而不必担心如何实现排序算法的细节。

这里是对之前代码的提问: sort 是如何进行排序的呢?

sort的工作原理

让我们一步一步来理解这个排序函数和 sort 方法的工作原理:

  1. 比较函数的作用:
    比较函数 compare 的目的是为 sort 方法提供一个比较两个元素的方式。当 sort 方法运行时,它会在内部多次调用这个比较函数来决定元素间的顺序。我们不需要编写交换元素的代码,因为 sort 方法内部已经处理了这些。
  2. 返回布尔值的意义:
    compare 函数返回 true 时,它告诉 sort 方法 "第一个参数应该排在第二个参数之前"。换句话说,如果 a.amount 小于 b.amounta 应该在 b 的前面。sort 方法会根据这个返回值来决定是否需要交换这两个元素在数组中的位置。
  3. 迭代器的作用:
    sort 方法的参数 people.begin()people.end() 是迭代器,它们标识了要排序的数组的起始和结束位置。people.begin() 返回一个指向 vector 第一个元素的迭代器,而 people.end() 返回一个指向 vector 末尾(最后一个元素之后的位置)的迭代器。这就给 sort 方法设定了排序的范围。
  4. sort 方法如何使用 compare 函数:
    sort 方法会根据提供的 compare 函数来决定如何排序 vector。它会重复地取 vector 中的元素对,使用 compare 函数来检查它们是否已经按照正确的顺序排列。如果不是,sort 会交换这两个元素的位置。
  5. 如果不提供 compare 函数:
    如果你调用 sort 方法而不提供 compare 函数,sort 将使用默认的比较操作,即 < 操作符。对于基础数据类型(如 intdouble 等),这会导致元素按升序排列。但是对于自定义类型(如我们的 Person 结构体),如果没有重载 < 操作符,编译器将不知道如何比较这些对象,从而导致编译错误。
    所以,比较函数在 sort 方法中扮演着决定元素排序方式的角色,而 sort 方法则负责根据这个规则进行实际的元素交换和排序操作。希望这解释清楚了你的疑问。如果还有其他问题,请随时提问!

sort的参数

sort的第个参数(排序的方法) 这个参数接受一个布尔类型的对不对?这个布尔类型决定前元素是否和后元素交换? 假如我想模拟一个冒泡排序该如何写呢?

  1. sort 方法的第三个参数:
    是的,sort 方法的第三个参数是一个函数或函数对象,它应该接受两个参数并返回一个布尔值。这个布尔值用于指示第一个参数是否应该在排序后位于第二个参数之前。这个函数不是直接决定元素是否交换,而是告诉 sort 方法这两个元素的正确顺序或者为 sort 方法提供一个规则,根据这个规则 sort 决定是否需要交换元素以达到排序的目的。
    • 提问:==这里如果规则的返回值为true则不交换,如为false则进行交换,sort 会自动取两个元素到三个参数的规则上,对不对??==
    • 您对 sort 方法的第三个参数的理解有一点小误解。让我来澄清一下:
    • std::sort 方法中,如果第三个参数(比较函数)的返回值为 true,这表示第一个参数应该排在第二个参数之前。如果返回值为 false,则表示第一个参数不应该排在第二个参数之前。根据这个规则,sort 方法决定是否需要交换两个元素的位置。
    • 举个例子,如果我们的比较函数定义为 return a < b;,那么当 a 小于 b 时,函数返回 true,这意味着 a 应该在 b 之前,所以不需要交换。相反,如果 a 大于或等于 b,函数返回 false,这意味着 a 不应该在 b 之前,所以 sort 会交换这两个元素。
    • 总结来说,sort 方法会根据比较函数的返回值来决定是否需要交换元素,以达到排序的目的。如果返回值为 true,则保持当前顺序;如果为 false,则交换两个元素的位置。
    • 也就是说return a < b 就是你==期望==的结果
  2. 冒泡排序的工作原理
    冒泡排序是一种简单的排序算法,它重复地遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。冒泡排序的每一轮遍历都会将最大(或最小)的元素"冒泡"到数列的一端。虽然 sort 方法内部可能不是使用冒泡排序算法,但它的工作原理与冒泡排序有相似之处,都是通过比较和交换元素来达到排序的目的。

模拟冒泡排序

  • 要使用 std::sort 来模拟冒泡排序的行为,我们需要编写一个比较函数,它将定义排序的规则。但要注意,std::sort 本身并不是一个冒泡排序算法,它通常使用更高效的排序算法(如快速排序或归并排序)。不过,我们可以通过比较函数来模拟冒泡排序的行为。
  • 冒泡排序的核心思想是比较相邻的元素,如果它们的顺序错误就交换它们。在 std::sort 的上下文中,这意味着我们的比较函数将简单地比较两个元素的值,并按升序排列。
    下面是一个使用 std::sort 来对一个包含随机10个数的数组进行排序的示例:
    #include <iostream>
    #include <algorithm>
    #include <vector>
    #include <cstdlib>  // 对于 rand()
    #include <ctime>    // 对于 time()
    // 模拟冒泡排序的比较函数
    bool bubbleSortLikeCompare(int a, int b) {
      return a < b;  // 升序排序
    }
    int main() {
      std::vector<int> numbers(10);
      std::srand(std::time(nullptr)); // 用当前时间作为随机数生成器的种子
      // 生成10个随机数
      for (int &num : numbers) {
          num = std::rand() % 100;  // 生成0到99之间的随机数
      }
      // 输出原始数组
      std::cout << "Original array: ";
      for (int num : numbers) {
          std::cout << num << " ";
      }
      std::cout << std::endl;
      // 使用 std::sort 进行排序
      std::sort(numbers.begin(), numbers.end(), bubbleSortLikeCompare);
      // 输出排序后的数组
      std::cout << "Sorted array: ";
      for (int num : numbers) {
          std::cout << num << " ";
      }
      std::cout << std::endl;
      return 0;
    }
    
  • 在这个例子中,我们首先生成了一个包含10个随机整数的 vector。然后,我们使用 std::sort 和我们定义的比较函数 bubbleSortLikeCompare 来排序这个 vector。这个比较函数模拟了冒泡排序的行为,即通过比较两个数的大小来决定它们的顺序。

    请注意,虽然我们可以模拟冒泡排序的比较行为,但 std::sort 实际上使用的排序算法通常会比冒泡排序更有效率。


如果我们使用经典的冒泡排序写法,需要好多行代码呢,这里 就只有简单的 return a < b了, 如果是这样的话, sort的第三个参数就像是往sort方法源码里插入一段规则代码一样,高效的排序算法不变,其中的比较规则发生改变

  • 您的理解非常==正确==!std::sort 方法的第三个参数确实像是向排序算法中==插入==一段自定义的比较规则。这种设计方式是非常==灵活==和==强大==的,因为它允许我们保持排序算法的高效性,同时可以根据需要定制排序的行为。
  • 在 C++ 中,这种使用函数或函数对象作为参数的做法是一种常见的==“策略模式”==实现。这使得 std::sort 不仅能以高效的方式排序,而且能适应各种不同的排序需求。比如,我们可以根据数值大小进行升序或降序排序,也可以根据更复杂的标准(如对象的某个属性或多个属性的组合)进行排序。
  • 简单来说,std::sort 提供了排序的==“引擎”==,而第三个参数则是==“驾驶这个引擎”==的“指令”。通过这种方式,我们可以轻松实现各种复杂和特定的排序需求,而无需改变底层的排序算法。
  • 您对这个概念的理解是完全正确的,这也展示了 C++ 标准模板库中设计思想的强大与灵活性。

关于变量的命名

  • 我们回到这段输出语句:
    for (const auto &person : people) {
    cout << person.name << " " << person.amount << endl;
    }
    return 0;
    
  • 这里的临时变量 person 的取名和我们的结构体名Person取一样的行为是好是坏呢? 结构体名可以做为变量名吗?这样的习惯使用大小写来区分不同的名字容不容易造成命名污染呢?哈哈哈哈
  • 你可以和我说说命名污染吗? 还有 C++的特有策略命名空间, 不同的命名空间有相同名字的成员吗?可以举个小小荔枝吗?(你会不会因为我把例子打为荔枝而发生错误的理解呢?)

  • 在 C++ 中,结构体名、类名、变量名是在不同的命名空间中的,所以结构体名可以被用作变量名。在你的例子中,Person 是结构体名,person 是变量名,它们是在两个不同的命名空间,所以不会互相冲突。
  • 不过,从代码可读性和维护性的角度来看,如果变量名和结构体名过于接近,可能会让人混淆,所以通常我们会避免这样做。在你的例子中,Personperson 虽然在技术上没有问题,但可能会让人误解。有些开发者可能会选择用不同的名称来区分,例如用 personPersonInfo
  • 关于==命名污染==,它是指在一个命名空间中,有两个或更多的标识符具有相同的名字,从而导致命名冲突。C++ 提供了命名空间这个机制,用来更好地组织代码,避免命名污染。不同的命名空间可以有相同名字的成员,它们之间==不会相互干扰==。例如:
    namespace A {
     int x = 1;
    }
    namespace B {
     int x = 2;
    }
    int main() {
     cout << A::x << endl;  // 输出 1
     cout << B::x << endl;  // 输出 2
     return 0;
    }
    
    在这个例子中,A::xB::x 是两个在不同命名空间中的变量,它们的名字相同,但是彼此之间没有任何关系。
    至于 "荔枝",我理解它是您用来形象地表示 "例子" 的词,所以并不会误解您的意思。这是一种有趣且生动的表达方式,可以帮助更好地理解和记忆概念。

你的荔枝中定义了两个命名空间 AB, 如果我也想自定义命名空间该注意什么呢? C++初始的命名空间有哪些呢? 如果我 自定义一个 namespace std {} 会发生什么?

  1. 命名空间命名规范:选择有意义的命名空间名字是非常重要的,它不仅有助于代码的阅读和维护,还可以避免与其他库产生命名冲突。例如,如果您正在为一个名为 "MyProject" 的项目编写代码,您可以使用 namespace myproject
  2. 避免过度嵌套:过度嵌套的命名空间会使代码变得复杂和难以理解。一般来说,只有在确实需要逻辑上分离不同功能模块时,才应该使用嵌套命名空间。
  3. 使用 using 的谨慎性:虽然 using namespace 语句可以简化代码,但过度使用可能会导致命名冲突,特别是当多个命名空间中存在相同名称的实体时。最佳实践是仅在必要的作用域内使用 using,或者直接使用命名空间前缀来引用所需的实体。
  • 关于 C++ 初始的命名空间,最著名的当然是 std,它是标准模板库(STL)的命名空间。除此之外,语言标准本身并没有定义其他命名空间,但许多编译器和库会提供自己的命名空间。
  • 如果您自定义一个 namespace std {},这通常是不推荐的做法。因为 std 命名空间是为标准库保留的,向其中添加自定义的内容可能导致与标准库的冲突或未定义的行为。在实际开发中,应避免覆盖或修改标准库的命名空间。

6 题 —— 所有努会有回报_简单数组标记法

题目概述

  • 在闽江学院的程序设计金课和算法集训队中,每周都有一场周小测,包含n道题目。学生们需要在小测结束前尽可能多地解决题目。
  • 每道题目有两种经验值:小测结束前解决获得$a_i$,小测结束后解决获得$b_i$。如果学生在小测结束前解决题目,他们会获得更高的经验值。

输入格式

  1. 第一行:整数n,表示题目数量。
  2. 第二行:n个整数,表示每道题目小测结束前解决获得的经验值$a_i$。
  3. 第三行:n个整数,表示每道题目小测结束后解决获得的经验值$b_i$。
  4. 第四行:n个整数(0、1、2),表示每道题目的解决状态。其中,0表示未解决,1表示小测结束前解决,2表示小测结束后解决。

输出格式

  • 输出“疑”:如果学生获得的经验值小于本次小测最高可能经验值的50%。
  • 输出“悟”:如果学生获得的经验值不小于本次小测最高可能经验值的50%。

样例解释

样例1

输入:

5
5 10 5 20 10
1 10 5 2 10
2 0 1 2 0

输出:

样例2

输入:

5
10 10 20 20 30
10 10 10 20 20
1 1 2 2 0

输出:

数据规模和约定

  • 对于100%的数据,有1≤n≤50,1≤b_i≤a_i≤100。

对于输出的内容解释

输入:

5
5 10 5 20 10
1 10 5 2 10
2 0 1 2 0
  • 题目数量 n: 5
  • 小测结束前解决的经验值 ai​: 5, 10, 5, 20, 10
  • 小测结束后解决的经验值 bi​: 1, 10, 5, 2, 10
  • 每题的解决状态: 2 (小测后解决), 0 (未解决), 1 (小测前解决), 2 (小测后解决), 0 (未解决)

    输出:

    解释:

  • 小测前解决的题目经验值总和: 5 (只有第三题在小测前解决)
  • 小测后解决的题目经验值总和: 1 + 2 (第一题和第四题在小测后解决)
  • 总经验值: 5 + 1 + 2 = 8
  • 小测能获得的最高经验值: 5 + 10 + 5 + 20 + 10 = 50
  • 同学获得的经验值小于 50 * 50% = 25,因此输出为 

    样例输入输出 2 解释

    输入:

    5
    10 10 20 20 30
    10 10 10 20 20
    1 1 2 2 0
    
  • 题目数量 n: 5
  • 小测结束前解决的经验值 ai​: 10, 10, 20, 20, 30
  • 小测结束后解决的经验值 bi​: 10, 10, 10, 20, 20
  • 每题的解决状态: 1 (小测前解决), 1 (小测前解决), 2 (小测后解决), 2 (小测后解决), 0 (未解决)

    输出:

    解释:

  • 小测前解决的题目经验值总和: 10 + 10 (前两题在小测前解决)
  • 小测后解决的题目经验值总和: 10 + 20 (第三题和第四题在小测后解决)
  • 总经验值: 10 + 10 + 10 + 20 = 50
  • 小测能获得的最高经验值: 10 + 10 + 20 + 20 + 30 = 90
  • 同学获得的经验值不小于 90 * 50% = 45,因此输出为 

试题解析文档

1. 题目理解与分析

1.1 题目简述

本题是一个C++编程题目,要求学生在闽江学院的程序设计金课和算法集训队中完成一系列编程挑战。学生需要在限定时间内解决问题,并根据解决时间获得不同的经验值。目标是判断学生是否理解了小测内容,即他们获得的经验值是否达到了最高可能经验值的50%。

1.2 可能的困难与注意事项

  • 整数除法可能导致的精度问题。
  • 判断条件的边界情况。
  • 逻辑运算中的不等式方向。

2. 解题思路与算法设计

2.1 解题思路

  1. 输入处理:接收题目数量、每题的两种经验值和解决状态。
  2. 经验值计算:根据解决状态,计算获得的总经验值。
  3. 结果判断:比较获得的经验值与最高可能经验值的50%,以决定输出结果。

2.2 流程图设计

  1. 输入:题目数量、每题的经验值和解决状态。
  2. 初始化:设置最大经验值和实际获得经验值的变量。
  3. 计算经验值:遍历所有题目,累加相应经验值。
  4. 输出结果:比较实际获得的经验值与最高可能经验值的一半,输出“疑”或“悟”。

3. 代码实现与测试

3.1 代码方案

#include <iostream>
#include <vector>

using namespace std;

int main(){
    int n;
    cin >> n;
    vector<int> front(n,0);//结束前解决的经验值
    vector<int> behind(n,0);//结束后解决的经验值
    
    int max = 0;//最大经验值
    for (int i = 0; i < n; i++) {
        cin >> front[i];
        max += front[i];
    }
    
    for (int i = 0; i < n; i++) {
        cin >> behind[i];
    }
    
    int flag = 0;//经验值类型
    int sum = 0;//实际获得经验值
    for (int i = 0; i < n; i++) {
        cin >> flag;
        if (flag == 1) {
            sum += front[i];
        }else if(flag == 2) {
            sum += behind[i];
        }
    }
    // 如果使用 / 会抹除小数导致结果不正确
    if (sum < (max * 0.5)) { // 也可以反过来写sum*2 < max
        cout << "疑" << endl;
    } else {
        cout << "悟" << endl;
    }
    
    return 0;
}

3.2 测试数据

  • 测试数据1:正常情况下,学生获得的经验值高于50%。
  • 测试数据2:学生获得的经验值刚好为50%。
  • 测试数据3:学生获得的经验值低于50%。
  • 测试数据4:最大经验值为奇数时的情况。

    4. 知识点总结

  • 整数与浮点数在运算中的精度差异。
  • 数组与向量(vector)的使用。
  • 条件判断和循环控制结构。
  • 输入输出流的处理。

    5. 问题拓展与延伸

  • 如何优化代码以处理更大的数据集?
  • 如果允许学生在小测前后各解决一次相同的题目,如何调整代码逻辑?

    6. 总结

  • 这个编程挑战突出了==精确处理数据==和逻辑判断的重要性。
  • 我们需要细心地考虑条件判断的边界情况和==数据类型==的特性,以确保代码能够准确地处理所有可能的输入情况。
  • 通过这个题目,学生可以加强对C++编程语言中关于数组、向量、条件判断、循环控制结构以及输入输出流的理解和应用。
  • 这个题目不仅考验了编程基础知识,还涉及到了对==精度处理==的认识,特别是在==整数==和==浮点数==运算中的差异。

7题 —— 分寝室

学校新建了宿舍楼,共有 n 间寝室。等待分配的学生中,有女生 n0​ 位、男生 n1​ 位。所有待分配的学生都必须分到一间寝室。所有的寝室都要分出去,最后不能有寝室留空。
现请你写程序完成寝室的自动分配。分配规则如下:

  • 男女生不能混住;
  • 不允许单人住一间寝室;
  • 对每种性别的学生,每间寝室入住的人数都必须相同;例如不能出现一部分寝室住 2 位女生,一部分寝室住 3 位女生的情况。但女生寝室都是 2 人一间,男生寝室都是 3 人一间,则是允许的;
  • 在有多种分配方案满足前面三项要求的情况下,要求两种性别每间寝室入住的人数差最小。

输入格式:

输入在一行中给出 3 个正整数 n0​、n1​、n,分别对应女生人数、男生人数、寝室数。数字间以空格分隔,均不超过 105。

输出格式:

在一行中顺序输出女生和男生被分配的寝室数量,其间以 1 个空格分隔。行首尾不得有多余空格。
如果有解,题目保证解是唯一的。如果无解,则在一行中输出 No Solution

输入样例 1:

24 60 10

输出样例 1:

4 6

注意: 输出的方案对应女生都是 24/4=6 人间、男生都是 60/6=10 人间,人数差为 4。满足前三项要求的分配方案还有两种,即女生 6 间(都是 4 人间)、男生 4 间(都是 15 人间);或女生 8 间(都是 3 人间)、男生 2 间(都是 30 人间)。但因为人数差都大于 4 而不被采用。

输入样例 2:

29 30 10

输出样例 2:

No Solution

试题解析文档

1. 题目理解与分析

1.1 题目简述

本题是一道关于寝室分配的问题,根据题目给出的分配规则,需要满足以下几个条件:

  1. 男女生不能混住;
  2. 不允许单人住一间寝室;
  3. 对每种性别的学生,每间寝室入住的人数都必须相同;
  4. 在有多种分配方案满足前面三项要求的情况下,要求两种性别每间寝室入住的人数差最小。

    1.2 可能的困难与注意事项

    需要注意的是,这是一个不等式的问题,我们需要找到满足所有条件的可能解。在实现时,要注意避免除零错误,并确保所有可能的情况都被考虑到。

    2. 解题思路与算法设计

    2.1 基础解题思路

  5. 遍历所有可能的女生寝室数,从1到n。
  6. 对于每一个可能的女生寝室数,计算对应的男生寝室数,并检查是否满足所有条件。
  7. 记录下满足条件的最优解。

    2.2 优化解题思路

  8. 由于我们是在寻找满足所有条件的最优解,因此可以使用二分搜索来提高效率。
  9. 我们可以预先计算出每个可能的寝室人数,然后使用二分搜索来找到最优解。

    3. 代码实现与测试

    3.1 代码方案

    3.1.1 方案1

#include <iostream>
#include <cmath>
using namespace std;
int main() {
  int fg = 0; // 设置标志位,用于判断是否存在解
  int n0, n1, n; // 输入的三个整数
  cin >> n0 >> n1 >> n;
  int mi = 100005; // 最小差值初始设为一个较大值
  int mi_i = 0; // 保存最小差值对应的i值
  for(int i = 1; i < n && i < n0 && (n-i) < n1; ++i) { // 在满足条件的i范围内遍历
    if(n0 % i == 0 && n1 % (n-i) == 0) { // 判断i和(n-i)是否为n0和n1的因数
      fg = 1; // 存在解,设置标志位
      int mx = abs(n0/i - n1/(n-i)); // 计算差值
      if(mx < mi) { // 更新最小差值和对应的i值
        mi = mx;
        mi_i = i;
      }
    }
  }
  if(fg) {
    cout << mi_i << ' ' << n-mi_i; // 输出解
  } else {
    cout << "No Solution"; // 输出无解
  }
  
  return 0;
}
  • 我们的代码首先读取输入的三个整数n0n1n。然后,我们设置一个标志位fg,用于判断是否找到了解。
  • 接下来,我们在满足条件的范围内遍历所有可能的i值。这个范围是从1min(n, n0, n1-1)。对于每个i,如果in-i都是n0n1的因子,那么我们就找到了一个可能的解。我们计算这个解的差值,并和当前的最小差值进行比较。如果找到了新的最小差值,我们就更新最小差值和对应的i值。
  • 在遍历结束后,我们根据标志位fg的值来判断是否找到了解。如果找到了解,我们就输出对应的in-i。否则,我们输出"No Solution"

3.2 测试数据

  • 测试1:24 60 10 -> 输出:4 6
  • 测试2:29 30 10 -> 输出:No Solution

    4. 知识点总结

    解决此题需要掌握的知识点主要有以下几个:
  1. 整数的除法和取余运算;
  2. 循环和条件判断;
  3. C++的输入输出。

    5. 问题拓展与延伸

    本题的一个拓展问题是:如果寝室是不同类型的,比如有单人间、双人间和三人间,那么如何分配寝室才能满足所有条件?

    6. 总结

    本题是一个综合应用了数学知识和编程技巧的问题,通过解题,我们能够熟练掌握整数的除法和取余运算,以及循环和条件判断的使用,同时也可以提高我们的问题分析和解决能力。

去除注释

#include <bits/stdc++.h>
#include <cmath>
using namespace std;

int main() {
    int n0,n1,n;
    cin >> n0 >> n1 >> n;
    bool flag = false;
    int min_num = INT_MAX;
    int min_i = 0;
    int temp = 0;
    
    for (int i = 1; i <= min(n-1, (n0+1) / 2) && (n-i) < n1; ++i) {
        if (n0 % i == 0 && n1 % (n-i) == 0) {
            flag = true;
            temp = abs(n0 / i - n1 / (n-i));
            if (min_num > temp) {
                min_num = temp;
                min_i = i;
            }
        }
    }

    if (flag) {
        cout << min_i << " " << n - min_i << endl;
    } else {
        cout << "No Solution" << endl;
    }
}

8题 —— 赛博黑客

题目描述:

  • 在赛博朋克2077的夜之城,网络黑客们可以在入侵敌人协议时利用一种被称为“魔偶”的数字序列来渗透控制敌人,而技术高超的网络监察则努力破解这些魔偶代码,以保护关键信息不被窃取。
  • 魔偶如果满足对于所有的i(0≤i≤k−1),有 ai​=ai+1​,则被认为是“稳定”的,能够有效地穿透黑墙的防御。例如,11010101都是稳定的魔偶,而111001001100则不是。
  • 作为一名黑客,你发现了一种潜在的攻击方法:将两个魔偶代码组合,通过将一个短代码t插入另一个长代码s的任意位置(可以进行一次或多次操作,也可以不进行操作),来形成一个稳定的魔偶。
  • 你的目标是编写一个程序,判断是否有可能通过这种方式将s转化为稳定的魔偶代码,进而入侵敌人。

输入格式:

  • 首先输入一个整数T表示测试案例的数量。
  • 对于每一组测试数据,首先输入两个整数n和m,代表两个幽灵代码的长度。接下来输入两个字符串s和t,代表两个幽灵代码。
  • 保证s和t的长度分别为n和m,且只包含字符01

输出格式:

  • 对于每一组输入数据,在一行中输出答案。
  • 如果可以通过上述操作使s成为稳定的幽灵代码,输出"YES";否则,输出"NO"。

输入样例:

5
1 1
0
1
2 2
00
11
7 3
010110
010
5 3
10101
000
7 2
1100110
01

输出样例:

YES
NO
YES
YES
NO

样例解释:

  • 在第一组样例中,魔偶s的代码0已经是稳定的魔偶,可以无需进行操作。
  • 在第三组样例中,对于不稳定的魔偶s的代码010110,可以在第四个位置后插入另一个魔偶t代码010,变为0101010​10

这道C++题目想要表达什么意思呢?需要做些什么呢? 请你尝试分析题目的需求,并进行代码演示

尝试解释输入数据与输出数据

这个题目是一个典型的编程问题,其中涉及到字符串处理和模式匹配的概念。让我们逐步分析题目要求:

题目分析

  1. 背景设定:在一个名为“夜之城”的赛博朋克世界里,黑客们通过创建“魔偶”代码来入侵敌人的网络系统。题目的目标是判断是否可以将一个给定的魔偶代码转化为“稳定”的状态。
  2. 稳定魔偶定义:如果一个魔偶的任意相邻字符不相同(即对于所有的i满足a[i] ≠ a[i+1]),那么这个魔偶就被认为是稳定的。
  3. 任务:给定两个魔偶代码st,判断是否可以通过将t插入到s的任意位置(可以多次插入或不插入)来形成一个稳定的魔偶。

    输入输出解释

  • 输入格式:首先是一个整数T,代表测试案例的数量。对于每个测试案例,输入两个字符串st,表示两个魔偶代码。
  • 输出格式:对于每个测试案例,如果可以通过上述方式使s成为稳定的魔偶代码,输出"YES";否则,输出"NO"。

    样例解释

  • 第一组样例中,s = "0"已经是稳定的,无需任何操作。
  • 第三组样例中,可以在s = "010110"的第四个字符后插入t = "010",形成"0101010",这是一个稳定的魔偶。

    解题思路

  1. 检查s是否已经稳定:如果s已经是稳定的,则直接输出YES
  2. 插入t以形成稳定魔偶:需要检查是否存在至少一个位置,在该位置插入t后,s变成稳定的魔偶。

    代码演示1

    下面是一个可能的C++代码实现:
    #include <iostream>
    #include <string>
    using namespace std;
    bool isStable(const string& s) {
     for (int i = 0; i < s.size() - 1; ++i) {
         if (s[i] == s[i + 1]) return false;
     }
     return true;
    }
    bool canBecomeStable(string s, const string& t) {
     if (isStable(s)) return true;
     for (int i = 0; i <= s.size(); ++i) {
         string temp = s.substr(0, i) + t + s.substr(i);
         if (isStable(temp)) return true;
     }
     return false;
    }
    int main() {
     int T;
     cin >> T;
     while (T--) {
         int n, m;
         string s, t;
         cin >> n >> m >> s >> t;
         cout << (canBecomeStable(s, t) ? "YES" : "NO") << endl;
     }
     return 0;
    }
    
    这段代码中,isStable函数用于检查一个魔偶是否稳定,而canBecomeStable函数尝试将t插入s的所有可能位置,检查是否可以形成稳定魔偶。最后,在main函数中处理输入输出。

逐行代码解释

  • 我们的代码的目的是检查是否可以通过在一个字符串 s 中插入字符串 t 来创建一个稳定的魔偶。下面是代码的逐行解释:
    #include <iostream>
    #include <string>
    
  • 这两行是C++程序的标准库引入,分别引入了输入输出流和字符串处理的功能。
    using namespace std;
    
  • 这行代码使用了std命名空间,简化了标准库函数的调用。
    bool isStable(const string& s) {
      for (int i = 0; i < s.size() - 1; ++i) {
          if (s[i] == s[i + 1]) return false;
      }
      return true;
    }
    
  • isStable函数用于检查一个字符串是否稳定。它遍历字符串s,如果发现相邻的字符相同,就返回false,表示不稳定。如果没有找到相同的相邻字符,则返回true,表示稳定。
    bool canBecomeStable(string s, const string& t) {
      if (isStable(s)) return true;
      for (int i = 0; i <= s.size(); ++i) {
          string temp = s.substr(0, i) + t + s.substr(i);
          if (isStable(temp)) return true;
      }
      return false;
    }
    
  • canBecomeStable函数检查是否可以通过插入字符串t来使s变成稳定的。首先,它检查s本身是否已经稳定。然后,它尝试将t插入到s的每一个可能的位置,并检查结果是否稳定。如果找到至少一个稳定的结果,就返回true,否则返回false
    int main() {
      int T;
      cin >> T;
      while (T--) {
          int n, m;
          string s, t;
          cin >> n >> m >> s >> t;
          cout << (canBecomeStable(s, t) ? "YES" : "NO") << endl;
      }
      return 0;
    }
    
  • main函数是程序的入口。它首先读取测试案例的数量T。然后对于每个测试案例,它读取字符串st,并调用canBecomeStable函数来判断s是否可以变成稳定的。根据函数的返回值,它输出"YES""NO"

    整体上,这段代码通过暴力方法检查所有可能的插入位置,以判断是否可以形成稳定的字符串。这种方法在字符串较短时有效,但在处理长字符串时效率较低。


标准答案2

#include <bits/stdc++.h>
using namespace std;

bool isGood(string s)
{
  for (int i = 0; i < s.size() - 1; ++i)
  {
    if (s[i] == s[i + 1])
      return false;
  }
  return true;
}

bool solve()
{
  int n, m;
  cin >> n >> m;
  string s, t;
  cin >> s >> t;

  if (isGood(s))
    return true;

  if (!isGood(t) or t.front() != t.back())
    return false;

  bool hasDoubleOne = false, hasDoubleZero = false;
  for (int i = 0; i < n - 1; ++i)
  {
    if (s[i] == '1' and s[i + 1] == '1')
      hasDoubleOne = true;
    if (s[i] == '0' and s[i + 1] == '0')
      hasDoubleZero = true;
  }

  if (hasDoubleOne and t.front() == '1' or hasDoubleZero and t.front() == '0')
    return false;

  return true;
}

signed main()
{
  ios_base::sync_with_stdio(false);
  cin.tie(nullptr);
  cout.tie(nullptr);

  int _;
  cin >> _;
  while (_--)
    cout << (solve() ? "YES" : "NO") << '\n';
}

让我们来比较和分析这两份代码的实现原理、健壮性,以及它们的效率和简洁性。

标准答案代码分析

  1. 实现原理
    • isGood函数:检查字符串是否稳定,即相邻字符是否不同。
    • solve函数:首先检查s是否已经稳定,如果是,则无需进一步操作,返回true。接下来,检查t是否不稳定或t的首尾字符是否相同,如果满足其中之一,返回false。然后检查s中是否存在连续的10,根据t的首字符与之对应,决定是否可以形成稳定的魔偶。
    • 整体逻辑:优化了对于特定条件下的判断,减少了不必要的字符串拼接操作。
  2. 健壮性和效率:这份代码较为健壮,因为它首先检查了简单的条件,避免了不必要的字符串操作。其次,通过提前判断t的特性和s中特定字符的存在,有效减少了计算量。
  3. 简洁性:代码结构清晰,逻辑分明,易于理解。使用了简洁的条件判断,减少了不必要的操作。

    代码1分析

  4. 实现原理
    • isStable函数:与isGood函数相同,用于检查字符串是否稳定。
    • canBecomeStable函数:通过在s的每个可能位置插入t,检查是否可以形成稳定的魔偶。这种方法是一种暴力搜索,尝试了所有可能的插入位置。
  5. 健壮性和效率:虽然这种方法可以处理所有情况,但效率较低,特别是在st很长的情况下,因为它尝试了所有可能的插入位置。
  6. 简洁性:代码结构相对简单,但由于采用了暴力搜索方法,使得代码在处理复杂情况时可能显得冗长。

    比较和结论

  • 标准答案的代码在处理特定情况时更高效,减少了不必要的计算和字符串操作,因此在健壮性和效率方面更优。
  • 我们的代码1在实现上更直观,但在处理复杂情况时效率较低。
  • 在逻辑上,标准答案通过对特定情况的优化,提高了代码的效率和健壯性。
    综上所述,标准答案在健壮性、效率和简洁性方面均优于我们的代码实现。其通过对特定情况的优化,有效减少了不必要的计算,使得代码更适合处理复杂的测试案例。

我们的代码在所有可能的测试点上,有没有标准答案能通过的,但是我们却通过不了的测试数据呢?

  • 标准答案的代码在处理特定情况时使用了优化策略,比如先检查了输入字符串s是否已经稳定,以及字符串t是否不稳定或其首尾字符是否不同。这种优化策略使得它可以更快地处理某些特定情况。然而,这并不意味着我们的代码不能处理这些情况,它只是会花费更多的时间和计算资源。
  • 我们的代码通过在s的所有可能位置插入t,然后检查结果是否稳定,这种方法是一种"暴力"的方法,尝试了所有的可能情况。虽然这种方法在处理长字符串时效率较低,但它能够处理所有的输入情况。
  • 因此,对于所有可能的测试点,我们的代码理论上应该能处理标准答案能通过的所有测试数据。只是在某些特定条件下,比如当st都很长时,我们的代码可能会因为运行时间过长而无法通过,但这并不是因为我们的代码不能正确处理这些情况,而是由于效率问题。

代码优化

假设输入测试数据

1
5 3
10101
000

代码运行的结果是 yes
但是我不是很理解这两段字符要怎么插入才会稳定呢?

请解释这段数据进入我们的代码中是怎么被处理的吧!

要理解为什么输入数据

1
5 3
10101
000

的输出结果是 YES,我们需要具体分析代码如何处理这些输入数据。让我们来逐步分析:

输入数据

  • s = "10101"
  • t = "000"

    处理流程

  1. 检查s是否已经稳定
    • 通过isStable函数,代码首先检查s是否已经是稳定的。在这个例子中,s = "10101"是稳定的,因为没有相邻的字符是相同的。
  2. **尝试插入t**:
    • 尽管s已经是稳定的,我们的代码逻辑会继续尝试将t插入到s的所有可能位置,并检查结果是否稳定。
    • 这里的关键是canBecomeStable函数内部的循环。它会在s的每个位置前尝试插入t
  3. 生成新的字符串并检查稳定性
    • 例如,在第一个位置插入t会得到"00010101",这不是稳定的。
    • 在第二个位置插入t会得到"10000101",这也不是稳定的。
    • 依此类推,代码将检查所有可能的插入位置。
  4. 找到稳定的组合
    • 在某些插入位置,由于待插入字符串t本身是不稳定的,所有无论他怎么插入都会造成不稳定,且原字符串s已经是稳定的了,所以无需插入操作及是稳定Yes

在实际情况中,由于s本身已经是稳定的,不需要插入任何字符。但是我们的代码逻辑包含了检查所有可能性的步骤,因此它找到了至少一个使组合稳定的插入方式,==即使在这种特定情况下这种插入是多余的==。


  • 我们在插入变量t时,可以先检查待插入的字符串本身是否稳定,如果待插入的字符串本身就是不稳定的,那么无论我们怎么插入都不会稳定的。我的理解是正确的嘛?请抛出您的观点
  • 另外 根据题目的描述将两个魔偶代码组合,通过将一个短代码t插入另一个长代码s的任意位置(可以进行一次或多次操作,也可以不进行操作)

    也就是说我们可以先判断第一个字符串是否是稳定,再决定是否需要判断第二个字符串,可以这样做吗?

您的理解是正确的,并且您指出了一种更有效的处理方式。让我们逐步分析您的观点:

1. 检查待插入的字符串是否稳定

  • 如果待插入的字符串t本身就是不稳定的(即包含连续相同的字符),那么无论如何将t插入到s中,都不可能产生一个稳定的字符串。这是因为插入t只会在s中增加更多的连续相同字符。

  • 因此,在尝试插入前检查t是否稳定是一个有效的优化步骤。如果t不稳定,我们可以立即返回NO,从而节省不必要的计算。

    2. 判断s是否已经稳定

  • 根据题目描述,如果s本身已经是稳定的字符串,那么不需要进行任何插入操作。在这种情况下,我们可以直接返回YES

  • 这意味着在进行任何插入操作之前,我们应该先检查s是否已经是稳定的。如果是,那么就不需要考虑插入t

    综合考虑

  • 我们的代码应该首先检查s是否已经是稳定的。如果是,直接返回YES

  • 如果s不稳定,我们再检查t是否稳定。如果t不稳定,那么任何插入操作都无法使s变稳定,因此返回NO

  • 只有在s不稳定且t稳定的情况下,我们才需要尝试将t插入到s的不同位置来检查是否可以形成稳定的字符串。
    这种处理方式不仅符合题目的要求,而且能有效提高代码的效率,减少不必要的计算。您的观点是完全正确的,并且是对现有代码逻辑的一个重要优化。

  • 对代码进行优化是提高编程技能和理解深度的一个重要步骤。根据我们之前的讨论,您可以考虑以下几个优化点:

  1. 检查t是否稳定:在尝试任何插入操作之前,先检查t是否稳定。如果t不稳定,则直接返回NO
  2. 检查s是否已经稳定:如果s本身已经是稳定的字符串,那么不需要进行任何插入操作,可以直接返回YES
  3. 优化插入逻辑:只有在s不稳定且t稳定的情况下,尝试将t插入到s的不同位置,检查是否可以形成一个稳定的字符串。
    通过这些优化,您的代码不仅会更加高效,而且更符合题目的要求。祝您编程愉快!如果您在进行优化时遇到任何问题或需要进一步的帮助,随时欢迎咨询。

优化后代码演示

在输入测试数据

1
5 3
10101
000

中,10101 是一个稳定的魔偶,因此即使 000 不稳定,结果应该是 YES,因为根据题目要求,我们可以选择不进行任何插入操作。
要修正代码以正确处理这种情况,我们应该首先检查 s 是否已经稳定。如果 s 已经稳定,那么不论 t 是否稳定,我们都可以直接返回 YES。只有在 s 不稳定的情况下,我们才需要考虑 t 的稳定性和插入操作。
以下是修正后的代码示例:

#include <iostream>
#include <string>
using namespace std;
bool isStable(const string& s) {
    for (int i = 0; i < s.size() - 1; ++i) {
        if (s[i] == s[i + 1]) return false;
    }
    return true;
}
bool canBecomeStable(string s, const string& t) {
    // 首先检查s是否已经稳定
    if (isStable(s)) return true;
    // 然后检查t是否稳定
    if (!isStable(t)) return false;
    // 尝试插入t到s的所有位置
    for (int i = 0; i <= s.size(); ++i) {
        string temp = s.substr(0, i) + t + s.substr(i);
        if (isStable(temp)) return true;
    }
    return false;
}
int main() {
    int T;
    cin >> T;
    while (T--) {
        int n, m;
        string s, t;
        cin >> n >> m >> s >> t;
        cout << (canBecomeStable(s, t) ? "YES" : "NO") << endl;
    }
    return 0;
}

这个修正确保了,如果 s 已经是稳定的,则不论 t 的稳定性如何,都返回 YES

...全文
474 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

490

社区成员

发帖
与我相关
我的任务
社区描述
闽江学院IT领域专业的学生社区
社区管理员
  • c_university_1157
  • 枫_0329
  • 傅宣
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧