PAT-甲级 做题笔记
目录
0000 做题 Tips 基本经验
1003 Emergency (Dijkstra 算法)
1004 Counting Leaves (计算叶节点数,DFS/BFS 树算法)
1007 Maximum Subsequence Sum(最大子序列和)
1010 Radix (进制转换/二分法)
1012 The Best Rank (应用问题,数据结构设计,多维度排序)
1013 Battle Over Cities (图的遍历,统计强连通分量的个数,DFS)
1014 Waiting in Line (队列应用,排队问题)
1015 Reversible Primes (进制转换+判断素数:可取用函数)
1016 Phone Bills (日期类计算)
1018 Public Bike Management (图最短路径 Dijkstra + DFS)
1020 Tree Traversals (已知后序和中序,转前序/层序)
1021 Deepest Root (求树中最长的路径,DFS,连通分量)
0000 做题 Tips 基本经验<span id="0000"></span>
最后千万别栽在头文件上,比如 reverse() 属于 < algorithm>
-
输出多行的时候,最后有一个多余的空行也没问题
比如使用cout << a << endl
三次,样例输出:3 4 5
实际输出:最后有一个多余空行没关系
3 4 5
1003 Emergency (Dijkstra 算法)<span id="1003"></span>
1. 题目大意
n个城市m条路,每个城市有救援小组,所有的边的边权已知。给定起点和终点,求从起点到终点的最短路径条数以及最短路径上的救援小组数目之和。如果有多条就输出点权(城市救援小组数目)最大的那个
2. 分析
用一遍Dijkstra算法~救援小组个数相当于点权,用Dijkstra求边权最小的最短路径的条数,以及这些最短路径中点权最大的值~dis[i]表示从出发点到i结点最短路径的路径长度,num[i]表示从出发点到i结点最短路径的条数,w[i]表示从出发点到i点救援队的数目之和~当判定dis[u] + e[u][v] < dis[v]的时候,不仅仅要更新dis[v],还要更新num[v] = num[u], w[v] = weight[v] + w[u]; 如果dis[u] + e[u][v] == dis[v],还要更新num[v] += num[u],而且判断一下是否权重w[v]更小,如果更小了就更新w[v] = weight[v] + w[u]
3. 个人代码
4. 学习要点
1.真正理解单源最短路径,每一次选择的 dis 都是相对于源点的最短路径
2.大数组要设为全局变量
3.外层循环 i:n 次循环,每次访问一个新点,保证 n 个点全访问到;内层循环 j:继续寻找还未访问的点,找 dis 最小的访问;内层循环 k:更新所有能够更新的 dis
1004 Counting Leaves (dfs/bfs 树算法)<span id="1004"></span>
1. 题目大意
给出一棵树,问每一层各有多少个叶子结点
2. 坑
list[tmp].lev = list[fa].lev + 1; //出问题,不一定按照顺序输入
有可能先 03 后 01,fa 的 lev 还没确定,不能给孩子节点 +1
3 2
03 1 02
01 1 03
3. 个人代码
4. 正确方法
void dfs(int fa){
for (auto &ch : list[fa].v) {
list[ch].lev = list[fa].lev + 1;
dfs(ch);
}
}
dfs(1); //通过 DFS 一层层给叶子节点 lev+1
1007 Maximum Subsequence Sum (最大子序列和)<span id="1007"></span>
1.题目大意
求最大连续子序列和,输出最大的和以及这个子序列的开始值和结束值。如果所有数都小于0,那么认为最大的和为0,并且输出首尾元素
2.分析
本质上是动态规划的思想,数组为vec[]
,设dp[i]
是以vec[i]
结尾的子数组的最大和,对于元素vec[i+1]
, 它有两种选择:vec[i+1]
接着前面的子数组构成最大和; vec[i+1]
自己单独构成子数组。则dp[i+1] = max{dp[i]+vec[i+1], vec[i+1]}
简化则用一个 temp_sum 和一个 temp_first 解决,真正最大和为 sum,起始点为 first 和 last,建立局部和全局的关系。如果 temp_sum < 0
, 说明目前这一段对后续的序列和已经没有加成作用,可以舍弃另立门户,令temp_sum = 0
3. 个人代码
//初始值设置容易坑:sum要为-1 才能开始
int sum=-1, temp_sum=0, first=0, last=k-1, temp_first=0;
for (int j = 0; j < k; ++j) {
temp_sum += a[j];
if(temp_sum < 0){
temp_sum = 0;
temp_first = j+1; //本段已经可以舍弃,开始新一段
} else if(temp_sum > sum){
sum = temp_sum;
first = temp_first;
last = j;
}
}
if(sum < 0) sum = 0; //如果全负则为 0,根据题目要求不要漏情况
4. 类似题
1010 Radix (进制转换/二分法)<span id="1010"></span>
1. 题目大意
给定两个相等数,已知一个数的进制,求另外一个数的进制(radix->基数)
2. 分析
通用函数cal(string str, int radix)
把任意进制的数转换为 10 进制数
在使用搜索遍历,找到另一个数对应的进制
3.个人代码
转换函数
long long cal(string str, long long radix){
long long target=0, bak = 1;
reverse(str.begin(), str.end());
for (char c : str) {
if(isdigit(c))
target += (c - '0') * bak;
else
target += (c - 'a' + 10) * bak;
bak *= radix; //也可以使用<cmath>的 pow()函数
}
return target;
}
搜索函数
long long find_radix(string str, long long tar){
char c = *max_element(str.begin(), str.end()); //最大字母
//进制至少比最大字母要大
long long low = isdigit(c) ? c-'0' : c-'a'+10;
low += 1; //必须要加一,至少多1
long long high = max(low, tar); //进制最大不会大于目标 tar
while (low <= high){
long long mid = (low+high)/2;
long long tmp = cal(str, mid);
if(tmp < 0 || tmp > tar) high = mid - 1; //小于 0 也是进制太大
else if(tmp == tar) return mid;
else low = mid + 1;
}
return -1;
}
4. 学习要点
- 进制转换这种大数字的乘法很容易溢出,必须要用
long long
- 暴力搜索(从 2 到 ∞ 容易超时)最好用二分法,并且限定范围:最小是
low
至少是最大的那个字母+1,比如 fff 最少是 15+1=16 进制,最大是high
不超过目标tar
- 可以使用反向迭代器
it = n.rbegin(); it != n.rend()
代替reverse()
1012 The Best Rank (数据结构设计/多维度排序)<span id="1012"></span>
1. 题目大意
找出每个人自己最优势的科目,也就是单独排名最好的科目,优先级 A > C > M > E
- Sample Input:
5 6
310101 98 85 88
310102 70 95 88
310103 82 87 94
310104 91 91 91
310105 85 90 90
310101
310102
310103
310104
310105
999999
- Sample Output:
1 C
1 M
1 E
1 A
3 A
N/A
2. 个人代码
3. 学习要点
数据结构设计
struct node{
//一定要用数组形式,避免过多变量 int c, m, e, c_r, m_r, e_r, a_r;
int id, best;
int score[4], rank[4];
}stu[2001]; //输入学生序列,非学号排序
int exist[1000000], flag=-1; //exist 至少 >0, 顺便建立 id-输入序号 映射
//使用 flag 配置作用,避免写过多重复的 cmp 函数
bool cmp(node &a, node &b){ return a.score[flag] > b.score[flag];}
输入后,利用exist[stu[k].id] = k + 1; k++
依次建立映射
输出时,利用cin >> tmp; ex = exist[tmp]
和stu[ex-1]
实现映射,无需 find
处理时,使用sort(stu, stu+n, cmp)
循环 flag++
,实现分学科多次排序,确定 rank[]
1013 Battle Over Cities(图的遍历,统计强连通分量的个数,dfs)<span id="1013"></span>
1. 题目大意
给出n个城市之间有相互连接的m条道路,当删除一个城市和其连接的道路的时候,问其他几个剩余的城市至少要添加多少个路线,才能让它们重新变为连通图
2. 分析
n 连通分量,最少需要 n-1 条边相连,所以本题实质就是求「去除某点及其相连的边」后,剩余的「连通分量个数」是一个图算法,需要用图的遍历来解决 (DFS)
求连通分量依据:一次
dfs()
走到底,可以访问完一个连通分量
数据结构:
涉及图的遍历,要考虑
visit[]
数组
图的路径存储,使用二维矩阵
3. 个人代码
for (int i = 0; i < k; ++i) {
int cur, cnt=0;
cin >> cur;
fill(visited, visited+n, false);
visited[cur] = true; //相当于把本点去除
for (int j = 1; j <= n; ++j) { //依次计算所有连通分量,而非从本点出发
if(!visited[j]){
dfs(j);
cnt++;
}
}
cout << cnt-1 << endl;
}
void dfs(int st){ //只用来 visit,不负责计数
visited[st] = true;
for (int i = 0; i <= n; ++i)
if(!visited[i] && e[st][i]){ //未访问且有路
dfs(i);
}
}
1014 Reversible Primes(队列应用 排队问题)<span id="1014"></span>
1. 题目大意
n个窗口,每个窗口可以排队m人。有k位用户需要服务,给出了每位用户需要的minute数,所有客户在8点开始服务,如果有窗口还没排满就入队,否则就在黄线外等候。如果有某一列有一个用户走了服务完毕了,黄线外的人就进来一个。如果同时就选窗口数小的。求q个人的服务结束时间。
如果一个客户在17:00以及以后还没有开始服务(此处不是结束服务是开始17:00)就不再服务输出sorry;如果这个服务已经开始了,无论时间多长都要等他服务完毕。
2. 学习要点
面对此类题目,总想按照 timeline 每分每秒去决策,其实队列的问题用队列解决。
以每个窗口为单位设计结构体,poptime
用于刚入黄线的人决策,endtime
用于已经在队内的人是否会 sorry 决策,q
表示本队列内排队情况。
给定客户序列,则以每一个客户为循环单位(index++)
,队列有一个进就会有一个出,每次变动一个单位,更新各个队列的poptime endtime q
等变量。
struct node {
int poptime, endtime; //队首的人出队(结束)的时间, 队尾的人结束的时间
queue<int> q;
};
int index = 1;
//第 1 步:n*m 个客户先抢占黄线内, 直接瞬间涌入,不存在选择
for (int i = 1; i <= cap; ++i) { //看做行和列,先抢第一行
for (int j = 1; j <= n; ++j) {
if(index <= k){
win[j].q.push(time[index]);
if(win[j].endtime >= 540)
sorry[index] = true;
win[j].endtime += time[index];
if(i == 1) //对于第一行,出队时间即本队结束时间,做一个初始化
win[j].poptime = win[j].endtime;
res[index] = win[j].endtime; //刚进来的,自己是队尾,队伍结束就是自己结束
index++;
}
}
}
//第 2 步:后续的客户
while (index <= k){ //while一次循环表示一个客户的选择
int temp_min = win[1].poptime, temp_win = 1; //初始化
for (int i = 2; i <= n; ++i) { //找最早出队的
if(win[i].poptime < temp_min){
temp_win = i;
temp_min = win[i].poptime;
}
}
win[temp_win].q.pop();
win[temp_win].q.push(time[index]);
win[temp_win].poptime += win[temp_win].q.front(); //更新:最前面那个人完成的时间就是出队时间
if(win[temp_win].endtime >= 540) //endtime 还未更新,是看的前一个人的完成时间,是否超时
sorry[index] = true;
win[temp_win].endtime += time[index]; //更新:刚进来的这个人的完成时间就是本队的完成时间
res[index] = win[temp_win].endtime;
index++;
}
1016 Phone Bills(日期类计算)<span id="1016"></span>
1. 题目大意
给定固定格式的日期mm:dd:hh:mm
计算各个时间的差值,以及每个时间段有不同的费率,计算总的账单费用。
2. 学习要点
1.数据结构设计
因为输入是给定一个个 call 序列,所以便于存储,也以每个 call 为单位,统一放在一个数组里,按照名字、时间排序,计算的时候,只要考虑 i 和 i-1 前后两个元素是否有同一个 name 以及 status 一个为 0 一个为 1 即可。
struct node {
string name;
int status, month, time, day, hour, minute;
};
bool cmp(node a, node b) {
return a.name != b.name ? a.name < b.name : a.time < b.time;
}
2.日期差值计算
直接”01:05:02:24“-”01:04:23:59“ 难以计算,每个小时还有不同的费率,所以 统一从本月 0 天 0 点开始计算,以dd:hh:mm
格式再相减即可,隔天的情况则加上这一整天
double billFromZero(node call, int *rate) {
double total;
total = rate[call.hour]; //本小时的费用
total += call.minute + rate[24] * 60 * call.day; //从本月第 0 天到今天
for (int i = 0; i < call.hour; i++)
total += rate[i] * 60; //本天内 0 点到现在累加
return total / 100.0;
}
1018 Public Bike Management(图最短路径 Dijkstra + DFS)<span id="1018"></span>
图例经过 Dijkstra 之后的结果
dis[1] = 1 pre[1] = {0}
dis[2] = 1 pre[2] = {0}
dis[3] = 2 pre[3] = {1, 2}
1020 Tree Traversals (已知后序和中序,转前序/层序)<span id="1020"></span>
1. 输入格式
7
2 3 1 5 7 6 4
1 2 3 4 5 6 7
2. 分析
和手动模拟差不多,找到根节点 (pre 的第一个,post 的最后一个) -> 找到左右两段的端点,分别作为左右子树 -> 递归重复。关键在于递归的写法,端点的寻找。
3. 个人代码
level
的作用即按照 index
存储,利用 2n+1 2n+2 的公式可以定位左右孩子,而索引的顺序恰好是层序遍历的顺序(具体第几层不知道)
转为前序遍历,只需在 pre 递归前打印 root 即可
vector<int> in, post, level(100000, -1);
void pre(int root, int start, int end, int index){ //start end 都是相对于中序序列
if(start > end) return;
level[index] = post[root];
int i = start;
while (i < end && in[i] != post[root]) i++; //找到中序 in 序列中的根节点
int left_size = i-start, right_size = end-i;
pre(root-right_size-1, start, i-1, index*2+1); //此 root 即为左孩子节点,可以确定为 index*2+1
pre(root-1, i+1, end, index*2+2);
//找到根节点 i,左子树 start ~ i-1, 右子树 i+1 ~ end
}
pre(n-1, 0, n-1, 0);
1021 Deepest Root (求树中最长的路径,DFS,连通分量)<span id="1021"></span>
1. 题目大意
给出n个结点(1~n)之间的n条边,问是否能构成一棵树,如果不能构成则输出它有的连通分量个数;如果能构成一棵树,输出能构成最深的树的高度时,树的根结点。如果有多个,按照从小到大输出。
2. 分析
连通分量的个数:基本套路,一次 DFS 走完一个连通分量,统计循环中 DFS 开启的次数即可
本题关键在于,两次 DFS 即可求出所有最远端点(即所有最长路径中的所有端点)如果是一棵树的话,线段无非是两个端点,第一次 DFS 能找到一端,在这些最远端点中(可能有多个同样最远的,可能只有一个最远的)随便选一个作为起点,再来一次 DFS,即可找到另一端的最远端点。
3. 个人代码
void dfs(int node, int height) {
if(height > maxheight) {
temp.clear();
temp.push_back(node);
maxheight = height;
} else if(height == maxheight){
temp.push_back(node);
}
visit[node] = true;
for(int i = 0; i < v[node].size(); i++) {
if(visit[v[node][i]] == false)
dfs(v[node][i], height + 1);
}
4. 做题 Tips
- 本题不用考虑权重,所以无需开一个二维数组用邻接矩阵存储
e[10010][10010]; e[a]=e[b]=weight;
只需使用邻接表,把和自己直接相连的点存起来即可
v[node][i]; resize(n+1);
- DFS 基本套路
dfs(int node, ...){ //遍历过程中顺带做其它操作 visit[node] = true; for 与本点直接相连的 i: if(!visit[i]) dfs(i, ...) }