2025 天梯赛 PTA 模拟赛 题解答案 L1+L2+L3
声明
- 不保证为官方标算,若有更优解法恳请大佬指点。
- 图片来自 PTA 习题集。
L1题解
L1-1 爱您爱我
题目描述
2025
2025
2025 谐音是 “爱您爱我”。本题就请你用汉语拼音输出这句话 2025 - ai nin ai wo
。
解题思路
- 直接输出即可。
完整代码
#include<iostream>
using namespace std;
int main(){
cout << "2025 - ai nin ai wo";
return 0;
}
L1-2 九牛一毛
题目描述
输入格式
输入在一行中给出一个不超过 1000 1000 1000 的正整数 N N N,即以 “元” 为单位的货币量。
输出格式
在一行中顺序输出 N N N 块钱能买多少斤猪肉、多少斤鸡肉、多少头牛。三个数字都只取整数部分,其间以 1 1 1 个空格分隔,行首尾不得有多余空格。
解题思路
- 按题意直接计算即可。
完整代码
#include<iostream>
using namespace std;
int main(){
int n; cin >> n;
printf("%d %d %d",n/15,n/20,n*10*9);
return 0;
}
L1-3 陈依涵的高情商体重计算器
题目描述
输入格式
输入第一行给出一个用户输入的体重值 w w w,以斤为单位,是一个不超过 500 500 500 的正整数。
输出格式
在一行中输出:Gong xi nin! Nin de ti zhong yue wei: x duo jin
,其中的 x
就是按照题面给出的规则计算出的结果。
样例输入 1
88
样例输出 1
Gong xi nin! Nin de ti zhong yue wei: 80 duo jin
样例输入 2
90
样例输出 2
Gong xi nin! Nin de ti zhong yue wei: 80 duo jin
样例输入 3
199
样例输出 3
Gong xi nin! Nin de ti zhong yue wei: 100 duo jin
解题思路
- 依题意,大于 100 100 100 斤输出 100 100 100 斤,否则输出 w − 1 w-1 w−1 后抹去个位数即可。
完整代码
#include<iostream>
using namespace std;
int main(){
int n; cin >> n;
if(n>100) printf("Gong xi nin! Nin de ti zhong yue wei: 100 duo jin");
else printf("Gong xi nin! Nin de ti zhong yue wei: %d duo jin",(n-1)/10*10);
return 0;
}
L1-4 记小本本
题目描述
输入格式
输入由一系列 0 0 0 和 1 1 1 组成,每个数字占一行。 1 1 1 代表红色按钮被按下, 0 0 0 代表绿色按钮被按下。当出现任何一个既不是 0 0 0 也不是 1 1 1 的数字时,表示他们把电源线扯断了,输入结束,最后那个数字不要处理。
输出格式
对每一个输入的
0
0
0,在一行中输出这次按下绿色按钮之前一共吵了多少次架。
题目保证每个输出的数字均不超过
1
0
4
10^4
104。
样例输入
1
1
1
0
1
1
0
1
2
样例输出
3
5
解题思路
- 依次输入,遇到 1 1 1 计数加一,遇到 0 0 0 输出当前计数。除此之外直接退出。
完整代码
#include<iostream>
using namespace std;
int main(){
int cnt = 0;
while(true){
int x; scanf("%d",&x);
if(x!=1 && x!=0) break;
if(x == 1) cnt++;
else printf("%d\n",cnt);
}
return 0;
}
L1-5 吉老师的回归
题目描述
输入格式
输入第一行是两个正整数 N , M N,M N,M ( 1 ≤ M ≤ N ≤ 30 1\le M\le N\le30 1≤M≤N≤30),表示本次天梯赛有 N N N 道题目,吉老师现在做完了 M M M 道。
接下来 N N N 行,每行是一个符合题目描述的字符串,表示天梯赛的题目内容。吉老师会按照给出的顺序看题——第一行就是吉老师看的第一道题,第二行就是第二道,以此类推。
输出格式
在一行中输出吉老师当前正在做的题目对应的题面(即做完了
M
M
M 道题目后,吉老师正在做哪个题)。如果吉老师已经把所有他打算做的题目做完了,输出一行 Wo AK le
。
样例输入 1
5 1
L1-1 is a qiandao problem.
L1-2 is so...easy.
L1-3 is Easy.
L1-4 is qianDao.
Wow, such L1-5, so easy.
样例输出 1
L1-4 is qianDao.
样例输入 2
5 4
L1-1 is a-qiandao problem.
L1-2 is so easy.
L1-3 is Easy.
L1-4 is qianDao.
Wow, such L1-5, so!!easy.
样例输出 2
Wo AK le
解题思路
-
依题意,找到第 m + 1 m+1 m+1 个不包含
qiandao
和easy
的字符串。若不存在第 m + 1 m+1 m+1 个,则输出
Wo AK le
。 -
那么我们每找到一个满足题意的字符串就将 m − 1 m-1 m−1。在某次找到字符串时,若 m = 0 m=0 m=0 说明此时为第 m + 1 m+1 m+1 个字符串,输出后直接结束程序即可。
完整代码
#include<iostream>
using namespace std;
int main(){
int n,m; cin >> n >> m;
string str; getline(cin,str); // 把第一行 n 和 m 后面的回车读掉
while(n--){
getline(cin,str);
if(str.find("qiandao")!=-1 || str.find("easy")!=-1) continue;
if(!m){ cout << str; return 0; }
m -= 1;
}
cout << "Wo AK le";
return 0;
}
L1-6 大勾股定理
题目描述
输入格式
输入在一行中给出正整数 n n n( ≤ 1 0 4 \le 10^4 ≤104)。
输出格式
分两行输出满足大勾股定理的解,格式如下:
a[0]^2 + a[1]^2 + ... + a[n]^2 =
a[n+1]^2 + ... + a[2n]^2
其中解的数列 a[0] ... a[2n]
按递增序输出。注意行首尾不得有多余空格。
样例输入
3
样例输出
21^2 + 22^2 + 23^2 + 24^2 =
25^2 + 26^2 + 27^2
解题思路
思路一
- 发现输入一个 n n n,答案一定是连续的 2 ∗ n + 1 2*n+1 2∗n+1 个正整数,前 n + 1 n+1 n+1 个的平方和等于后 n n n 个的平方和。
- 那么我们可以用一个 滑动窗口,窗口长度为 2 ∗ n + 1 2*n+1 2∗n+1。每次窗口向右移一位时,等式左边的平方和只需要减去出窗口的那个数的平方,再新加上进窗口的数的平方。等式右边的平方和同理。
- 直到某刻等式左边的平方和 等于 等式右边的平方和,此时窗口内的 2 ∗ n + 1 2*n+1 2∗n+1 个数即为答案。
- 注意输出格式呀。
完整代码
#include<iostream>
using namespace std;
int main(){
int n; scanf("%d",&n);
int l1 = 1,r1 = n+1,l2 = n+2,r2 = 2*n+1;
long long sum1 = 0,sum2 = 0;
for(int i=l1;i<=r1;i++) sum1 += i*i;
for(int i=l2;i<=r2;i++) sum2 += i*i;
while(sum1 != sum2){
sum1 = sum1 - l1*l1 + (r1+1)*(r1+1);
sum2 = sum2 - l2*l2 + (r2+1)*(r2+1);
++l1,++r1,++l2,++r2;
}
for(int i=l1;i<=r1;i++){
if(i!=l1) printf(" + ");
printf("%d^2",i);
}printf(" =\n");
for(int i=l2;i<=r2;i++){
if(i!=l2) printf(" + ");
printf("%d^2",i);
}
return 0;
}
思路二
-
我们先写一个朴素的暴力:枚举每一个起始位置,再判断此时是否满足题目要求。
-
但提交发现代码超时。若没想到思路一,我们可以找找答案存不存在什么规律。
-
将 n = 1 , 2 , 3...10 n=1,2,3...10 n=1,2,3...10 时的答案起始数字全部输出出来:
n = 1 : begin = 3 n = 2 : begin = 10 n = 3 : begin = 21 n = 4 : begin = 36 n = 5 : begin = 55 n = 6 : begin = 78 n = 7 : begin = 105 n = 8 : begin = 136 n = 9 : begin = 171 n = 10 : begin = 210
-
我们不难发现,答案的起始数字为 n ∗ ( 2 ∗ n + 1 ) n*(2*n+1) n∗(2∗n+1)。所以从 n ∗ ( 2 ∗ n + 1 ) n*(2*n+1) n∗(2∗n+1) 开始的连续的 2 ∗ n + 1 2*n+1 2∗n+1 个数字即为答案,直接输出即可。
-
注意输出格式呀。
完整代码
#include<iostream>
using namespace std;
int main(){
int n; scanf("%d",&n);
int st = n*(2*n+1);
for(int i=0;i<=n;i++){
if(i) printf(" + ");
printf("%d^2",st+i);
}printf(" =\n");
for(int i=0;i<n;i++){
if(i) printf(" + ");
printf("%d^2",st+n+1+i);
}
return 0;
}
L1-7 乘法口诀数列
题目描述
输入格式
输入在一行中给出 3 个整数,依次为 a 1 a_1 a1、 a 2 a_2 a2 和 n n n,满足 0 ≤ a 1 , a 2 ≤ 9 0\le a_1,a_2\le 9 0≤a1,a2≤9, 0 < n ≤ 1 0 3 0\lt n\le 10^3 0<n≤103。
输出格式
在一行中输出数列的前 n n n 项。数字间以 1 1 1 个空格分隔,行首尾不得有多余空格。
样例输入
2 3 10
样例输出
2 3 6 1 8 6 8 4 8 4
解题思路
- 按照题意,先生成 n n n 个数字,然后一次性输出。
- 用一个变量 p p p 指向当前数组的末尾(新数字要插入的位置)。从前往后每次取相邻两个数字,计算二者乘积,若是一位数直接填在末尾,若是两位数则拆开填入末尾两次。
- 当 p > n p\gt n p>n 时直接输出整个数组即可。
- 要注意输出格式呀。
完整代码
#include<iostream>
using namespace std;
const int N = 1e4+5;
int n,a[N];
int main(){
scanf("%d%d%d",&a[1],&a[2],&n);
for(int i=1,p=3;p<=n;i++){ // 生成至少 n 个数字
int cur = a[i]*a[i+1];
if(cur>=10) a[p++] = cur/10,a[p++] = cur%10; // 两位数
else a[p++] = cur;
}
for(int i=1;i<=n;i++){
if(i>1) printf(" "); // 注意输出格式
printf("%d",a[i]);
}
return 0;
}
L1-8 新年烟花
题目描述
输入格式
输入第一行是三个正整数 N , M , H N,M,H N,M,H ( 1 ≤ N , M ≤ 50 1\le N,M\le50 1≤N,M≤50 , 1 ≤ H ≤ 210 1\le H\le 210 1≤H≤210),表示活动场地矩阵大小为 N × M N×M N×M,小 C 的身高为 H H H。
接下来的 N N N 行,每行 M M M 个整数,整数的含义如下:
- 如果是一个正整数,则表示该格子被人或建筑物占据,高度为对应的值。
- 如果是一个负整数,则表示该格子用于燃放烟花。所有燃放烟花的格子视为没有高度。
- 如果是 0 0 0,则表示该格子是空格子。
所有整数的绝对值不超过 500 500 500。
输出格式
输出第一行是一个正整数,表示对小 C 而言的优秀观赏位置有多少个。
接下来输出能看到最多的燃放烟花的格子的坐标 ( X , Y ) (X,Y) (X,Y),即格子在第 X X X 行、第 Y Y Y 列,数字间以 1 1 1 个空格分隔。当存在多组坐标时,只输出最小的那组,即输出 X X X 最小的解; X X X 相同时输出 Y Y Y 最小的解。
矩阵左上角坐标为 ( 0 , 0 ) (0,0) (0,0) ,保证至少存在一个对小 C 而言的优秀观赏位置。
样例输入
10 10 175
0 0 0 0 0 0 0 0 0 0
0 50 0 180 -100 180 0 70 30 0
0 30 0 0 300 0 0 0 0 0
0 250 0 0 -100 0 0 0 0 0
0 -100 174 0 0 0 0 169 -100 0
0 -100 0 0 0 0 0 0 -100 0
0 -1 0 0 170 0 0 0 0 0
0 5 0 0 300 0 0 0 0 0
0 20 0 0 -100 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
样例输出
6
3 8
解题思路
-
我们发现 N N N 和 M M M 都很小,不超过 50 50 50,那么可以暴力枚举每一个坐标 ( x , y ) (x,y) (x,y),若其是空地,则计算从这个坐标可以看到的烟花数量。
-
那么我们只需要统计有多少个位置,看到的烟花数量 ≥ 3 \ge3 ≥3,并记录一下看到烟花数量最多的位置即可。
-
对于一个位置 ( x , y ) (x,y) (x,y),我们站在该点,当向一个方向看去时,统计直到第一个高度大于 H H H 的格子之前,出现过多少个烟花,即是这个方向上能看到的烟花数量。
四个方向分别加起来即为该点 ( x , y ) (x,y) (x,y) 能看到的烟花数量。
-
由于题目要求在看到烟花数量相同时,输出最小的坐标。我们可以按题意先从小到大遍历 X X X,再从小到大遍历 Y Y Y,那么只需要在烟花数量更大时更新即可。
完整代码
#include<iostream>
using namespace std;
const int N = 55;
int n,m,c,h[N][N];
inline int check(int x,int y){
if(h[x][y]) return 0; // 若不为空地,返回 0
int res = 0;
for(int i=1;x-i>=0;i++){ // 分别统计四个方向上的答案
if(h[x-i][y]>=c) break;
if(h[x-i][y]<0) res++;
}
for(int i=1;x+i<=n;i++){
if(h[x+i][y]>=c) break;
if(h[x+i][y]<0) res++;
}
for(int i=1;y-i>=0;i++){
if(h[x][y-i]>=c) break;
if(h[x][y-i]<0) res++;
}
for(int i=1;y+i<=m;i++){
if(h[x][y+i]>=c) break;
if(h[x][y+i]<0) res++;
}
return res;
}
int main(){
scanf("%d%d%d",&n,&m,&c);
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
scanf("%d",&h[i][j]);
int res = 0,x = -1,y = -1,cnt = 0; //res 为最大的烟花数,(x,y 记录此时的位置)
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
int ans = check(i,j);
if(ans < 3) continue;
cnt +=1; // 优秀的观赏位置
if(ans > res) res = ans, x = i, y = j; // 更新最大数量的位置
}
}
printf("%d\n%d %d",cnt,x,y);
return 0;
}
L2题解
L2-1 包装机
题目描述
输入格式
输出格式
在一行中输出能够被批准的最大申请数量。
样例输入
7
18:00:01 23:07:01
04:09:59 11:30:08
11:35:50 13:00:00
23:45:00 23:55:50
13:00:00 17:11:22
06:30:50 11:42:01
17:30:00 23:50:00
样例输出
5
样例说明
除了最后两个申请,其它都可以被批准。
解题思路
-
区间调度问题,一个简单的贪心。
-
首先显然的,若两个区间的起始时间相同,为了选更多的区间,此时我们必然选择终止时间更早的区间。
其次由于选择的区间严格不能重叠,所以我们可以从左往右(时间轴)依次选择结束时间最早的区间,此时在这种贪心思路下可以取得的区间数便是最大数量。
-
那么我们可以先将输入的时间转化为以秒为单位,随后根据区间的右端点从小到大排序,在结束时间相同时根据区间的左端点从大到小排序。
-
最后从小到大遍历一遍所有区间,能选时就立刻选,便能得到最大值。
完整代码
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 2e3+5;
int n;
struct Node{
int l,r;
// 先按结束时间从小到大排序,相同时按开始时间从大到小
bool operator < (const Node y) const{
return r == y.r ? l > y.l : r < y.r;
}
}a[N];
int main(){
scanf("%d",&n);
for(int i=1,hl,ml,sl,hr,mr,sr;i<=n;i++){
scanf("%d:%d:%d %d:%d:%d",&hl,&ml,&sl,&hr,&mr,&sr);
a[i].l = hl*3600+ml*60+sl; // 单位转化
a[i].r = hr*3600+mr*60+sr;
}
sort(a+1,a+1+n);
int res = 0, last = -1; // 记录当前最右边的区间的结束时间
for(int i=1;i<=n;i++){
if(a[i].l >= last) res++, last = a[i].r;
}
printf("%d",res);
return 0;
}
L2-3 完全二叉树的层序遍历
题目描述
输入格式
输入在第一行中给出正整数 N N N( ≤ 30 \le30 ≤30),即树中结点个数。第二行给出后序遍历序列,为 N N N 个不超过 100 100 100 的正整数。同一行中所有数字都以空格分隔。
输出格式
在一行中输出该树的层序遍历序列。所有数字都以 1 1 1 个空格分隔,行首尾不得有多余空格。
样例输入
8
91 71 2 34 10 15 55 18
样例输出
18 34 55 71 2 10 15 91
解题思路
-
由于是一颗完全二叉树,除了最后一层前面每一层都填满了节点。
故我们可以计算出当前节点的左子树的节点个数,以及右子树的节点个数。
-
由后序遍历的性质,左子树的所有节点先出现,然后右子树所有节点出现,最后出现根节点。
-
那么我们递归建树,最后跑一遍 b f s bfs bfs 求出层序遍历即可。
完整代码
#include<iostream>
#include<queue>
using namespace std;
const int N = 55;
int n,a[N],ls[N],rs[N],val[N],idx;
int build(int l,int r){ // 由完全二叉树的后序遍历建树
int u = ++idx, siz = r-l+1; // 当前树的大小
val[u] = a[r];
if(siz == 1) return idx; // 叶节点
int pre = 1; // 倒数第二层的个数
for(int i=1;i+(pre<<1)<siz;pre<<=1,i+=pre);
int last = siz - (pre<<1) + 1; // 最后一层的节点个数
int lsiz = pre-1,rsiz = pre-1; // 左子树和右子树不算最后一层的节点数量
if(last > pre) lsiz += pre, rsiz += last-pre;
else lsiz += last; // 算上最后一层的节点数量
ls[u] = build(l,l+lsiz-1);
if(rsiz) rs[u] = build(r-rsiz,r-1); // 递归建树
return u; // 返回当前节点的序号
}
inline void bfs(int st){ // 层序遍历
queue<int> qu; qu.push(st);
int siz = 1;
while(!qu.empty()){
int cnt = 0;
for(int i=1;i<=siz;i++){
int u = qu.front(); qu.pop();
if(u != st) printf(" ");
printf("%d",val[u]);
if(ls[u]) qu.push(ls[u]),++cnt;
if(rs[u]) qu.push(rs[u]),++cnt;
}
siz = cnt;
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
int rt = build(1,n);
bfs(rt);
return 0;
}
L2-4 势均力敌
题目描述
输入格式
输入第一行给出正整数 n n n( 2 < n ≤ 4 2\lt n\le 4 2<n≤4),随后一行给出 n n n 个不同的、在区间 [ 1 , 9 ] [1, 9] [1,9] 内的个位数字,其间以空格分隔。
输出格式
将所有组成的
n
!
n!
n! 个不同的
n
n
n 位数分为平方和相等、且个数也相等的两组。但你只需要输出其中一组就可以了。每个数字占一行,共输出
n
!
/
2
n!/2
n!/2 行。
注意:解可能不唯一,输出任何一组解就可以。
样例输入
3
5 2 1
样例输出
125
512
251
解题思路
-
注意到 n ≤ 4 n\le 4 n≤4 时,最多只有 4 ! = 24 4!=24 4!=24 个不同的数字。
-
那么我们可以将这 n ! n! n! 个数字全部求出来,然后依次枚举每个数字取还是不取。
当取了 n ! / 2 n!/2 n!/2 个数字的时候,计算取了的数字的平方和 m m m ,与提前算好的所有数字的平方和 s s s 相比。
若 m ∗ 2 = = s m*2==s m∗2==s 则找到了一组可行解,输出当前取的这 n ! / 2 n!/2 n!/2 个数字,随后直接结束程序。
完整代码
#include<iostream>
#include<algorithm>
#define ll long long
using namespace std;
const int N = 30;
int m,n,a[N],val[N]; bool vis[N];
// 将所有可能的数字求出,存在 val[] 中
inline void make(int pos,int s,int k){ // 正在取第pos位,已经选好的所有数字,当前位需要乘的权值
if(pos == m+1) return val[++n] = s,void();
for(int i=1;i<=m;i++)
if(!vis[i]){
vis[i] = true;
make(pos+1,s+k*a[i],k*10);
vis[i] = false;
}
}
ll tot,ans[N]; bool vs[N];
// 暴力枚举每一个数字取不取
inline bool dfs(int pos,ll sum,int have){ // 正在取第pos位,已经取的平方和是sum,已经取了have个数字
if(have == n/2){ // 取够数字了
if(sum*2!=tot) return false; // 检查平方和的两倍是否等于总平方和
for(int i=1;i<=have;i++) printf("%d\n",ans[i]);
return true;
}
// 若当前取得数字平方和的两倍已经大于了总平方和,或剩余数字不够取满一半
if(sum*2 > tot || n-pos+1+have < n/2) return false; // 直接剪枝
ans[have+1] = val[pos];
if(dfs(pos+1,sum+val[pos]*val[pos],have+1)) return true; // 取
return dfs(pos+1,sum,have); // 不取
}
inline bool cmp(int x,int y){ return x > y;}
int main(){
scanf("%d",&m);
for(int i=1;i<=m;i++) scanf("%d",&a[i]);
make(1,0,1);
sort(val+1,val+1+n,cmp); // 将数字从大到小排序,先取大的数字,更容易触发剪枝
for(int i=1;i<=n;i++) tot += val[i]*val[i];
dfs(1,0,0);
return 0;
}
L3题解
L3-1 City 不 City
题目描述
输入格式
输入第一行首先给出
4
4
4 个正整数:
n
n
n 和
m
m
m(
1
<
n
≤
1
0
3
1\lt n\le 10^3
1<n≤103,
1
≤
m
≤
5
∗
n
1\le m \le 5*n
1≤m≤5∗n),依次为城镇数量(于是城镇编号从
1
1
1 到
n
n
n)和城镇间的通路条数;
s
s
s 和
t
t
t 依次为旅行者的出发地和目的地的城镇编号。
随后一行给出
n
n
n 个不超过
100
100
100 的正整数,依次为
n
n
n 个城镇的旅游热度。
再后面是
m
m
m 行,每行给出一条通路连接的两个城镇的编号、这条通路的最小花销(其数值为不超过
103
103
103 的正整数)。通路是双向的,题目保证任一对城镇间至多给出一条通路。
同一行的数字间均以空格分隔。
输出格式
题目要求从
s
s
s 到
t
t
t 的最小花销路线;若这样的路线不唯一,则取 途径 城镇的最高旅游热度值最小的那条路线。
在一行中输出从
s
s
s 到
t
t
t 的最小花销、以及途经城镇的最高旅游热度值(若没有途经的城镇,则热度值为
0
0
0)。数值间以 1 个空格分隔,行首尾不得有多余空格。
若从
s
s
s 根本走不到
t
t
t,则在一行中输出 Impossible
。
样例输入 1
8 14 7 8
100 20 30 10 50 80 100 100
7 1 1
7 2 2
7 3 1
7 4 2
1 2 1
1 5 2
2 5 1
3 4 1
3 5 3
3 6 2
4 6 1
5 6 1
5 8 1
6 8 2
样例输出 1
4 50
样例说明
从 7 7 7 到 8 8 8 的最短路径有 3 3 3 条,其中 2 2 2 条都经过城镇 1 1 1,于是对应的最高旅游热度值是城镇 1 1 1 的热度值 100 100 100。解路径为 7 7 7 -> 2 2 2 -> 5 5 5 -> 8 8 8,途径城镇 2 2 2 和 5 5 5,对应的最高旅游热度值是城镇 5 5 5 的热度值 50 50 50。在最短路径长度相等的情况下,取热度值小的解,故输出的热度值为 50 50 50。
样例输入 2
3 1 1 2
10 20 30
1 3 1
样例输出 2
Impossible
解题思路:
- 题目要求求出距离最短时热度最低的路径,在 Dijkstra 的模板上改改,除了记录
dist[]
表示到每个点的最短距离,再开一个数组up[]
记录最短路径上的最大热度值。 - 每次更新的时候,当最短距离相同时,如果最大热度值更小,则也需要更新路径,修改 up 数组并将热度值更小的当前路径入队。
完整代码
#include<iostream>
#include<vector>
#include<queue>
#define ll long long
using namespace std;
const int N = 1e3+5,M = 1e4+5,INF = 0x7fffffff;
int n,m,ha[N],idx,val[N],st,ed;
struct Edge{int from,to,ne,w;}edge[M];
inline void ins(int u,int v,int w){ // 链式前向星存图
edge[++idx].from = u,edge[idx].to = v,edge[idx].w = w;
edge[idx].ne = ha[u], ha[u] = idx;
}
struct Node{
int id; ll dis; int mx;
// 按距离排序从小到大排序,距离相同按热度从小到大排序
const bool operator > (const Node &y) const{
return dis == y.dis ? mx > y.mx : dis > y.dis;
}
};
ll dist[N]; int up[N]; bool vis[N]; // 分别为 最短距离,最短路上的最大热度值,出队标记
priority_queue<Node,vector<Node>,greater<Node> > qu;
inline void dijkstra(){
for(int i=1;i<=n;i++) dist[i] = INF;
qu.push({st,0,0});
while(!qu.empty()){
Node cur = qu.top(); qu.pop();
int u = cur.id, dis = cur.dis;
if(vis[u]) continue;
vis[u] = true;
for(int i=ha[u];i;i=edge[i].ne){
int v = edge[i].to,w = edge[i].w;
int mx = max(val[v],cur.mx); // 与当前节点的热度值取 max
if(dist[v] > dis + w){ // 距离更短
dist[v] = dis + w;
up[v] = mx;
qu.push({v,dist[v],up[v]});
}else if(dist[v] == dis + w && mx < up[v]){ // 距离相同 热度更小
up[v] = mx;
qu.push({v,dist[v],up[v]});
}
}
}
}
int main(){
scanf("%d%d%d%d",&n,&m,&st,&ed);
for(int i=1;i<=n;i++) scanf("%d",&val[i]);
val[st] = val[ed] = 0; // 只算途径的城市的热度值
for(int i=1,u,v,w;i<=m;i++) scanf("%d%d%d",&u,&v,&w),ins(u,v,w),ins(v,u,w);
dijkstra();
if(dist[ed] == INF) printf("Impossible"); // 不可达
else printf("%lld %d",dist[ed],up[ed]);
return 0;
}
L3-2 逆序对
题目描述
输入格式
输入首先在一行中给出一个正整数 n n n( l e 1 0 3 le 10^3 le103),随后一行给出 1 1 1 到 n n n 的一个排列 { a 0 , . . . , a n − 1 a_0,...,a_{n−1} a0,...,an−1 }。数字间以空格分隔。
输出格式
对于每一对下标
0
≤
i
≤
j
<
n
0\le i\le j\lt n
0≤i≤j<n,计算逆转了从
a
i
a_i
ai 到
a
j
a_j
aj 的元素后,序列中逆序对的个数。在一行中输出这
n
∗
(
n
+
1
)
/
2
n*(n+1)/2
n∗(n+1)/2 个整数,按照
i
i
i 优先的顺序。即:前
n
n
n 个数字对应逆转了 {
a
0
,
a
0
a_0,a_0
a0,a0 }、{
a
0
,
a
1
a_0,a_1
a0,a1 }、……、{
a
0
,
.
.
.
,
a
n
−
1
a_0,... ,a_{n−1}
a0,...,an−1 } 的结果;下面
n
−
1
n−1
n−1 个数字对应逆转了 {
a
1
,
a
1
a_1,a_1
a1,a1 }、{
a
1
,
a
2
a_1,a_2
a1,a2 }、……、{
a
1
,
.
.
.
,
a
n
−
1
a_1,...,a_{n−1}
a1,...,an−1 } 的结果;以此类推。
一行中的数字间以
1
1
1 个空格分隔,行首尾不得有多余空格。
输入样例
3
2 1 3
输出样例
1 0 2 1 2 1
解题思路
-
首先观察到数据范围 n ≤ 1 0 3 n\le 10^3 n≤103,肯定不能真的将这 n ∗ ( n + 1 ) / 2 n*(n+1)/2 n∗(n+1)/2 个区间一个一个翻转,然后分别求此时的逆序对。我们需要考虑一个区间 [ l , r ] [l,r] [l,r] 翻转后,相比翻转前逆序对的数量的变化。
-
我们将区间 [ l , r ] [l,r] [l,r] 看作一个整体 x x x,则 x x x 内部序列翻转带来的变化对于 [ 0 , x − 1 ] [0,x-1] [0,x−1] 和 [ x + 1 , n ) [x+1,n) [x+1,n) 是没有任何影响的。所以对于 x x x 左侧和 x x x 右侧的数字,他们对于整个排列中的逆序对数量贡献不变。
-
那么我们只需要考虑 [ l , r ] [l,r] [l,r] 翻转后内部的逆序对数量变化:
注意到对于一个区间 [ l , r ] [l,r] [l,r] 内的有序数对:要么是逆序对,要么是正序对。并且这两种数对的数量相加一定为 C n 2 C_n^2 Cn2。
所以我们将 [ l , r ] [l,r] [l,r] 翻转后原先的 逆序对 变成了 正序对,正序对 变成了 逆序对。
假设原先的逆序对数量为 x x x,翻转后的逆序对数量就为 C n 2 − x C_n^2-x Cn2−x 可以直接求得。
-
因此,我们需要预处理出任意一个区间 [ l , r ] [l,r] [l,r] 内的逆序对数量,便可以在 O ( 1 ) O(1) O(1) 的时间内求出任意一个区间翻转后整个序列的逆序对数量了。
-
我们用
cnt[i][j]
记录区间 [ i , j ] [i,j] [i,j] 内的逆序对数量,很明显 c n t [ i ] [ j ] = c n t [ i ] [ j − 1 ] + 第 j 个数贡献的逆序对数量 cnt[i][j] = cnt[i][j-1] + 第j个数贡献的逆序对数量 cnt[i][j]=cnt[i][j−1]+第j个数贡献的逆序对数量。那么我们可以先枚举区间的左端点 i i i,依次插入 a [ j ] a[j] a[j] 到权值树状数组中。对于第 j 个数贡献的逆序对数量即为大于 j 的数字的个数。
于是我们就可以在 O ( n 2 l o g n ) O(n^2log\space n) O(n2log n) 的时间内求出任意一个区间 内 的逆序对数量了。
-
最后再枚举每个区间,计算翻转 [ i , j ] [i,j] [i,j] 时的逆序对数量:
[ 0 , j ] [0,j] [0,j] 的数字对于 [ 0 , i − 1 ] [0,i-1] [0,i−1] 的数字的总逆序对数量为
cnt[0][j] - cnt[i][j]
;[ j + 1 , n ) [j+1,n) [j+1,n) 的数字对于 [ 0 , j ] [0,j] [0,j] 的数字的总逆序对数量为
cnt[0][n-1] - cnt[0][j]
;设
m = j-i+1
, [ i , j ] [i,j] [i,j] 内部翻转后的总逆序对数量为m*(m-1)/2 - cnt[i][j]
-
故每个区间的总逆序对数为:
cnt[0][j] - cnt[i][j] + cnt[0][n-1] - cnt[0][j] + m*(m-1)/2 - cnt[i][j]
,注意输出格式即可。
完整代码
#include<iostream>
using namespace std;
const int N = 1e3+5;
int n,a[N],tr[N];
inline void add(int x,int v){
for(int i=x;i<=n;i+=i&-i) tr[i]+=v;
}
inline int sum(int x){
int ans = 0;
for(int i=x;i>0;i&=i-1) ans+=tr[i];
return ans;
}
int cnt[N][N];
int main(){
scanf("%d",&n);
for(int i=0;i<n;i++) scanf("%d",&a[i]);
for(int i=0;i<n;i++){
for(int j=i;j<n;j++){
cnt[i][j] = cnt[i][j-1] + j-i - sum(a[j]); // 加上大于 a[j] 的数字个数
add(a[j],1); // 插入树状数组
}
for(int k=0;k<N;k++) tr[k] = 0;
}
for(int i=0;i<n;i++){
for(int j=i;j<n;j++){
int m = j-i+1; // 区间长度
int ans = cnt[0][j] - cnt[i][j] + cnt[0][n-1] - cnt[0][j] + m*(m-1)/2 - cnt[i][j]; // 逆序对数量
if(i || j) printf(" "); // 注意输出格式
printf("%d",ans);
}
}
return 0;
}
L3-3 攀岩 赛时解题思路(无思考量26/30)
声明
- 本题解非正解,旨在分享赛时思路,同时恳请大佬指点本题标算。
题目描述
简化后:
- 依次给出平面上 n n n 个点,找到一个最小的 r r r,使得存在某个序列 { a i a_i ai} 满足, a 1 = 1 a_1 = 1 a1=1, a 2 = 2 a_2=2 a2=2, a n = n a_n=n an=n 且对任意的 i ≥ 3 i\ge 3 i≥3 有: a i − 2 , a i − 1 , a i a_{i-2},a_{i-1},a_i ai−2,ai−1,ai 三点可以被某个半径为 r r r 的圆覆盖。
原题面:
输入格式
第一行输入一个整数
t
t
t (
t
≤
100
t\le100
t≤100),表示攀岩馆内攀岩任务的数量。
每个任务的第一行都是一个整数
n
n
n (
3
≤
n
≤
1500
3\le n\le1500
3≤n≤1500),表示攀岩任务涉及的岩点数量。
接下来
n
n
n 行,每行两个整数 (
x
i
,
y
i
x_i,y_i
xi,yi),表示第
i
i
i 个岩点的坐标。输入保证
0
≤
x
i
,
y
i
≤
1
0
6
0\le x_i,y_i\le 10^6
0≤xi,yi≤106 且同一个攀岩任务中的岩点坐标两两不同。
输入保证满足
n
>
100
n\gt 100
n>100 的数据不超过
3
3
3 组。
输出格式
对于每个攀岩任务输出一行一个浮点数,表示最短可能的机械臂长度。
你的答案在绝对误差或者相对误差不超过 1 0 − 6 10^{−6} 10−6 的情况下都会被视为正确。
输入样例
4
4
0 0
1 0
0 1
1 2
4
0 0
2 0
0 3
2 2
7
0 0
1 0
2 0
2 1
2 2
1 2
0 2
15
13 2
13 3
12 44
6 17
4 71
14 58
4 49
2 51
8 37
1 18
5 43
5 35
1 84
10 23
13 69
输出样例
0.99999999498
1.41421355524
1.00000000498
10.13227667717
样例解释
解题思路( 26 / 30 26/30 26/30)
首先我们需要解决如何判断三个点可以被某个半径为 r r r 的圆覆盖:
-
我们可以先将三个点间的三条边中的最长边作为直径,此时的半径记为 m m m,若 m ≤ r m\le r m≤r 则 m m m 是足以覆盖这两个最远点的最小值,圆心为最长边的中点。
若此时剩余的那个点与圆心的距离 ≤ r \le r ≤r(注意是 r r r),则可以被半径为 r r r 的圆覆盖。
-
若刚才的最小值 m m m 不满足,则考虑给定的三个点构成的三角形的外接圆。外接圆的圆心到三个顶点距离相等且最短,所以我们只需要判断此时的半径若 m ′ ≤ r m' \le r m′≤r 就可以被覆盖。
题目要求求最小的半径 r r r,我们可以转化为二分这个半径 r r r,每次只需要检测对于当前的 r ′ r' r′,是否存在一个序列满足题目要求。
随后观察数据乍一看 n ≤ 1500 n\le 1500 n≤1500 暴力明显超时,但题目表明 n > 100 n\gt 100 n>100 的数据不超过三组,那么我们可以直接无脑暴力,得分期望很高。
- 选用 b f s bfs bfs 暴力方便优化,我们对于当前点对 { p , q p,q p,q },可以到达的点 i i i 需满足: p , q , i p,q,i p,q,i 三点可以被半径为 r r r 的圆覆盖。
- 我们从初始点对 { 1 , 2 1,2 1,2 } 开始,每次暴力枚举下一个点 i i i,若可以被覆盖则将点对 { 1 , i 1,i 1,i } 和 { 2 , i 2,i 2,i } 入队,使下一次可以分别从 { 1 , i 1,i 1,i } 和 { 2 , i 2,i 2,i } 出发寻找可能解。
- 那么总共有 n 2 n^2 n2 个状态,每次拓展需要枚举 n n n 次,总复杂度 O ( n 3 ) O(n^3) O(n3)。
接着我们可以进行一些朴素的优化:
- 对于当前点 {
p
,
q
p,q
p,q },若可以直接到点
n
n
n,可以直接返回
true
。 - 使用优先队列。每次取编号最大的节点进行拓展。可能到达编号为 n n n 的速度更快。
- 提前将节点排序。使得在枚举可能的节点时,先枚举距离编号为 n n n 的节点距离更近的节点。
- 不走回头路。当前点对 { p , q p,q p,q } 拓展结束后,后续节点都不能再选 p p p 和 q q q 进行拓展。
然后随便跑一下就 ( 26 / 30 ) (26/30) (26/30) 了。
完整代码
#include<iostream>
#include<queue>
#include<math.h>
using namespace std;
const int N = 1505; const double esp = 1e-7;
int n;
struct Node{ int x,y; }a[N];
inline double len(const Node &x,const Node &y){
return sqrt((x.x-y.x)*(x.x-y.x)+(x.y-y.y)*(x.y-y.y));
}
struct Info{
int p,q; Info(int p=0,int q=0):p(p),q(q){}
bool operator > (const Info &y)const{
return q > y.q; // 编号大的优先
}
};
// 返回半径 m 的圆能否覆盖三个点
inline bool check(Node &p,Node &q,Node &t,double m){
double x1 = p.x,y1 = p.y,x2 = q.x,y2 = q.y,x3 = t.x,y3 = t.y;
double R,X,Y,dis;
double r1 = sqrt((x3-x2)*(x3-x2) + (y3-y2)*(y3-y2));
double r2 = sqrt((x3-x1)*(x3-x1) + (y3-y1)*(y3-y1));
double r3 = sqrt((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1));
// 最远的两个点作直径, dis 为剩下一个点与圆心的距离
if(r1 >= r2 && r1 >= r3){
X = (x2+x3)/2, Y = (y2+y3)/2, R = sqrt((X-x2)*(X-x2) + (Y-y2)*(Y-y2));
dis = sqrt((X-x1)*(X-x1) + (Y-y1)*(Y-y1));
}else if(r2 >= r1 && r2 >= r3){
X = (x1+x3)/2, Y = (y1+y3)/2, R = sqrt((X-x1)*(X-x1) + (Y-y1)*(Y-y1));
dis = sqrt((X-x2)*(X-x2) + (Y-y2)*(Y-y2));
}else if(r3 >= r1 && r3 >= r2){
X = (x1+x2)/2, Y = (y1+y2)/2, R = sqrt((X-x1)*(X-x1) + (Y-y1)*(Y-y1));
dis = sqrt((X-x3)*(X-x3) + (Y-y3)*(Y-y3));
}
if(R > m) return false;
if(dis <= m) return true; // 只要小于 m 即可
double A,B,C,D;
A = x1*(y2-y3) - y1*(x2-x3) + x2*y3 - x3*y2;
B = (x1*x1 + y1*y1)*(y3-y2) + (x2*x2 + y2*y2)*(y1-y3) + (x3*x3 + y3*y3)*(y2-y1);
C = (x1*x1 + y1*y1)*(x2-x3) + (x2*x2 + y2*y2)*(x3-x1) + (x3*x3 + y3*y3)*(x1-x2);
D = (x1*x1 + y1*y1)*(x3*y2 - x2*y3) + (x2*x2 + y2*y2)*(x1*y3 - x3*y1) + (x3*x3 + y3*y3)*(x2*y1 - x1*y2);
if(!A) return false;
X = -B/(2*A), Y = -C/(2*A);
R = sqrt((B*B + C*C - 4*A*D)/(4*A*A)); // 计算外接圆半径
return R <= m;
}
priority_queue<Info,vector<Info>,greater<Info> > qu;
bool vis[N][N],vis2[N];
inline bool check(double m){
for(int i=0;i<=n;i++) for(int j=0;j<=n;j++) vis[i][j] = false;
for(int i=0;i<=n;i++) vis2[i] = false;
while(!qu.empty()) qu.pop();
qu.push(Info(1,2)); vis[1][2] = vis[2][1] = true;
while(!qu.empty()){
Info cur = qu.top(); qu.pop();
int p = cur.p, q = cur.q;
if(check(a[p],a[q],a[n],m)) return true;
for(int i=1;i<n;i++){
if(i == p || i == q || vis2[i]) continue;
if(!check(a[p],a[q],a[i],m)) continue;
if(!vis[q][i]) vis[q][i] = vis[i][q] = true, qu.push(Info(q,i));
if(!vis[p][i]) vis[p][i] = vis[i][p] = true, qu.push(Info(p,i));
}
vis2[p] = vis2[q] = true;
}
return false;
}
inline bool cmp(const Node &x,const Node &y){
return len(x,a[n]) < len(y,a[n]);
}
inline void solve(){
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d%d",&a[i].x,&a[i].y);
sort(a+3,a+n,cmp); // 与终点距离短的优先
double l = 0, r = 800000, mid, res; // 10^6 / 2^0.5
while(l+esp <= r){
mid = (l+r)/2;
if(check(mid)) r = mid-esp, res = mid;
else l = mid+esp;
}
printf("%lf\n",res);
}
int main(){
int T; scanf("%d",&T);
while(T--) solve();
return 0;
}
补充
- 后续尝试了加上 双端 BFS,卡常,预处理,仍然暴力无法过两个大数据。
- 再次望大佬分享本题正解。