PAT Advanced Level Practice 解题思路和代码,主要用的是 C++。每22题一篇博客,可以按目录来进行寻找。
文章目录
- 1041 Be Unique
- 1042
- 1043 Is It a Binary Search Tree
- 1044 Shopping in Mars
- 1045
- 1046
- 1047 Student List for Course
- 1048 Find Coins
- 1049 Counting Ones
- 1050 String Subtraction
- 1051 Pop Sequence
- 1052 Linked List Sorting
- 1053 Path of Equal Weight
- 1054 The Dominant Color
- 1055 The World's Richest
- 1056 Mice and Rice
- 1057
- 1058
- 1059 Prime Factors
- 1060 Are They Equal
1041 Be Unique
题意:
输入只有一行,第一个数是参与打赌的人数 N,随后 N 个赌数。现在要求你输出,这 N 个数中,第一个出现次数为1的数,没有符合要求的数则输出 None。
思路:
定义数组 num 保存每个人赌的数,数组 cnt 保存每个赌数出现的次数。输入 N 个数时,将其存入 num 中,同时计数值 cnt[num[i]] 加1。枚举 num 中的每个数,如果其出现次数是1,就输出赌数然后 return。
#include <iostream>
using namespace std;
int num[100005], cnt[100005];
int main()
{
int N, bet;
cin >> N;
for (int i = 0; i < N; ++i)
{
cin >> num[i];
++cnt[num[i]];
}
for (int i = 0; i < N; ++i)
{
if (cnt[num[i]] == 1)
{
cout << num[i];
return 0;
}
}
cout << "None";
return 0;
}
1042
1043 Is It a Binary Search Tree
题意:
输入第一行给出树中结点个数 N,随后一行是 N 个正整数,表明树中各结点的键值。该 N 个值按照左小右大于等于的规则构成一棵二叉排序树,或者是左大于等于右小于的规则构成其镜像树。现在要求你判断第二行的输入是否为该二叉排序树或其镜像树的前序遍历序列,如果是的话输出 YES,然后输出其后序遍历序列;否则输出 NO。
思路:
使用递归遍历解决。
常规思路——将第二行的值保存到数组 init 中,表明其为初始的序列。将序列中的值逐个插入到二叉排序树中,构建一棵左小右大于等于的二叉排序树,以及一棵左大于等于右小于的镜像树(也是二叉排序树)。然后分别先序遍历、后序遍历这两棵树,将两个先序遍历序列分别保存到 pre 和 mpre 中,将后序遍历序列分别保存到 post 和 mpost 中。再检查 pre 和 mpre 谁等于 init(有且只有一个会等于 init),如果 pre 等于 init,就打印 post,如果 mpre 等于 init,就打印 mpost,否则输出 NO。
#include <iostream>
#include <vector>
using namespace std;
struct Node
{
int data;
Node *left = nullptr, *right = nullptr;
Node (int x) { data = x; }
};
void Insert(Node *&root, int x, int flag)
{
if (root == nullptr) // 到达空结点,即为需要插入的位置
{
root = new Node(x); // 新建空结点
return; // 必须返回,否则会无限新建结点
}
if (flag) // flag 为0表明建立左小右大于等于的二叉排序树,反之建立其镜像树
{
if (x < root->data) Insert(root->left, x, flag);
else Insert(root->right, x, flag);
}
else
{
if (x >= root->data) Insert(root->left, x, flag);
else Insert(root->right, x, flag);
}
}
void PreOrder(Node *root, vector<int> &v)
{
v.push_back(root->data);
if (root->left) PreOrder(root->left, v);
if (root->right) PreOrder(root->right, v);
}
void PostOrder(Node *root, vector<int> &v)
{
if (root->left) PostOrder(root->left, v);
if (root->right) PostOrder(root->right, v);
v.push_back(root->data);
}
void Print(vector<int> v) // 打印后序遍历序列
{
cout << "YES\n" << v[0]; // 第一个元素前不打印空格
for (int i = 1; i < v.size(); ++i)
cout << " " << v[i];
}
int main()
{
int N, n;
cin >> N;
Node *root = nullptr, *mroot = nullptr;
vector<int> init, pre, mpre, post, mpost;
while (N--)
{
cin >> n;
init.push_back(n);
Insert(root, n, 0); // 将 n 插入左小右大的二叉排序树
Insert(mroot, n, 1); // 将 n 插入镜像树
}
PreOrder(root, pre); // 先序遍历二叉排序树
PreOrder(mroot, mpre); // 先序遍历镜像树
PostOrder(root, post); // 后序遍历二叉排序树
PostOrder(mroot, mpost); // 后序遍历镜像树
if (pre == init) Print(post);
else if (mpre == init) Print(mpost);
else cout << "NO";
return 0;
}
更简洁的思路(来源于柳婼大神):
由先序遍历的特点可以知道,如果输入的序列是二叉排序树或其镜像树的先序遍历序列,那么序列中的第一个元素一定是根结点。因此 GetPostOrder 函数的第一个参数是树的根结点。
GetPostOrder 函数主要完成这么一件事:因为第一个元素 root 是根结点,因此令变量 i 指向 i 右侧的元素,j 指向序列中最后一个元素 tail。对 i 和 j 分别执行循环,如果这是一棵左小右大于等于的二叉排序树,i 就寻找并标记第一个大于 root 值的结点,j 寻找并标记第一个小于等于 root 值的结点。
那么由于二叉排序树的特点所在,左子树的所有结点值都小于根结点的值,右子树的所有结点值都大于等于根结点的值。以第一个输入 8 6 5 7 10 8 11 为例,循环结束时 j 指向的是7,i 指向的是10。可以清楚地看到,i 和 j 的差值刚好为1,这也符合将一棵二叉排序树按照从小到大的顺序保存在数组中的逻辑,因此 i 和 j 的差值是否为1是递归结束返回的标志。
由此我们便可以知道,从 root + 1 到 j 的所有结点都是根结点的左子树中的结点,从 i 到 tail 的所有结点都是根结点的右子树中的结点。递归遍历左右子树,然后将根结点的值放入数组 v 中,便能得到当前树的后序遍历序列。但是这是不是就是题目要求的后序遍历序列还需要做进一步的判断。
如果这是一棵镜像树,递归便会更早结束,从而无法递归树中的所有结点,因此得到的数组 v 的长度不会等于 N。通过这个来判断其是否为镜像树,进而设置 ismirror 的值。最后再调用一次 GetPostOrder 函数,将后序遍历的结果保存在 ans 中。对于镜像树的递归遍历,i 和 j 的规则是反过来的,所以在 GetPostOrder 中加了个对 ismirror 的判断。
部分代码有所区别,核心逻辑仍然是她的思路。
#include <iostream>
#include <vector>
using namespace std;
bool ismirror = false;
vector<int> pre(1005), temp, post;
void GetPostOrder(int root, int tail, vector<int> &v)
{
int i = root + 1, j = tail;
if (ismirror) // 左大于等于右小于的镜像树
{
while(i <= tail && pre[root] <= pre[i]) ++i;
while(j > root && pre[root] > pre[j]) --j;
}
else // 左小右大于等于的二叉排序树
{
while(i <= tail && pre[root] > pre[i]) ++i;
while(j > root && pre[root] <= pre[j]) --j;
}
if (i - j != 1) return; // 结束递归
if (root + 1 <= j) GetPostOrder(root + 1, j, v); // 获取左子树的后序遍历序列
if (i <= tail) GetPostOrder(i, tail, v); // 获取右子树的后序遍历序列
v.push_back(pre[root]); // 将根结点的值放入数组 v 中
}
int main()
{
int N;
cin >> N;
for (int i = 0; i < N; ++i) cin >> pre[i];
GetPostOrder(0, N - 1, temp); // 假定初始都是二叉排序树,得到一个后续遍历序列
ismirror = temp.size() == N ? false : true; // 根据该后序遍历序列重新判断 ismirror 的值
GetPostOrder(0, N - 1, post); // 获取真正的后序遍历序列
if (post.size() == N)
{
cout << "YES\n" << post[0];
for (int i = 1; i < N; ++i)
cout << " " << post[i];
}
else cout << "NO";
return 0;
}
1044 Shopping in Mars
题意:
输入第一行给出两个正整数:链子上的钻石数 N,顾客需要支付的总数目 M。随后一行是 N 个正整数,表示链子上每一个钻石的价值。假设该序列中,任意连续子序列的和为 m。现在要求你找出所有 m = M 的连续子序列,并输出该子序列的头和尾的序号(从1开始)。如果 m != M,就寻找最小的一个 m(m > M),输出其连续子序列的头和尾的序号。
思路:
正常的双重循环是必定会超时的,这个不需要思考。在这里采用二分查找的思想,但并不是真的进行查找,而是类似于二分查找一样,有一个会动态变化的上下界。
定义全局数组 chain 保存所有钻石的价值,N 和 M 也都定义成全局的,main 函数中定义 ans 接受向 findOptions 函数传入参数 M 后返回的结果。
findOptions 函数根据传入的 m (注意不是 M)的值,来寻找所有连续的和为 m 的子序列,并打印符合条件的序列的头和尾的序号。首先定义变量 i 和 j 分别表示区间下界和上界。sum 保存 [i, j] 中所有整数的(也就是当前 i 和 j 标记出的连续子序列的和),mini 保存与 m 的最小差值。
随后开始循环。首先检查 sum 与 m 的关系,若 0 <= sum - M < mini,那么就更新最小差值。随后检查 sum 和 m 的大小关系:
- 当 sum = m 时,表明当前区间 [i, j] 内所有整数的和为 m,符合题目要求,因此输出 i 和 j 的值,也即为连续子序列的头和尾的序号;
- 当 sum < m 或者 i = j 时,此时需要将上界往后走一位,来使区间内的和变大;
- 当 sum > m 时,此时需要将下界往后走一位,来使区间内的和变小。
循环结束后返回最小差值。ans 得到最小差值后,先检查其值是否为0,为0说明最小的 m 就等于 M,因此程序结束;若 ans 不为0,说明不存在 m = M,就将 ans + M 传入 findOptions 即可。
注意:
因为我是一遍过的,所以不知道测试点有哪些坑,但还是附一组我随机生成的测试数据:
16 16
2 13 1 19 5 3 17 4 12 18 20 16 0 10 6 14
应当输出结果:
1-3
8-9
12-12
12-13
13-15
#include <iostream>
using namespace std;
int chain[100005], N, M;
int findOptions(int m)
{
int i = 1, j = 1, sum = chain[1], mini = 0x3fffffff;
while (i <= N && j <= N)
{
if (0 <= sum - m && sum - m < mini) mini = sum - m;
if (sum == m)
{
cout << i << "-" << j << endl;
sum += chain[++j]; // 与 m 相等时 j 后移,sum 变大
}
else if (sum < m || i == j) // 小于 m 或 i,j 相遇时,j 后移,sum 变大
sum += chain[++j];
else sum -= chain[i++]; // 大于 m 时 i 后移, sum 变小
}
return mini; // 返回最小差值
}
int main()
{
cin >> N >> M;
for (int i = 1; i <= N; ++i) cin >> chain[i];
int ans = findOptions(M);
if (ans != 0) findOptions(ans + M); // 若最小差值不为0,就按最近的继续寻找
return 0;
}
1045
1046
1047 Student List for Course
题意:
输入第一行是查询课程的学生数量 N,总的课程数 K;接下来 N 行,每行输入学生的姓名、选课数量、以及他所选的所有的课程 id。要求你按课程 id 从小到大输出所有课程的选课学生姓名。
思路:
定义两个全局数组:一个二维数组 stu_name,一维是索引,二维是姓名字符串,保存学生的姓名;另一个是课程数组 course。前者可以直接用一维的下标来访问到学生的姓名,排序时也只需要传入下标而不是传入姓名,省时 + 方便。
输入 N,K 后,枚举 i 从1 ~ N - 1,i 即为学生下标,再输入学生姓名 stu_name[i] 和选课数量 regist_num,再将该学生的选的所有课程 id 依次保存到 course[i] 就好,course[i] 是一个 vector 数组。输入完后,枚举 i 从 1 ~ K,表示课程 id,访问每一个 course[i],里面保存的是所有学生的姓名。先排序,再按要求输出即可。
经验:
- 像这种会输入很多字符串的题目使用 char 数组来存放字符串。
- 使用全局数组 + vector 有效防止空间超限的问题。
注意点:
- 使用 string 来存储学生姓名的话,最后一组数据会超时。
- 如果排序时直接对字符串排序,那么会导致大量的字符串移动,非常消耗时间。因此使用对应的下标来代替字符串进行排序,消耗的时间会少很多。
- strcmp 的返回值不一定是-1、0、+1,也有可能是其他正数和负数。这是因为 C++ 标准中并没有规定返回值必须是-1、0、+1,所以不同的编译器在这点上的实现不同。因此,在写 cmp 函数时不能写 strcmp 的返回值等于-1,而必须写小于0,否则不具有普适性,甚至直接出错。
#include <cstdio>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
const int maxn = 40010; // 最大的学生数量
const int maxc = 2510; // 最大的课程数量
// 定义为全局数组防止空间超限
// 同时可以使得 cmp 只传入数组下标即可
char name[maxn][5]; // maxn 个学生
vector<int> course[maxc]; // course[i] 存放第 i 门课的所有学生编号
bool cmp(int a, int b) // 按姓名字典序从小到大排序
{
return strcmp(name[a], name[b]) < 0;
}
int main()
{
int n, k, c; // n 为学生人数,k 为课程数量,c 为每个学生的选课数量
scanf("%d%d", &n, &k);
for (int i = 0; i < n; ++i)
{
int d; // 学生的选课编号
scanf("%s %d", name[i], &c);
for (int j = 0; j < c; ++j)
{
scanf("%d", &d);
course[d].push_back(i); // 将学生 i 加入课程 d 中
}
}
for (int i = 1; i <= k; ++i)
{
printf("%d %d\n", i, course[i].size());
sort(course[i].begin(), course[i].end(), cmp); // 对课程 i 下的学生姓名进行排序
for (int j = 0; j < course[i].size(); ++j)
printf("%s\n", name[course[i][j]]); // 输出选课程 i 的学生的姓名
}
return 0;
}
1048 Find Coins
题意:
输入第一行给出两个正整数:硬币总数 N 和需要支付的数额 M,随后一行是 N 个硬币的面值。现在要求你从中找出两个硬币 V1 和 V2,使得其和为 M。当有多个组合时,选出最小的 V1,如果没有符合要求的组合就输出 No Solution。
思路:
定义全局数组 face 保存每个币值的硬币的数量。输入完成后,用 i 枚举所有的币值。符合要求的组合存在两种情况:
- i 不等于 M - i,因此两者的数量都大于0即可。
- i 等于 M - i,此时 i 的数量至少是2才满足。
#include <iostream>
using namespace std;
int face[1005];
int main()
{
int N, M, n;
cin >> N >> M;
while (N--)
{
cin >> n;
++face[n];
}
for (int i = 2; i <= 500; ++i)
{
if ((i != M - i && face[i] && face[M - i]) || (i + i == M && face[i] > 1))
{
cout << i << " " << M - i;
return 0;
}
}
cout << "No Solution";
return 0;
}
1049 Counting Ones
题意:
输入只有一个整数 N,要求你计算从 0 ~ N 的所有整数中,出现过多少个数字1。
思路:
如果枚举1到 N N N 的每个数并统计数字1的个数是必定会超时的,不妨先试试能否找出其中的数字规律。假设 N = n 1 n 2 n 3 n 4 n 5 = 30710 N = n_1 n_2 n_3 n_4 n_5=30710 N=n1n2n3n4n5=30710。用 k = 1 k = 1 k=1 遍历 N N N 的每一位,讨论在该位为1时左右两侧能组合出多少种数。
- k k k 在5号位时, N = n 1 n 2 n 3 n 4 1 N = n_1 n_2 n_3 n_4 1 N=n1n2n3n41。
由于 n 5 = 0 < k = 1 n_5 = 0 < k = 1 n5=0<k=1,所以 k k k 的左侧 n 1 n 2 n 3 n 4 n_1 n_2 n_3 n_4 n1n2n3n4 最高只能取到3070(不能取到3071),取值范围是 [ 0000 , 3070 ] [0000,3070] [0000,3070],总共有3071种情况。
- k k k 在4号位时, N = n 1 n 2 n 3 1 n 1 N = n_1 n_2 n_3 1 \ n_1 N=n1n2n31 n1。
由于 n 4 = 1 ≡ k = 1 n_4 = 1 \equiv k = 1 n4=1≡k=1,所以有两种情况:
- k k k 的左侧 n 1 n 2 n 3 n_1 n_2 n_3 n1n2n3 的取值范围为 [ 0 , 306 ] [0, 306] [0,306] 时,右侧的取值范围是 [ 0 , 9 ] [0,9] [0,9],共有 307 * 10 = 3070 种情况;
- k k k 的左侧 n 1 n 2 n 3 = 307 n_1 n_2 n_3 = 307 n1n2n3=307,右侧的取值范围是 [ 0 , n 5 ] [0,n_5] [0,n5],共有 n 5 + 1 n_5 + 1 n5+1 种情况。
综上总共有3070种情况。
- k k k 在3号位时, N = n 1 n 2 1 n 4 n 5 N = n_1 n_2 1 \ n_4 n_5 N=n1n21 n4n5。
由于 n 3 = 7 > k = 1 n_3 = 7 > k = 1 n3=7>k=1,所以 k k k 的左侧 n 1 n 2 n_1 n_2 n1n2 取值范围为 [ 0 , 30 ] [0,30] [0,30](全取满都没关系),右侧 n 3 n 4 n_3 n_4 n3n4 的取值范围为 [ 00 , 99 ] [00,99] [00,99]总共有 31 * 100 = 3100 种情况。
- k k k 在2号位时, N = n 1 1 n 3 n 4 n 5 N = n_1 1\ n_3n_4 n_5 N=n11 n3n4n5。
由于 n 2 = 0 < k = 1 n_2 = 0 < k = 1 n2=0<k=1,所以 k k k 左侧 n 1 n 2 n 3 n 4 n_1 n_2 n_3 n_4 n1n2n3n4 最高只能取到2(不能取到3),取值范围是 [ 0 , 2 ] [0,2] [0,2],右侧 n 3 n 4 n 5 n_3 n_4 n_5 n3n4n5 的取值范围为 [ 000 , 999 ] [000,999] [000,999],总共有 3 * 1000 = 3000 种情况。
- k k k 在1号位时, N = 1 n 2 n 3 n 4 n 5 N = 1\ n_2 n_3 n_4 n_5 N=1 n2n3n4n5。
由于 n 1 = 3 > k = 1 n_1 = 3 > k = 1 n1=3>k=1,所以 k k k 的右侧 n 3 n 4 n_3 n_4 n3n4 的取值范围为 [ 0000 , 9999 ] [0000,9999] [0000,9999]总共有 10000 种情况。
综上,总共有 3071 + 3071 + 3100 + 3000 + 10000 种情况。
有了思路就可以看代码怎么写了。定义变量 left、now、right 分别表示左侧、当前位以及右侧的数,ans 保存情况数,p 用于控制位数。
例如 30710,当 k k k 在5号位时,left = 30710 / 10 = 3071,now = 30710 / 1 % 10 = 0,right = 30710 % 1;当 k k k 在4号位时,left = 30710 / 100,now = 30710 / 10 % 10,right = 30710 % 100。所以只要令 p 初始值为1,每轮循环 p *= 10 即可,同时要保证 N / p 不为0,否则循环就没有意义了。
凝结上面的思路后核心判断代码就是(结合前面分析的过程看能更清楚):
- 当前位 now = 0 时,有 ans += left * p;
- 当前位 now = 1 时,有 ans += left * p + right + 1;
- 除此之外,有 ans += (left + 1) * p。
主要是对 p 的理解:假设 right 是 i 位数,p 的值就是 p i p^i pi,即 10 的 i 次方。这很好理解,一位数最大为9,两位数最大为99,三位数最大为999等等,故令 p 在每一次循环都自乘10。
注意:
- 边界数据一定要测试一下:如1和 2 30 = 1073741824 2^{30} = 1073741824 230=1073741824,结果分别是1和1036019223。
#include <iostream>
using namespace std;
int main()
{
int N, left = 0, right = 0, now = 1, ans = 0, p = 1;
cin >> N;
while (N / p)
{
left = N / (p * 10), now = N / p % 10, right = N % p;
if (now == 0) ans += left * p;
else if (now == 1) ans += left * p + right + 1;
else ans += (left + 1) * p;
p = p * 10;
}
cout << ans;
return 0;
}
1050 String Subtraction
题意:
给出两个字符串 s1 和 s2(均不超过 1 0 4 10^4 104个字符),要求你输出 s1 - s2 的结果。s1 - s2 的意思是:如果 s1 中的字符在 s2 中也存在,就从 s1 里面去掉它。
思路:
定义 ch 数组表示字符 c 是否出现过,c 的 ASCII 码值作为索引。用 getline 函数读取两个字符串,枚举 s2 中的每个字符,同时令其哈希值为1。再枚举 s1 中的每一个字符,如果其哈希值为0就打印。
#include <iostream>
using namespace std;
int ch[130];
int main()
{
string s1, s2;
getline(cin, s1);
getline(cin, s2);
for (auto i : s2) ch[i] = 1;
for (auto i : s1)
if (!ch[i]) cout << i;
return 0;
}
1051 Pop Sequence
题意:
有一个容量限制为 M 的栈,入栈顺序固定为1, 2, 3… , n。输入给出一系列出栈顺序,问这些出栈顺序是否可能由该入栈顺序得到。
思路:
按照题目的要求进行模拟,将 1 ~ n 依次入栈,用一个标记指向出栈序列第一个待出栈元素。如果入栈的元素恰好等于出栈序列当前等待出栈的元素,那么就让栈顶元素出栈,同时让标记指向下一个待出栈元素。此时只要栈顶元素仍然等于出栈序列当前等待出栈的元素,则持续出栈。
- 初始化栈,读入需要测试的出栈序列;
- 由于入栈顺序为 1 ~ n,因此用 i 枚举 1 ~ n,对每一个 i,先将 i 入栈。然后检查栈顶元素和待出栈元素是否相等,相等则栈顶元素出栈,待出栈标记指向下一个,继续循环检查;
- 如果循环检查结束了,发现栈是满的,说明此时栈顶元素与待出栈元素不相等,而无法继续入栈,故表明该出栈序列不能由该入栈序列得到,退出循环;
- 如果上述操作结束后栈空,则说明该出栈顺序合法,输出“YES";否则,输出“NO”。
注意点:
- 首先要注意的是题目对栈的大小有限制,不能忽视这一点,因此要在 for 循环末尾检查栈的状态。
- 栈满之后如果栈顶元素与待出栈元素不相等要退出循环,因为此时不论如何都无法得到该出栈序列。
#include <iostream>
#include <stack>
using namespace std;
int main()
{
int m, n, k;
cin >> m >> n >> k;
while (k--) // k 个需要判定的出队顺序
{
stack<int> st;
int pop[n + 1];
for (int i = 1; i <= n; ++i) // 输入出栈序列
cin >> pop[i];
for (int i = 1, j = 1; i <= n; ++i) // 入栈序列逐个入栈,j 指向待出栈元素
{
st.push(i);
while (!st.empty() && st.top() == pop[j]) // 栈不空且入栈元素和出栈元素相等时循环
{
st.pop(); // 入栈元素出栈
++j; // j 指向下一个待出栈元素
}
if (st.size() == m) // 栈满且栈顶与待出栈元素不相等
break; // 退出循环
}
if (st.empty() == 1) // 栈空,说明由入栈顺序可以得到该出栈顺序
cout << "YES\n"; // 输出 YES
else cout << "NO\n";
}
return 0;
}
1052 Linked List Sorting
题意:
首先给出结点的个数 N 以及头结点的地址,接下来是 N 个结点的信息,最后要求将结点按照关键值从小到大进行排序。其实就是排序 + 将每个结点的 next 域进行修改。
思路:
- 定义静态链表,flag 表示结点是否在链表中;
- 由题目给出的链表首地址 _begin 遍历整条链表,标记有效结点的 flag 为 true,同时计数有效结点的个数 count;
- 对结点进行排序,排序函数 cmp 的排序原则是:如果 cmp 的两个参数结点中有无效结点的话,则按 flag 从小到大排序,以把有效结点排到数组左端;否则按数据域从小到大排序;
- 由于有效结点已经按照数据域从小到大排序,因此按要求输出有效结点即可;
注意点:
- 直接使用 %05d 的输出格式,以在不足五位时再高位补0。但是要注意-1不能使用 %05d 输出,否则会输出-0001,因此必须要留意-1的输出。
- 题目可能会有无效结点,即不在题目给出的首地址开始的链表上。
- 数据里面还有均为无效的情况,这时就要根据有效结点的个数特判输出“0 -1”。
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 100010;
struct Node{ // 定义静态链表
int address, data, next; // 数据域
bool flag = false; // 标志结点是否在链表上
}node[maxn];
bool cmp(Node a, Node b)
{
if (a.flag == false || b.flag == false)
return a.flag > b.flag; // 只要 a 和 b 中有一个无效结点,就把它放到后面去
else
return a.data < b.data; // 如果都是有效结点,则按要求排序
}
int main()
{
//freopen("input.txt", "r", stdin);
int n, _begin, address; // s1 与 s2 分别代表两条链表的首地址
scanf("%d%d", &n, &_begin);
for (int i = 0; i < n; ++i)
{
scanf("%d", &address);
scanf("%d%d", &node[address].data, &node[address].next);
node[address].address = address;
}
int _count = 0, p = _begin;
// 枚举链表,对 flag 进行标记,同时计数有效结点个数
while (p != -1)
{
node[p].flag = true;
++_count;
p = node[p].next;
}
if (_count == 0) // 特判,新链表中没有结点时输出0 -1
printf("0 -1");
else
{
// 筛选有效结点,并按 data 从小到大排序
sort(node, node + maxn, cmp);
printf("%d %05d\n", _count, node[0].address); // 输出结果
for (int i = 0; i < _count; ++i)
{
if (i != _count - 1) // 防止-1被 %05d 化,提前判断
printf("%05d %d %05d\n", node[i].address, node[i].data, node[i + 1].address);
else
printf("%05d %d -1\n", node[i].address, node[i].data);
}
}
return 0;
}
1053 Path of Equal Weight
题意:
输入第一行是三个正整数,第一个是树中结点的总数 N(编号 0 ~ N),第二个是树中非叶子结点的个数 M,第三个是给定的权数 S。第二行是 N 个正整数,分别对应编号从0开始的各结点的权值。随后是 M 行输入,每行输入的第一个整数是结点的编号 nodeid,第二个整数是结点的孩子结点个数 K,然后是 K 个孩子结点的编号 childid。现在要求你打印出树中所有权值之和等于 S 的路径,并按照非递增的顺序打印所有路径。注意,打印的是路径上的权值,而不是路径上结点的编号。题目规定了根结点的编号是00。
思路:
用深度优先搜索遍历解决。
定义数组 weight 保存按照下标保存每一个结点的权值,数组 childs[i] 保存结点的 i 所有孩子结点的编号,数组 path 保存当前路径上的所有权值。paths 数组是二维数组,每一维保存一个符合题目要求的路径。cmp 函数会对两个 vector 数组进行排序,将更大的排在更前面。
在 DFS 中,每次进入一个结点,都将其权值加到 sum 上,然后将权值放入 path 中保存。如果结点是叶子结点,且当前权值和等于 S,就将当前路径 path 放入 paths 中;如果是非叶子结点,就枚举其所有的孩子结点进行递归遍历。在退出每个结点的 DFS 前,要先将 sum 减去其权值,并从 path 中弹出其权值,来表明当前路径不包含此结点。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
vector<int> weight(105), childs[105], path;
vector<vector<int>> paths;
int N, M, S, sum = 0;
bool cmp(vector<int> a, vector<int> b) { return a > b; }
void DFS(int root)
{
sum += weight[root]; // 更新当前路径的权值和
path.push_back(weight[root]); // 将结点权值加入当前路径中
if (childs[root].size() == 0 && sum == S) // 如果结点是叶结点且权值和等于给定数值
paths.push_back(path); // 将当前路径加入符合条件的路径数组中
for (int i = 0; i < childs[root].size(); ++i)
DFS(childs[root][i]);
sum -= weight[root]; // 退出当前递归时要减去该结点权值
path.pop_back(); // 将结点权值弹出当前路径
}
int main()
{
cin >> N >> M >> S;
for (int i = 0; i < N; ++i) cin >> weight[i];
int nodeid, K, childid;
while (M--)
{
cin >> nodeid >> K;
while (K--)
{
cin >> childid;
childs[nodeid].push_back(childid);
}
}
DFS(0);
sort(paths.begin(), paths.end(), cmp);
for (auto i : paths)
{
cout << i[0];
for (int j = 1; j < i.size(); ++j)
cout << " " << i[j];
cout << endl;
}
return 0;
}
1054 The Dominant Color
题意:
输入第一行给出两个正整数 M 和 N,代表图像的分辨率。随后 N 行数据,每行 M 个在 [ 0 , 2 24 ) [0, 2^{24}) [0,224) 内的整数,要求你输出这个二位整数矩阵中,占比不小于一半的整数。
思路:
因为整数的范围比较大,用散列表开数组不现实,因此用 map 代替散列表记录一下每个数字出现的次数即可。
#include <cstdio>
#include <map>
using namespace std;
int main()
{
map<int, int> image;
int m, n, color;
scanf("%d%d", &m, &n); // 图像的分辨率
while (n--)
{
int k = m; // 每行循环 m 次
while (k--)
{
scanf("%d", &color); // map 的类型为 int 型时,默认键值为0,故可以直接+1
image[color] += 1;
}
}
int c = 0, maxc = image[0]; // 最大整数及该整数出现的次数
for (map<int, int>::iterator it = image.begin(); it != image.end(); ++it)
{
if (it -> second > maxc)
{
maxc = it -> second;
c = it -> first;
}
}
printf("%d", c);
return 0;
}
1055 The World’s Richest
题意:
输入第一行是总人数 N 和查询次数 K,随后 N 行信息,每行信息分别是一个人的姓名(不超过8个字符且没有空格的字符串)、年龄和财富。再随后是 K 行查询,每一行是三个正整数,分别是最大输出人数 M(小于等于100)、最小年龄 Amin 和最大年龄 AMax。
要求你找在 [Amin, Amax] 内,且财产在前 M 的人。输出时先打印一行 Case #i:,i 是第几个查询,然后是每次查询得到的结果,每一行分别是姓名、年龄和财富。要求输出时按照财富从大到小输出,财富相等时按照年龄从小到大,年龄也相等时按照姓名字母序从小到大。输入保证不存在两个姓名、年龄以及财富都相同的人。
思路:
定义 people 结构体,保存每个人的姓名、年龄和财产,再定义 people 数组 allPeople 保存所有输入的人的信息。随后定义 cmp 函数定义排序规则。
除了定义必要的变量,再定义数组 ageCount 用来统计每个年龄的人数。因为题目说明了每次查询最多输出前一百名的人,所以考虑同一个年龄下就存在超过一百个人的极端情况。排序好后,定义 vector 数组 front 来保存每个年龄财富前一百的人,每放入一个,对应的 ageCount 的值加1。
因为 front 中的人都是 allPeople 中按顺序放入的,所以 front 也保持着财富、年龄以及姓名的排序顺序。
查询的时候,定义 vector 数组 ans,用于保存本次查询的人。先枚举一遍 front 中的人,只有年龄在 Amin 和 Amax 之中的人才放入 ans 中。随后枚举 ans 中的每个人,按照格式要求输出即可。如果 ans 的大小为0,就按题目要求输出 “None”。
经验:
- 大数据量的输入输出用 scanf、printf 要比 cin 和 cout 快很多。
注意:
- front 数组最大也就是100 * 199。这一步是关键,要不然不管优化的多好,在测试点2都一定会因为数据量太大而超时。
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
struct people {
char name[10];
int age, worth;
} allPeople[100005];
bool cmp(people a, people b)
{
if (a.worth != b.worth) return a.worth > b.worth;
else return a.age != b.age ? a.age < b.age : strcmp(a.name, b.name) < 0;
}
int main()
{
int N, K, M, Amin, Amax, ageCount[205] = {0};
scanf("%d%d", &N, &K); // 总人数 查询次数
for (int i = 0; i < N; ++i)
scanf("%s%d%d", allPeople[i].name, &allPeople[i].age, &allPeople[i].worth);
sort(allPeople, allPeople + N, cmp);
vector<people> front; // front 数组保存每个年龄前一百的人
for (int i = 0; i < N; ++i)
{
if (ageCount[allPeople[i].age] < 100)
{
front.push_back(allPeople[i]);
++ageCount[allPeople[i].age]; // 计数+1
}
}
for (int i = 1; i <= K; ++i)
{
scanf("%d%d%d", &M, &Amin, &Amax);
vector<people> ans; // ans 保存本次查询的人
for (int j = 0; j < front.size(); ++j)
if (front[j].age >= Amin && front[j].age <= Amax)
ans.push_back(front[j]); // 只有在年龄段之间的才放入 ans 中
printf("Case #%d:\n", i);
for (int j = 0; j < min((int)ans.size(), M); ++j)
printf("%s %d %d\n", ans[j].name, ans[j].age, ans[j].worth);
if (!ans.size()) printf("None\n"); // ans 大小为0时输出 None
}
return 0;
}
1056 Mice and Rice
题意:
输入第一行给出参赛老鼠数 Np 和每组最多的老鼠数 Ng;输入第二行是 Np 个非负整数,代表了 Np 只老鼠的重量;输入第三行是 Np 个整数,以此对应第二行中每只老鼠的编号。现在要求你在一行中,打印所有老鼠的排名。
比赛的规则是这样的:按照参赛顺序,在每一轮中,将每 Ng 只老鼠分为一组,最后一组不足 Ng 只也算一组。每组最重的老鼠胜出,晋级下一轮;循环往复,直至角逐出最终的胜利者,排名为1。每一轮
被淘汰老鼠的排名都是一样的。
思路:
定义结构体 MICE 保存一只老鼠的排名和体重。初始时把所有参赛老鼠的编号按顺序加入队列 q。
进入 while 循环,每一层循环代表一轮比赛。每轮比赛把老鼠分成 group 组:设当前轮的参赛老鼠数有 temp 只,如果 temp % ng 为0,那么说明能够把老鼠完整划分,因此 group = temp / NG;否则,说明最后会有少于 ng 只老鼠会单独分为一组,此时组数 group = temp / ng +1。
对每一轮比赛,枚举队列内的当前轮的 temp 只老鼠,按每 ng 只老鼠一组选出组内质量最大的老鼠,并将其入队表示晋级,可在选出当前轮重量最大老鼠的过程中可以直接对其排名赋值,即 group + 1(晋级的老鼠在下一轮比赛时会得到新的排名)。
重复上述过程,直到队列中只剩下1只老鼠,就把它的排名记为1。最后输出所有老鼠的排名。
注意:
- 第二行给出的老鼠顺序就是他们的参赛顺序,第三行只是他们的编号而已,即使不给出第三行,自行编号也是能给出每只老鼠的排名的。
- C++ 的容器 queue 中有一个出队函数 pop(),要注意它是用 void 定义的,没有返回值。因此想直接用 push 函数将弹出的元素压入队列即 match.push(wait.pop()) 是错误的语法。
#include <iostream>
#include <queue>
using namespace std;
struct MICE {
int rank, weight; // 排名和体重
};
int main()
{
int playernum, groupnum, number;
cin >> playernum >> groupnum;
MICE mice[playernum];
for (int i = 0; i < playernum; ++i)
cin >> mice[i].weight; // 输入每只老鼠的体重
queue<int> q;
for (int i = 0; i < playernum; ++i)
{
cin >> number;
q.push(number); // 老鼠编号入队
}
int temp = playernum, group; // temp 为当前轮比赛总老鼠数
while (q.size() != 1) // 等待队列只剩一只老鼠时结束循环
{
if (temp % groupnum == 0) // 计算本轮比赛有多少组
group = temp / groupnum;
else group = temp / groupnum + 1;
for (int i = 0; i < group; ++i)
{
int index = q.front(); // k 保存该组质量最大的老鼠的编号
for (int j = 0; j < groupnum; ++j) // 枚举每一组中的所有老鼠
{
if (i * groupnum + j >= temp) break; // 最后一组老鼠数不足 groupnum 时退出循环
int fi = q.front(); // 获取队首老鼠编号
if (mice[fi].weight > mice[index].weight)
index = fi; // 找出质量最大的老鼠
mice[fi].rank = group + 1; // 更新该轮被淘汰老鼠排名
q.pop();
}
q.push(index); // 该组获胜老鼠晋级
}
temp = group; // group 只老鼠晋级,因此下一轮总老鼠数为 group
}
mice[q.front()].rank = 1; // 最后剩下的一只老鼠为最终获胜者,排名为1
cout << mice[0].rank; // 输出所有老鼠的排名
for (int i = 1; i < playernum; ++i)
cout << " " << mice[i].rank;
return 0;
}
1057
1058
1059 Prime Factors
题意:
输入只有一个整数 N,要求你求出它所有的质因子,并按照指定的格式输出。
思路:
对于质因子分解的原理可以参考这篇博客:PAT OJ 刷题必备知识总结 第26、27的知识点。
注意点:
- 因为是 long int (大多数情况下,long int 就是 int) 范围内的整数,素数表需要开到 1 0 5 10^5 105。
- 注意 n == 1 需要特判输出 “1=1”。
#include<cstdio>
#include<cmath>
using namespace std;
const int maxn = 100010;
int prime[maxn], pNum = 0, num = 0;
bool p[maxn] = {false};
struct factor
{
int x, cnt; // x 为质因子,cnt 为该质因子的个数
}fac[maxn];
void decomposed(int n)
{
int temp = n, sqr = (int)sqrt(n * 1.0); // 用 temp 暂存整数 n 的值
for (int i = 0; i < pNum && prime[i] <= sqr; ++i)
{ // 循环执行的条件要尤其注意
if (temp % prime[i] == 0) // 如果 prime[i] 是 temp 的质因子
{
fac[num].x = prime[i]; // 记录该质因子
fac[num].cnt = 0; // 初始化个数为0,在整除时才加1
while (temp % prime[i] == 0) // 计算出质因子 prime[i] 的个数
{
++fac[num].cnt; // 质因子个数加1
temp /= prime[i]; // 整除
}
++num; // 不同质因子个数加1
}
if (temp == 1) break; // 及时退出循环节省时间
}
if (temp != 1) // 如果无法被根号 n 以内的质因子除尽
{
fac[num].x = temp; // 那么一定有一个大于根号 n 的质因子
fac[num++].cnt = 1;
}
}
int main()
{
int n;
scanf("%d", &n);
if (n == 1) printf("1=1");
else
{
for (int i = 2; i < maxn; ++i) // 这个 for 循环为欧拉分解
{
if (p[i] == false)
prime[pNum++] = i;
for (int j = 0; i * prime[j] <= maxn; ++j)
{
p[i * prime[j]] = true;
if (i % prime[j] == 0)
break;
}
}
decomposed(n); // 进行质因子分解
printf("%d=", n);
for (int i = 0; i < num; ++i) // 该循环按格式输出结果
{
if (i > 0) printf("*"); // 第一个质因子前不输出 *
printf("%d", fac[i].x);
if (fac[i].cnt > 1) // 质因子个数大于1才以指数形式输出
printf("^%d", fac[i].cnt);
}
}
return 0;
}
1060 Are They Equal
题意:
在一行中给出有效位数 N,以及两个浮点数 A 和 B,要求你判断它们的科学计数法是否相等。
思路:
题目交代了浮点数是非零的,但是小于 1 0 100 10^{100} 10100,表明不能用正常的 double 来接受,而是用字符串。定义数组 exp[2] 保存两个浮点数的指数,ans[2] 保存它们的有效数据部分。分别处理两个浮点数:
- 处理第一个浮点数,以字符串的形式输入到 str 中;
- 用下标 p 枚举 str,找到小数点的下标,然后删除小数点,例如00012300.56变成0001230056,此时 p = 8;
- 用下标 e 枚举 str,找到第一个有效位的下标,然后删除下标 [0, e) 之间所有前导0,例如0001230056变成120056,此时 e = 3;
- 如果 e 等于 str.size(),说明输入的浮点数的所有数位都是0,删除后就成了空串了,此时它的指数应该是10^0,所以令 exp[i] = 0;
- 如果 e 不等于 str.size(),观察就可以发现,指数的大小刚好等于第一个有效位的下标减去小数点的下标,所以令 exp[i] = p - e;
- 获取 [0, N) 之间的子串,即为科学计数法下的有效位。例如120056在 [0, 3) 的有效位是120;
- 如果 N 比 str.size() 大,有效位不够,则需要在末尾补0。
- 第二个浮点数的处理方式同上。
经验:
即使 N 比 字符串长度更大,调用 s.substr(0, N) 也没关系,因为 substr() 最多只返回到最后一个字符,例如 s = “abc”,调用 s.substr(1, 10) 也只会返回 “bc”。
#include <iostream>
using namespace std;
int main()
{
int N, exp[2];
cin >> N;
string str, ans[2];
for(int i = 0; i < 2; ++i)
{
int e = 0, p = 0;
cin >> str;
while (p < str.size() && str[p] != '.') ++p; // 找到小数点的下标
if (p < str.size()) str.erase(str.begin() + p); // 删除小数点
while (e < str.size() && str[e] == '0') ++e; // 找到第一个有效位的下标
str.erase(str.begin(), str.begin() + e); // 删除前导0
if (!str.size()) exp[i] = 0; // 如果删除后字符串长度为0,说明输入的就是0,令指数为0
else exp[i] = p - e; // 获取指数大小
ans[i] = str.substr(0, N); // 获取有效位
while (ans[i].size() < N) ans[i] += '0'; // 有效位不足 N,补0
}
if (ans[0] == ans[1] && exp[0] == exp[1])
cout << "YES 0." + ans[0] + "*10^" << exp[0] << endl;
else cout << "NO 0." + ans[0] + "*10^" << exp[0] << " 0." + ans[1] + "*10^" << exp[1] << endl;
}
一定要自己写一遍哦~~