赞
踩
题意:
输入第一行是四个正整数,第一个是城市(点)的总数 N,第二个是城市之间道路(边)的总数 M,起点城市编号 C1 和终点城市编号 C2。随后一行是 N 个点,表示编号为 0 ~ N - 1 的城市的救援队伍的数量(点的权重)。然后是 M 行数据,每行三个整数 c1、c2 和 L,分别表示两个城市的编号,以及它们之间道路的长度(边长)。现在要求你计算,从起始点 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; }
题意:
输入第一行是两个正整数,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; }
思路:
这题不要想太多,不是像平常电梯运行那样,只能上到顶,然后再下到底,这一题就是来一层去一层。
注意:
#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; }
题意:
输入只有一行,分别是整数 N1 和 N2,标签 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 进制太大了,上界需要变小。
对二分法不熟悉的读者可以学习这篇博客:二分法及其拓展全面讲解。
注意:
#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; }
题意:
输入第一行给出学生总数 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,而全局数组就能直接通过下标访问来判断是否存在,节省许多的时间。
注意:
#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; }
题意:
输入第一行是城市(图的结点)的数量 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; }
题意:
给出正整数 N 和进制 D,如果 N 是素数,且 N 在进制 D 下反转后的数在十进制下也是素数,则输出“Yes";否则输出“No”。比如73是素数,在十进制下反转后得到的37也是素数,所以输出“Yes”。23是素数,其二进制表示为10111,反转后得到的11101在十进制下为29,也是素数,所以输出“Yes”。
思路:
注意点:
#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; }
题意:
输入第一行给出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 来说,计算规则如下:
( 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[hour−1]∗60)+rate[hour]∗minute
输出的时候记得要将结果除以100,因为输出样例是按美元为单位输出的。
注意:
#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; }
题意:
输入第一行是四个正整数,第一个是每个站点最多容纳的自行车数 Cmax,第二个是站点数 N(结点数),第三个是问题站点 Sp,第四个是道路数量 M(边数)。随后一行是 N 个正整数,分别代表编号 1 ~ N 的站点中自行车的数量(根站点编号为0)。然后是 M 行数据,每一行有三个正整数 Si、Sj 和 Tij,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; }
题意:
输入总共有三行,第一行是结点总数 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。从而得到:
函数最后返回的是该树的根结点的编号。对左右子树分别递归调用 PostInToBTree 函数,并用 lchild 和 richild 保存返回的编号,最后就能将整个二叉树的结构还原了。
注意:
#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; }
一定要自己写一遍哦~~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。