历届 CSP 刷题记录
\(\texttt{CSP 2019}\)
J 组
\(\texttt{T3}\)
注意到一点:每天卖出纪念品换回的金币可以立即用于购买纪念品,当日购买的纪念品也可以当日卖出换回金币。当然,一直持有纪念品也是可以的。
这告诉我们:在一天内,纪念品就是钱,钱就是纪念品,钱和纪念品没有本质区别,这满足动态规划对于最优化原理和无后效性的要求,可以大胆地购买。
所以可以做如下处理:
把今天手里的钱当做背包的容量
把商品今天的价格当成它的消耗
把商品明天的价格当做它的价值
每一天结束后把总钱数加上今天赚的钱,做 \(t - 1\) 遍完全背包即可。
\(\texttt{T4}\)
题目太过冗杂,转化一下就是:
给定一张无向图,边权都是 \(1\),\(q\) 次询问,每次询问 \(1\) 到 \(a\) 有没有长度为 \(L\) 的的路径。
若递归来做直接 T 飞,其实使用无向图的一个小 tip 就可以快速想出正解。
重要 tip:无向图的每一条边都是一个长度为 \(2\) 的环,所以可以一直在两个点之间转圈圈,再走到终点。
所以先求出 \(1\) 到各点的最短路,若 \(L\le dist_a\),就一定不行,接着再考虑转圈圈能否可以使路径长度为 \(L\)。
注意到环的长度为 \(2\),所以可以从奇偶性的角度来思考。
求出 \(1\) 到各点的长度为奇数和偶数的最短路,偶数为 \(0\),奇数为 \(1\)。若 \(L\) 为奇数且 \(dist[a][1]\le l\) 就行,否则不行,偶数同理。
S 组
\(\texttt{D1 T2}\)
江西
\(\texttt{CSP 2020}\)
J 组
\(\texttt{T3}\)
一道妥妥的中等模拟。
首先看到这种抽象的逻辑运算式子还要搞一些乱七八糟的询问,那多半都要建立一棵表达式树。
(其实建完树之后就已经做完了)
建完树之后结构清晰显然,树形 dp 即可。
用 \(dp_i\) 表示树上第 \(i\) 个节点取反是否会改变根结点的值,然后根据运算法则分讨一下就结束了。
写起来也不算太难,毕竟连我都两遍写过。

\(\texttt{T4}\)
一眼 dp,先确定阶段,因为竖着可以上下走,横着只能往右走,所以将列数作为阶段,放在循环最外层。
但竖着有两个方向,不满足后效性怎么办?
想到了这道题,它也是有两个方向走。
借用这道题的思想,设置一个 \(0/1\) 状态机。
设 \(dp_{i, j, 0}\) 表示从下往上到达 \((i, j)\) 能获得的最大数值,\(dp_{i, j, 1}\) 表示从上往下到达 \((i, j)\) 能获得的最大数值。
接下来就可以写出状态转移方程:
\(dp_{i, j, 0} = \max(dp_{i + 1, j, 0},\max(dp_{i, j - 1, 0}, dp_{i, j - 1, 1})) + a_{i, j}\)
\(dp_{i, j, 1} = \max(dp_{i - 1, j, 1},\max(dp_{i, j - 1, 0}, dp_{i, j - 1, 1})) + a_{i, j}\)
最后的结果即为 \(\max(dp_{n, m, 0}, dp_{n, m, 1})\)。
发现计算 \(j\) 时只会用到 \(j - 1\),所以可以滚动数组滚掉一维。
\(\texttt{update on 2024.10.09:}\)
发现今天刚好是做这道题的一周年,但是挫折重重……
这道题需要处理的细节比较多:
-
第一列和最后一列都只能往下走,所以状态转移方程有些不同,要单独拎出来求;
-
\((1, 2)\) 只能从左径直走来或从下走过来,而其他列的第一个可以从左下走来或、左边径直走来或从下走来,所以应该这么写:
if(i > 2) dp[i][1][1] = max(dp[i - 1][1][0], dp[i - 1][1][1]) + a[1][i];
else dp[i][1][1] = dp[i - 1][1][1] + a[1][i];
而除第一列和最后一列之外每一列的最后一行都可以从左边径直走来、左上走来或从上走来,所以可以这么写:
dp[i][n][0] = max(dp[i - 1][n][0], dp[i - 1][n][1]) + a[n][i];
- 加滚动数组时要注意以下 hack 数据:
1 1
1
把最后一句改成:
printf("%lld", max((ll)a[m][n], max(dp[m & 1][n][0], dp[m & 1][n][1])));
即可。

(一年前的我不会这道题看题解过的)
S 组
\(\texttt{T1}\)
好,把模拟放在 \(\texttt{T1}\)。
题面太过冗杂,先把题意梳理一下:
-
将公元前 \(4713\) 年 \(1\) 月 \(1\) 日正午 \(12\) 点定为初始时间;
-
在公元 \(1582\) 年 \(10\) 月 \(4\) 日即以前都采用儒略历,即:若年份是公元后 \(x\) 年,则只要 \(x\bmod 4 = 0\),则 \(x\) 就为闰年,否则为公元前 \(y\) 年,若 \(y\bmod 4 = 1\),则 \(y\) 就为闰年;
-
公元 \(1582\) 年 \(10\) 月 \(5\) 日至 \(10\) 月 \(14\) 日不存在,要扣去,即 \(1582\) 年 \(10\) 月 \(4\) 日的后一天为 \(1582\) 年 \(10\) 月 \(15\) 日;
-
从公元 \(1582\) 年 \(10\) 月 \(15\) 日开始,改用格里高利历,即:当年份是 \(400\) 的倍数,或日期年份是 \(4\) 的倍数但不是 \(100\) 的倍数时,该年为闰年。
-
没有公元 \(0\) 年。
现在给定一个数 \(n\),请求出 \(n\) 天后的年月日。
做这种问题考虑分段,先考虑采用儒略历的区间,在此基础上抠去不存在的那几天再考虑格里高利历。
先考虑用儒略历的时间区间。
不难发现在儒略日下,\(4\) 年为一个周期,一个周期为 \(1461\) 天(钦定第一年为闰年,因为公元前 \(4713\) 年是闰年),所以根据周期算出经过多少个 \(4\) 年之后再将能过的年过完,最后再枚举月份并推出日。
注意:由于每月没有 \(0\) 号,所以一定是剩余天数大于该月份天数再度过这个月!
计算儒略历部分的代码:
void solve1(ll x) {
int year = -4713;
year += x / per_4 * 4; //per_4 是 1461,表示四年的周期
x %= per_4;
//将剩下不满四年的年先过完
int flag = -1;
if(x > 365) x -= 365, year++, flag++;
if(x > 365 && !flag) x -= 365, year++, flag++;
if(x > 365 && flag == 1) x -= 365, year++, flag++;
if(flag == -1) x++; //不要忘记将闰年多的一天加回来
int month;
for(int i = 1; i <= 12; i++) {
int days = months[i];
if(i == 2 && flag == -1) days++;
if(x > days) x -= days;
else {
month = i;
break;
}
}
if(year < 0) printf("%d %d %d BC\n", x, month, -year);
else printf("%d %d %d\n", x, month, year + 1);
} //由于没有公元 0 年,所以年份需要 + 1
接着考虑后续时间区间。
用计算器算出公元前 \(4713\) 年 \(1\) 月 \(1\) 日到公元 \(1582\) 年 \(10\) 月 \(4\) 日共 \(2299160\) 天,先将这部分时间减去,再将时间线推进至 \(1600\) 年(因为是最近的一个整百年,算起来更方便),并算出周期 \(400\) 年的总天数:\(146097\),然后故技重施,推算出经过了多少个 \(400\) 年,不满 \(400\) 的部分一年一年推,最后计算出月和日。
计算格里高利历的代码:
void solve2(ll x) {
x -= to_gc;
if(x > 17) x -= 17;
else {
//没有过完 10 月
printf("%d %d %d\n", 14 + x, 10, 1582);
return ;
}
// 1582.11.1
int month;
bool flag = false;
for(int i = 11; i <= 12; i++) {
if(x > months[i]) x -= months[i];
else {
month = i;
flag = true;
break;
}
}
if(flag) {
//没有过完 1582 年
printf("%d %d %d\n", x, month, 1582);
return ;
}
// 1583.1.1
int year;
for(int i = 1583; i < 1600; i++) {
int days = check(i) ? 366 : 365;
if(x > days) x -= days;
else {
year = i;
flag = true;
break;
}
}
if(flag) {
//没有过完 1599 年
for(int i = 1; i <= 12; i++) {
int days = months[i];
if(i == 2 && check(year)) days++;
if(x > days) x -= days;
else {
month = i;
break;
}
}
printf("%d %d %d\n", x, month, year);
return ;
}
//1600.1.1
year = 1600;
year += x / per_400 * 400;
x %= per_400;
for(int i = year; i < year + 400; i++) {
int days = check(i) ? 366 : 365;
if(x > days) x -= days;
else {
year = i;
flag = true;
break;
}
}
for(int i = 1; i <= 12; i++) {
int days = months[i];
if(i == 2 && check(year)) days++;
if(x > days) x -= days;
else {
month = i;
break;
}
}
//若刚好整除 400 年的天数,那么应该是某年 12 月 31 日,需要倒推一天
if(!x) {
x = months[(month + 11) % 13];
month = (month + 11) % 13;
if(month == 12) year--;
}
printf("%d %d %d\n", x, month, year);
return ;
}
总的说来,我们将整个时间轴分成了以下四段:
- \(-4713.1.1\sim 1582.10.4\);
- \(1582.10.15\sim 1582\) 年末;
- \(1583\sim 1599\) 年;
- \(1600\) 年以后。
注意:最后一个点只说了答案年份不超过 \(10^9\) ,但是输入的 \(n\) 可能爆 int!
\(\texttt{T2}\)
不得不说,\(\texttt{T2}\) 比 \(\texttt{T1}\) 友好多了。
稍微想一想就能想出正解(第一遍做的时候虽然做对了,但是思路没理顺,用了两个 unordered_map 来做映射,效率较低)。
首先用 \(state\) 记录下目前的动物中哪些二进制位是 \(1\),然后用 \(stateq\) 记录要求了哪些二进制位。
思考一下:一个动物要满足什么条件才能养?
- 有饲料供应了;
- 未被要求。
所以求出哪些二进制位是可以随意设置的即可,设这样的二进制位有 \(cnt\) 个,那么答案就是 \(2^{cnt} - n\)。
注意:\(2^{64}\) 会爆 unsigned long long!,要么用 __int128 要么特殊处理!
\(\texttt{T3}\)
\(\texttt{CSP 2021}\)
J 组
S 组
\(\texttt{T1}\)
\(\texttt{T2}\)
\(\texttt{T3}\)
\(\texttt{CSP 2022}\)
J 组
\(\texttt{T3}\)
\(\texttt{T4}\)
S 组
\(\texttt{T1}\)
\(\texttt{T2}\)
\(\texttt{CSP 2023}\)
J 组
\(\texttt{T4}\)
同余最短路入坑题。
分析题意,发现到达每个点的时间必定是 \(\lambda k + b (b < k)\),因为可以晚 \(k\) 的非负整数倍秒出发,所以解的集合是无限的,不妨考虑集合的划分,换个说法,就是动态规划。
其实这种有关同余的题目见多之后,条件反射根据它来划分集合。具体地,因为 \(b < k\),而又有 \(k\le 100\),所以从此入手,从状态机的角度来思考,可以给每个点设计 \(100\) 种状态。
设 \(dist_{u, r}\) 表示从 \(1\) 号点走到点 \(u\) 的最短时间模 \(k\) 等于 \(r\) 时,最短时间为多少。
这道题最毒瘤的一点就是每条边都有时间限制,其实解决方法也很简单:如果在计算过程中发现当前时间小于这条路的开放时间,那么我们就晚一点出发,使得走到这里时刚好能够通过,形式化地:当走到点 \(u\) 时,当前时间为 \(t\),而 \(t < edge\),那么就晚 \(\mu k\) 秒出发,使得到达 \(u\) 的时间变成 \(t + \mu k\),且满足 \(t + \mu k \ge edge,t + (\mu - 1)k < t\)。
这里有一个细节:虽然边权都是 \(1\),但并不能用 bfs 来扩展,因为根据我们对 \(dist\) 的定义,每次需要贪心地寻找一个时间最小的点来扩展,所以要使用 dijkstra 来扩展。
最终答案就是 \(dist_{n, 0}\)。
最后不要忘了无解输出 \(-1\)。

浙公网安备 33010602011771号