算法刷题总结——字符串处理(二)

题目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^kd[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(),可以大大减少冗余的代码量。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值