PAT (Advanced Level) Practice 1021~1040

PAT Advanced Level Practice 解题思路和代码,主要用的是 C++。每22题一篇博客,可以按目录来进行寻找。

1021 Deepest Root

题意:

输入第一行是图中结点的个数 N,随后 N - 1 行,每行两个整数 n1n2,表明在 n1 和 n2 之间有一条边。对于一个无环的连通图来说,可以将任何一个结点当做根结点,从而构成一棵树。而现在要求你求出这个图,将哪一个结点当做根结点,可以构成一棵最高的树。如果这样的结点不止一个,就按照升序将其输出。如果图不能构成一棵树,比如存在环,或者存在两个或以上的连通图,就输出 Error: K components,其中 K 是连通图的个数。

思路:

深度优先搜索遍历并查集解决。

对于一个图来说,N 个结点和 N - 1 条边可以构成一个无环的连通图,而这样一个连通图一定可以构成一棵树。无法构成时,就一定存在多个连通图。所以首先需要判断图中有几个连通图。对于这一部分可以用并查集来解决。对于后一部分的问题,其实就是求一个连通图的最长路径,并将最长路径两边的端点(结点)保存在集合中,因为最长路径可能不唯一,所以需要输出所有最长路径两边的端点。

father 数组记录每一个结点的根结点,初始化时,每一个结点的根结点都是它自己。对于输入的一条边的两个端点 n1 和 n2,如果其根结点不同,就将 n2 的根结点设置为 n1 的根结点。当然反过来也可以,因为并查集是合并集合,所以集合中谁做根结点都是可以的。因此在输入结束后,根结点还为自己的就表明存在一个集合,如果所有结点的根结点都是一样的,就表明整个图都在一个集合里面;反之就存在多个连通图,就输出 Error 即可。

接下来的过程都是建立在整个图是一个连通图的基础上探讨的。

寻找连通图最长路径的时候有一个定理:如果图中所有最长路径的端点构成一个集合 A。假如先从1号结点开始遍历,将从1号结点开始,所能到达的最长路径的叶结点保存在集合 temp 中,那么集合 temp 中的结点一定都属于集合 A。将 temp 中的结点全都放入集合 ans 中,再从 ans 中任意一个结点开始遍历,就能找到剩余的所有集合 A 中的结点。读者可以自行搜索相关证明。

注意:

  • 如果对每个结点都遍历一遍求出最长路径,是一定会超时的;其次用邻接矩阵也会超时,需要使用邻接表。
  • 注意只有一个结点的情况,此时的输出是1。
  • 只有两个结点和一条边时,比如 1-2,输出的应该是1和2,因为无论把1还是2当成根结点都能构成最高的树。
#include <iostream>
#include <set>
#include <vector>
using namespace std;

int N, maxdepth = -1, father[10010];
vector<int> G[10010];
set<int> ans, temp;

int FindFather(int x)
{
    while (x != father[x]) x = father[x];
    return x;
}

void DFS(int u, int depth, int pre)
{
    if (depth > maxdepth)       // 如果深度更大
    {
        maxdepth = depth;       // 更新最大深度
        temp.clear();           // 清空集合
        temp.insert(u);         // 插入结点 u
    }
    else if (depth == maxdepth) temp.insert(u);     // 深度等于最大深度,插入结点 u
    for (int i = 0; i < G[u].size(); ++i)   // 枚举数组 G[u] 中的所有结点
    {
        if (G[u][i] == pre) continue;       // 不再递归访问当前路径的前驱结点
        DFS(G[u][i], depth + 1, u);         // 递归访问子结点
    }
}

int main()
{
    int n1, n2, K = 0;
    cin >> N;
    for (int i = 1; i <= N; ++i) father[i] = i;
    for (int i = 1; i < N; ++i)
    {
        cin >> n1 >> n2;
        G[n1].push_back(n2), G[n2].push_back(n1);   // 标记结点 c1 和 c2 之间存在边
        int f1 = FindFather(n1), f2 = FindFather(n2);
        if (f1 != f2) father[f2] = f1;  // 如果 n1 和 n2 不在同一个集合中则将 n2 加入到 n1 的集合中
    } 
    for (int i = 1; i <= N; ++i)
        if (father[i] == i) ++K;        // 统计连通图个数
    if (K > 1) cout << "Error: " << K << " components" << endl;
    else
    {
        DFS(1, 0, -1);                  // 从1号结点开始遍历,初始高度为0,前驱结点为-1
        for (auto i : temp) ans.insert(i);          // 将第一遍遍历的结果放入 ans 中
        DFS(*ans.begin(), 0, -1);       // 从 ans 的第一个结点开始遍历
        for (auto i : temp) ans.insert(i);          // 将第二遍遍历的结果放入 ans 中
        for (auto i : ans) cout << i << endl;
    }

    return 0;
}

1022 Digital Library

题意:

第一行输入书的总数 N,然后是 N 块输入,每一块输入包含6行信息:

  • 7位 id:唯一标识一本书;
  • 书的标题:不超过80个字符的字符串;
  • 作者:不超过80个字符的字符串;
  • 关键词:每个关键词都是不超过10个字符的字符串,彼此用一个空格分隔。
  • 出版商:不超过80个字符的字符串
  • 出版年份:在 [1000, 3000] 范围内的4位数。

输入保证每本书只属于一个作者,关键词不超过5个,不超过1000个不同的关键词,不超过1000个不同的出版商。

随后是用户的查询数量 M,然后是 M 行查询,每行查询是以下五种格式之一:

  • 1: 书名。
  • 2: 作者名。
  • 3: 关键词。
  • 4: 出版商。
  • 5: 4位整数,表示出版年份。

现在要求你对于每一个查询,在第一行打印查询的内容,然后以 id 升序输出所有查询的书本,每个书本占一行,如果没有找到符合条件的书,就输出 “Not Found”。

思路:

题目只要求输出书本的 id,所以不妨将它其他的所有信息都当做字符串,建立一个由信息到 id 的映射,也即 unordered_map<string, set<int>> title, author, keyword, publisher, year。其中键是字符串,值是涉及该信息的所有书本 id 的集合。之所以用 unordered_map 是因为对字符串不需要排序,只需要对 id 排序,所以值用 set。输入的时候,将 id 加入到对应的映射名 [str] 中即可。

对于每行查询,都直接读取进字符串 str 中,根据 str[0] 来判断查询的是哪种信息。因此在作为映射的键查询时,就需要把 str 开头的前导去掉,比如 1: The Testing Book 的 "1: ",为此只需要对 str 调用 erase() 函数即可。

经验:

  • 把查询操作提炼成一个函数,就需要对参数使用引用,否则最后一组数据会超时。对于字符串以及 map 这类传递较慢的类型,如果需要作为函数参数的话,尽可能地加上引用。
  • 在 scanf 或者 cin 输入书的编号 id 后,必须用 getchar 接收掉后面的换行符,否则 getline 会把换行符当成一行输入。

注意:

  • 查询内容最前面的序号表明了其属于什么信息。因为可能两种信息存在同样的字符串,但是是需要分开记录 id 的,所以定义映射时就要分开定义。
  • 测试用例中存在有着前导0的 id,例如“0012345”,如果用 int 类型保存 id,只会保存为12345,因此输出的时候需要控制一下格式,不足7位用0补齐。另外也可以用 string 来保存 id,这样就不需要在输出的时候控制格式,相应的时间和空间就会上去,读者可以自己权衡一下。
#include <iostream>
#include <unordered_map>
#include <set>
#include <iomanip>
using namespace std;

unordered_map<string, set<int>> title, author, keyword, publisher, year;

void query(unordered_map<string, set<int>> &mp, string &str)
{
	if (mp.find(str) != mp.end())
		for (auto it = mp[str].begin(); it != mp[str].end(); ++it)
			cout << setw(7) << setfill('0') << *it << endl;
	else cout << "Not Found" << endl;
}

int main()
{
	int N, id, M;
	cin >> N;
	string str;
	for (int i = 0; i < N; ++i)
	{
		cin >> id;
		char c = getchar();				// 过滤输入 id 后的回车
		getline(cin, str);				// 输入书名
		title[str].insert(id);			// 把 id 放入 title 映射
		getline(cin, str);				// 输入作者
		author[str].insert(id);			// 把 id 放入 author 映射
		while (cin >> str)				// 输入关键词
		{
			keyword[str].insert(id);	// 把 id 放入 keyword 映射
			if ((c = getchar()) == '\n') break;
		}
		getline(cin, str);				// 输入出版商
		publisher[str].insert(id);		// 把 id 放入 publisher 映射
		getline(cin, str);				// 输入出版年份
		year[str].insert(id);			// 把 id 放入 year 映射
	}

	cin >> M;
	char c = getchar();			// 过渡输入 M 后的回车
	while (M--)
	{
		getline(cin, str);
		cout << str << endl;	// 打印查询的内容
		if (str[0] == '1') query(title, str.erase(0, 3));			// 查询该书名下的所有 id
		else if (str[0] == '2') query(author, str.erase(0, 3));		// 查询该作者下的所有 id
		else if (str[0] == '3') query(keyword, str.erase(0, 3));	// 查询该关键词下的所有 id
		else if (str[0] == '4') query(publisher, str.erase(0, 3));	// 查询该出版商下的所有 id
		else query(year, str.erase(0, 3));							// 查询该出版年份下的所有 id
	}

	return 0;
}

1023 Have Fun with Numbers

题意:

输入只有一个不超过20位的正整数 N,现要求你将这个整数乘以2,如果整数中每一个数字的个数和之前的保持一致,就输出 “Yes” 和乘以2之后的结果,否则就输出 “No” 和乘以2之后的结果。

思路:

基本想法是读取大整数的时候记录各个数位出现的次数,乘以2后再减去相应的次数,如果最后得到的数组 cnt 所有元素值都为0说明 乘以2后的大整数是原数数位的一个排列。

  1. 按字符串方式读入整数,并且逆序存入数组 num 中,这样数组 num 的低位(下标更小的位置)存放的就是大整数的低位。存入的过程用数组 table 记录原大整数中每个数出现的次数。
  2. 对大整数进行乘2操作,由于是逆序存放,故下标从0开始逐位乘2即可。每一位乘完得到新的数,根据这个数将数组 table 相应下标的值减1。乘完后,若依然存在数位,表明多出现了一个数,此时数组 table 相应位应该执行+1操作,同时乘2后的大整数数位+1。
  3. 最后遍历数组 table,若存在不为0的值,输出“No”,否则输出“Yes”。

注意点:

  • 对于大整数的概念可以参考文章 PAT OJ 刷题必备知识总结 第29点。
  • 一定要注意,大整数进行加法或乘法操作后,数位有可能+1,而这个进位在最后需要输出,否则2、7测试点会答案错误。
#include <iostream>
using namespace std;

int main()
{
    int cnt[10] = {0}, num[25];
    string str;
    cin >> str;
    int len = str.length();     // len 保存整数的长度
    for (int i = len - 1; i >= 0; --i)
    {
        num[i] = str[len - i - 1] - '0';    // 将字符转为数字保存到 num 中
        ++cnt[num[i]];                      // 数字 num[i] 出现次数加1
    }

    int temp, carry = 0, flag = 0;
    for (int i = 0; i < len; ++i)           // 从低位开始枚举
    {
        temp = num[i] + num[i] + carry;     // 当前位乘2加上低位的进位
        num[i] = temp % 10;                 // 只保留10以内的余数
        carry = temp / 10;                  // 进位
        --cnt[num[i]];                      // 出现次数减1
    }
    if (carry != 0)             // 进位不为0,最高位还得加1
    {
        ++cnt[carry];           // 输入 carry 出现次数加1
        num[len++] = carry;     // 最高位变 carry,整数长度加1
    }

    for (int i = 0; i < 10; ++i)
        if (cnt[i] != 0) flag = 1;      // 如果某一个整数的 cnt 不为0
    if (flag) cout << "No\n";
    else cout << "Yes\n";
    for (int i = len - 1; i >= 0; --i)  // 输出两倍后的结果
        cout << num[i];

    return 0;
}

1024 Palindromic Number

题意:

输入只有一行两个正整数,分别是初始整数 N 和最大的操作步骤 K。现在要求你输出 N 操作多少次后会变成回文数,输出其回文数结果并在下一行中输出操作次数;如果在 K 次操作内无法得到回文数,就给出此时的操作结果和操作次数。

一个整数不管从左往右写还是从右往左写都是一样的,那么就称其为回文数。若一个整数不是回文数,那么它一定可以通过若干次操作得到回文数,操作过程:将该整数加上这个整数逆序的整数,得到一个新的整数,这样就为操作一次。若结果整数不为回文数,就再执行一次这样的相加操作。

思路:

由于存在多次逆序相加的可能,即使用 long long 也有可能溢出,因此采用数组来存储每个数位,即大整数的思想。

  1. 用数组 before 和数组 after 分别表示逆置前和逆置后的大整数,以字符串形式读入整数,然后存入数组 before 中。
  2. step 记录操作次数,当 step >= k 时跳出循环。每次循环首先判断 before 是否为回文数,如果是就结束循环。
  3. 如果不是,则将 before 逆序存入 after 中,然后执行操作,将两者逐位相加。最后若进位不为0,还需将其添加进 before 中。step + 1,进入下次循环,重复2的步骤。

注意点:

  • 对于大整数的概念可以参考文章 PAT OJ 刷题必备知识总结 第29点。
  • 最后输出 before 时记得逆序输出。如果是回文数则从下标0开始输出即可,如果最终结果不是回文数则需要从数组尾部往前输出。
#include <iostream>
#include <string>
using namespace std;

const int maxl = 1010;

bool isPal(int a[], int n) // 判断数组 a 中存储的大整数是否为回文数,n 为数组长度
{
    for (int i = 0; i <= n / 2; ++i)
        if (a[i] != a[n - i - 1])
            return false;
    return true;
}

int main()
{
    int k, before[maxl], after[maxl]; // 分别表示逆置前的大整数和逆置后的大整数
    string str;
    cin >> str >> k; // 读取大整数字符串和最多相加次数
    int len = str.length(), step = 0;
    for (int i = 0; i < len; ++i) // 将字符串的数存入原大整数数组
        before[i] = str[i] - '0';
    while (step < k) // 当步数大于 k 时结束循环
    {
        if (isPal(before, len))
            break;
        for (int i = len - 1; i >= 0; --i) // 将数组 before 逆序存入数组 after
            after[i] = before[len - i - 1];
        int temp, carry = 0;
        for (int i = 0; i < len; ++i) // 用数组 before 存储相加后的结果
        {
            temp = before[i] + after[i] + carry; // 同位相加
            before[i] = temp % 10; // 余数
            carry = temp / 10; // 进位
        }
        if (carry != 0)
            before[len++] = carry;
        ++step;
    }
    for (int i = len - 1; i >= 0; --i) // 要从高位开始输出
        cout << before[i];
    cout << "\n" << step;

    return 0;
}

1025 PAT Ranking

题意:

输入第一行是考场数 N,随后 N 组数据。每组数据的第一行是考场的考生数 K,随后 K 行考生数据,分别是考生 id 和考试成绩。

现在要求你在第一行打印总的考生人数,随后是每名考生的 id、总排名、考场号(从1开始)、以及考场排名。分数相同时按 id 顺序排序。

思路:

定义结构体 student 保存学生的 id、分数 score、最终排名 f_rank、考场号 loc_num、考场排名 l_rank,定义全局 vector 数组 ranklist 按排名顺序保存所有的考生。

输入考场数后进入循环,定义 vector 数组 local 保存当前考场的考生。输入考生的 id 和得分后进行排序,将排序后数组中的第一个元素,也即第一名的 l_rank 标记为1,用 i 遍历其后的每一名考生,如果分数和前一名相等,排名不变;如果不等,则等于 i + 1。最后保存每名考生的考场号后,将其加入 ranklist 等待最终排名。输入完所有考场的所有考生后,再重复上述步骤即可得到最终的所有排名和考场数据。

经验:

  • 三元运算符的效率比 if-else 还是要高上不少。

注意:

  • 如果考生的分数分别是 100、99、99、90,那排名应该是1224而不是1223。
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

struct student {
    string id;
    int score, f_rank, loc_num, l_rank;
};
vector<student> ranklist;

bool cmp(student a, student b) { return a.score != b.score ? a.score > b.score : a.id < b.id; }

int main()
{
    int N, K, score;
	string id;
	cin >> N;		// N 个考场
	for (int i = 1; i <= N; ++i)
	{
		cin >> K;	// 每个考场考点中的考生人数
		vector<student> local(K);
		for (int j = 0; j < K; ++j)
			cin >> local[j].id >> local[j].score;	// 考生 id 和得分
		sort(local.begin(), local.end(), cmp);		// 按分数和 id 排名
		local[0].l_rank = 1;						// 第一名的排名标记为1
		for (int j = 1; j < K; ++j)
			local[j].l_rank = local[j].score == local[j - 1].score ? local[j - 1].l_rank : j + 1;
		for (auto j : local)
		{
			j.loc_num = i;				// 保存考场号
			ranklist.push_back(j);		// 放入总排名表
		}
	}

	sort(ranklist.begin(), ranklist.end(), cmp);	// 按分数和 id 排名
	ranklist[0].f_rank = 1;							// 第一名的排名标记为1
	for (int i = 1; i < ranklist.size(); ++i)
		ranklist[i].f_rank = ranklist[i].score == ranklist[i - 1].score ? ranklist[i - 1].f_rank : i + 1;

	cout << ranklist.size() << endl;	// 先打印总的考生人数
	for (auto i : ranklist)
		cout << i.id << " " << i.f_rank << " " << i.loc_num << " " << i.l_rank << endl;
    return 0;
}

1026


1027


1028


1029


1030 Travel Plan

题意:

输入第一行是四个正整数,第一个是城市的总数 N,第二个是城市之间道路的总数 M,第三个是起点城市编号 S,第四个是终点城市编号 D。随后 M 行道路信息,城市编号为 0 ~ N - 1,每一行四个整数 c1c2L 以及 cost,分别表示两个城市的编号,以及它们之间道路的长度和花费。现在要求你计算从起始点 S 到终点 D 的最短路径,如果最短路径不唯一,则选择花费最少的那一个。打印最短路径、路径长度以及花费。

思路:

迪杰斯特拉算法解决。

d[i] 表示由起始点到点 i 的最短路径的路径长度,c[i] 表示起始点到点 i 的最短路径的花费,G[u][v] 表示从点 u 到点 v 的路径长度(值为0时表明 u, v 之间没有边),C[u][v] 表示从点 u 到点 v 的花费,pre[i] 表示到点 i 的前驱结点,visit[i] 表示点 i 是否被访问过,数组 path 保存符合题意的最短路径。

此题是在迪杰斯特拉算法的基础上,多考虑一个花费。只需要在寻找最短路径的过程中,遇到路径长度相等的情况时,继续判断花费是否最少即可。

我这段代码不能 AC,我自己写的测试用例都是通过的,和别人的代码对比过程中变量的变化值都是正确的,但就是答案错误,我也找不出问题在哪,只能期待一个有缘的读者能帮我找找问题了。

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

int N, M, S, D, c1, c2, L, cost, G[510][510], C[510][510];
vector<int> pre(510, 0), d(510, 0x3fffffff), c(510, 0x3fffffff), path;
bool visit[510];

int main()
{
    cin >> N >> M >> S >> D;
    while (M--)
    {
        cin >> c1 >> c2 >> L >> cost;
        G[c1][c2] = G[c2][c1] = L;      // 由 c1 到 c2 距离为 L
        C[c1][c2] = C[c2][c1] = cost;   // 由 c1 到 c2 花费为 C
    }
    d[0] = c[0] = 0;    // 到起始点的路径长度和花费均为0
    pre[0] = -1;        // 置起始点的前驱为-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)     // 枚举所有的点,寻找未访问过的点中最短道路的点
        {
            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 有边
            {   // 如果以 u 为中介点可以使 d[v] 更小,或者距离相等但是花费更少
                if ((d[u] + G[u][v] < d[v]) || (d[u] + G[u][v] == d[v] && c[u] + C[u][v] < c[v]))          
                {
                    d[v] = d[u] + G[u][v];          // 更新最短路径
                    c[v] = c[u] + C[u][v];          // 更新到点 v 的花费
                    pre[v] = u;                     // 点 v 的前驱结点为 u
                }
            }
        }
    }
    for (int i = D; i != -1; i = pre[i])        // 由终点开始往前找前驱,将其加入路径 path
        path.push_back(i);
    for (int i = path.size() - 1; i >= 0; --i)  // 打印最短且花费最短路径
        cout << path[i] << " ";
    cout << d[D] << " " << c[D];

    return 0;
}

1031


1032 Sharing

题意:

输入第一行给出两条链表的首地址 s1 和 s2,以及所有结点(每个结点代表一个字母)的个数 N。随后 N 行结点的信息,包括首地址、字母、后继结点的地址。要求你找出两条链表首个公共结点的地址。如果两条链表没有公共结点,则输出-1。要注意的是,两个链表中所有的结点是打乱顺序给出的。

思路:

  1. 地址范围比较小,定义静态链表即可,其中结点性质由 bool 型变量 flag 定义,表示结点是否在某条链表中出现过。
  2. 由题目给出的第一条链表的首地址 s1 遍历整条链表,将遍历到的结点 flag 标记为 true,表示其在第一条链表上出现过。
  3. 枚举第二条链表,当出现第一个 flag == true 的结点,说明是第一条链表中出现过的结果,即为两条链表的第一个共用结点。如果第二条链表枚举完仍然没有发现共用结点,则输出-1。

注意点:

  • 使用 %05d 格式输出地址,可以使不足5位的整数的高位补0。
  • 使用 scanf 可以更方便的处理输入数据之间的空格。
#include <iostream>
#include <iomanip>
#include <algorithm>
using namespace std;

struct Node {
    char data;          // 数据域
    int next, flag;     // 指针域 结点是否在第一条链表中出现
} node[100010];

int main()
{
    int s1, s2, N, p;   // s1与s2分别代表两条链表的首地址
    cin >> s1 >> s2 >> N;
    int address, next;  // 结点地址与后继地址
    char data;          // 数据
    while (N--)
    {
        cin >> address >> data >> next;
        node[address].data = data;
        node[address].next = next;
    }

    for (p = s1; p != -1; p = node[p].next)
        node[p].flag = 1;           // 枚举第一条链表的所有结点,令其出现次数为1
    for (p = s2; p != -1; p = node[p].next)
        if (node[p].flag) break;    // 找到第一个在第一条链表中出现的结点,退出循环
    if (p != -1)        // 如果第二条链表还没有到达结尾,说明找到了共用结点
        cout << setiosflags(ios::right) << setw(5) << setfill('0') << p << endl;
    else cout << "-1\n";

    return 0;
}

1033 To Fill or Not to Fill

题意:

输入第一行给出四个整数:油箱最大容量 Cmax,目的城市的距离 D,每单位油能跑的距离 Davg,加油站的数量 N。接下来 N 行是每个加油站的信息,分别是该加油站的单位油价以及其离出发点的距离。

现在要求你输出从起始点到目的城市的过程中,汽油花费上最少的方案,只需要输出油费是多少,精确到小数点后两位。如果无法到达目的城市,就输出 The maximum travel distance = X,X 是最远能到达的距离,同样精确到小数点后两位。

思路:

定义结构体 Station 保存每个加油站的油价和距离,结构体数组 st 保存所有的加油站信息。cmp 函数则按照加油站的距离对加油站进行排序。

输入所有加油站的信息后,将目的城市也当做一个加油站,只不过油价为0,距离为 D。因为初始时油箱离没有油,因此在距离0处必须得有一个加油站,否则就无法出发,故要先判断一下 st[0] 的距离是否为0。

进入循环前,定义 ans 保存油费,nowTank 保存当前油箱油量,farRun 保存油箱满油状态下最远的行驶距离,now 表示当前加油站的索引。

在沿途加油站加油的主要思路:

  • 用 i 枚举 now 之后,距离在最远行程内的所有加油站,保存最低油价 minPrice 和加油站的索引 k
  • 如果 minPrice < now 的油价,就加上恰好能到达 k 的油,然后前往加油站 k。
  • 如果 minPrice >= now 的油价,就加满油,然后前往加油站 k。
  • 如果在满油状态下找不到能到达的加油站,就结束循环。

可以看出核心思想就是,当前油价不是最低,就只加能到达最低油价的加油站的油;如果是最低,就直接加满,前往最后一个加油站 k(这种情况下 k 一定是最远行程内最远的加油站的索引)。

定义变量 need 保存前往加油站 k 需要的油(距离之差除以单位油能跑的距离)。如果 minPrice 比 now 的油价低,就比较当前油箱油量是否足以前往 k,不够的话就需要加油,因此要更新油费 ans。同时更新到达加油站 k 后的油箱油量;否则,加满油,前往加油站 k。循环结束后根据 now 的值来判断是否到达了目的城市。不是的话就输出 st[now].distance + farRun,表示能到达的最远的加油站的距离+ 加满油能跑的最远距离,

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

struct Station {
    double price, distance;
} st[501];

bool cmp(Station &a, Station &b) { return a.distance < b.distance; }

int main()
{
    int N, now = 0;
    double Cmax, D, Davg;
    scanf("%lf%lf%lf%d", &Cmax, &D, &Davg, &N);
    for (int i = 0; i < N; ++i)
        scanf("%lf%lf", &st[i].price, &st[i].distance);
    st[N].price = 0;        // 终点的油价为0
    st[N].distance = D;     // 终点的距离为 D
    sort(st, st + N, cmp);
    if (st[0].distance != 0)
        printf("The maximum travel distance = 0.00\n");
    else
    {
        double ans = 0, nowTank = 0, farRun = Cmax * Davg;
        while (now < N)     // now 保存当前加油站的索引
        {
            int k = -1;     // k 保存油价最低的加油站的索引
            double minPrice = 100000;
            for (int i = now + 1; i <= N && st[i].distance - st[now].distance <= farRun; ++i)
            {
                if (st[i].price < minPrice) // 如果找到了油价更低的加油站
                {
                    minPrice = st[i].price; // 更新最低油价
                    k = i;                  // 更新索引
                    if (minPrice < st[now].price) break;
                }
            }
            if (k == -1) break; // 即便满油也到不了任何加油站,退出循环
            double need = (st[k].distance - st[now].distance) / Davg;
            if (minPrice < st[now].price)   // 加油站 k 的油价比当前加油站的更低
            {
                if (nowTank < need) ans += (need - nowTank) * st[now].price;
                nowTank = nowTank < need ? 0 : nowTank - need;
            }
            else
            {
                ans += (Cmax - nowTank) * st[now].price;    // 油箱加满
                nowTank = Cmax - need;      // 更新到达加油站 k 后的剩余油量
            }
            now = k;    // 前往加油站 k
        }
        if (now == N) printf("%.2f\n", ans);
        else printf("The maximum travel distance = %.2f\n", st[now].distance + farRun);
    }
    
    return 0;
}

1034 Head of a Gang

题意:

输入第一行给出图中通话记录个数 N,以及权重阈值 K。随后 N 行,每行两个字符串 name1name2,以及一个整数 w。字符串表明通话的两个人的姓名(由三个大写字母组成),整数 w 是该次通话的时长,也就是权重。题目规定,若 A 与 B 有通话(即结点 A 和结点 B 之间有边相连),B 与 C 有通话,就把 A、B、C 称作为一个团体,也就是在一个连通图中。如果一个团体的成员数量超过2个人,且总的通话时长(总权重大于 K,就可以认定其为一个帮派,帮派中权重最大的结点就是这个帮派的头目(与它相连的边的权重之和)。现在要求你求出图中有多少个帮派,并按照字典升序输出每个帮派的头目名字以及该帮派的成员数量。

思路:

深度优先搜索遍历解决。

题目的意思就是求图中结点个数超过2,且总权值超过 K 的连通图(或者说是连通分量)的个数。那么整体思路就是通过深度优先搜索遍历访问图中的所有结点,记录结点个数超过2的连通图,并将其中权值最大的结点保存到一个数组中,最后统一输出。为了解决这一问题,涉及到的变量比较多,请读者耐心阅读。

因为姓名是通过字符串输入的,但是为了方便读取数组里的数据,需要建立由姓名到下标以及由下标到姓名的映射。因此将下标当做 id 来标记每一个结点,定义哈希映射 nametoididtoname。在输入通话记录的 name1 和 name2 的时候,如果遇到了一个新的 name,就建立一个互相的映射;如果该 name 已经建立过映射了,就返回它的 id

maxid 就是用于记录当前已分配的 id,二维数组 G 是邻接矩阵,G[i][j] 保存着两个结点间边的权值,数组 weight 保存每个结点的权重(其所有邻边的权重之和),而数组 visit 用于记录某个结点是否已经访问过了,从而防止重复访问。对于输入数据的处理就不多赘述了,相信读者自己看一遍也能明白。

DFSTravel 函数的作用是枚举图中的所有结点,如果它没有被访问过,就对其进行访问。由于深度优先搜索遍历的特性,我们从连通图中的任何一个结点进入,一定能访问到该连通图的所有结点。因此每次从 DFSTravel 函数进入一次 DFS,一定表明是一个新的连通图。也因此在开始遍历之前,先定义一些必要的变量,来保存遍历过程中需要记录的数据。

进入 DFS 后,首先将当前编号 u 的结点标记为已访问,然后对 membercnt 加1,表明该连通图结点个数加1。随后检查结点 u 的权重是否大于 head 的权重,如果是就更新头目的 id。然后枚举所有与 u 相连的结点,更新连通图的总权重,并对所有没有访问过的相邻结点递归执行 DFS 来访问。

返回 DFSTravel 函数后,表明当前连通图已经遍历完。遂检查其结点个数和总权重是否达到帮派的定义要求,如果达到了,就将其加入到哈希映射 ans 中,ans 保存了由头目到其所在帮派的总权重的映射。

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

map<string, int> nametoid, ans;
map<int, string> idtoname;
int maxid = 1, K, G[2010][2010], weight[2010];  // 邻接矩阵 G,每个人的权重
bool visit[2010] = { false };       // 标记是否被访问过

int GetId(string name)
{
    if (nametoid.find(name) == nametoid.end())  // 如果 name 没有出现过
    {
        nametoid[name] = maxid;     // 将 name 映射到 maxid
        idtoname[maxid] = name;     // 新 maxid 映射到 name
        return maxid++;             // maxid 加1,以分配给下一个名字
    }
    else return nametoid[name];     // 否则直接返回 name 对应的 id
}

void DFS(int u, int &head, int &membercnt, int &totalweight)
{
    visit[u] = true;    // 标记 u 为已访问
    ++membercnt;        // 成员人数加1
    if (weight[u] > weight[head]) head = u;     // 如果当前成员权重大于头目的权重就更新头目
    for (int v = 1; v < maxid; ++v)             // 枚举所有人
    {
        if (G[u][v])    // 如果 u 和 v 的边权不为0(有通话记录)
        {
            totalweight += G[u][v];             // 更新该团体的总权重
            G[u][v] = G[v][u] = 0;              // 删除该权重防止重复计算
            if (visit[v] == false)
                DFS(v, head, membercnt, totalweight);   // 如果成员 v 未被访问过就递归访问成员它
        }
    }
}

void DFSTravel()
{
    for (int i = 1; i < maxid; ++i) // 枚举所有人
    {
        if (visit[i] == false)      // 如果 i 未被访问过
        {
            int head = i, membercnt = 0, totalweight = 0;   // 头目,成员人数,团体权重
            DFS(i, head, membercnt, totalweight);
            if (membercnt > 2 && totalweight > K)       // 成员人数大于2且总权重大于 K
                ans[idtoname[head]] = membercnt;        // 建立团体头目与成员人数的映射
        }
    }
}

int main()
{
    int N, w;
    cin >> N >> K;
    string name1, name2;
    for (int i = 0; i < N; ++i)
    {
        cin >> name1 >> name2 >> w;
        int id1 = GetId(name1);     // 获取 name1 和 name2 的 id
        int id2 = GetId(name2);
        weight[id1] += w;           // 更新 id1 和 id2 的总权重
        weight[id2] += w;
        G[id1][id2] += w;           // 更新由 id1 到 id2 之间的边,加上权重 w
        G[id2][id1] += w;
    }
    DFSTravel();    // 遍历所有图(所有团体)

    cout << ans.size() << endl;
    for (auto i : ans)
        cout << i.first << " " << i.second << endl;

    return 0;
}

1035


1036


1037 Magic Coupon

题意:

输入总共四行,第一行是整数 Nc,第二行是 Nc 个整数,表示所有优惠券的面额,第三行是整数 Np,第四行是 Np 个整数,表示所有产品的面额。

现在要求你输出两个集合中,将整数相乘后的和的最大值,每个整数只能用一次。

思路:

定义数组 couponproduct 分别保存两个集合的整数。要求它们相乘的和的最大值,只需要将两个数组排序。用两个变量 pq 分别枚举两个数组的数,先从最小的负数开始逐个相乘,将结果加到 ans 上;再从各自最大的正数开始逐个相乘,将结果加到 ans 上。

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

int coupon[100005], product[100005];

int main()
{
    int Nc, Np, ans = 0, p = 0, q = 0;
    cin >> Nc;
    for (int i = 0; i < Nc; ++i) cin >> coupon[i];
    cin >> Np;
    for (int i = 0; i < Np; ++i) cin >> product[i];
    sort(coupon, coupon + Nc);
    sort(product, product + Np);
    while (p < Nc && p < Np && coupon[p] < 0 && product[p] < 0)
        ans += coupon[p] * product[p++];
    p = Nc - 1, q = Np - 1;
    while (p >= 0 && q >= 0 && coupon[p] > 0 && product[q] > 0)
        ans += coupon[p--] * product[q--];
    cout << ans;

    return 0;
}

1038 Recover the Smallest Number

题意:

输入只有一行,第一行数字字符串个数 N,随后 N 个数字字符串。要求你输出这些字符串所组合出的最小的数。当然输入的并不一定是字符串,题目说了每个数字片段是不超过8位的正整数。

思路:

由于需要组合成一个最小的数,所以还是需要用字符串来保存每个数字。其实仔细观察用例可以知道,这一题并不是简单的将所有字符串按字典序排序,然后连在一起输出的。比如 32 和 321,从字典序上来看 32 < 321,所以 32 应当排在 321 之前,但事实上是 321 排在 32 之前。这是因为 32132 < 32321。因此在排序的时候不是按照谁小谁排在前面,而是看 s1 + s2 与 s2 + s1(此处的加号是字符串里的连接符)谁更小。

还有一个注意点就是先导0,我用的方法是用 pos 来寻找结果字符串中的第一个非零字符,然后从 pos 开始将所有的字符如输出。但是需要注意的是,题目有可能输入的全是0,所以结果也应该是0。因此需要对这个也进行一个判断。

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

string ans, numbers[10005];

bool cmp(string &a, string &b) { return a + b < b + a; }    // 如果 a + b < b + a,就把 a 排在 b 之前

int main()
{
    int N, pos = 0;
    cin >> N;
    for (int i = 0; i < N; ++i) cin >> numbers[i];
    sort(numbers, numbers + N, cmp);
    for (int i = 0; i < N; ++i) ans += numbers[i];      // 将所有字符串连接起来
    while (pos < ans.size() && ans[pos] == '0') ++pos;  // 去除前导0
    if (pos == ans.size()) cout << "0"; // pos 等于 ans 长度时,表明整个字符串都是0,所以最小值就是“0”
    else for (int i = pos; i < ans.size(); ++i) cout << ans[i];

    return 0;
}

1039 Course List for Student

解释:

输入第一行是查询课程的学生数量 N总的课程数 K;接下来 K 组数据,每组数据两行:第一行是课程索引 course_id注册该门课程的学生数量 regist_num;第二行是 regist_num 个注册该门课程的学生姓名 stu_name。K 组数据输入完后,再输入一行 N 个查询课程的学生姓名。题目要求你输出 N 行查询结果,每行对应一个查询的学生:学生姓名,他注册的课程数量,并按照从小到大的顺序输出每个课程的索引。

思路:

定义学生课程表字典: map<string, set<int>> stu_courseList,键是学生姓名,值是一个集合,保存学生注册的课程 id。用集合的原因在于 set 会自动排序,输入的时候就直接将课程 id 加入到该学生的课程表里。语句 stu_courseList[stu_name].insert(course_id); 会先查看字典中是否存在学生 stu_name,有的话就直接把课程 id 加入到他的课程表,没有的话就会直接创建新的键值对,所以不需要使用查找。

输出的时候,先打印一个空格,在输出课程 id 前先输出一个空格就能满足题目的输出格式要求。

此题的变形可参考 PAT (Advanced Level) Practice 1041~10601047 Student List for Course,使用 map、set 和 string 的话就会超时。

注意:

  • 使用 map、string 可能会导致超时,因此我这种写法不是很可取。可以采用将字符串哈希映射的做法,将字符串映射成一个整数。题目中的姓名只有 26*26*26*10 = 175760 种可能,因此可以定义一个常数 M = 26*26*26*10 和 vector<int> courseList[M],对于每个输入的姓名都计算其哈希值 name_hash,然后将他的课程 id 加入 courseList[name_hash]。
  • 数据量很大的情况下,不要使用 cin 和 cout 进行输入和输出。
  • 使用二维数组一定会超时!记住这点,因为二维数组是直接定义 n*n 的矩阵,用 vector 就能减少很多的空间。
#include <iostream>
#include <map>
#include <set>
using namespace std;

int main()
{
    int N, K, course_id, regist_num;
    string stu_name;
    map<string, set<int>> stu_courseList;   // 课程 id 都是唯一的,使用 set 可以自动实现排序
    cin >> N >> K;
    while (K--)
    {
        cin >> course_id >> regist_num; 	// 课程 id,注册学生数
        while (regist_num--)
        {
            cin >> stu_name;	// 注册课程 id 的学生姓名
            stu_courseList[stu_name].insert(course_id); // 将课程 id 加入学生课程列表
        }
    }

    while (N--)
    {
        cin >> stu_name;    // 查询课程的学生姓名
        cout << stu_name << " " << stu_courseList[stu_name].size();
        for (auto it = stu_courseList[stu_name].begin(); it != stu_courseList[stu_name].end(); ++it)
            cout << " " << *it;
        cout << endl;
    }

    return 0;
}

1040


一定要自己写一遍哦~~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值