CSP-J 2023题解
T1
90 分做法:
与经典问题 “约瑟夫问题” 类似,我们可以对约瑟夫问题的背景稍加改动,即可模拟从第
- 每一轮报数从剩余的最左边的人开始;
- 每一轮报数从
1 开始,若报数除以3 的余数为1 ,则出列。
我们可以用布尔数组
此方法需要开一个大小至少为
100分做法:
先考虑第一问:
- n = 9 时,仍然只拿走第 1、4、7 个苹果,共 3 个;
- n = 10 时,需拿走第 1、4、7、10 个苹果,共 4 个;
- n = 11 时,仍然只拿走第 1、4、7、10 个苹果,共 4 个;
其实,问题中拿走苹果的规则,实际就是 “每三个苹果拿走一个”,我们可以考虑将
再考虑第二问:编号为
- 当
n 模3 余1 时,这一天就会拿走最后一个苹果; - 反之,当
n 模3 余数不为1 时,这一天不会拿走最后一个苹果。
从而初始时,若
最后,这一做法并不需要开数组,唯一需要分析的是时间复杂度。我们可以粗略估计:每一轮循环,
T2
25 分做法:
我们会发现此时
额外的
对于性质
100 分做法:
我们可以发现从
- 如果
k 小于走到下次买油的地方的路程(也就是k < (v_{i}) ),我们就在这里买油,并且更新k 和答案(花费)。 此时所需要买油的数量其实就是(\left\lceil\frac{v_{i}-k}{d}\right\rceil)升,(v_{i}-k) 是我们走到下一站还需要的路程,(\left\lceil\frac{v_{i}-k}{d}\right\rceil) 表示走(v_{i}-k) 需要消耗的油的数量。k 的变化就是新的(k=\left\lceil\frac{v_{i}-k}{d}\right\rceil * d - (v_{i}-k)) 。 - 否则的话,我们直接更新
k ,让k 减少我们走到下一个站点买油地方的路程(k = k - v_{i}) 。 - 不断重复上述讨论直到走到 n 号站点。
时间复杂度:
#include<bits/stdc++.h>
using namespace std;
const int N = 100005;
int n, d, v[N], a[N];
long long ans;
int main(){
cin >> n >> d;
for(int i = 1; i < n; i++) cin >> v[i];
for(int i = 1; i <= n; i++) cin >> a[i];
int minn = a[1]; // 能加的最便宜的油
int rest = 0; // 剩下还能走的公里数
for(int i = 1; i < n; i++){
minn = min(a[i], minn);
if(rest >= v[i]){
rest -= v[i];
continue;
}
int t = (v[i] - rest + d - 1) / d;
ans += 1ll * t * minn;
rest = t * d + rest - v[i];
}
printf("%lld\n",ans);
return 0;
}
T3
25 分做法:
先来讨论特殊性质
-
当
(x_{1}\geq x_{2}) 时,直接输出0 。 -
当
(x_{1}< x_{2}) 时,我们来考虑输出(x_{2}) (其实本质上相当于输出一个形如(\frac{q}{p}) 的数字,p,q 均为整数)。
现在有两种情况。
- 第一种情况
(x_{2}) 是个整数,也就是(b\%a == 0) ,此时直接输出(-\frac{b}{a}) 的值。 - 第二种情况
(x_{2}) 不是整数,也就是(b\%a!= 0) 。此时我们需要按格式输出(x_{2}) ,可以给a,b 都除以它们的最大公约数,从而把(x_{2}) 化成最简真分数的形式。记a,b 的最大公约数是(gcd(a,b)),(a' = a / gcd(a,b)),(b' = b / gcd(a,b)) 。
此时需要输出的解
50 分做法:
接着我们来讨论性质 NO;否则,方程解的形式一定是
这里主要讨论
-
当
(\sqrt{-ac}) 为整数时: 记(k = \sqrt{-ac}) ,需要输出的解就是(x=\frac{k}{a}) ,接下来直接利用处理性质B 时的方法来做。 -
当
(\sqrt{-ac}) 不为整数时:- 先把
(\sqrt{-ac}) 转化为(k*\sqrt{r}) 的形式,保证不存在大于1 的数d 使得(d^{2}|r) 。 因为(-ac\leq M^{2}) ,令(k = 1),(r = -ac) ,用i 从2 ~M 枚举完全平方数,每次检查(-ac) 是否是(i^{2}) 的倍数,若是则让k 变为(k*i) (k 为根号前系数),r 变为\$(\frac{r}{i^{2}}) 。 此时要输出的解\$(x_{1}=\frac{k}{a}*\sqrt{r}) 。 - 然后将
(\frac{k}{a}) 化为最简分数形式: 记k 和a 的最大公约数是(gcd(a,k)) ,令(k' = k/gcd(a,k)),(a' = a/gcd(a,k)) 。 此时输出的解(x_{1}=\frac{k'}{a'}*\sqrt{r} ) 。若(a'<0) ,让(a') 和(k') 均变为相反数。 由于(\sqrt{-ac}) 不是整数,所以(r\neq1) ,输出形式大致为(\{k'\}*sqrt(\{r\})/\{a'\}) 。
- 先把
需要注意两个特殊情况:
- 当
(k' = 1) 时,不需要输出(\{k'\}*) 这个部分。 - 当
(a' = 1) 时,不需要输出(/\{a'\}) 这个部分。
额外的 20 分:
现在我们来讨论性质
-
当
(\Delta < 0) 时,方程无实数解,直接输出NO。 -
当
(\Delta \geq 0) 时,我们知道两个解(x_{1}*x_{2}=\frac{c}{a}) 。然后由于性质C 保证如果有解,解一定是整数,所以我们可以通过枚举法来枚举这两个解。 由于答案一定是整数,说明(\Delta) 一定是一个完全平方数,而且式子除以2a 后可以整除,因此这里直接输出((-b + sqrt(\Delta))/2/a) 就可以了。
100 分做法:
根据题意,我们可以知道对于一元二次方程
-
当
(\Delta < 0) 时,方程无实数解,直接输出NO。 -
当
(\Delta \geq 0) 时,方程两个解分别为(x_{1}=\frac{-b}{2a}+\frac{\sqrt{\Delta}}{2a}),(x_{2}=\frac{-b}{2a}-\frac{\sqrt{\Delta}}{2a}) 。 -
当
(a < 0) 时,最大的实数解是(x_{2}) 。此时我们记(a^{*} = -2a),(b^{*} = b) 。 -
当
(a > 0) 时,最大的实数解是(x_{1}) 。此时我们记(a^{*} = 2a),(b^{*} = -b) 。
我们会发现需要输出的解
接下来我们分两种情况进行讨论:
(\sqrt{\Delta}) 为整数时: 记(k = \sqrt{\Delta}) 。我们要输出的数是(x=\frac{b^{*}+k}{a^{*}}) ,输出这种形式的解的做法和讨论性质B 时一致。(\sqrt{\Delta})不是整数时 : 我们要输出的数是(x=\frac{b^{*}}{a^{*}}+\frac{\sqrt{\Delta}}{a^{*}}) ,我们要输出的其实是两个部分,一个部分是(\frac{b^{*}}{a^{*}}) ,另一个部分是(\frac{\sqrt{\Delta}}{a^{*}}) ,中间用+连接。 所以我们可以先用之前的做法把第一部分输出。
需要特别注意,当+。
而关于第二部分的输出,做法和讨论性质
注意:现在我们需要枚举的完全平方数的范围不再是
满分做法其实就是把之前的对于特殊性质 A 和特殊性质 B 的做法进行整合。
时间复杂度:(O(TM))
#include<bits/stdc++.h>
using namespace std;
int t, m, a, b, c;
int aa, bb, gd1, gd2;
int gcd(int a, int b){
if(a % b == 0) return b;
return gcd(b, a % b);
}
int main(){
scanf("%d%d", &t, &m);
while(t--){
scanf("%d%d%d", &a, &b, &c);
int det = b * b - 4 * a * c;
if(det < 0){
printf("NO\n");
continue;
}
int ans1 = 1;
for(int i = 2; i * i <= det; i++) {
while(det % (i*i) == 0)
ans1 *= i, det /= i * i;
}
aa = 2 * a; bb = -b;
if(aa < 0) aa = -aa, bb = -bb;
if(det == 1) det = 0, bb += ans1;
gd1 = gcd(abs(bb), aa);
gd2 = gcd(ans1, aa);
if(det == 0) {
if(bb % aa == 0) printf("%d", bb / aa);
else printf("%d/%d", bb / gd1, aa / gd1);
}
else {
if(bb != 0) {
if(bb % aa == 0) printf("%d", bb / aa);
else printf("%d/%d", bb / gd1, aa / gd1);
printf("+");
}
if(ans1 / gd2 != 1) printf("%d*", ans1 / gd2);
printf("sqrt(%d)", det);
if(aa / gd2 != 1) printf("/%d", aa / gd2);
}
printf("\n");
}
return 0;
}
T4 P9751 [CSP-J 2023] 旅游巴士
35分做法:
设想如果开放时间为
4出发,扩展到5,刚好离开。
那么在上述
改进办法就是先计算
怎么操作? 利用优先队列 替换普通队列即可!
只需将上述代码再次简单修改!
#include<bits/stdc++.h>
using namespace std;
const int N = 1e4 +10;
int n, m, k;
struct edge{
int v, a; // 顶点, 及这个点开放时间
};
struct node{
int p, t; // 顶点,及到这个点的时间
friend bool operator < (node x, node y){
return x.t > y.t;//重载,时间越小,优先级越高
}
};
vector<edge>g[N];
bool vis[N][110];
int ans = INT_MAX;
void bfs(){
priority_queue<node>q;// 替换成优先队列
q.push((node){1, 0});
while(q.size()){
node cur = q.top(); q.pop();
if(cur.p == n && cur.t % k == 0) // 这次不是第一次搜到就是最优解了
ans = min(ans, cur.t); // 第一次搜到可能延迟了很多个k
if(vis[cur.p][cur.t % k]) continue;
vis[cur.p][cur.t % k] = true;
for(int i = 0; i < g[cur.p].size(); i++){
edge x = g[cur.p][i];
if(cur.t >= x.a)
q.push((node){x.v, cur.t + 1});
else{
int wait = (x.a - cur.t + k - 1) / k * k;
q.push((node){x.v, cur.t + 1 + wait});
}
}
}
}
int main(){
cin >> n >> m >> k;
for(int i = 1; i <= m; i++){
int u, v, a;
cin >> u >> v >> a;
g[u].push_back((edge){v, a});
}
bfs();
if(ans == INT_MAX) puts("-1");
else cout << ans << '\n';
return 0;
}
总结:
测试数据范围可能会引导我们思考的方向。从小范围数据开始着手考虑问题,慢慢再进一步优化算法,从而解决整个问题。