题目1:电话账单(选自 Acwing 1493)
题目描述:
长途电话公司按以下规则向客户收费:
拨打长途电话每分钟要花费一定的费用,具体收费取决于拨打电话的时间。
客户开始拨打长途电话的时间将被记录,客户挂断电话的时间也将被记录。
每个月都要给客户发送一次话费账单,账单中应包含每次通话记录以及相关收费等信息。
给定一组电话记录,你的工作是为客户准备帐单。
输入格式
输入包含两部分:费率结构和电话记录。
费率结构由一行组成,该行包含24个非负整数,分别表示从 00:00-01:00
的收费(分/分钟),从 01:00-02:00
的收费,以此类推…
下一行包含一个正整数 N。
接下来 N 行,每行包含一条记录。
每个记录由客户名称(最多 2020 个字符的字符串,不带空格),时间和日期(mm:dd:hh:mm
)以及单词 on-line
或 off-line
组成。
所有日期都在同一个月内,每个 on-line
记录都与按时间顺序排列的同一位客户的下一条记录配对,但前提是这条记录是 off-line
。
所有未与 off-line
记录配对的 on-line
记录以及未与 on-line
记录配对的 off-line
记录都必须忽略。
输入中至少包含一个成功的配对。
同一客户在同一时间不会有两个或以上的电话记录。
使用 2424 小时制记录时间。
输出格式
你需要为每个客户打印电话费。
账单必须按照客户姓名的字母顺序(按ASCII码顺序,大写字母在前,小写字母在后)打印。
对于每个客户,首先以示例显示的格式在一行中打印客户名称和帐单月份。
然后,对于每个通话时间段,在一行中分别打印开始和结束时间和日期(dd:hh:mm
),持续时间(以分钟为单位)和通话费用。
通话必须按时间顺序列出。
最后,以示例显示的格式打印该月的总费用。
注意,没有任何有效通话记录的客户直接忽略,不予打印账单。
数据范围
1≤N≤1000
输入样例:
10 10 10 10 10 10 20 20 20 15 15 15 15 15 15 15 20 30 20 15 15 10 10 10
10
CYLL 01:01:06:01 on-line
CYLL 01:28:16:05 off-line
CYJJ 01:01:07:00 off-line
CYLL 01:01:08:03 off-line
CYJJ 01:01:05:59 on-line
aaa 01:01:01:03 on-line
aaa 01:02:00:01 on-line
CYLL 01:28:15:41 on-line
aaa 01:05:02:24 on-line
aaa 01:04:23:59 off-line
输出样例:
CYJJ 01
01:05:59 01:07:00 61 $12.10
Total amount: $12.10
CYLL 01
01:06:01 01:08:03 122 $24.40
28:15:41 28:16:05 24 $3.85
Total amount: $28.25
aaa 01
02:00:01 04:23:59 4318 $638.80
Total amount: $638.80
代码:
#include <iostream>
#include <algorithm>
#include <map>
#include <cstring>
#include <cstdio>
using namespace std;
const int N = 1010, M = 31 * 1440 + 10; //M代表一个月一共的分钟数
int costs[24]; //每个时间段的花费
double sum[M]; //从1号00:00时刻开始到每个时刻所花费的钱数,前缀和思想
int n;
//定义每个用户记录的结构体
struct Record {
int minutes; //通话时间
string format_time; //时间记录
string state; //状态
// 自定义比较函数
bool operator < (const Record &t) const {
return minutes < t.minutes;
}
};
map<string, vector<Record>> People; //因为一个人可能有多个通话记录,采用map存储
int main() {
for (int i = 0; i < 24; i ++) cin >> costs[i]; //输入24个时间段的花费
sum[0] = 0;
//前缀和, 适合多区间域求区间和问题,降低复杂度
//第i个时刻的花费用i-1来计算, 比如6:00 - 7:00 计算的为6:01-6:02 ... 6:58 - 6.59,结束时刻为7,但还是用6时刻的来计算
for (int i = 1; i < M; i ++) sum[i] = sum[i - 1] + costs[(i - 1) % 1440 / 60] / 100.0;
//读入数据
cin >> n;
char name[25], format_time[20], state[10];
int month, day, hour, minute;
for (int i = 0; i < n; i ++) {
scanf("%s %d:%d:%d:%d %s", name, &month, &day, &hour, &minute, state);
//格式化字符串,采用sprintf
sprintf(format_time, "%02d:%02d:%02d", day, hour, minute);
//计算总时间
int minutes = (day - 1 ) * 1440 + hour *60 + minute;
People[name].push_back ({minutes, format_time, state});
}
for(auto &person : People) {
string name = person.first;
auto record = person.second;
//根据时间顺序排序
sort(record.begin(), record.end());
double total = 0;
for (int i = 0; i + 1 < record.size(); i ++) {
auto a = record[i];
auto b = record[i + 1];
if (a.state == "on-line" && b.state == "off-line") {
if (total == 0) { //表明该用户为新用户,输出其姓名
printf("%s %02d\n", name.c_str(), month);
}
cout << a.format_time << ' ' << b.format_time;
double cur_cost = sum[b.minutes] - sum[a.minutes];
printf(" %d $%.2lf\n", b.minutes - a.minutes, cur_cost);
total += cur_cost;
}
}
if (total) {
printf("Total amount: $%.2lf\n", total);
}
}
return 0;
}
思路:
①每个用户包含多个信息,涉及开始通话时间、结束通话时间、以及当前记录状态,可采用结构体存储。同时,由于后续需要根据时间先后顺序进行匹配,故需要在结构体内重载比较运算符
②计算花费部分
计算花费问题本质是一个求区间和的问题,已知多个区间的情况,求任意区间之间的花费,可转化为前缀和问题。
前缀和的优势:
以O(1)的复杂度得到某块区间的总和
③时间处理上,统一将时间转换成秒,处理更为简便
④由于一个人可能由多条通话记录,可采用map来存储人的姓名和通话记录的映射关系,多条通话记录采用vector存储
注意:
①sprintf用法
sprintf是把格式化字符串输出到指定字符串
②printf保留小数位数
格式:%.nlf,n代表保留的小数位数
题目2:银行排队(选自Acwing 1494)
题目描述:
假设一家银行有 K个服务窗口。
窗户前面有一条黄线,将等候区分为两部分。
所有客户都必须在黄线后面排队等候,直到轮到他/她服务并且有可用的窗口为止。
假定一个窗口不能被单个客户占用超过 11 小时,即如果某位顾客的业务已经办理了一小时,则立即终止此项业务。
现在给定每个客户的到达时间 T 和业务办理时间 P,请计算所有客户的平均等待时间。
输入格式
第一行包含两个整数 N 和 K,分别表示客户数量以及窗口数量。
接下来 N 行,每行包含两个时间,分别是一个客户的到达时间,用 HH:MM:SS
表示,以及一个客户的业务办理时间 P(单位:分钟)。
HH
在 [00,23][00,23] 范围内,MM
和 SS
都在 [00,59][00,59] 范围内。
所有客户的到达时间均不相同。
请注意,银行的营业时间为 08:00
至 17:00
。
任何人提前到达都必须排队等候至 08:00
,而任何人来得太晚(在 17:00:01
或之后到达)都将不被服务也无需计入平均值。
注意只要客户在17:00
之前排上队,则即使办理业务时超过17:00
,也会被服务。
输出格式
输出平均等待时间(单位:分钟),结果保留一位小数。
注意,从到达银行至开始办理业务这一期间视为等待期间。
数据范围
1≤N≤10^4
1≤K≤100
输入样例:
7 3
07:55:00 16
17:00:01 2
07:59:59 15
08:01:00 60
08:00:00 30
08:00:02 2
08:03:00 10
输出样例:
8.2
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 10010, M = 110;
int n, m;
//每个顾客信息结构体
struct Person {
int arrive_time; //用户到达时间
int service_time; //服务时间
bool operator <(const Person& t) const {
return arrive_time < t.arrive_time;
}
}Person[N];
int main() {
cin >> n >> m;
for (int i = 0; i < n; i ++ ) {
int hour, minute, second, service_time;
scanf("%d:%d:%d %d", &hour, &minute, &second, &service_time);
//最长服务时间只能为60min
service_time = min(service_time, 60);
Person[i] = {hour * 3600 + minute * 60 + second, service_time * 60};
}
//需要对每个窗口的结束时间进行排序
//每次选择最早结束的窗口为下一个顾客提供服务
//采用优先队列(最小堆)
priority_queue<int, vector<int>, greater<int> > windows;
for (int i = 0; i < m; i ++) windows.push(8 * 3600); //初始化,初始时间即为银行开业时间
//按照到达时间对顾客进行排序
sort(Person, Person + n);
int total_time = 0, cnt_people = 0; //总等待时间,服务总人数
for (int i = 0; i < n; i ++) {
//到达时间超过17:00,不予以办理
if (Person[i].arrive_time > 17 * 3600) break;
int end_time = windows.top(); //最早结束窗口的时间
windows.pop(); //更新窗口队列
int start_time = max(Person[i].arrive_time, end_time); //顾客开始办理业务时间,弄清用max的含义
int wait_time = start_time - Person[i].arrive_time;
windows.push(start_time + Person[i].service_time);
total_time += wait_time;
cnt_people ++;
}
printf("%.1lf", total_time / cnt_people / 60.0);
}
思路:
①每个顾客有到达时间和服务时间两个信息,可以用结构体数组存储所有顾客信息,由于之后需要对顾客到达时间排序,需要重载比较运算符
②如何选择合适的窗口
对于每个顾客,选择的一定是最先结束前一个人服务的窗口,故需要在所有窗口中找最早结束时间的窗口,可以采用最小堆(优先队列)来维护所有窗口的信息,队列中存放的是每个窗口的结束时间
注意:
①顾客最长服务时间只能为60min,需要对输入的顾客服务时间和60min取一个min
②时间统一用秒来描述,相对于00:00,便于计算
③顾客开始办理业务的时间为顾客到达的时间和窗口最早结束时间取max
题目3:它们是否相等(选自Acwing 1546)
问题描述:
如果机器只能保存 33 个有效数字,则将浮点数 1230012300 和 12358.912358.9 视为相等(多余位数直接舍弃,不进行四舍五入),因为它们都保存为 0.123×1050.123×105。
现在给定一个机器能够保存的有效数字位数,以及两个浮点数,请你判断两个数在该机器上是否相等。
注意:
- 数字不一定标准,可能有前导 00。
- 如果数值是0,则指数规定为0。
输入格式
共一行,包含三个数 N,A,B,分别表示有效位数,以及两个用来比较的浮点数。
输出格式
共一行,如果两个数相等,则先输出 YES
,然后输出它们共同的保存方法,格式为 0.d[1]...d[N]*10^k
(d[1]
> 0,除非数字为 00)。
如果两个数不相等,则先输出 NO
,然后分别输出两个数的保存方法,格式同上。
数据范围
1≤N<100,
A和 B 都不大于 10^100,且总位数不超过 100。
输入样例1:
3 12300 12358.9
输出样例1:
YES 0.123*10^5
输入样例2:
3 120 128
输出样例2:
NO 0.120*10^3 0.128*10^3
代码:
#include<iostream>
#include<cstring>
using namespace std;
string change(string s, int n){
int k = s.find(".");
if(k == -1){
s += '.';
k = s.find("."); //寻找小数点的位置,若无,则在最后一位补小数点
}
string res = s.substr(0, k) + s.substr(k + 1); //移除小数点(将小数点移到最前面)
while(res.size() && res[0] == '0')
{
res = res.substr(1);
k--; //28 = 0.028*10^3 = 0.28*10^2 移除前导0
}
if(res.empty()) k = 0;
if(res.size() > n) res = res.substr(0, n); //超出有效位数,只取前n位
else res = res + string(n - res.size(), '0'); //不足有效位数,补0
return "0." + res + "*10^" + to_string(k);
}
int main () {
int n;
string a, b;
cin >> n >> a >> b;
string res1 = change(a, n);
string res2 = change(b, n);
if (res1 == res2) cout << "YES " << res1;
else cout << "NO " << res1 << ' ' << res2;
return 0;
}
思路:
①找小数点位置,若无小数点,则在数的末尾补小数点,记小数点位置索引位K
②将小数点移到最前面
③移除前导0,每移动一位,K--
④将有效数字补位规定位数
K即为最终的指数
注意:
①字符串切割可用substr()函数
对于字符串a
a.substr(i,j)表示从索引位i的位置切割长度为j的字符串
a.substr(k)表示从索引为k的位置到末尾的字符串
②string初始化可以用string(n,'0'),表示一个含有n个'0'的字符串
总结:
本次刷题过程中,对于结构体使用较为频繁,同时涉及结构体内排序问题需要重载运算符,还需注意一些常见的函数,比如字符串切割substr()函数,格式化字符串sprintf(),可以大大减少冗余的代码量。