PTA团体程序设计天梯赛L2真题讲解L2-001-004

官网https://pintia.cn/problem-sets/994805046380707840/exam/problems/type/7

文章目录

      • [L2-001 紧急救援](#L2-001 紧急救援)
      • [L2-002 链表去重](#L2-002 链表去重)
      • [L2-003 月饼](#L2-003 月饼)
      • [L2-004 这是二叉搜索树吗?](#L2-004 这是二叉搜索树吗?)

L2-001 紧急救援

题目大意:给定n个城市和m条双向道路,每个城市有一定数量的救援队。从起点出发前往终点,在保证路径总长度最短的前提下,尽可能召集更多救援队。输出最短路径的条数、最多可召集的救援队数量,以及一条对应的最优路径。

核心思路:在Dijkstra单源最短路径算法的基础上扩展,额外维护三个数组:最短路径条数、到达该节点的最大救援队数量、路径前驱节点。松弛操作分情况更新:找到更短路径时重置计数与救援数;路径长度相等时累加路径数,若救援数更多则更新救援数与前驱。

算法步骤

  1. 初始化距离数组为无穷大,起点距离设为0;起点的救援队数量为自身救援队数,最短路径条数为1。
  2. 循环n次,每次选出未访问的距离最小的节点,标记为已访问。
  3. 用当前节点松弛所有邻接节点:
    • 若发现更短路径:更新最短距离,路径条数继承自当前节点,更新最大救援队数,记录前驱节点。
    • 若路径长度相等:路径条数累加;若新方案救援队数量更多,则更新最大救援数与前驱节点。
  4. 从终点递归回溯前驱节点,正序输出完整路径。

正解代码

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=510;
int n,t,m,be,ed,d[N],ans[N],a[N],p[N],cnt[N];
// p 父节点  ansi 到i的最大救援人数   cnti 到i的最短路径条数
bool st[N];
int g[N][N];
void dijkstra(){
    //初始化
    memset(d,0x3f,sizeof d);
    d[be]=0;
    ans[be]=a[be];
    cnt[be]=1;
    for(int i=0;i<n;i++){
        int t=-1;
        for(int j=0;j<n;j++){
            if(!st[j]&&(t==-1||d[t]>d[j]))
                t=j;
        }
        //if (t == -1) return;
        st[t]=1;
        for(int j=0;j<n;j++)
        {
            if(d[j]>d[t]+g[t][j])
            {
                d[j]=d[t]+g[t][j];
                ans[j]=ans[t]+a[j];
                cnt[j]=cnt[t];
                p[j]=t;
            }
            else if(d[j]==d[t]+g[t][j])
            {
                cnt[j]+=cnt[t];
                if(ans[j]<ans[t]+a[j])
                {
                    ans[j]=ans[t]+a[j];
                    p[j]=t;
                }
            }
        }
    }

}
///*
void pt(int x){
    if(x==be){
        cout<<be<<' ';
        return ;
    }
    pt(p[x]);
    cout<<x;
    if(x!=ed)cout<<' ';
}

//*/

int main(){
    cin>>n>>m>>be>>ed;
    memset(g,0x3f,sizeof g);
    for(int i=0;i<n;i++)cin>>a[i];
    while(m--){
        int a,b,c;
        cin>>a>>b>>c;
        g[a][b]=g[b][a]=c;
    }
    dijkstra();
    cout<<cnt[ed]<<' '<<ans[ed]<<'\n';
    pt(ed);
    

    return 0;
}

代码关键细节

  • 使用邻接矩阵存图,适配500以内的节点规模,初始化所有边权为无穷大。
  • 路径输出采用递归回溯,先输出前驱再输出当前节点,保证路径正序。
  • 题目保证最优解唯一,最大救援队对应的前驱路径是确定的。

L2-002 链表去重

题目大意:给定一个链表,按遍历顺序删除键值绝对值重复的节点,仅保留第一个出现的节点。被删除的节点按原顺序组成另一个链表,分别输出去重后的链表和被删除的链表。

核心思路:用数组模拟链表存储全部节点,通过地址建立下标映射实现O(1)查找。从头节点顺序遍历链表,用布尔数组标记已出现的绝对值,首次出现归入保留链表,重复的归入删除链表,最后按格式分别输出。

算法步骤

  1. 读取所有节点信息,建立「地址→数组下标」的映射,方便快速定位节点。
  2. 从头节点开始顺序遍历:
    • 当前节点键值的绝对值未标记时,标记为已出现,加入保留链表。
    • 已标记时,加入删除链表。
  3. 按格式输出两个链表,每个节点输出地址、键值、下一个节点地址,末尾节点的下一地址为-1。

正解代码

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5+9;
int n,head,a[N];//节点下标
bool s[N];
struct node{
    int id,x,nt;//节点地址 键值 next地址
}g[N];
vector<node>v;
int main() {
    cin>>head>>n;
    for(int i=0;i<n;i++){
        cin>>g[i].id>>g[i].x>>g[i].nt;
        a[g[i].id]=i;
    }
    int now=head;
    while(now!=-1){
        int j=a[now];
        if(!s[abs(g[j].x)]){
            s[abs(g[j].x)]=1;
        if(now==head)printf("%05d %d ",head,g[j].x);
        else printf("%05d\n%05d %d ",g[j].id,g[j].id,g[j].x);
        }
        else v.push_back(g[j]);
        now=g[j].nt;
    }
    cout<<"-1\n";
    for(int i=0;i<v.size();i++){
         if(i==0)printf("%05d %d ",v[i].id,v[i].x);
        else printf("%05d\n%05d %d ",v[i].id,v[i].id,v[i].x);
    }
    if(v.size())cout<<"-1\n";

    
    return 0;
}

代码关键细节

  • 地址为5位整数,输出必须用%05d补前导零,-1直接原样输出。
  • 输出时保证链表逻辑连贯,上一节点的下一地址即为下一节点的地址。
  • 删除链表可能为空,此时不需要输出删除链表部分。

L2-003 月饼

题目大意:给定n种月饼的库存量和总售价,以及市场最大需求量,月饼可拆分销售,求能获得的最大收益。

核心思路:典型的分数背包问题,使用贪心策略。先计算每种月饼的单位售价,优先选择单价最高的月饼出售,直到满足市场需求量。

算法步骤

  1. 读取每种月饼的库存量与总售价,计算单价(总售价/库存量)。
  2. 按单价从高到低对所有月饼排序。
  3. 依次遍历排序后的月饼:
    • 剩余需求量大于等于当前库存量时,全部卖出,累加总售价,扣减对应需求量。
    • 剩余需求量不足时,按比例卖出部分月饼,累加收益后结束循环。
  4. 按两位小数格式输出最终收益。

正解代码

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;

struct Item {
    double value;  // 价值
    double weight; // 重量
    double ratio;  // 价值重量比
};

int n, m;  // n:物品数量, m:背包容量
Item items[N];

bool cmp(const Item &a, const Item &b) {
    return a.ratio > b.ratio;  // 按价值比降序排列
}

int main() {
    cin >> n >> m;
    
    // 输入重量和价值
    for(int i = 0; i < n; i++) cin >> items[i].weight;
    for(int i = 0; i < n; i++) cin >> items[i].value;
    
    // 计算价值重量比
    for(int i = 0; i < n; i++) {
        items[i].ratio = items[i].value / items[i].weight;
    }
    
    // 按价值比降序排序
    sort(items, items + n, cmp);
    
    double ans = 0;
    int remaining = m;
    
    for(int i = 0; i < n && remaining > 0; i++) {
        if(remaining >= items[i].weight) {
            // 可以装下整个物品
            ans += items[i].value;
            remaining -= items[i].weight;
        } else {
            // 只能装下一部分
            ans += items[i].ratio * remaining;
            remaining = 0;
        }
    }
    
    printf("%.2f", ans);
    return 0;
}

代码关键细节

  • 相关变量统一使用double类型,避免整数除法造成精度丢失。
  • 循环终止条件为剩余需求量为0,或所有月饼遍历完毕。
  • 输出使用%.2f格式化,严格保留两位小数。

L2-004 这是二叉搜索树吗?

题目大意:给定一个整数序列,判断它是否是一棵二叉搜索树或其镜像树的前序遍历结果。如果是则输出 YES 并给出对应的后序遍历序列,否则输出 NO。其中二叉搜索树定义为左子树所有节点值小于根,右子树所有节点值大于等于根。

核心思路:利用二叉搜索树前序遍历「根-左-右」的性质递归验证,同时在递归过程中记录后序遍历结果。分别对正常二叉搜索树和镜像二叉搜索树各做一次校验,只要其中一种成立即可输出答案。

算法步骤

  1. 定义两个校验函数 ck1(正常BST)和 ck2(镜像BST),传入当前序列的左右区间 [l,r]
  2. 取区间首元素作为当前子树的根节点 x
  3. 正常BST:从左向右找到第一个不小于 x 的位置,划分出左子树区间;再检查剩余区间是否全部大于等于 x,若右边界能到达 r 则结构合法。
  4. 镜像BST:逻辑相反,左子树全部大于等于 x,右子树全部小于 x
  5. 递归校验左右子树,递归返回后将根节点存入后序数组,天然形成后序遍历顺序。
  6. 主函数先调用 ck1,成功则输出后序结果;失败再调用 ck2,均失败则输出 NO。

正解代码

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=1001;
int n,pre[N],suf1[N],pos1=0,suf2[N],pos2=0;

bool ck1(int l,int r){
    if(l>r)return 1;
    int x=pre[l];
    
    int i=l+1,j;
    while(pre[i]<x&&i<=r)i++;
    i--;//左子树右端点
    j=i+1;
    while(pre[j]>=x&&j<=r)j++;
    j--;//右子树右端点
    if(j!=r)return 0;
    bool fg = ck1(l+1, i)&&ck1(i+1, r);
    suf1[pos1++] = x; // 后序遍历
    return fg;
}
bool ck2(int l,int r){
    if(l>r)return 1;
    int x=pre[l];
    
    int i=l+1,j;
    while(pre[i]>=x&&i<=r)i++;
    i--;//左子树右端点
    j=i+1;
    while(pre[j]<x&&j<=r)j++;
    j--;//右子树右端点
    if(j!=r)return 0;
    bool fg = ck2(l+1, i)&&ck2(i+1, r);
    suf2[pos2++] = x; // 后序遍历
    return fg;
}
int main(){
    cin>>n;
    for(int i=0;i<n;i++)cin>>pre[i];
    if(ck1(0,n-1)){
        cout<<"YES\n";
        for(int i=0;i<n;i++){
            if(i)cout<<' ';
            cout<<suf1[i];
        }
    }
    else if(ck2(0,n-1)){
        cout<<"YES\n";
        for(int i=0;i<n;i++){
            if(i)cout<<' ';
            cout<<suf2[i];
        }
    }
    else cout<<"NO";
    

    return 0;
}

代码关键细节

  • 左子树右端点计算:循环找到第一个不满足条件的下标后,需要减 1 才是左子树的最后一个位置。
  • 后序数组使用全局下标累加,递归结束时存入根节点,保证左右子树都处理完再记录根。
  • 边界条件 l > r 时返回 true,空树视为合法二叉搜索树。
相关推荐
云贝教育-郑老师1 小时前
8.29 杭州线下沙龙|OpenTenBase城市行:当 AI 智能体成为数据库的新生产力
学习·ai·dba
杨航 AI2 小时前
** AI 面试算法 50 题清单**,不是单纯的 LeetCode 题号列表,而是按照“**考察概率 × 面试价值 × 你当前准备方向**”排序
算法·leetcode·面试
cszn20262 小时前
AI如何自动识别招标文件废标风险?智能评审项目实践
经验分享
m0_720245019 小时前
1543.统计好三元组(简单)
开发语言·算法
To_OC9 小时前
LC 560 和为 K 的子数组:前缀和配哈希表,这对组合我是真的服了
javascript·算法·程序员
XGeFei10 小时前
【AI应用/Agent智能体搭建平台2】Dify——自部署
人工智能·笔记·学习
hans汉斯10 小时前
《软件工程与应用》期刊推荐&10月版面征稿中
图像处理·人工智能·深度学习·算法·音视频·软件工程
hsjiasb10 小时前
FreeRTOS学习(二十三)——消息缓冲区(Message Buffer)
stm32·单片机·嵌入式硬件·学习·freertos
叠层归一研究院10 小时前
AGI 系统(八):符号范畴嵌入函子 — SymCat ↪ C_M107 严格化
c语言·开发语言·人工智能·算法·transformer·agi