PAT (Advanced Level) Practice 1061~1080


1061


1062 Talent and Virtue

题解可参考: PAT (Basic Level) Practice 1001~1022

原题请点击:1015 德才论

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

struct stu
{
    int id, d_score, c_score, t_score;  // 准考证号、德分、才分、总分
    stu(int id, int d_score, int c_score)
    {
        this->id = id;
        this->d_score = d_score;
        this->c_score = c_score;
        this->t_score = d_score + c_score;
    }
};

vector<stu> vec[4];     // 定义二维数组
int m, l, h, d, c, i;

bool compare(struct stu p, struct stu q)
{
    if (p.t_score != q.t_score) return p.t_score > q.t_score;           // 总分不相同时,总分高的排在前面
    else if (p.d_score != q.d_score) return p.d_score > q.d_score;      // 总分相同时,德分更高的排在前面
    else return p.id < q.id;    // 总分和德分相同的话按照准考证号升序排列
}

int main()
{
    cin >> m >> l >> h;
    while (m--)
    {
        cin >> i >> d >> c;
        struct stu temp(i, d, c);
        if (d >=h && c >= h) vec[0].push_back(temp); // 第一类考生:“才德全尽”
        else if (d >= h &&  l <= c && c < h) vec[1].push_back(temp);    // 第二类考生:“德胜才”
        else if (l <= d && d < h && l <= c && c < h && d >= c)
            vec[2].push_back(temp); // 第三类考生:“才德兼亡”但尚有“德胜才”
        else if (l <= d && d < h && l <= c) vec[3].push_back(temp);     // 第四类考生
    }
    for (int i = 0; i < 4; ++i)
        sort(vec[i].begin(), vec[i].end(), compare);
    cout << vec[0].size() + vec[1].size() + vec[2].size() + vec[3].size() << endl;
    for (int i = 0; i < 4; ++i)
    {
        for (int j = 0; j < vec[i].size(); ++j)
            cout << vec[i][j].id << " " << vec[i][j].d_score << " " << vec[i][j].c_score << endl;
    }

    return 0;
}

1063 Set Similarity

题意:

输入第一行是集合的个数 N,随后 N 行整数,每行第一个是整数 M,紧接着 M 个正整数,表示输入到集合里数。然后是查询次数 K,随后 K 行,每行两个集合的序号。要求你输出每次查询的两个集合的相似度,相似度是交集元素个数并集元素个数的比值的百分比形式,保留一位小数。

思路:

因为要求集合的交并集,所以在输入的时候就用集合来保存,从而过滤掉重复的整数,因此定义集合数组 sets[maxn]。按照输入的顺序给集合编号,将 M 行整数加入到各自的集合中。随后 K 次查询,输入两个集合的序号 ab

如果集合 a 的大小比集合 b 大,就交换一下 a 和 b 的值。定义整数 unite 记录交集元素个数。枚举集合 a 中的元素 *it,如果在集合 b 中能找到该元素,则 unite 加1。最后计算相似度,并集元素个数就是两个集合的元素个数和再减去交集元素个数。

经验:

  • 对集合中元素的顺序不做要求时,用 unordered_set 速度会快很多。
  • 使用 set 自带的 find() 函数比 algorithm 库里的泛型 find() 函数速度快很多!用后者会直接超时!
  • set.count() 和 set.find() 效果一样好!

注意点:

  • 集合可能存在重复的元素,所以需要用 set 来存储元素。
  • 输出百分比形式,所以 unite 要乘以100。
#include <cstdio>
#include <unordered_set>
using namespace std;

const int maxn = 55;
unordered_set<int> sets[maxn];

int main()
{
    int N, M, m, K, a, b;
    scanf("%d", &N);        // N 个集合
    for (int i = 1; i <= N; ++i)
    {
        scanf("%d", &M);    // M 个整数
        while (M--)
        {
            scanf("%d", &m);
            sets[i].insert(m);  // 将整数插入集合
        }
    }
    scanf("%d", &K);        // K 次查询
    while (K--)
    {
        scanf("%d%d", &a, &b);
        int unite = 0;
        for (auto it = sets[a].begin(); it != sets[a].end(); ++it)
            if (sets[b].find(*it) != sets[b].end()) ++unite;
        printf("%.1f\%\n", unite * 100.0 / (sets[a].size() + sets[b].size() - unite));

    }

    return 0;
}

1064

题意:

输入第一行是树中结点个数 N,随后一行是 N 个正整数,表明树中每个结点的键值。现在要求你输出将这些值排列成一个完全二叉排序树后的层序遍历序列。

思路:

如果将一棵完全二叉排序树的值保存在数组 CBT 中。假设根结点的下标为1,那么对于树中的任何一个结点,若其编号为 x,则其左孩子的编号为 2x,右孩子的编号为 2x + 1。对于一棵二叉排序树来说,其中序遍历序列是递增的,所以可以先将输入的值从小到大排好序,保存在数组 keys 中。对 CBT 进行中序遍历,在遍历过程中将 keys 中的值按从小到大填入 CBT 中,最后就能得到一棵完全二叉排序树。又由于 CBT 数组就是按照二叉树的层序遍历来存放结点的值的,因此只要按顺序打印 CBT 中所有的值即可。

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

int N, j = 0, keys[1005], CBT[1005];

void InOrder(int root)
{
    if (root > N) return;   // 空结点,直接返回
    InOrder(root * 2);      // 中序递归遍历左子树
    //cout << "root:" << root << " j:" << j << endl;
    CBT[root] = keys[j++];  // 赋值
    InOrder(root * 2 + 1);  // 中序递归遍历右子树
}

int main()
{
    cin >> N;
    for (int i = 0; i < N; ++i) cin >> keys[i];
    sort(keys, keys + N);   // 对键值排序
    InOrder(1);             // 从根结点开始遍历
    cout << CBT[1];
    for (int i = 2; i <= N; ++i)
        cout << " " << CBT[i];
    
    return 0;
}

1065


1066 Root of AVL Tree

题意:

输入第一行是平衡二叉树中的结点个数 N,随后是每个结点的值。现在要求你按照输入的所有值构建出一棵平衡二叉树,然后返回根结点的值。

#include <iostream>
using namespace std;

struct AVLNode
{
    int data;
    AVLNode *lchild = nullptr, *rchild = nullptr;
};

void LeftRotation(AVLNode *&root)   // 左旋操作
{
    AVLNode *temp = root->rchild;   // temp 指向根结点的右孩子
    root->rchild = temp->lchild;    // 令 temp 的左孩子成为 root 的右孩子
    temp->lchild = root;            // 令根结点成为 temp 的左孩子
    root = temp;                    // 将 temp 的值赋给 root,即令 temp 成为新的根结点
}

void RightRotation(AVLNode *&root)  // 右旋操作
{
    AVLNode *temp = root->lchild;   // temp 指向根结点的左孩子
    root->lchild = temp->rchild;    // 令 temp 的右孩子成为 root 的左孩子
    temp->rchild = root;            // 令根结点成为 temp 的右孩子
    root = temp;                    // 将 temp 的值赋给 root,即令 temp 成为新的根结点
}

void LeftRightRotation(AVLNode *&root)
{
    LeftRotation(root->lchild);     // 先对根结点的左孩子左旋
    RightRotation(root);            // 再对根结点右旋
}

void RightLeftRotation(AVLNode *&root)
{
    RightRotation(root->rchild);    // 先对根结点的右孩子右旋
    LeftRotation(root);             // 再对根结点左旋
}

int GetHeight(AVLNode *root)
{
    if (root == nullptr) return 0;  // 空结点高度为0
    return max(GetHeight(root->lchild), GetHeight(root->rchild)) + 1;   // 否则为左右子树高度较大值加1
}

void Insert(AVLNode *&root, int x)  // 向平衡二叉树中插入值为 x 的结点
{
    if (root == nullptr)             // 到达空结点
    {
        root = new AVLNode();       // 创建新结点
        root->data = x;             // 新结点的值为 x
        return;
    }
    if (x < root->data)             // x 比根结点的值小
    {
        Insert(root->lchild, x);    // 往左子树插入
        if (GetHeight(root->lchild) - GetHeight(root->rchild) == 2)
            x < root->lchild->data ? RightRotation(root) : LeftRightRotation(root);
    }
    else
    {
        Insert(root->rchild, x);    // x 比根结点的值大,往右子树插入
        if (GetHeight(root->lchild) - GetHeight(root->rchild) == -2)
            x > root->rchild->data ? LeftRotation(root) : RightLeftRotation(root);
    }
}

int main()
{
    int N, n;
    cin >> N;
    AVLNode *root = nullptr;
    for (int i = 0; i < N; ++i)
    {
        cin >> n;
        Insert(root, n);
    }
    cout << root->data;
    
    return 0;
}

1067 Sort with Swap(0, i)

题意:

输入第一行给你正整数 N,随后一行是 N 个正整数,表示一个由 0 ∼ N − 1 0 \sim N - 1 0N1 这 N 个数打乱的排列。你需要将0与其它整数进行不断进行交换,最后使得排列有序(由小到大),问交换次数最少是多少。

思路:

观察用例的思路可以知道,最后要的效果就是0在索引0的位置,1在索引1的位置,2在索引2的位置…因此在交换的时候,只需将0与本该在这个位置上的数交换即可,这样除了0以外,每个数都只会被交换一次,从而达到最小的交换次数。

定义数组 pos 保存每个整数的所在位置。定义变量 ans 保存交换的次数, left 表示除了0以外有几个数不在本位上,k 表示第一个不在本位上的数,稍后会讲其作用。

所以我的基本思路是这样的,对于初始的排列中的每个数 n,pos[n] 表示的是其当前所在的位置。因此若要将其位置和0的位置互换,即只需交换 pos[n] 和 pos[0] 即可。那么 n 是多少呢?n 就是 pos[0] 所在的位置。比如说0当前在索引7的位置,于是存在 pos[0] = 7。若要将0跟整数7交换,让7回到它本该在的位置,就是交换 pos[0] 和 pos[7] 的值,也就是执行语句 swap(pos[0], pos[pos[0]]),如果你能明白这个基本逻辑,那这题你就能懂了。

现在的问题就是,在这么不断交换的过程中,0有可能会回到自己的本位,而排列却还是无序的状态。如下图所示,经过三次交换后0就回到了本位,此时语句 swap(pos[0], pos[pos[0]]) 就是原地打转了。
在这里插入图片描述
那么解决的策略就是在0回到本位之后,从其后的位置寻找一个不在本位上的数,将其和0交换。因为这个数本来就不在本位上,后面依然会将其换回来,所以这样一次交换并不会有影响。所以在代码中就多了 if (pos[0] == 0) 的判断,在这个判断中,k 不断自增的同时还需要判断 pos[k] 是否等于 k,也就是数 k 是否在本位上,如果不在,就将它与0进行交换。

#include <iostream>
using namespace std;

int pos[100005];

int main()
{
    int N, n, ans = 0;
    cin >> N;
    int left = N - 1, k = 1;
    for (int i = 0; i < N; ++i)
    {
        cin >> n;
        pos[n] = i; // 数 n 所处的位置是 i
        if (n == i && n != 0) --left;   // left 表示除了0有几个数不在本位上
    }
    while (left)    // 只要还有数不在本位上就继续循环
    {
        if (pos[0] == 0)        // 如果0回到了本位
        {
            while (k < N)
            {
                if (pos[k] != k)        // 寻找第一个不在本位上的数 k
                {
                    swap(pos[0], pos[k]);
                    ++ans;      // 交换次数加1
                    break;      // 退出循环
                }
                ++k;            // 继续判断 k + 1 是否在本位
            }
        }
        while (pos[0] != 0)
        {
            swap(pos[0], pos[pos[0]]);  // 将0与 pos[0] 交换
            ++ans; --left;              // 交换次数加1,不在本位上的数减1
        }
    }
    cout << ans;

    return 0;
}

1068


1069 The Black Hole of Numbers

此题为 PAT (Basic Level) Practice 1019 数字黑洞 的英文版,点击链接进入原题,题解可看 PAT (Basic Level) Practice 1001~1022


1070

此题为 PAT (Basic Level) Practice 1020 月饼 的英文版,题解可看 PAT (Basic Level) Practice 1001~1022


1071 Speech Patterns

题意:

给出一行长度不超过1048576个字符的文本,包含很多由数字或者字母构成的“单词”(不是纯英文单词),这些单词用非数字字母的字符隔开。现在要求你输出这一行文本里,出现次数最多的“单词”和它出现的次数。

思路:

定义字典 words 保存文本中的所有单词,键是单词,值是其出现的次数。定义字符串 word 保存每个单词。

利用 c = getchar() 边输入边判断。如果是数字或者字母,就加入 word;如果不是,就先判断 word 是否为空,如果为空就进入下次循环,不为空的话就将其出现的次数加1。

注意点:

  • 题目规定了单词由大小写字母、数字组成,因此 can1 与 can 是两个不同的单词。
  • 如果字典里没有 word,words[word] 会被自动创建且赋值为0。
#include <iostream>
#include <map>
using namespace std;

int main()
{
	map<string, int> words;
	string word(""), max_key("");
	int max_val = -1;
	char c;
	while (c = getchar())		// 输入字符
	{
		if (isalnum(c)) word += tolower(c);	// 转为小写后加入 word
		else if (word != "")
		{
			words[word] = words[word] ? words[word] + 1 : 1;
			if (words[word] > max_val)		// 找到新的最大值,这两条语句不能调换顺序
			{	
				max_val = words[word];		// 更新出现最多的次数
				max_key = word;				// 更新出现最多的单词
			}
			word = "";			// 清空 word 用来保存下一个单词
		}
		if (c == '\n') break;	// 读到回车结束循环
	}
	cout << max_key << " " << max_val;

	return 0;
}

1072

题意:

输入第一行是四个正整数,第一个是住宅的总数 N,第二个是加油站点的总数 M,第三个是点之间的道路的总数 K,第四个是加油站的服务范围 Ds。随后 M 行是每条道路的信息,每行三个数据,前两个数据是点的编号,第三个数据是道路的长度,是一个正整数。

现在要在 M 个加油站点上建一个加油站,要求这个加油站尽可能的远离所有住宅,同时,该站点与所有住宅的平均路径长度得是最小的,还得保证所有住宅都在其服务范围内(服务范围是长度,不是以站点为圆心的半径)。所以对于每一个加油站点,要计算出其到达每一个住宅的最短路径,然后选择其中路径最短的作为该站点离住宅的距离。然后选择距离最大的站点作为建加油站的位置,如果有多个站点的离住宅的距离一样,就选择到所有住宅平均距离最小的站点。住宅的编号是 1 ~ N,而加油站点的编号是 G1 ~ GM。最后要求你打印符合要求的站点编号,在下一行分别打印其与住宅的距离,以及平均距离,后两者精确到小数点后一位。

注意一点,加油站点也是算在最短路径里面的,也就是说,是可以通过某一个加油站点到达另一个住宅的,即使该站点不用于建加油站。

思路:

迪杰斯特拉算法解决。

d[i] 表示由某一加油站点到点 i 的最短路径的路径长度,G[u][v] 表示从点 u 到点 v 的路径长度(值为0时表明 u, v 之间没有边),st 保存符合要求站点的编号,farest 保存符合要求站点与住宅的距离,minsum 保存符合要求站点与所有住宅的最短路径的和。

因为加油站点的编号不是一个整数,所以要用字符串保存。处理的时候就是根据第一个字符是否为 G,如果是就将剩余子串转化为整数;不是 G 的话就直接转为整数即可。然后加油站点也是点,所以可以将其编号在住宅点之后,所以对于加油站点的编号就取数字部分后再加上 N 即可。

通过迪杰斯特拉算法求出每一个加油站点到达每一个住宅的最短路径长度。然后枚举其与所有住宅的最短路径长度,找出其与住宅的距离,加油站点与住宅的距离等于其与所有住宅的最短路径中的最小值。同时需要关注该站点是否能服务到所有的住宅。得到这些数据之后再将其与 farest 作对比,在 farest 相等的情况下,就需要比较平均距离,所以在枚举的过程还需要统计一下每个加油站点的最短路径的和。

#include <iostream>
using namespace std;

int N, M, K, Ds, G[1020][1020], d[1020], st = -1, farest = -1, minsum = 999999999;
bool visit[1020];
inline int StrToInt(string s) { return stoi(s[0] == 'G' ? s.substr(1) : s) + (s[0] == 'G' ? N : 0); }

int main()
{
    string st1, st2;
    cin >> N >> M >> K >> Ds;
    for (int i = 0, dis; i < K; ++i)
    {
        cin >> st1 >> st2 >> dis;
        G[StrToInt(st1)][StrToInt(st2)] = G[StrToInt(st2)][StrToInt(st1)] = dis;
    }
    for (int i = N + 1; i <= N + M; ++i)  // 枚举所有加油站
    {
        fill(d, d + 1020, 0x3fffffff);      // 将起始点到所有点的距离初始化为 0x3fffffff
        fill(visit, visit + 1020, false);   // 将所有点置为未访问过
        d[i] = 0;   // 起始点到本身的最短路径长度为0
        for (int j = 0; j < N + M; ++j)    // 循环 N + M 次来保证访问到每一个点
        {
            int u = -1, MIN = 0x3fffffff;
            for (int k = 1; k <= N + M; ++k)    // 枚举所有的点,寻找未访问过的点中道路最短的点
            {
                if (!visit[k] && d[k] < MIN)
                {
                    u = k;
                    MIN = d[k];
                }
            }
            if (u == -1) break;     // 找不到小于 0x3fffffff 的数,说明剩下的点和起点 i 都不连通,进入下次循环
            visit[u] = true;        // 标记 u 为已访问
            for (int v = 1; v <= N + M; ++v)     // 枚举所有的点
            {   // 如果 v 没有访问过,且由 u 到 v 有边,且以 u 为中介点可以使 d[v] 更小
                if (!visit[v] && G[u][v] != 0 && d[u] + G[u][v] < d[v])
                    d[v] = d[u] + G[u][v];      // 更新最短路径
            }
        }
        int mind = 0x3fffffff, flag = 1, sum = 0;   // mind 是站点与住宅的距离,flag = 1 表明该站点能服务所有住宅
        for (int v = 1; v <= N; ++v)                // 枚举所有的住宅,寻找最短路径中最小的一个作为站点与住宅的距离
        {
            flag = d[v] > Ds ? 0 : 1;               // 如果存在最短路径超过服务范围,该站点就不符合要求
            mind = d[v] < mind ? d[v] : mind;       // 更新该站点与住宅的距离
            sum += d[v];                            // 更新最短路径的和
        }
        if (flag && ((mind > farest) || (mind == farest && sum < minsum)))
        {   // 若该站点与住宅的距离更大,且所有住宅都在服务范围内
            st = i;         // 更新站点编号
            farest = mind;  // 更新站点与住宅的距离
            minsum = sum;   // 更新站点与所有住宅的最短路径的和
        }
    }
        
    if (st == -1) printf("No Solution");
    else printf("G%d\n%.1lf %.1lf", st - N, farest * 1.0, minsum * 1.0 / N);

    return 0;
}

1073


1074 Reversing Linked List

此题为 PAT (Basic Level) Practice 1025 反转链表 的英文版,点击链接进入原题,题解可看 PAT (Basic Level) Practice 1023~1044


1075 PAT Judge

题意:

输入第一行是三个整数,分别是用户总数 N ,题目总数 K 和提交总数 M。随后一行是 K 个整数,分别对应 K 个题目的满分。

接下来是 M 行提交,首先是用户的 id(00001 ~ N),然后是题目的 id(1 ~ K),最后是该用户的得分(编译通过的话得分最低为0)。如果得分为-1,表明提交无法通过编译。

现在要求你按照要求计算出排名表并按照指定的格式打印出来:排名,用户 id,总分,该用户提交的每道题的得分。对于某一题来说,如果用户没有提交过代码,但他是在排名表上的,就输出 ‘-’。如果用户对某一题提交过多次,就取最高分作为该题最终得分。

排名表是按照总分来计算的,总分一样的排名一样,且必须是非递减的顺序;排名一样的用户取得满分的个数来排名,如果依然存在相等的,就以 id 递增的顺序排。对于没有任何通过编译的提交,或者是没有任何提交的用户都不能放在排名表上,换句话说,能放到排名表上的用户,至少提交过一道题且通过编译。输入保证了至少有一名用户可以置于排名表上。

思路:

定义结构体 user,保存每个用户的 id,总分 total,是否在排名表上 ifRanked,取得满分的题目数 fullCnt,排名 rank。同时定义 users 数组保存所有的用户数据,score 数组保存每个用户每道题的得分情况。然后定义比较函数 cmp。

定义 fullScore 数组保存每一道题目的满分,再利用 memset 函数将 score 数组初始化为-1。正常输入 M 行提交记录:用户 id uid,题目 id pid 以及得分 scp。通过 max 函数更新该用户的最大得分。关键点来了,若用户的提交结果不是-1,表明他编译通过了,不管得分是否为0,他都应该放到排名表上,故要令 ifRanked 等于1;若是-1,则再紧接着判断 score[uid][pid] 是否为-1,如果是的话就令 score[uid][pid] = -2,表明用户 uid 在题目 pid 上编译未通过;如果不是就保持-1的值表明用户 uid 未提交题目 pid 的解答。

输入完就需要对 score 数组做一点处理。用 i 枚举 users 数组,如果用户 i 在排名表上,计数变量 valid 加1表明排名表上人数加1。对每个用户,用 j 枚举他的得分 score[i]。如果用户在排名表上且 score[i][j] 是-2,就需要将他在该题的得分情况改为0;同时还要判断一下该题是否得了满分;最后再更新用户的总得分。

后面的就都比较简单了。

注意:

  • 如果用户本身能参与排名,那么他对某题提交的代码未通过编译时,需要把得分计为0。
  • 并列排名的时候要注意,比如五名用户的总分为 10 8 8 8 7 时,排名应该是 1 2 2 2 5 而不是 1 2 2 2 3。
  • 若用户 uid 对题目 pid 编译通过并得到分了,那么在后面如果编译未通过时,不要把 score[uid][pid] 改成-2了。
  • 存在同一个用户对同一个题目多次提交满分的情况,此时只能统计一次,而不要统计多次。
  • cmp 函数中一定要加上对 ifRanked 的排序,否则可能会出现相邻的几个用户的 ifRanked 的值是 1 0 1 这样交叉的情况。这个时候你对他们进行排名或者输出的时候就很麻烦了。
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;

struct user {
    int id, total, ifRanked, fullCnt, rank;
} users[10005];
int score[10005][6];

bool cmp(user a, user b)
{
	if (a.total != b.total) return a.total > b.total;
	else if (a.fullCnt != b.fullCnt) return a.fullCnt > b.fullCnt;
	else return a.ifRanked != b.ifRanked ? a.ifRanked > b.ifRanked : a.id < b.id;
}

int main()
{
	int N, K, M, fullScore[6], valid = 0, uid, pid, scp;
	scanf("%d%d%d", &N, &K, &M);		// 用户总数 题目总数 提交总数
	for (int i = 1; i <= K; ++i) scanf("%d", &fullScore[i]);	// 输入每道题的满分
	memset(score, -1, sizeof(score));	// 每一名用户在每一道题的得分初始化为-1
	while (M--)
	{
		scanf("%d%d%d", &uid, &pid, &scp);				// 用户 id   题目 id    得分
		users[uid].id = uid;							// 保存用户 id
		score[uid][pid] = max(score[uid][pid], scp);	// 更新每道题的最高得分
		if (scp != -1) users[uid].ifRanked = 1;			// ifRanked 为1表明可以参与排名
		else if (score[uid][pid] == -1) score[uid][pid] = -2;	// -2表明未通过编译,-1表明未提交
	}
	for (int i = 1; i <= N; ++i)
	{
		if (users[i].ifRanked) ++valid;	// valid 统计排名表上的人数
		for (int j = 1; j <= K; ++j)
		{	// 对于在排名表上的用户,如果某一题未通过编译,视作得了零分
			if (users[i].ifRanked && score[i][j] == -2) score[i][j] = 0;
			if (score[i][j] == fullScore[j]) ++users[i].fullCnt;	// 统计取得满分的题目数
			users[i].total += score[i][j] == -1 ? 0 : score[i][j];	// 统计总分
		}
	}
	sort(users + 1, users + N + 1, cmp);

	users[1].rank = 1;
	for (int i = 2; i <= valid; ++i)	// 计算每个用户的排名
		users[i].rank =  users[i].total == users[i - 1].total ? users[i - 1].rank : i;
	for (int i = 1; i <= valid; ++i)
	{
		printf("%d %05d %d", users[i].rank, users[i].id, users[i].total);
		for (int j = 1; j <= K; ++j)
			score[users[i].id][j] == -1 ? printf(" -") : printf(" %d", score[users[i].id][j]);
		printf("\n");
	}

    return 0;
}

1076 Forwards on Weibo

题意:

输入第一行是用户的总数 N 和最大转发层数 L,随后是 N 行对应编号第 1 ~ N 名用户的信息。每行的第一个整数是该用户的关注数量 M,随后 M 个数是其关注的用户的编号。最后一行第一个整数是查询次数 K,随后 K 个数是查询的用户编号。题目规定每名用户只能转发一次他关注的用户的推文,现在要求你计算,对于要查询的每一个用户,其发出的推文在最大转发层数内,最多能被多少人转发。

思路:

广度优先搜索遍历解决。

使用邻接表 G 来存储两个结点之间是否有边,但在这里需要注意,因为指明了只能转发关注者的推文,所以此题用有向表更好解决。G[j][i] = 1 表明由结点 j 到结点 i 有边,同样也表明了结点 i 可以转发结点 j 的推文。在广度优先搜索遍历中,这么设定就可以通过结点 j 访问到结点 i。

定义数组 level 表明每个结点所处的层数,初始进入广度优先搜索遍历的结点层数便为0,其子结点的层数就是1,孙子结点的层数就是2,曾孙结点的层数就是3。具体的实现细节读者可以阅读 BFS 的代码来更好的理解。

至于为什么选用广度优先搜索遍历而不是深度优先搜索遍历,是因为如果采用深度优先搜索遍历,还需要考虑某一个结点是否访问过、是否转发过。采用广度优先搜索遍历时,我们只用考虑结点是否在队中,因此只需要设置 isinq 数组来表明这一点。在所有结点入队的时候先判断其是否在队中,不再队中就入队,同时标记它的层数。然后统一标记它为已入队。那么怎么计算转发的次数呢?出队一个转发一个即可。

注意:

  • 此题如果用深度优先搜索遍历就很容易超时,花时大头在中断并递归上。
#include <iostream>
#include <vector>
#include <queue>
using namespace std;

int N, L, forwards;
vector<int> G[1010], level(1010);   // 数组 G[i] 保存所有与 i 有边的结点编号
bool isinq[1010] = { false };

void BFS(int u)
{
    queue<int> q;
    q.push(u);          // 根结点入队
    isinq[u] = true;
    while (!q.empty())
    {
        int temp = q.front();   // temp 保存队首结点的编号
        q.pop();                // 队首结点出队
        ++forwards;             // 队列中每弹出一个结点,转发数加1
        if (level[temp] == L) continue;                 // 当结点层数等于 L 时,不再执行子结点入队的操作
        for (int i = 0; i < G[temp].size(); ++i)        // 枚举结点 temp 的所有子结点
        {
            if (!isinq[G[temp][i]])     // 如果子结点未入队
            {
                q.push(G[temp][i]);     // 子结点入队
                level[G[temp][i]] = level[temp] + 1;    // 子结点层数为结点 temp 的层数加1
            }
            isinq[G[temp][i]] = true;   // 标记子结点为已入队
        }
    }
}

int main()
{
    int M, n, K;
    cin >> N >> L;
    for (int i = 1; i <= N; ++i)
    {
        cin >> M;
        while (M--)
        {
            cin >> n;
            G[n].push_back(i);  // 由 n 到 i 有边,表明 n 可以转发 i 的推文
        }
    }
    cin >> K;
    for (int i = 0; i < K; ++i) // K 个查询
    {
        cin >> n;
        forwards = -1;           // 重置转发次数为-1
        for (int i = 1; i <= N; ++i) isinq[i] = false;
        for (int i = 1; i <= N; ++i) level[i] = 0;
        BFS(n); // 开始广度优先搜索遍历
        cout << forwards << endl;
    }

    return 0;
}

1077


1078 Hashing

题意:

给出散列表长 TSize 和欲插入的元素,将这些元素按读入的顺序插入散列表中,其中散列函数为H(key) = key % TSize,解决冲突采用只往正向增加的二次探查法。另外,如果题目给出的 TSize 不是素数,那么需要将 TSize 重新赋值为第一个比 TSize 大的素数再进行元素插入。

思路:

  1. 首先,对于一个输入的哈希表大小 m,如果不是素数,则必须找到第一个比它大的素数。
  2. 开一个 bool 型数组 hashTable[],hashTable[x] == false 表示 x 号位未使用。对每个插入的元素 num,计算 num % m 并判断对应位置是否被使用。如果未被使用,那么就找到可以插入的位置并输出。
  3. 如果已被使用,根据二次方探查法,令步长 step 初值为1,然后令下一个检测值为 (num + step * step) % m,判断该位置是否已被占用:如果已被占用,则令 ++step,再进行判断。当 step 自增达到 m 时如果还没有找到可用位置,则表明这个这个元素无法被插入。

注意点:

  • Quadratic probing 是指二次方探查法,即当 H(a) 发生冲突时,让 a 按 a + 1 2 , a − 1 2 , a + 2 2 , a − 2 2 , a + 3 2 , a − 3 2 . … a + 1^2, a - 1^2, a + 2^2, a - 2^2, a + 3^2, a - 3^2.… a+12,a12,a+22,a22,a+32,a32. 的顺序调整 a 的值。本题中已经说明只要往正向解决冲突,因此需要按 a + 1 2 , a + 2 2 , a + 3 2 . . . . a + 1^2, a + 2^2, a + 3^2.... a+12,a+22,a+32.... 的顺序调整 a 的值。
  • 出现“格式错误”的话需要注意,在最后一个输出结束后不能有空格。

为什么 step 自增到 m 仍找不到插入位置,对于大于等于 m 的值都不可能找到位置呢?

证明:

0 ≤ x < m 0 \le x < m 0x<m,那么
( a + ( m + x ) ( m + x ) ) % m (a + (m + x) (m + x)) \% m (a+(m+x)(m+x))%m
= ( a + m 2 + 2 m x + x 2 ) % m =(a + m^2 + 2mx + x^2) \% m =(a+m2+2mx+x2)%m
= ( a + x 2 ) % m + m 2 % m + 2 m x % m =(a + x^2) \% m + m^2 \% m + 2mx \% m =(a+x2)%m+m2%m+2mx%m
= ( a + x 2 ) % m ≠ 0 =(a + x^2) \% m \ne 0 =(a+x2)%m=0
因此对于大于等于 m 的值都不可能找到位置插入,注意此处 m 2 % m m^2 \% m m2%m 以及 2 m x % m 2mx \% m 2mx%m 的结果都为0。

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

bool hashTable[11111] = {false}; // hashTable[x] == false 表示 x 号位未使用

bool isPrime(int a) // 判断 a 是否为素数
{
    if (a <= 1) return false;
    for (int i = 2; i * i <= a; ++i)
        if (a % i == 0)
            return false;
    return true;
}

int main()
{
    int m, n, num, d;
    cin >> m >> n; // 初始哈希表大小,n 个待散列数据
    while (isPrime(m) == false)
        ++m; // 寻找第一个大于等于 m 的素数
    while (n--)
    {
        cin >> num; // 待散列数
        d = num % m; // 用 d 存储余数,后面的过程就不用再计算
        if (hashTable[d] == false) // 如果 d 号位未使用,则插入
        {
            cout << d;
            hashTable[d] = true;
            if (n != 0)
                cout << " ";
        }
        else
        {
            int step = 1;
            while (step < m)
            {
                d = (num + step * step) % m;
                if (hashTable[d] == false)
                {
                    cout << d;
                    hashTable[d] = true;
                    break;
                }
                ++step;
            }
            if (step >= m) // 找不到插入的地方
                cout << '-';
            if (n != 0)
                cout << " ";
        }
    }

    return 0;
}


1079 Total Sales of Supply Chain

题意

输入第一行是一个正整数 N,表明树中总共有几个结点(编号 0 ~ N - 1)。第二、三个数都是浮点数,用 pricerisk 保存,分别表示供应商处批发产品的单价以及每一层的价格涨幅。

随后 N 行数,分别对应第 0 ~ N - 1 个结点。每行第一个整数为0表明该结点是零售商,随后一个整数表明该零售商的销量;如果不是0则表明该结点是经销商,其数值表明该结点的孩子结点个数,随后就是该结点的所有孩子结点的编号。现在要求你出输入所有零售商销售完所有商品所得到的总销售额,精确到小数点后一位

题目保证了只有一个供应商,也就是结点0,而且所有经销商和零售商的产品都得从供应商那以单价 price 进货,同时每往下走一层,相应的售价就要上涨 risk%。题目也保证了只有零售商会直接面对消费者出售产品。

思路:

搞清楚题目结点的关系后解决起来就不难了。定义结构体 Node 保存每个结点的销量 amount,以及一个数组 childs 保存其所有孩子结点的编号,数组 nodes 则保存树中的所有结点。

输入时,根据第一个整数是否为0判断其是零售商还是经销商或供应商,这关系到其后面的整数是存入 amount 还是 childs 数组。通过深度优先搜索遍历,可以遍历树的每一层,同时将遍历的深度也作为参数传入。在遍历的过程中,根据其孩子结点数组的长度判断其是否为零售商,如果是零售商就更新销售额;否则就遍历其所有孩子结点,对每个孩子都执行一次深度优先搜索遍历。

根据题给测试样例建树如下所示:
在这里插入图片描述
注意:

  • 输入的 rise,因为单位是 %,输入的是1.00,实际上是1.00%,所以需要将其除以100才能得到实际的数值。
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;

struct Node {
    int amount;
    vector<int> childs;
};
vector<Node> nodes;

double price, rise, sales = 0.0;

void DFS(int root, int depth)
{
    if (nodes[root].childs.size() == 0) // 如果结点 root 的孩子结点数组的大小为0,说明其是零售商
        sales += nodes[root].amount * price * pow(1 + rise / 100, depth);   // 更新销售额
    else for (int i = 0; i < nodes[root].childs.size(); ++i)
        DFS(nodes[root].childs[i], depth + 1);
}

int main()
{
    int N, n, m;
    scanf("%d%lf%lf", &N, &price, &rise);
    nodes.resize(N);
    for (int i = 0; i < N; ++i)
    {
        scanf("%d", &n);
        if (n == 0) scanf("%d", &nodes[i].amount);  // 如果是零售商,就保存其销量
        else
        {
            while (n--)
            {
                scanf("%d", &m);                    // 输入孩子结点编号
                nodes[i].childs.push_back(m);       // 将其放入结点 i 的孩子结点编号数组
            }
        }
    }
    DFS(0, 0);  // 进行深度优先遍历确定销售额
    printf("%.1lf", sales);

    return 0;
}

1080 Graduate Admission

题意:

输入第一行给出三个正整数:考生总数 N,学校总数 M,志愿数 K。随后一行给出 M 个正整数,表示所有学校的招生限额。考生编号、学校编号都是从0开始。

接下来是 N 行考生的数据,前两个整数是考生的高考分和面试分,随后是 K 个数,表明考生的意向学校。

现在要求你对考生排名后进行一个模拟录取的工作,然后输出每一所学校所招到的所有考生的编号,并且是以编号递增的顺序输出。如果该学校没有招到任何一名学生,则输出一个空行。

进行排名时,首先按总分(高考分 + 面试分)进行非递减的排序,如果总分一样就按照高考分排序,还一样的话两者排名相等。录取时,即便该学校的名额已经用完了,只要该考生的排名和该校当前所招的最后一名考生的排名一样,也需要将他录取。最后每个考生按照志愿表的顺序来依次录取学校,如果每个学校都招满了,且他的排名也满足不了要求,那么他就无法被录取。

思路:

定义结构体 applicant 保存每名考生的数据:编号 id,高考分 ge,面试分 gi,总分 gf,志愿表 prefer[5]。然后定义两个规则不同的 cmpcmp1 函数。

定义 vector 数组 quoat 保存每所学校的招生名额、apts 数组保存所有的考生数据、admit 数组保存每所学校录取的考生。正常输入后进行排序,这里使用的是 cmp 函数的排序规则,先按总分,后按高考分。

用 i 枚举排完序的每一名考生,用 j 枚举该考生的每一个志愿。c 保存志愿学校的编号,pre 保存 admit[c] 数组的尾元素的索引。然后进行判断,如果学校 c 录取名额未满,或者满了,但是最后一名考生的排名与考生 i 相等就录取,接着退出循环,以免被重复录取。

最后输出的时候,记得要先将每所学校所录取的考生按照考生编号进行排序,使用 cmp1 函数的排序规则。

经验:

  • 数据量比较大的时候,cmp 的形参使用引用速度会快一点,但是因为结构体里只有几个整型变量,且最多也就40000名考生,所以差距不大。结构体中有很多字符串时效果会比较明显一点。
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

struct applicant {
	int id, ge, gi, gf, prefer[5];
};

bool cmp(applicant a, applicant b) { return a.gf != b.gf ? a.gf > b.gf : a.ge > b.ge; }
bool cmp2(applicant a, applicant b) { return a.id < b.id; }

int main()
{
	int N, M, K;
	cin >> N >> M >> K;	// 考生总数 大学数 志愿上限
	vector<int> quoat(M);
	vector<applicant> apts(N), admit[M];
	for (int i = 0; i < M; ++i) cin >> quoat[i];	// 每个学校的招生名额
	for (int i = 0; i < N; ++i)
	{
		apts[i].id = i;
		cin >> apts[i].ge >> apts[i].gi;			// 高考分和面试分
		apts[i].gf = apts[i].ge + apts[i].gi;		// 总分
		for (int j = 0; j < K; ++j) cin >> apts[i].prefer[j];	// 志愿学校
	}
	sort(apts.begin(), apts.end(), cmp);
	for (int i = 0; i < N; ++i)		// 枚举排名表上的每一个学生
	{
		for (int j = 0; j < K; ++j)	// 枚举每个学生的志愿表
		{	
			int c = apts[i].prefer[j], pre = admit[c].size() - 1;
			if (admit[c].size() < quoat[c] || (apts[i].gf == admit[c][pre].gf && apts[i].ge == admit[c][pre].ge))
			{	
				admit[c].push_back(apts[i]);	// 名额没满或者排名和该学校最后录取的一个学生相等时直接录取
				break;							// 结束循环避免多次录取
			}
		}
	}

	for (int i = 0; i < M; ++i)
	{
		sort(admit[i].begin(), admit[i].end(), cmp2);
		if (admit[i].size()) cout << admit[i][0].id;
		for (int j = 1; j < admit[i].size(); ++j)
			cout << " " << admit[i][j].id;
		cout << endl;
	}

    return 0;
}

一定要自己写一遍哦~~~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值