PAT (Advanced Level) Practice 1001~1020

1001


1002


1003 Emergency

题意:

输入第一行是四个正整数,第一个是城市(点)的总数 N,第二个是城市之间道路(边)的总数 M,起点城市编号 C1 和终点城市编号 C2。随后一行是 N 个点,表示编号为 0 ~ N - 1 的城市的救援队伍的数量(点的权重)。然后是 M 行数据,每行三个整数 c1c2L,分别表示两个城市的编号,以及它们之间道路的长度(边长)。现在要求你计算,从起始点 C1 到终点 C2 有多少条最短路径并输出,然后输出所有最短路径中的最大权重和。

思路:

迪杰斯特拉算法解决。

d[i] 表示由起始点 s 到点 i 的最短路径的路径长度,weight[i] 表示点 i 的权重,w[i] 表示到达点 i 的最短路径中最大的权值和,G[u][v] 表示从点 u 到点 v 的路径长度(值为0时表明 u, v 之间没有边),num[i] 表示到达点 i 的最短路径有多少条,visit[i] 表示点 i 已被访问过。

此题是在基础迪杰斯特拉算法的基础上,需要多考虑一个最短路径的权重和,以及有多少条最短路径。权重和很好理解,顺着找到的新的点逐个加上去就好了,可能比较多疑惑的地方在于 num,也就是如何统计有多少条最短路径。其实很好理解,加入 A、B 两点都是最短路径上的一个点,它们各自的 num 的值都为 1,最短路径的下一个点是 C,且从 A、B 两点到达点 C 的距离一样。那么在第一次从 A 到 C 时,C 继承了 A 的 num 值,在下一次从 B 到 C 时,发现路径一样了,也就是代码中 else if 的部分,就将 B 的 num 值加到 C 上,表示到达点 C 的最短路径有两条。

#include <iostream>
using namespace std;

int w[510], weight[510], G[510][510], d[510], num[510];
bool visit[510];

int main()
{
    int N, M, C1, C2, c1, c2, L, u;
    cin >> N >> M >> C1 >> C2;
    for (int i = 0; i < N; ++i) cin >> weight[i];
    fill(d, d + N, 0x3fffffff); // 初始化起始点到每个点的最短路径的长度为最大值
    while (M--)
    {
        cin >> c1 >> c2 >> L;
        G[c1][c2] = G[c2][c1] = L;
    }
    d[C1] = 0;              // 到起始点的路径长度初始化为0
    w[C1] = weight[C1];     // 到起始点的权重初始化为起始点的权重
    num[C1] = 1;            // 到达起始点的最短路径只有本身一条
    for (int i = 0; i < N; ++i)         // 循环 N 次来保证访问到 N 个点
    {
        int u = -1, MIN = 0x3fffffff;   // u 使得 d[u] 最小,MIN 保存 d 中最小值
        for (int j = 0; j < N; ++j)     // 枚举所有的点,寻找未访问过的点中最短道路的点00000000000
        {
            if (!visit[j] && d[j] < MIN)    // 如果 j 未访问过且 d[j] 小于 MIN
            {
                u = j;          // 更新 u
                MIN = d[j];     // 更新 MIN
            }
        }
        if (u == -1) break;     // 找不到小于 0x3fffffff 的数,说明剩下的点和起点 s 都不连通,结束循环
        visit[u] = true;        // 标记 u 为已访问
        for (int v = 0; v < N; ++v)     // 枚举所有的点
        {
            if (!visit[v] && G[u][v] != 0)          // 如果 v 没有访问过,且由 u 到 v 有边
            {
                if (d[u] + G[u][v] < d[v])          // 如果以 u 为中介点可以使 d[v] 更小
                {
                    d[v] = d[u] + G[u][v];          // 更新最短路径长度
                    w[v] = w[u] + weight[v];        // 更新最大权值和
                    num[v] = num[u];                // 继承最短路径条数
                }
                else if (d[u] + G[u][v] == d[v])    // 如果找到一条相同长度的路径
                {
                    w[v] = (w[u] + weight[v] > w[v]) ? w[u] + weight[v] : w[v];   // 以 u 为中介点时权值之和更大就更新
                    num[v] += num[u];               // 更新最短路径条数
                }
            }
        }
    }
    cout << num[C2] << " " << w[C2];

    return 0;
}

1004 Counting Leaves

题意:

输入第一行是两个正整数,N 为树中结点的个数(结点编号 01 ~ N),M 加粗样式为树中非叶子结点的个数。随后 M 行输入,每行第一个整数 id 表明结点的编号,第二个整数 K 表明其孩子结点的个数,随后是其 K 个孩子结点的编号 childid。要求你输出这棵数,每一层中叶结点的个数。题目规定了叶结点的

思路:

深度优先搜索遍历解决。

定义数组的数组 childs,其中 childs[i] 表明结点 i 的数组,保存其所有孩子结点的编号。数组 cnt 保存每一层中孩子结点的个数。maxdepth 表明树的最大层数。

#include <iostream>
#include <vector>
using namespace std;

int maxdepth = -1;
vector<int> childs[105], cnt(105);

void DFS(int root, int depth)
{
    if (childs[root].size() == 0) ++cnt[depth];     // depth 层叶结点个数加1
    maxdepth = depth > maxdepth ? depth : maxdepth; // 更新最大层数
    for (int i = 0; i < childs[root].size(); ++i)
        DFS(childs[root][i], depth + 1);
}

int main()
{
    int N, M, id, K, childid;
    cin >> N >> M;
    while (M--)
    {
        cin >> id >> K;
        while (K--)
        {
            cin >> childid;
            childs[id].push_back(childid);
        }
    }
    DFS(1, 0);
    cout << cnt[0]; // 第一个数据前不需要空格
    for (int i = 1; i <= maxdepth; ++i)
        cout << " " << cnt[i];

    return 0;
}

1005


1006


1007


1008 Elevator

思路:

这题不要想太多,不是像平常电梯运行那样,只能上到顶,然后再下到底,这一题就是来一层去一层。

注意:

  • 要注意输入数字序列的第一个数字是 n 而不是楼层。
  • 实际情况下, 不仅要考虑到电梯的上升下降,还需要考虑继续等待的情况。比如出现连续的两个1,就表明需要在当前楼层多等待5秒。
  • 到达输入序列的最后一个楼层后依然要等待5秒,不要进行特判。
#include <iostream>
using namespace std;

int main()
{
    int N, total, preFloor, newFloor;   // total 为总花费时间,preFloor 为当前楼层, newFloor 为要去的楼层
    cin >> N;
    total = preFloor = 0;
    while (N--)
    {
        cin >> newFloor;
        if (newFloor > preFloor)
            total += (newFloor - preFloor) * 6 + 5;
        else total += (preFloor - newFloor) * 4 + 5;
        preFloor = newFloor;
    }
    cout << total;

    return 0;
}

1009


1010 Radix

题意:

输入只有一行,分别是整数 N1N2,标签 tag,以及基数 radix。tag = 1 表明 N1 是 radix 进制数,tag = 2 表明 N2 是 radix 进制数。现在要求你根据给出的进制 radix 和对应的数,判断另一个数,有没有可能在某种进制下等于已知进制对应的数,如果存在,输出该进制,不存在输出 Impossible。

思路:

基本的核心思路便是,将两个数都转为十进制再进行比较,因为任何数转十进制的方法都是一样的,只不过是基数不同而已。代码的思路流程如下。

用字符串来读取整数 N1 和 N2,因为整数中存在小写字母代表的数组。根据 tag 的值,如果 tag 为1,就找整数 N2 的进制,反之找 N1 的进制。

convert 函数根据传入的字符串 n 和进制 radix,将传入的字符串转为十进制数并返回。任意进制数转十进制的方法可以参考这篇博客:PAT OJ 刷题必备知识总结 20.1 P 进制转十进制。

findRadix 函数根据传入的字符串 n 和十进制数 num,来找到 n 的进制。首先定义字符 c 保存 n 中最大的字符,定义 low 保存 n 的最小进制,它至少要大于字符 c 所代表的整数,high 则保存 low 和 num 中的较大者。接下来通过二分法,查找区间是 [low, high],中间的每一个数都有可能是 n 的进制。mid 保存中间位置的进制,t 保存在 mid 进制下 n 表示的十进制数,如果它比 num 小,说明 mid 进制不够大,下界需要变大;如果它比 num 大或者小于0(溢出),说明 mid 进制太大了,上界需要变小。

对二分法不熟悉的读者可以学习这篇博客:二分法及其拓展全面讲解

注意:

  • 0 0 1 10 或者 0 0 2 10 可以用于排除测试点0的坑,如果你是卡在了这里,仔细想想为什么呢?(提示:进制最低是1)
  • 题目并没有说最低是36进制!只是说每个数位上的数最大是 z 所代表的35,所以 high 是 num 和 low 中的较大者,而不是 radix 和 low 中的较大者。
  • 即便是用 long long 定义也有可能溢出,溢出的时候值就是-1,因此需要加在判断里面。
  • 不使用二分法查找进制会超时。
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;

long long convert(string n, long long radix)
{
    long long sum = 0, index = 1;
    for (int i = n.size() - 1; i >= 0; --i) // 从字符串尾部往头部枚举(从低数位往高数位枚举)
    {
        sum += (isdigit(n[i]) ? (n[i] - '0') : (n[i] - 'a' + 10)) * index;
        index *= radix;
    }
    return sum;
}

long long findRadix(string n, long long num)
{
    char c = *max_element(n.begin(), n.end());  // c 保存 n 中最大的字符(或者说是最大的数位)
    long long low = (isdigit(c) ? c - '0' : c - 'a' + 10) + 1;  // 进制比最大的数位大
    long long high = max(num, low), mid;
    while (low <= high)                 // 二分法查找进制
    {
        mid = (low + high) / 2;         // mid 是中间位置
        long long t = convert(n, mid);  // 求出 mid 进制下 n 的十进制数
        if (t == num) return mid;       // 与另一个数的十进制数相等则返回 mid
        else if (t < 0 || t > num)      // 溢出,说明 mid 大了,上界变小
            high = mid - 1;
        else low = mid + 1;             // mid 太小了,下界变大
    }
    return -1;  // 不存在进制满足两者相等,返回-1
}

int main()
{
    string n1, n2;
    long long tag = 0, radix = 0, ans;
    cin >> n1 >> n2 >> tag >> radix;    // 根据 tag 的值来决定寻找谁的进制
    ans = tag == 1 ? findRadix(n2, convert(n1, radix)) : findRadix(n1, convert(n2, radix));
    if (ans != -1) cout << ans;
    else cout << "Impossible";

    return 0;
}

1011


1012 The Best Rank

题意:

输入第一行给出学生总数 N 和 M 个检查排名的学生,接下来 N 行,分别是学生 id(6位整数),C语言、数学和英语的成绩,再然后是 M 行检查排名的学生,按照学生的 id,要求你打印出学生的最佳排名,以及相对应的科目;如果在成绩表上没有该名学生的 id,就输出 N/A。

最佳排名指的是,该学生在每门科目外加平均分的排名中,最高的那一个。如果他有多个排名相等,就按照 A > C > M > E 选择排名最高的那一科。

思路:

因为学生 id 和其分数是绑定的,所以定义一个结构体数组 stu,其数据成员 id 保存每个学生的 id,数组 score 保存该学生的四项成绩,下标0123分别对应 ACME。定义全局变量 n 表示当前枚举的科目,数组 Rank 保存每个学生的四项成绩的排名,大小开1000000是因为学生 id 是六位的整数,定义数组来保存便可以直接通过下标来访问其排名,而不需要查询。最后定义数组 course 保存四项对应的符号,通过下标来访问。

读取数据时,将科目 C、M、E 的成绩依次保存到到 score 的下标1、2、3处,平均值的计算采用四舍五入,即在最后加上0.5。因为 score 本身就是用 int 定义的,所以求完平均数后不需要强制转换类型。平均值保存到 score[0]。

读取完后,用变量 n 枚举每一个科目,然后根据利用 sort 函数进行排序。因为 cmp 函数是不能传参的,这意味着不能将科目 n 当做参数传给 cmp 来让 sort 对我们指定的科目进行排序,所以需要将 n 定义成全局变量,然后在 cmp 中通过 n 进行科目的指定。例如当 n = 0 时 cmp 中就是 return a.score[0] > b.score[0],就表明对每个学生按照平均值进行从大到小的排序。

排序完后,stu 中的学生顺序就是根据科目 n 排序后的结果。令 Rank[stu[0].id][n] = 1,就是让学生 stu[0].id 的科目 n 的排名为1。然后枚举其后面每一个学生 stu[i]。如果第 i 个人的成绩和第 i - 1 个人的成绩是一样的,那他们的排名就相等;否则排名就是 i + 1。最后读取检查排名的学生的 id,根据 Rank[check][0] 是否为0判断成绩表中是否有该名学生。此处开全局数组的重要性就体现出来了,如果将排名定义在结构体内,那么此处就需要先遍历一遍结构体来查找是否存在该 id,而全局数组就能直接通过下标访问来判断是否存在,节省许多的时间。

注意:

  • 如果存在并列的排名,应该是 1、1、3、4、5,而不不是 1、1、2、3、4,否则测试点2过不去。
  • 因为优先级顺序是 ACME,所以将其对应到数组下标,从前往后枚举排名,只有当新排名比前面的都要“小于”时才更新最优排名。
  • 此题就算不用四舍五入也能 AC,而根据测试用例也不能完全看出这一点,所以是否直接取整都可以。
#include <iostream>
#include <algorithm>
using namespace std;

struct student{
	int id, score[4];
} stu[2005];

int n, Rank[1000000][5];	// 这两个要定义成全局
char course[4] = {'A', 'C', 'M', 'E'};

bool cmp(student a, student b) { return a.score[n] > b.score[n]; }

int main()
{
	int N, M, check;
	cin >> N >> M;			// 学生总数 检查排名的学生人数 
	for (int i = 0; i < N; ++i)
	{
		cin >> stu[i].id >> stu[i].score[1] >> stu[i].score[2] >> stu[i].score[3];
		stu[i].score[0] = (stu[i].score[1] + stu[i].score[2] + stu[i].score[3]) / 3.0 + 0.5;	// 四舍五入向上取整
	}
	for (n = 0; n < 4; ++n)			// 对科目进行枚举
	{
		sort(stu, stu + N, cmp);	// 按照科目 n 进行排序
		Rank[stu[0].id][n] = 1;		// 分最高的学生排名为1
		for (int i = 1; i < N; ++i)	// 对后面的每个学生进行枚举
		{
			if (stu[i].score[n] == stu[i - 1].score[n])		// 如果该学生的成绩和前一名一样,排名保持不变
				Rank[stu[i].id][n] = Rank[stu[i - 1].id][n];
			else Rank[stu[i].id][n] = i + 1;				// 不一样的话则为 i 的值加1
		}
	}

	while (M--)
	{
		cin >> check;		// 检查排名的学生的id
		if (Rank[check][0] != 0)	// 排名不存在0,如果为0说明没有这个学生的 id
		{
			int best_rank = 5, index = 0;	// 排名最低是4,保证一定能取到最佳排名
			for (int i = 0; i < 4; ++i)
			{
				if (Rank[check][i] < best_rank)
				{
					best_rank = Rank[check][i];
					index = i;
				}
			}
			cout << best_rank << " " << course[index] << endl;
		}
		else cout << "N/A\n";
	}
	
	return 0;
}

1013 Battle Over Cities

题意:

输入第一行是城市(图的结点)的数量 N,高速公路(结点之间的边)的数量 M,需要分别检查的城市数量 K。随后 M 行,每一行两个整数,表明城市的编号(从 1 ~ N),表明这两个城市之间有高速公路。最后一行是 K 个待检查的城市。现在要求你分别计算,去掉某个城市后,需要修建几条高速公路才能将所有城市继续连通。

思路:

深度优先搜索遍历解决。

题目的意思就是问,去掉图中的某个结点后,有几个连通分量(连通图),需要连接的边就是连通图的个数减1。那么思路就很清晰了,只需遍历图中的每个结点,因为 DFS 能保证从连通图中的某一个结点进入后能访问到所有结点。所以只需计算从 DFS 返回了几次(广义的返回),返回几次就表明图中有几个连通图。

#include <iostream>
using namespace std;

int G[1010][1010], N, M, K, c1, c2, n;
bool visit[1010] = { false };       // 标记是否被访问过

void DFS(int u)
{
    visit[u] = true;                // 标记结点 u 为已访问
    for (int v = 1; v <= N; ++v)    // 枚举所有结点
        if (G[u][v] && !visit[v]) DFS(v);	// 如果结点 u, v 之间存在边且 v 未访问过
}

int main()
{
    cin >> N >> M >> K;
    for (int i = 0; i < M; ++i)
    {
        cin >> c1 >> c2;
        G[c1][c2] = G[c2][c1] = 1;      // 标记结点 c1 和 c2 之间存在边
    }
    for (int i = 0; i < K; ++i)
    {
        for (int i = 1; i <= N; ++i) visit[i] = false;
        cin >> n;
        visit[n] = true;
        int cnt = 0;                    // cnt 统计图中拿去结点 n 后连通图的数量
        for (int i = 1; i <= N; ++i)    // 枚举所有结点
        {
            if (visit[i] == false)      // 如果结点 i 未被访问过
            {
                DFS(i);
                ++cnt;
            }
        }
        cout << cnt - 1 << endl;
    }

    return 0;
}

1014


1015 Reversible Primes

题意:

给出正整数 N 和进制 D,如果 N 是素数,且 N 在进制 D 下反转后的数在十进制下也是素数,则输出“Yes";否则输出“No”。比如73是素数,在十进制下反转后得到的37也是素数,所以输出“Yes”。23是素数,其二进制表示为10111,反转后得到的11101在十进制下为29,也是素数,所以输出“Yes”。

思路:

  1. 判断 N 是否是素数:如果是素数,则进入步骤2;如果不是素数,则输出“No",结束算法。
  2. 将 N 转换为 D 进制,并保存于数组中。然后采用“逆序遍历”的方式重新转换为十进制(逆序遍历数组等价于将数组颠倒)。如果转换后的数为素数,则输出“Yes”;否则,输出“No”。

注意点:

  • 也可使用 while(scnaf(“%d”, &n) != EOF) 的写法,此时在 while 循环内就要加一个 if (n < 0) break; 的判断条件。
  • 题目描述中保证了0不作为输入,但要注意当 n 等于1时,需要输出“No”。
#include <iostream>
using namespace std;

bool isPrime(int n)
{
    if (n <= 1) return false;
    for (int i = 2; i * i <= n;  ++i)
        if (n % i == 0)
            return false;
    return true;
}

int radix[111];

int main()
{
    int N, d;
    cin >> N;       // 整数 n 和进制 d
    while (N > 0)   // n 大于0时循环
    {
        cin >> d;
        if (!isPrime(N)) cout << "No" << endl;
        else        // n 是素数,判断 n 在进制 d 下的逆序是不是素数
        {
            int len = 0;
            do      // 进制转换
            {
                radix[len++] = N % d;
                N /= d;
            } while (N);
            for (int i = 0; i < len; ++i)   // 按逆序转换进制
                N = N * d + radix[i];
            if (isPrime(N))  cout << "Yes" << endl;    // 逆序是素数
            else cout << "No" << endl;      // 逆序不是素数
        }
        cin >> N;
    }

    return 0;
}

1016 Phone Bills

题意:

输入第一行给出24个非负整数,分别表示00:00~01:00、01:00~02:00等等时间段内的收费标准,单位是(美分/每秒)。

第二行输入通话记录数 N ,随后 N 行通话记录。每行通话记录首先给出用户的名称(不超过20个字符的字符串,不含空格)、通话日期和时间(格式为月:日:时:分),以及通话状态(即是拨通电话还是挂掉电话)。

现在要求你按照用户名的字母顺序,在第一行打印用户名称和其通话的月份,随后是数行该用户的通话情况。每行通话情况首先打印拨通电话的时间,然后打印挂掉电话的时间,随后是数行通话的总时长(单位分钟),最后是该次通话的账单消费。在输出下一名用户之前,还要另外输出一行该名用户的总账单消费。

具体的格式看题目的输入输出样例就能知道。题目还表明只考虑 on-line 和 off-line 配对的通话,连续两个 on-line 或者 off-line,或者先是 off-line 再是 on-line 的通话记录都不考虑在内。

思路:

定义结构体 record 保存用户姓名 name、通话状态 status、月:日:时:分 month:day:hour:minute、以及从00:00开始经过的分钟数 time。同时定义全局结构体数组 calls 保存所有输入的通话记录,再定义比较函数 cmp 和计算账单消费的函数。

定义数组 rate 保存每个时间段的收费标准,rate[0] 是 00:00~01:00 的收费标准,rate[1] 是 01:00~02:00,刚好对应下标。其中 rate[24] 保存的是每小时收费标准总和,也就是 rate[0] + rate[1] + ··· + rate[23] 的结果。输入通话记录时,先输入用户姓名,然后利用 scanf 格式化输入快速保存各个时长,再输入通话状态,并根据通话状态标记通话状态,status 等于0表示 off-line,等于1表示 on-line,因为整数比较比字符串比较更省时。最后计算从00:00开始经过的分钟数,保存在 time 中。

sort() 函数利用 cmp 提供的规则进行排序:如果姓名不相等,按姓名字母序;否则按照经过分钟数 time 排序。

排完序定义字典 custom 保存每个用户的通话记录,键是用户姓名,值是 record 类型的 vector 数组,当然并不是保存每个用户的所有通话记录。因为 calls 已经排过序了,此时的 calls 中同名的通话记录都是连在一起的。接下来一个循环,首先判断姓名是否相同,相同的话,我们只考虑两个在时间顺序上挨在一起、且前一个 mark 为1,后一个 mark 为0的两个通话记录(因为要先拨通再挂掉才算一次有效通话)。所以枚举变量 i 要从1开始。

循环结束后,custom 中保存的就是每名用户的所有有效通话。用 it 枚举 custom,再用 i 枚举 it.second ,即通话记录数组,i 在每轮循环中自增2,因为每次都是直接处理两条通话记录(拨通和挂掉)。通过 calculateBills() 函数计算从00:00其至拨通电话时间需要的花费,再计算从00:00其至挂掉电话时间需要的花费,两者之差便是该次通话的花费。再按要求按格式打印即可,用 total 统计总账单消费。

之所以采用经过分钟数来计算消费,有的读者可能会想着先计算从拨通到挂掉经过多久,再去计算花费。但这就涉及到经过多少天、多少小时、每个小时里经过多少分钟这些很具体的部分,都考虑进去的话会更复杂。直接计算总的时间反而更加简单,对于一个时间 day:hour:minute 来说,计算规则如下:

  • day 天花费: d a y ∗ ( 60 ∗ r a t e [ 1 ] + 60 ∗ r a t e [ 2 ] + ⋅ ⋅ ⋅ + 60 ∗ r a t e [ 23 ] ) = d a y ∗ 60 ∗ ( r a t e [ 1 ] + r a t e [ 2 ] + ⋅ ⋅ ⋅ + r a t e [ 23 ] ) = d a y ∗ 60 ∗ r a t e [ 24 ] day*(60*rate[1]+60*rate[2]+···+60*rate[23]) = day*60*(rate[1]+rate[2]+···+rate[23]) = day*60*rate[24] day(60rate[1]+60rate[2]+⋅⋅⋅+60rate[23])=day60(rate[1]+rate[2]+⋅⋅⋅+rate[23])=day60rate[24]
  • hour 时和 minute 分花费:从当天00:00到 hour:00 的花费 + minute 分钟的花费

( r a t e [ 0 ] ∗ 60 + r a t e [ 1 ] ∗ 60 + ⋅ ⋅ ⋅ + r a t e [ h o u r − 1 ] ∗ 60 ) + r a t e [ h o u r ] ∗ m i n u t e (rate[0]*60+rate[1]*60+···+rate[hour - 1]*60) + rate[hour] * minute (rate[0]60+rate[1]60+⋅⋅⋅+rate[hour1]60)+rate[hour]minute

输出的时候记得要将结果除以100,因为输出样例是按美元为单位输出的。

注意:

  • 没有任何有效通话(即花费为0)的用户不输出。
  • 必须是时间上相邻的 on-line 和 off-line 通话记录才算做有效通话。
#include <iostream>
#include <algorithm>
#include <map>
#include <vector>
using namespace std;

struct record {
    string name;
    int status, month, day, hour, minute, time;
} calls[1005];

bool cmp(record a, record b) { return a.name != b.name ? a.name < b.name : a.time < b.time; }

double calculateBills(record call, int *rate)
{
    double total = rate[call.hour] * call.minute + rate[24] * 60 * call.day;
    for (int i = 0; i < call.hour; ++i) total += rate[i] * 60;
    return total / 100.0;
}

int main()
{
    int rate[25] = {0}, n;
    for (int i = 0; i < 24; ++i)
	{
        scanf("%d", &rate[i]);	// 收费标准
        rate[24] += rate[i];	// 总的收费
    }
    scanf("%d", &n);			// n 条通话记录
    for (int i = 0; i < n; ++i)
	{
        cin >> calls[i].name;	// 用户名称
        scanf("%d:%d:%d:%d", &calls[i].month, &calls[i].day, &calls[i].hour, &calls[i].minute);
		string temp;
        cin >> temp;
        calls[i].status = (temp == "on-line") ? 1 : 0;	// 根据 temp 来判断通话状态
        calls[i].time = calls[i].day * 24 * 60 + calls[i].hour * 60 + calls[i].minute;	// 计算分钟数
    }
    sort(calls, calls + n, cmp);
    map<string, vector<record> > custom;
    for (int i = 1; i < n; ++i)
	{
        if (calls[i].name == calls[i - 1].name && calls[i - 1].status == 1 && calls[i].status == 0)
		{
            custom[calls[i - 1].name].push_back(calls[i - 1]);	// 根据姓名,将拨通的通话记录放入数组中
            custom[calls[i].name].push_back(calls[i]);			// 根据姓名,将挂掉的通话记录也放入数组中
        }
    }

    for (auto it : custom)
	{
        vector<record> temp = it.second;	// 令 temp 保存用户的通话记录数组
        printf("%s %02d\n", it.first.c_str(), temp[0].month);
        double total = 0.0;
        for (int i = 1; i < temp.size(); i += 2)	// 每次计算两条通话记录,所以 i 每轮循环自增2
		{
            double t = calculateBills(temp[i], rate) - calculateBills(temp[i - 1], rate);
            printf("%02d:%02d:%02d %02d:%02d:%02d %d $%.2f\n", temp[i - 1].day, temp[i - 1].hour, temp[i - 1].minute, temp[i].day, temp[i].hour, temp[i].minute, temp[i].time - temp[i - 1].time, t);
            total += t;
        }
        printf("Total amount: $%.2f\n", total);
    }

    return 0;
}

1017


1018 Public Bike Management

题意:

输入第一行是四个正整数,第一个是每个站点最多容纳的自行车数 Cmax,第二个是站点数 N(结点数),第三个是问题站点 Sp,第四个是道路数量 M(边数)。随后一行是 N 个正整数,分别代表编号 1 ~ N 的站点中自行车的数量(根站点编号为0)。然后是 M 行数据,每一行有三个正整数 SiSjTij,Tij 表示站点 Si 和 Sj 之间道路的长度。每个站点自行车停放数量的完美状态Cmax 的一半,现在要从根站点 PBMC 出发前往问题站点,沿途需要使得每个站点的自行车数量都达到完美状态。题目规定了,前往问题站点的过程总是选择最短路径,若不止存在一个最短路径,就选择从问题站点带回自行车数最少的一条路径,且题目保证在最短路径不唯一的情况下,后者是唯一的。输出的时候,先输出从根站点携带的自行车数,然后打印从根站点到问题站点的路径,最后打印从问题站点带回的自行车数。

要注意,沿途的调整是单向的,只在前往问题站点的过程中进行调整。比如,若到达某站点,该站自行车数比完美状态多就收集起来,如果比完美状态少就补充,若手上剩余的自行车数不够补充,就需要从根站点携带,返程途中不再进行调整。

思路:

迪杰斯特拉算法深度优先搜索遍历解决。

d[i] 由起始点 s 到点 i 的最短路径的路径长度,bikes[i] 表示点 i 的自行车数,G[u][v] 表示从点 u 到点 v 的路径长度(值为0时表明 u, v 之间没有边),visit[i] 表示点 i 已被访问过。minneed 保存从根站点需要携带的自行车数的最小值,minremain 保存从问题站点带回的剩余自行车数的最小值,pre[i] 数组保存站点 i 的所有前驱结点,temp 数组保存最短路径,ans 数组保存最终结果的最短路径。

在输入时,不妨将每一个站点的初始自行车数减去 Cmax 再保存到相应的 bikes中,这样就可以通过 bikes 的值是正是负来表明该站点需要取走多少自行车还是补充多少自行车。然后通过迪杰斯特拉算法求出图中的最短路径,因为最短路径可能存在多条,所以需要用数组来保存每一个结点的所有前驱结点

DFS 中执行这么一个过程:由于 pre 数组保存的是前驱结点,所以需要从路径的末尾,也就是问题站点开始递归遍历。对于每一个结点,先将它加入 temp,然后递归遍历其所有前驱结点。当结点的编号为0时,说明回到了根站点,此时就可以求这条路径需要携带的自行车数和剩余自行车数。枚举最短路径中的每一个结点,如果该站点需要补充自行车,就先查看手头上剩余自行车数够不够补充,如果足够,就将 remain 减去补充的值;如果不够,就表明需要从根站点携带自行车,need 就加上额外需要携带的,同时将 remain 清零。枚举完一条路径上的结点后,就更新 minneed 和 minreamin 的值。

在这里需要注意一点,由于 temp 中保存的是由根站点到达问题站点的一条最短路径,但是由于是从问题站点往根站点进行递归的,所以保存的路径是倒过来的,所以在枚举时需要从数组的尾部开始枚举

#include <iostream>
#include <vector>
using namespace std;

int Cmax, N, Sp, M, Ci, Si, Sj, Tij, minneed = 0x3fffffff, minremain = 0x3fffffff;
int bikes[510], G[510][510], d[510];
vector<int> pre[510], temp, ans;
bool visit[510];

void DFS(int v)
{
    temp.push_back(v);
    if (v == 0)
    {
        int need = 0, reamin = 0;
        for (int i = temp.size() - 2; i >= 0; --i)
        {
            if (bikes[temp[i]] < 0)
            {   // 剩余自行车数 >= 该站点缺乏自行车数,就将 reamin 减去缺的值
                int r = -1 * bikes[temp[i]];	// 转换成正数保存在 r 中
                if (reamin >= r) reamin -= r;
                else
                {
                    need += r - reamin;     	// 不够补充时说明需要从 PBMC 携带
                    reamin = 0;             	// 将剩余自行车数清零
                }
            }
            else reamin += bikes[temp[i]];  	// 增加剩余自行车数
        }
        if ((need < minneed) || (need == minneed && reamin < minremain))     
        {   // 如果该条最短路径初始需要携带数更小,或者需要携带数相等但剩余自行车数更少
            ans = temp;         // 更新最短路径
            minneed = need;     // 更新最小需要携带数
            minremain = reamin; // 更新最小剩余自行车数
        }
    }
    for (int i = 0; i < pre[v].size(); ++i) // 枚举结点 v 的所有前驱结点
        DFS(pre[v][i]);
    temp.pop_back();
}

int main()
{
    cin >> Cmax >> N >> Sp >> M;
    for (int i = 1; i <= N; ++i)
    {
        cin >> Ci;
        bikes[i] = Ci - Cmax / 2;
    }
    fill(d, d + N + 1, 0x3fffffff); // 初始化起始点到每个点的最短路径的长度为最大值,注意这里是 N + 1
    while (M--)
    {
        cin >> Si >> Sj >> Tij;
        G[Si][Sj] = G[Sj][Si] = Tij;
    }
    d[0] = 0;              // 到起始点的路径长度初始化为0
    for (int i = 0; i <= N; ++i)        // 循环 N 次来保证访问到 N 个点
    {
        int u = -1, MIN = 0x3fffffff;   // u 使得 d[u] 最小,MIN 保存 d 中最小值
        for (int j = 0; j <= N; ++j)     // 枚举所有的点,寻找未访问过的点中最短道路的点00000000000
        {
            if (!visit[j] && d[j] < MIN)    // 如果 j 未访问过且 d[j] 小于 MIN
            {
                u = j;          // 更新 u
                MIN = d[j];     // 更新 MIN
            }
        }
        if (u == -1) break;     // 找不到小于 0x3fffffff 的数,说明剩下的点和起点 s 都不连通,结束循环
        visit[u] = true;        // 标记 u 为已访问
        for (int v = 0; v <= N; ++v)     // 枚举所有的点
        {
            if (!visit[v] && G[u][v] != 0)          // 如果 v 没有访问过,且由 u 到 v 有边
            {
                if (d[u] + G[u][v] < d[v])          // 如果以 u 为中介点可以使 d[v] 更小
                {
                    d[v] = d[u] + G[u][v];          // 更新最短路径
                    pre[v].clear();                 // 清空 v 的前驱结点数组
                    pre[v].push_back(u);            // 将 u 放入 v 的前驱结点数组
                }
                else if (d[u] + G[u][v] == d[v])    // 如果找到一条相同长度的路径
                    pre[v].push_back(u);            // 将 u 放入 v 的前驱结点数组
            }
        }
    }
    DFS(Sp);
    cout << minneed << " " << ans[ans.size() - 1];
    for (int i = ans.size() - 2; i >= 0; --i)
        cout << "->" << ans[i];
    cout << " " << minremain;

    return 0;
}

1019


1020 Tree Traversals

题意:

输入总共有三行,第一行是结点总数 N,第二行是后序遍历得到的序列,第三行是中序遍历得到的序列。要求你根据后序序列以及中序序列,找出二叉树的层次遍历得到的序列

思路:

题目交代了,序列是二叉树结点的,且不存在重复的值,所以可以将其当做结点的编号。定义结构体 Node 来保存结点左右孩子结点的编号,与其同时就需要定义一个 Node 数组 bt 来保存所有的结点的左右结点的编号,根据编号来访问 bt 数组从而得到其左右孩子结点的编号。post、in、level 数组分别保存后序序列、中序序列以及层次序列下,各结点的编号

这里假定你已经了解了前中后序遍历的原理和特点,如果有不懂的同学可以点击二叉树的遍历和线索二叉树的深刻理解进行学习。

PostInToBTree 函数的原理是这样的:由于后序序列的最后一个元素就是根结点,所以对于某一棵树的后序序列区间 [pl, pr] 来说,k = post[pr] 就是该子树的根结点的编号,对于子树同理。那么在该树的中序序列区间中,找到根结点的位置 i,在 i 的左侧就是该树的左子树中序序列,在 i 的右侧就是该树的右子树中序序列。

因为中序序列区间为 [il, ir],因此在 i 的右侧有 ir - i 个结点。后序遍历是左右中,所以在后序序列中, 比较靠近根结点的是右子树的序列。因此从 pr - 1 往 pl 方向数 ir -i 个结点,便是右子树的后序序列,令 rightnum = ir - i。假设右子树后序序列的第一个结点的索引为 x,便有 (pr- 1) - x + 1 = rightnum,得到 x = pr - rightnum。从而得到:

  • 左子树后序序列区间:[pl, pr - rightnum - 1],右子树后序序列区间:[pr - rightnum, pr - 1]。
  • 左子树中序序列区间:[il, i - 1],右子树中序序列区间:[i + 1, ir]。

函数最后返回的是该树的根结点的编号对左右子树分别递归调用 PostInToBTree 函数,并用 lchild 和 richild 保存返回的编号,最后就能将整个二叉树的结构还原了。

注意:

  • 测试点5测试的是比较极端的情况。题目只说了结点数不大于30,但这30个结点有可能构成一层一个结点的二叉树。如果你的代码打算用结点在一棵完全二叉树中的编号来保存每个结点,那很有可能就产生段错误了。
#include <iostream>
#include <queue>
using namespace std;

struct Node
{
    int lchild = 0, rchild = 0;    // 左右孩子结点的编号
};

int N, post[35], in[35], level[35];
Node bt[35];

int LevelOrder(int postleft, int postright, int inleft, int inright)
{
    if (postleft > postright) return 0;     // 区间长度为0时返回
    int k = post[postright], i;             // 获取根结点的编号
    for (i = inleft; i <= inright; ++i)     // 寻找中序序列下根结点的编号 i
        if (k == in[i]) break;
    bt[k].lchild = LevelOrder(postleft, postright - inright + i - 1, inleft, i - 1);   // 递归处理左子树
    bt[k].rchild = LevelOrder(postright - inright + i, postright - 1, i + 1, inright); // 递归处理右子树

    return k;   // 返回根结点的编号
}

int main()
{
    cin >> N;
    for (int i = 0; i < N; ++i) cin >> post[i];
    for (int i = 0; i < N; ++i) cin >> in[i];
    int root = LevelOrder(0, N - 1, 0, N - 1);       // 获取根结点的编号
    queue<int> ans;
    ans.push(root);
    while (!ans.empty())
    {
        if (ans.front() != root) cout << " ";           // 除了根结点,其他结点前都要打印空格
        cout << ans.front();                            // 打印队首结点编号结点
        if (bt[ans.front()].lchild) ans.push(bt[ans.front()].lchild);   // 左孩子不空就入队
        if (bt[ans.front()].rchild) ans.push(bt[ans.front()].rchild);   // 右孩子不空就入队
        ans.pop();  // 队首编号出队
    }

    return 0;
}

别急着走。在这里我给你提供一个更好的思路:

我们知道,如果从1开始、从上至下、从左至右给一棵完全二叉树编号,对于一个编号为 i 的结点,其左子结点的编号就为 2 * i右子结点的编号就为 2 * i + 1。因为在 PostInToBTree 函数中,会找到所有的子树的根结点。所以不妨换个思路,在 PostInToBTree 函数中加上一个参数 index,index 表示该结点在完全二叉树中的编号。定义一个哈希映射 level,将每个结点的编号对应到“键”上(前面的思路是将“键”当做编号),这样就不需要用一个完全二叉树大小的数组来保存所有结点,而是存在该编号我们才去将其保存。

那么怎么找到每个结点对应的编号呢?修改 PostInToBTree 函数,传入子树的根结点的索引 root,只传入中序序列区间 [left, right]。然后 i 依然保存中序序列根结点的位置,将 post[root] 作为值,给到键 index,即 level[index] = post[root],表明完全二叉树中编号为 index 的值为 post[root]。然后对左右子树分别递归调用 PostInToBTree 函数,递归左子树,就传入左子树根结点的编号 2 * index;递归右子树,就传入右子树根结点的编号 2 * index + 1。

因为 PostInToBTree 函数会访问到所有子树的根结点,所以它可以将所有的的结点都映射到对应的编号上。为什么要用 map 来定义呢?因为 map 可以根据键(即编号)自动排序,按照编号顺序打印出各个结点的 key,刚好就是层次遍历序列。

#include <iostream>
#include <map>
using namespace std;

int N, post[35], in[35];
map<int, int> level;

void LevelOrder(int root, int left, int right, int index)
{
    if(left > right) return ;   // 区间长度为0时返回
    int i = left;               // i 保存中序序列下根结点的位置(索引)
    while(i < right && in[i] != post[root]) ++i;                
    level[index] = post[root];
    LevelOrder(root - right + i - 1, left, i - 1, 2 * index);    // 递归处理中序序列左子树
    LevelOrder(root - 1, i + 1, right, 2 * index + 1);           // 递归处理中序序列右子树
}
int main()
{
    cin >> N;
    for (int i = 0; i < N; ++i) cin >> post[i];
    for (int i = 0; i < N; ++i) cin >> in[i];
    LevelOrder(N - 1, 0, N - 1, 1);
    auto it = level.begin();
    cout << it->second;
    while(++it != level.end()) cout << " " << it->second;

    return 0;
}

一定要自己写一遍哦~~

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值