讲义
课程
作业
真题
竞赛
课时
模板
  • 2023天津CSP-J组 第一轮 成绩
  • 2023天津CSP-S组 第一轮 成绩
  • 2023天津CSP第二轮晋级名单(J&S)
  • 2023竞赛大纲
  • 2023机试环境(禁止使用手机下载,镜像文件过大)
课程
作业
真题
竞赛
课时
模板
  • 2023天津CSP-J组 第一轮 成绩
  • 2023天津CSP-S组 第一轮 成绩
  • 2023天津CSP第二轮晋级名单(J&S)
  • 2023竞赛大纲
  • 2023机试环境(禁止使用手机下载,镜像文件过大)

2021 csp-j 复赛真题

2024/10/13

[CSP-J 2021] 分糖果

题目背景

红太阳幼儿园的小朋友们开始分糖果啦!

题目描述

红太阳幼儿园有 nnn 个小朋友,你是其中之一。保证 n≥2n \ge 2n≥2。

有一天你在幼儿园的后花园里发现无穷多颗糖果,你打算拿一些糖果回去分给幼儿园的小朋友们。

由于你只是个平平无奇的幼儿园小朋友,所以你的体力有限,至多只能拿 RRR 块糖回去。

但是拿的太少不够分的,所以你至少要拿 LLL 块糖回去。保证 n≤L≤Rn \le L \le Rn≤L≤R。

也就是说,如果你拿了 kkk 块糖,那么你需要保证 L≤k≤RL \le k \le RL≤k≤R。

如果你拿了 kkk 块糖,你将把这 kkk 块糖放到篮子里,并要求大家按照如下方案分糖果:只要篮子里有不少于 nnn 块糖果,幼儿园的所有 nnn 个小朋友(包括你自己)都从篮子中拿走恰好一块糖,直到篮子里的糖数量少于 nnn 块。此时篮子里剩余的糖果均归你所有——这些糖果是作为你搬糖果的奖励。

作为幼儿园高质量小朋友,你希望让作为你搬糖果的奖励的糖果数量(而不是你最后获得的总糖果数量!)尽可能多;因此你需要写一个程序,依次输入 n,L,Rn, L, Rn,L,R,并输出你最多能获得多少作为你搬糖果的奖励的糖果数量。

输入格式

输入一行,包含三个正整数 n,L,Rn, L, Rn,L,R,分别表示小朋友的个数、糖果数量的下界和上界。

输出格式

输出一行一个整数,表示你最多能获得的作为你搬糖果的奖励的糖果数量。

样例 #1

样例输入 #1

7 16 23

样例输出 #1

6

样例 #2

样例输入 #2

10 14 18

样例输出 #2

8

样例 #3

样例输入 #3

见附件中的 candy/candy3.in。

样例输出 #3

见附件中的 candy/candy3.ans。

提示

【样例解释 #1】

拿 k=20k = 20k=20 块糖放入篮子里。

篮子里现在糖果数 20≥n=720 \ge n = 720≥n=7,因此所有小朋友获得一块糖;

篮子里现在糖果数变成 13≥n=713 \ge n = 713≥n=7,因此所有小朋友获得一块糖;

篮子里现在糖果数变成 6<n=76 < n = 76<n=7,因此这 666 块糖是作为你搬糖果的奖励。

容易发现,你获得的作为你搬糖果的奖励的糖果数量不可能超过 666 块(不然,篮子里的糖果数量最后仍然不少于 nnn,需要继续每个小朋友拿一块),因此答案是 666。

【样例解释 #2】

容易发现,当你拿的糖数量 kkk 满足 14=L≤k≤R=1814 = L \le k \le R = 1814=L≤k≤R=18 时,所有小朋友获得一块糖后,剩下的 k−10k - 10k−10 块糖总是作为你搬糖果的奖励的糖果数量,因此拿 k=18k = 18k=18 块是最优解,答案是 888。

【数据范围】

测试点n≤n \len≤R≤R \leR≤R−L≤R - L \leR−L≤
111222555555
222555101010101010
333103{10}^3103103{10}^3103103{10}^3103
444105{10}^5105105{10}^5105105{10}^5105
555103{10}^3103109{10}^9109000
666103{10}^3103109{10}^9109103{10}^3103
777105{10}^5105109{10}^9109105{10}^5105
888109{10}^9109109{10}^9109109{10}^9109
999109{10}^9109109{10}^9109109{10}^9109
101010109{10}^9109109{10}^9109109{10}^9109

对于所有数据,保证 2≤n≤L≤R≤1092 \le n \le L \le R \le {10}^92≤n≤L≤R≤109。

解析

题目描述

给你三个数 nnn、lll、rrr,让你在区间 ([l, r]) 中找到一个整数 xxx,使得 nmod  xn \mod xnmodx 最大,并输出这个最大的 nmod  xn \mod xnmodx 的值。

解题思路

70分思路:暴力枚举

我们可以通过暴力枚举的方式,遍历区间 ([l, r]) 中的每一个整数 xxx,并计算 nmod  xn \mod xnmodx 的值,找出其中的最大值。虽然这种方法能解决问题,但效率较低,尤其当区间 ([l, r]) 较大时,时间复杂度较高。

100分思路:利用取余运算的性质

为得到最优解,我们需要运用取余运算的两个简单性质:

  1. 对于任意正整数 xxx,nmod  xn \mod xnmodx 的结果总是在 ([0, n-1]) 的范围内。
  2. 若 xmod  n=yx \mod n = yxmodn=y,则 (x+n)mod  n=y(x + n) \mod n = y(x+n)modn=y,即加上 nnn 后,模 nnn 的结果不变。

基于这两个性质,我们可以更有效地求解此题。

解决方案

情况 1:当 r−l+1≥nr - l + 1 \geq nr−l+1≥n

如果区间的长度大于或等于 nnn,则区间 ([l, r]) 中的整数可以覆盖 ([0, n-1]) 之间的所有余数。因此,最大的 nmod  xn \mod xnmodx 值即为 n−1n - 1n−1。这种情况下,答案为 n−1n - 1n−1。

情况 2:当 r−l+1<nr - l + 1 < nr−l+1<n

如果区间的长度小于 nnn,则需要进一步分类讨论:

  1. 若 lmod  n≤rmod  nl \mod n \leq r \mod nlmodn≤rmodn:这种情况下,区间 ([l, r]) 中的整数 xxx 对 nnn 取余的结果将包含从 lmod  nl \mod nlmodn 到 rmod  nr \mod nrmodn 的所有余数。此时,最大的取余值为 rmod  nr \mod nrmodn。

  2. 若 lmod  n>rmod  nl \mod n > r \mod nlmodn>rmodn:在这种情况下,区间 ([l, r]) 中的取余结果跨越了模 nnn 的边界,余数的范围分为两段。此时,最大的取余值是 n−1n - 1n−1,因为这是模 nnn 取余的最大值。

总结

这道题的关键在于分类讨论。必须确保不重不漏地处理所有可能的情况,否则容易出错。

代码实现

#include <cstdio>
using namespace std;

int n, L, R;

int main() {
    freopen("candy.in", "r", stdin);  // 重定向输入文件
    freopen("candy.out", "w", stdout);  // 重定向输出文件
    
    scanf("%d%d%d", &n, &L, &R);  // 输入 n、L、R
    
    if (R - L + 1 >= n) {
        printf("%d\n", n - 1);  // 若区间长度大于等于 n,输出 n-1
    } else {
        if (L % n <= R % n) {
            printf("%d\n", R % n);  // 若 L mod n <= R mod n,输出 R mod n
        } else {
            printf("%d\n", n - 1);  // 若 L mod n > R mod n,输出 n-1
        }
    }
    
    return 0;
}

代码解析

  1. 输入输出重定向:通过 freopen 函数从文件中读取输入并将输出写入文件。
  2. 输入 n、L、R:表示取余的基数 nnn 以及区间的左边界 LLL 和右边界 RRR。
  3. 判断区间长度:
    • 若 R−L+1≥nR - L + 1 \geq nR−L+1≥n,则区间内可以包含所有 nnn 的余数,此时答案为 n−1n - 1n−1。
    • 若区间长度不足 nnn,根据 Lmod  nL \mod nLmodn 和 Rmod  nR \mod nRmodn 的关系进行进一步的分类讨论。
  4. 输出结果:根据条件输出最大的 nmod  xn \mod xnmodx 的值。

这道题主要考查对模运算性质的理解和分类讨论的能力,通过合理的推理可以大幅降低时间复杂度,避免暴力枚举的低效做法。

[CSP-J 2021] 插入排序

题目描述

插入排序是一种非常常见且简单的排序算法。小 Z 是一名大一的新生,今天 H 老师刚刚在上课的时候讲了插入排序算法。

假设比较两个元素的时间为 O(1)\mathcal O(1)O(1),则插入排序可以以 O(n2)\mathcal O(n^2)O(n2) 的时间复杂度完成长度为 nnn 的数组的排序。不妨假设这 nnn 个数字分别存储在 a1,a2,…,ana_1, a_2, \ldots, a_na1​,a2​,…,an​ 之中,则如下伪代码给出了插入排序算法的一种最简单的实现方式:

这下面是 C/C++ 的示范代码:

for (int i = 1; i <= n; i++)
	for (int j = i; j >= 2; j--)
		if (a[j] < a[j-1]) {
			int t = a[j-1];
			a[j-1] = a[j];
			a[j] = t;
		}

这下面是 Pascal 的示范代码:

for i:=1 to n do
	for j:=i downto 2 do
		if a[j]<a[j-1] then
			begin
				t:=a[i];
				a[i]:=a[j];
				a[j]:=t;
			end;

为了帮助小 Z 更好的理解插入排序,小 Z 的老师 H 老师留下了这么一道家庭作业:

H 老师给了一个长度为 nnn 的数组 aaa,数组下标从 111 开始,并且数组中的所有元素均为非负整数。小 Z 需要支持在数组 aaa 上的 QQQ 次操作,操作共两种,参数分别如下:

1 x v1~x~v1 x v:这是第一种操作,会将 aaa 的第 xxx 个元素,也就是 axa_xax​ 的值,修改为 vvv。保证 1≤x≤n1 \le x \le n1≤x≤n,1≤v≤1091 \le v \le 10^91≤v≤109。注意这种操作会改变数组的元素,修改得到的数组会被保留,也会影响后续的操作。

2 x2~x2 x:这是第二种操作,假设 H 老师按照上面的伪代码对 aaa 数组进行排序,你需要告诉 H 老师原来 aaa 的第 xxx 个元素,也就是 axa_xax​,在排序后的新数组所处的位置。保证 1≤x≤n1 \le x \le n1≤x≤n。注意这种操作不会改变数组的元素,排序后的数组不会被保留,也不会影响后续的操作。

H 老师不喜欢过多的修改,所以他保证类型 111 的操作次数不超过 500050005000。

小 Z 没有学过计算机竞赛,因此小 Z 并不会做这道题。他找到了你来帮助他解决这个问题。

输入格式

第一行,包含两个正整数 n,Qn, Qn,Q,表示数组长度和操作次数。

第二行,包含 nnn 个空格分隔的非负整数,其中第 iii 个非负整数表示 aia_iai​。

接下来 QQQ 行,每行 2∼32 \sim 32∼3 个正整数,表示一次操作,操作格式见【题目描述】。

输出格式

对于每一次类型为 222 的询问,输出一行一个正整数表示答案。

样例 #1

样例输入 #1

3 4
3 2 1
2 3
1 3 2
2 2
2 3

样例输出 #1

1
1
2

提示

【样例解释 #1】

在修改操作之前,假设 H 老师进行了一次插入排序,则原序列的三个元素在排序结束后所处的位置分别是 3,2,13, 2, 13,2,1。

在修改操作之后,假设 H 老师进行了一次插入排序,则原序列的三个元素在排序结束后所处的位置分别是 3,1,23, 1, 23,1,2。

注意虽然此时 a2=a3a_2 = a_3a2​=a3​,但是我们不能将其视为相同的元素。

【样例 #2】

见附件中的 sort/sort2.in 与 sort/sort2.ans。

该测试点数据范围同测试点 1∼21 \sim 21∼2。

【样例 #3】

见附件中的 sort/sort3.in 与 sort/sort3.ans。

该测试点数据范围同测试点 3∼73 \sim 73∼7。

【样例 #4】

见附件中的 sort/sort4.in 与 sort/sort4.ans。

该测试点数据范围同测试点 12∼1412 \sim 1412∼14。

【数据范围】

对于所有测试数据,满足 1≤n≤80001 \le n \le 80001≤n≤8000,1≤Q≤2×1051 \le Q \le 2 \times {10}^51≤Q≤2×105,1≤x≤n1 \le x \le n1≤x≤n,1≤v,ai≤1091 \le v,a_i \le 10^91≤v,ai​≤109。

对于所有测试数据,保证在所有 QQQ 次操作中,至多有 500050005000 次操作属于类型一。

各测试点的附加限制及分值如下表所示。

测试点n≤n \len≤Q≤Q \leQ≤特殊性质
1∼41 \sim 41∼4101010101010无
5∼95 \sim 95∼9300300300300300300无
10∼1310 \sim 1310∼13150015001500150015001500无
14∼1614 \sim 1614∼16800080008000800080008000保证所有输入的 ai,va_i,vai​,v 互不相同
17∼1917 \sim 1917∼19800080008000800080008000无
20∼2220 \sim 2220∼228000800080002×1052 \times 10^52×105保证所有输入的 ai,va_i,vai​,v 互不相同
23∼2523 \sim 2523∼258000800080002×1052 \times 10^52×105无

解析

题目简述

给定长度为 nnn 的序列 aia_iai​,需要维护两个操作:

  1. 单点修改:修改序列中某个位置的元素值;
  2. 查询排序后的某个元素的原始下标。

输入的约束为:

  • n≤8000n \leq 8000n≤8000,查询次数 q≤2×105q \leq 2 \times 10^5q≤2×105,其中修改操作不超过 5000 次。

解题思路

对于一个已经有序的数列,当我们对其中的一个元素进行单点修改时,可以通过局部的前后冒泡各一次来保持数列的有序性。

具体操作示例

假设原始序列为:

1,1,4,5,6,7 1, 1, 4, 5, 6, 7 1,1,4,5,6,7

将第 3 个元素修改为 9 后,得到序列:

1,1,9,5,6,7 1, 1, 9, 5, 6, 7 1,1,9,5,6,7

这时,我们可以通过从前往后进行一次冒泡操作,使序列重新保持有序。这样每次操作的时间复杂度是 O(n)O(n)O(n)。

维护有序序列和下标关系

我们可以维护一个有序序列,同时记录每个元素的原始下标和排序后的下标之间的关系。修改操作后,我们更新有序序列的同时,调整下标关系。

通过这种方式:

  • 修改操作的时间复杂度是 O(n)O(n)O(n);
  • 查询操作的时间复杂度是 O(1)O(1)O(1)。

这样可以高效解决问题。

代码实现

下面是实际的考场代码:

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXN=8005;  // 最大长度
int n, q;  // n 表示序列长度,q 表示查询次数
int t[MAXN];  // t[i] 表示元素 i 在排序后的下标
struct node {
    int pre, id;  // pre 表示元素值,id 表示元素的原始下标
} a[MAXN];  // 存储序列元素
// 比较函数,用于按值和下标对元素排序
bool cmp(node x, node y) {
    if (x.pre != y.pre) return x.pre < y.pre;
    return x.id < y.id;
}

int main() {
    // freopen("sort.in", "r", stdin);  // 输入重定向(可选)
    // freopen("sort.out", "w", stdout);  // 输出重定向(可选)
    scanf("%d%d", &n, &q);  // 读取序列长度 n 和查询次数 q
    
    for (int i = 1; i <= n; i++) {
        scanf("%d", &a[i].pre);  // 读取每个元素的值
        a[i].id = i;  // 记录元素的原始下标
    }
    // 初始排序
    sort(a + 1, a + n + 1, cmp);  // 根据值和下标排序
    // 更新每个元素的排序后下标
    for (int i = 1; i <= n; i++) {
        t[a[i].id] = i;
    }
    
    for (int i = 1; i <= q; i++) {
        int opt, x, v;
        scanf("%d", &opt);  // 读取操作类型
        if (opt == 1) {  // 修改操作
            scanf("%d%d", &x, &v);  // 修改第 x 个元素的值为 v
            a[t[x]].pre = v;  // 更新元素值
            // 从后往前进行冒泡操作,保持有序
            for (int j = n; j >= 2; j--)
                if (cmp(a[j], a[j-1])) {
                    node kkksc03 = a[j];
                    a[j] = a[j-1];
                    a[j-1] = kkksc03;
                }
            // 从前往后进行冒泡操作,保持有序
            for (int j = 2; j <= n; j++)
                if (cmp(a[j], a[j-1])) {
                    node kkksc03 = a[j];
                    a[j] = a[j-1];
                    a[j-1] = kkksc03;
                }
            // 更新下标关系
            for (int i = 1; i <= n; i++) {
                t[a[i].id] = i;
            }
        } else {  // 查询操作
            scanf("%d", &x);  // 查询第 x 个元素
            printf("%d\n", t[x]);  // 输出排序后该元素的下标
        }
    }
    return 0;
}

代码解析

  1. 输入输出:
    • scanf 用于读取序列长度、查询次数以及每次的修改或查询操作。
  2. 结构体 node:
    • pre:记录元素的值;
    • id:记录元素的原始下标,方便修改后重新映射排序后的下标。
  3. 排序与下标更新:
    • 使用 sort 函数对初始序列按照值和下标排序;
    • 维护一个数组 t,其中 t[i] 代表元素 i 在排序后的位置。
  4. 修改操作:
    • 修改某个元素后,通过从前向后、从后向前各进行一次冒泡操作来保持有序性。
  5. 查询操作:
    • 查询某个元素在排序后的下标,直接输出。

为什么前后各冒泡一次?

当修改一个元素时,我们不知道该元素是变大了还是变小了。因此:

  • 如果该元素变小了,可能需要将它向前移动,保持有序;
  • 如果该元素变大了,可能需要将它向后移动,保持有序。

因此,我们需要前后各进行一次冒泡操作,以确保整个序列重新有序。

示例

原始序列为 4,5,64, 5, 64,5,6,如果将 5 改为 1,则需要向前移动,保持有序;如果将 5 改为 9,则需要向后移动,保持有序。

[CSP-J 2021] 网络连接

题目描述

TCP/IP 协议是网络通信领域的一项重要协议。今天你的任务,就是尝试利用这个协议,还原一个简化后的网络连接场景。

在本问题中,计算机分为两大类:服务机(Server)和客户机(Client)。服务机负责建立连接,客户机负责加入连接。

需要进行网络连接的计算机共有 nnn 台,编号为 1∼n1 \sim n1∼n,这些机器将按编号递增的顺序,依次发起一条建立连接或加入连接的操作。

每台机器在尝试建立或加入连接时需要提供一个地址串。服务机提供的地址串表示它尝试建立连接的地址,客户机提供的地址串表示它尝试加入连接的地址。

一个符合规范的地址串应当具有以下特征:

  1. 必须形如 a.b.c.d:e 的格式,其中 a,b,c,d,ea, b, c, d, ea,b,c,d,e 均为非负整数;
  2. 0≤a,b,c,d≤2550 \le a, b, c, d \le 2550≤a,b,c,d≤255,0≤e≤655350 \le e \le 655350≤e≤65535;
  3. a,b,c,d,ea, b, c, d, ea,b,c,d,e 均不能含有多余的前导 000。

相应地,不符合规范的地址串可能具有以下特征:

  1. 不是形如 a.b.c.d:e 格式的字符串,例如含有多于 333 个字符 . 或多于 111 个字符 : 等情况;
  2. 整数 a,b,c,d,ea, b, c, d, ea,b,c,d,e 中某一个或多个超出上述范围;
  3. 整数 a,b,c,d,ea, b, c, d, ea,b,c,d,e 中某一个或多个含有多余的前导 000。

例如,地址串 192.168.0.255:80 是符合规范的,但 192.168.0.999:80、192.168.00.1:10、192.168.0.1:088、192:168:0:1.233 均是不符合规范的。

如果服务机或客户机在发起操作时提供的地址串不符合规范,这条操作将被直接忽略。

在本问题中,我们假定凡是符合上述规范的地址串均可参与正常的连接,你无需考虑每个地址串的实际意义。

由于网络阻塞等原因,不允许两台服务机使用相同的地址串,如果此类现象发生,后一台尝试建立连接的服务机将会无法成功建立连接;除此之外,凡是提供符合规范的地址串的服务机均可成功建立连接。

如果某台提供符合规范的地址的客户机在尝试加入连接时,与先前某台已经成功建立连接的服务机提供的地址串相同,这台客户机就可以成功加入连接,并称其连接到这台服务机;如果找不到这样的服务机,则认为这台客户机无法成功加入连接。

请注意,尽管不允许两台不同的服务机使用相同的地址串,但多台客户机使用同样的地址串,以及同一台服务机同时被多台客户机连接的情况是被允许的。

你的任务很简单:在给出每台计算机的类型以及地址串之后,判断这台计算机的连接情况。

输入格式

第一行,一个正整数 nnn。

接下来 nnn 行,每行两个字符串 op,ad\mathit{op}, \mathit{ad}op,ad,按照编号从小到大给出每台计算机的类型及地址串。

其中 op\mathit{op}op 保证为字符串 Server 或 Client 之一,ad\mathit{ad}ad 为一个长度不超过 252525 的,仅由数字、字符 . 和字符 : 组成的非空字符串。

每行的两个字符串之间用恰好一个空格分隔开,每行的末尾没有多余的空格。

输出格式

输出共 nnn 行,每行一个正整数或字符串表示第 iii 台计算机的连接状态。其中:

如果第 iii 台计算机为服务机,则:

  1. 如果其提供符合规范的地址串且成功建立连接,输出字符串 OK。
  2. 如果其提供符合规范的地址串,但由于先前有相同地址串的服务机而无法成功建立连接,输出字符串 FAIL。
  3. 如果其提供的地址串不是符合规范的地址串,输出字符串 ERR。

如果第 iii 台计算机为客户机,则:

  1. 如果其提供符合规范的地址串且成功加入连接,输出一个正整数表示这台客户机连接到的服务机的编号。
  2. 如果其提供符合规范的地址串,但无法成功加入连接时,输出字符串 FAIL。
  3. 如果其提供的地址串不是符合规范的地址串,输出字符串 ERR。

样例 #1

样例输入 #1

5
Server 192.168.1.1:8080
Server 192.168.1.1:8080
Client 192.168.1.1:8080
Client 192.168.1.1:80
Client 192.168.1.1:99999

样例输出 #1

OK
FAIL
1
FAIL
ERR

样例 #2

样例输入 #2

10
Server 192.168.1.1:80
Client 192.168.1.1:80
Client 192.168.1.1:8080
Server 192.168.1.1:80
Server 192.168.1.1:8080
Server 192.168.1.999:0
Client 192.168.1.1.8080
Client 192.168.1.1:8080
Client 192.168.1.1:80
Client 192.168.1.999:0

样例输出 #2

OK
1
FAIL
FAIL
OK
ERR
ERR
5
1
ERR

样例 #3

样例输入 #3

见附件中的 network/network3.in。

样例输出 #3

见附件中的 network/network3.ans。

样例 #4

样例输入 #4

见附件中的 network/network4.in。

样例输出 #4

见附件中的 network/network4.ans。

提示

【样例解释 #1】

计算机 111 为服务机,提供符合规范的地址串 192.168.1.1:8080,成功建立连接;

计算机 222 为服务机,提供与计算机 111 相同的地址串,未能成功建立连接;

计算机 333 为客户机,提供符合规范的地址串 192.168.1.1:8080,成功加入连接,并连接到服务机 111;

计算机 444 为客户机,提供符合规范的地址串 192.168.1.1:80,找不到服务机与其连接;

计算机 555 为客户机,提供的地址串 192.168.1.1:99999 不符合规范。

【数据范围】

测试点编号n≤n \len≤特殊性质
111101010性质 1 2 3
2∼32 \sim 32∼3100100100性质 1 2 3
4∼54 \sim 54∼5100010001000性质 1 2 3
6∼86 \sim 86∼8100010001000性质 1 2
9∼119 \sim 119∼11100010001000性质 1
12∼1312 \sim 1312∼13100010001000性质 2
14∼1514 \sim 1514∼15100010001000性质 4
16∼1716 \sim 1716∼17100010001000性质 5
18∼2018 \sim 2018∼20100010001000无特殊性质

“性质 1”为:保证所有的地址串均符合规范;
“性质 2”为:保证对于任意两台不同的计算机,如果它们同为服务机或者同为客户机,则它们提供的地址串一定不同;
“性质 3”为:保证任意一台服务机的编号都小于所有的客户机;
“性质 4”为:保证所有的地址串均形如 a.b.c.d:e 的格式,其中 a,b,c,d,ea, b, c, d, ea,b,c,d,e 均为不超过 109{10}^9109 且不含有多余前导 000 的非负整数;
“性质 5”为:保证所有的地址串均形如 a.b.c.d:e 的格式,其中 a,b,c,d,ea, b, c, d, ea,b,c,d,e 均为只含有数字的非空字符串。

对于 100%100 \%100% 的数据,保证 1≤n≤10001 \le n \le 10001≤n≤1000。

解析

题目分析

主要问题是判断一个地址(形如 a.b.c.d:e)是否合法,并进行服务机和客户机的地址注册与查找操作。

解决方案

1. 地址合法性判断

为了判断输入的地址是否合法,可以使用 sscanf 读取形如 a.b.c.d:e 的字符串,并解析成 5 个部分:a, b, c, d 和 e。

判断步骤如下:

  1. 检查读取结果的数量:sscanf 会返回读取成功的项数。如果读取的项数不是 5 个,则该地址不合法。
  2. 检查各部分的取值范围:
    • a, b, c, d 是 IP 地址的四个部分,必须满足范围:0 ≤ a, b, c, d ≤ 255。
    • e 是端口号,必须满足范围:0 ≤ e ≤ 65535。
  3. 检查字符串一致性:再将 a.b.c.d:e 按格式拼接成一个新的字符串,检查它是否与原始输入字符串相同。这一步可以有效判断前导 0 的情况。例如,输入 01.2.3.4:5 时,解析后的 a = 1,拼接后字符串为 1.2.3.4:5,此时与原输入字符串 01.2.3.4:5 不同,因此该地址不合法。

2. 前导 0 的问题

前导 0 的问题通过解析后字符串的重构解决。如果输入 01.2.3.4:5,解析后 a = 1,拼接成的新字符串是 1.2.3.4:5,与原字符串不相同,因此不合法。这种方式自然地处理了前导 0 的问题。

3. 服务机和客户机操作

对于服务机的注册和客户机的连接操作,使用一个 map<string, int> 来记录已经注册的服务机地址。map 中的 string 用来存储地址,int 用来存储注册的编号。

操作流程:

  • 服务机(注册):如果地址合法并且未被注册,则输出 OK 并保存该地址和编号;如果该地址已被注册,输出 FAIL。
  • 客户机(查找):如果地址合法并且存在对应的服务机,输出服务机的编号;如果不存在,输出 FAIL。

特殊情况

  • 非法地址:输入的地址不合法时,直接输出 ERR。
  • 前导 0:解析后重构的字符串与原始字符串不一致时,认为地址不合法。

代码实现

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

// 定义一个 map 来记录已注册的服务机地址和其编号
map<string,int> vis;
int n;

// 判断输入的地址是否合法
bool check(char s[]) {
    int a=-1, b=-1, c=-1, d=-1, e=-1;
    // 尝试解析地址并将结果保存在 a, b, c, d, e 中
    int t = sscanf(s, "%d.%d.%d.%d:%d", &a, &b, &c, &d, &e);
    
    // 判断解析的结果是否有 5 个部分
    if(t != 5) return false;
    
    // 判断 a, b, c, d 是否在 0 到 255 之间,e 是否在 0 到 65535 之间
    if(a < 0 || a > 255) return false;
    if(b < 0 || b > 255) return false;
    if(c < 0 || c > 255) return false;
    if(d < 0 || d > 255) return false;
    if(e < 0 || e > 65535) return false;
    
    // 将 a.b.c.d:e 格式化成字符串 s2
    char s2[35];
    sprintf(s2, "%d.%d.%d.%d:%d", a, b, c, d, e);
    
    // 判断原字符串 s 和格式化后的字符串 s2 是否完全相同
    int lens = strlen(s); // 原字符串长度
    bool ok = true; // 是否相同的标志
    for(int i = 0; i < lens; i++) {
        if(s[i] != s2[i]) {
            ok = false;
            break;
        }
    }
    return ok; // 返回是否相同
}

int main() {
    cin >> n; // 读取操作的总数
    for(int i = 1; i <= n; i++) {
        char op[1005], ad[1005]; // 操作类型和地址
        cin >> op >> ad;
        string t(ad); // 将地址转换为字符串
        
        if(op[0] == 'S') { // 如果是服务机注册操作
            if(!check(ad)) cout << "ERR" << endl; // 地址不合法
            else if(vis.count(t) != 0) { // 地址已被注册
                cout << "FAIL" << endl;
            } else {
                cout << "OK" << endl; // 注册成功
                vis[t] = i; // 保存该地址和编号
            }
        } else { // 客户机连接操作
            if(!check(ad)) { // 地址不合法
                cout << "ERR" << endl;
            } else if(vis.count(t) == 0) { // 地址未注册
                cout << "FAIL" << endl;
            } else {
                cout << vis[ad] << endl; // 输出服务机编号
            }
        }
    }
}

代码解析

  1. 输入输出:读取操作次数 n,然后逐条读取操作类型(op)和地址(ad)。
  2. 服务机注册:
    • 先通过 check() 函数判断地址是否合法。
    • 若地址合法且未被注册,输出 OK 并保存到 map 中。
    • 若地址已注册,输出 FAIL。
  3. 客户机连接:
    • 通过 check() 函数判断地址是否合法。
    • 若合法且对应服务机已注册,输出服务机的编号。
    • 若未注册,输出 FAIL。
  4. 地址验证:通过 check() 函数进行格式验证,并解决前导 0 的问题。

[CSP-J 2021] 小熊的果篮

题目描述

小熊的水果店里摆放着一排 nnn 个水果。每个水果只可能是苹果或桔子,从左到右依次用正整数 1,2,…,n1, 2, \ldots, n1,2,…,n 编号。连续排在一起的同一种水果称为一个“块”。小熊要把这一排水果挑到若干个果篮里,具体方法是:每次都把每一个“块”中最左边的水果同时挑出,组成一个果篮。重复这一操作,直至水果用完。注意,每次挑完一个果篮后,“块”可能会发生变化。比如两个苹果“块”之间的唯一桔子被挑走后,两个苹果“块”就变成了一个“块”。请帮小熊计算每个果篮里包含的水果。

输入格式

第一行,包含一个正整数 nnn,表示水果的数量。

第二行,包含 nnn 个空格分隔的整数,其中第 iii 个数表示编号为 iii 的水果的种类,111 代表苹果,000 代表桔子。

输出格式

输出若干行。

第 iii 行表示第 iii 次挑出的水果组成的果篮。从小到大排序输出该果篮中所有水果的编号,每两个编号之间用一个空格分隔。

样例 #1

样例输入 #1

12
1 1 0 0 1 1 1 0 1 1 0 0

样例输出 #1

1 3 5 8 9 11
2 4 6 12
7
10

样例 #2

样例输入 #2

20
1 1 1 1 0 0 0 1 1 1 0 0 1 0 1 1 0 0 0 0

样例输出 #2

1 5 8 11 13 14 15 17
2 6 9 12 16 18
3 7 10 19
4 20

样例 #3

样例输入 #3

见附件中的 fruit/fruit3.in。

样例输出 #3

见附件中的 fruit/fruit3.ans。

提示

【样例解释 #1】

这是第一组数据的样例说明。

所有水果一开始的情况是 [1,1,0,0,1,1,1,0,1,1,0,0][1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0][1,1,0,0,1,1,1,0,1,1,0,0],一共有 666 个块。

在第一次挑水果组成果篮的过程中,编号为 1,3,5,8,9,111, 3, 5, 8, 9, 111,3,5,8,9,11 的水果被挑了出来。

之后剩下的水果是 [1,0,1,1,1,0][1, 0, 1, 1, 1, 0][1,0,1,1,1,0],一共 444 个块。

在第二次挑水果组成果篮的过程中,编号为 2,4,6,122, 4, 6, 122,4,6,12 的水果被挑了出来。

之后剩下的水果是 [1,1][1, 1][1,1],只有 111 个块。

在第三次挑水果组成果篮的过程中,编号为 777 的水果被挑了出来。

最后剩下的水果是 [1][1][1],只有 111 个块。

在第四次挑水果组成果篮的过程中,编号为 101010 的水果被挑了出来。

【数据范围】

对于 10%10 \%10% 的数据,n≤5n \le 5n≤5。
对于 30%30 \%30% 的数据,n≤1000n \le 1000n≤1000。
对于 70%70 \%70% 的数据,n≤50000n \le 50000n≤50000。
对于 100%100 \%100% 的数据,1≤n≤2×1051 \le n \le 2 \times {10}^51≤n≤2×105。

【提示】

由于数据规模较大,建议 C/C++ 选手使用 scanf 和 printf 语句输入、输出。

解析

题目描述

小熊的水果店里有一排 nnn 个水果,每个水果要么是苹果,要么是桔子,从左到右依次用正整数 1 到 nnn 编号。连续排列在一起的同种水果称为一个“块”。小熊将这排水果挑到若干个果篮里,挑选方法如下:

  • 每次从每个“块”中最左边的水果挑出,组成一个果篮。
  • 每次挑选后,若某些水果块相邻,且种类相同,则这些块会合并成一个新的“块”。
  • 重复该过程,直到所有水果都被挑走。

你需要帮助小熊计算每个果篮里包含的水果。

题解思路

为了模拟挑选和合并的过程,我们可以使用双向链表来维护水果块的结构。具体步骤如下:

  1. 双向链表的构建:建立两个双向链表,一个用于维护每个水果的前后关系,另一个用于维护每个“块”的块头。块头是指每个块中最左边水果的位置。
  2. 吃掉水果的操作:每次从每个“块”中最左边的水果开始挑出,即删除链表中的相应元素。与此同时,检查块是否需要合并,如果相邻块的水果种类相同,则将它们合并。
  3. 遍历与删除:遍历块头链表,对于每个块的块头,挑出水果后更新其指向的下一个水果。若该水果与上一个被吃掉的水果种类相同,则合并块,否则继续处理。

时间复杂度分析:每个水果被遍历一次,每个块被删除一次,因此整体时间复杂度为 Θ(n)\Theta(n)Θ(n)。

代码实现

#include <iostream>
#include <cstdio>
#include <vector>

using namespace std;

const int MAXN = 2e5+10;

struct Node {
  int prev;  // 前一个水果的位置
  int val;   // 当前水果的位置
  int next;  // 下一个水果的位置
};

int n;  // 水果数量
int shuiguo[MAXN];  // 水果种类数组
Node headList[MAXN];  // 维护块头链表
Node shuiguoList[MAXN];  // 维护水果链表
int cc;  // 块的数量

// 删除水果
void EatOne(int pos) {
  int prev = shuiguoList[pos].prev;
  int next = shuiguoList[pos].next;
  shuiguoList[prev].next = next;
  shuiguoList[next].prev = prev;
  printf("%d ", pos);  // 输出当前被吃掉的水果编号
}

// 删除块头
void Del(int pos) {
  int prev = headList[pos].prev;
  int next = headList[pos].next;
  headList[prev].next = next;
  headList[next].prev = prev;
}

// 模拟吃水果的过程
void Chi() {
  int solo = headList[0].next;  // 从第一个块头开始
  int nowcolor = shuiguo[headList[solo].val];  // 当前水果的种类
  while (solo != cc + 1) {  // 遍历所有块
    if (shuiguo[headList[solo].val] != nowcolor) {  // 如果块头水果种类不同
      Del(solo);  // 删除块头
      solo = headList[solo].next;
      continue;
    }
    EatOne(headList[solo].val);  // 吃掉块头的水果
    headList[solo].val = shuiguoList[headList[solo].val].next;  // 更新块头为下一个水果
    if (shuiguo[headList[solo].val] != nowcolor) {  // 若下一个水果种类不同
      Del(solo);  // 删除块头
    }
    solo = headList[solo].next;
    nowcolor ^= 1;  // 切换当前水果种类
  }
  putchar('\n');  // 输出换行符
}

int main() {
  scanf("%d", &n);  // 读取水果数量
  shuiguoList[0].next = 1;  // 初始化第一个水果
  for (int i = 1; i <= n; i++) {
    scanf("%d", &shuiguo[i]);  // 读取水果种类,1表示苹果,0表示桔子
    shuiguoList[i].prev = i - 1;  // 链接前一个水果
    shuiguoList[i].next = i + 1;  // 链接下一个水果
  }
  shuiguo[0] = 2;  // 边界条件,设置左边界水果种类为 2(非苹果非桔子)
  shuiguo[n + 1] = 2;  // 边界条件,设置右边界水果种类为 2
  headList[0].next = 1;  // 初始化块头链表
  for (int i = 1; i <= n; i++) {
    if (shuiguo[i] != shuiguo[i - 1]) {  // 若水果种类与前一个不同,则形成新的块
      cc++;  // 块数量加1
      headList[cc].prev = cc - 1;  // 链接前一个块头
      headList[cc].next = cc + 1;  // 链接下一个块头
      headList[cc].val = i;  // 当前块头的位置
    }
  }
  while (shuiguoList[0].next != n + 1) {  // 循环处理直到所有水果都被吃完
    Chi();  // 每次处理一轮挑水果
  }
  return 0;
}

代码解析

  1. 双向链表的构建:

    • shuiguoList 用于维护每个水果的前后关系。
    • headList 用于维护每个块的块头,即最左边的水果位置。
  2. 吃掉水果:

    • EatOne 函数用于从链表中删除一个水果。
    • Del 函数用于删除块头,合并相邻的同种水果块。
  3. 处理水果块:

    • Chi 函数循环处理块头,吃掉块头水果并更新块头。如果相邻块水果种类相同,则合并块。
  4. 主循环:

    • 主循环调用 Chi 函数,直到所有水果都被吃完。
津ICP备2020007982号-2