官网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单源最短路径算法的基础上扩展,额外维护三个数组:最短路径条数、到达该节点的最大救援队数量、路径前驱节点。松弛操作分情况更新:找到更短路径时重置计数与救援数;路径长度相等时累加路径数,若救援数更多则更新救援数与前驱。
算法步骤:
- 初始化距离数组为无穷大,起点距离设为0;起点的救援队数量为自身救援队数,最短路径条数为1。
- 循环n次,每次选出未访问的距离最小的节点,标记为已访问。
- 用当前节点松弛所有邻接节点:
- 若发现更短路径:更新最短距离,路径条数继承自当前节点,更新最大救援队数,记录前驱节点。
- 若路径长度相等:路径条数累加;若新方案救援队数量更多,则更新最大救援数与前驱节点。
- 从终点递归回溯前驱节点,正序输出完整路径。
正解代码
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。
正解代码
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种月饼的库存量和总售价,以及市场最大需求量,月饼可拆分销售,求能获得的最大收益。
核心思路:典型的分数背包问题,使用贪心策略。先计算每种月饼的单位售价,优先选择单价最高的月饼出售,直到满足市场需求量。
算法步骤:
- 读取每种月饼的库存量与总售价,计算单价(总售价/库存量)。
- 按单价从高到低对所有月饼排序。
- 依次遍历排序后的月饼:
- 剩余需求量大于等于当前库存量时,全部卖出,累加总售价,扣减对应需求量。
- 剩余需求量不足时,按比例卖出部分月饼,累加收益后结束循环。
- 按两位小数格式输出最终收益。
正解代码
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。其中二叉搜索树定义为左子树所有节点值小于根,右子树所有节点值大于等于根。
核心思路:利用二叉搜索树前序遍历「根-左-右」的性质递归验证,同时在递归过程中记录后序遍历结果。分别对正常二叉搜索树和镜像二叉搜索树各做一次校验,只要其中一种成立即可输出答案。
算法步骤:
- 定义两个校验函数
ck1(正常BST)和ck2(镜像BST),传入当前序列的左右区间[l,r]。 - 取区间首元素作为当前子树的根节点
x。 - 正常BST:从左向右找到第一个不小于
x的位置,划分出左子树区间;再检查剩余区间是否全部大于等于x,若右边界能到达r则结构合法。 - 镜像BST:逻辑相反,左子树全部大于等于
x,右子树全部小于x。 - 递归校验左右子树,递归返回后将根节点存入后序数组,天然形成后序遍历顺序。
- 主函数先调用
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,空树视为合法二叉搜索树。