第36次 CCF CSP 计算机软件能力认证 题解(民间版)
写在前面
你说得对,但是 I AK 36th CSP。

Update on 2024.12.13:代码已替换为赛时 AC 代码。
A 签到,模拟
直接模拟即可。
复制复制#include <bits/stdc++.h> #define LL long long const int kN = 1e6 + 10; std::map <char, int> ex; std::map <char, int> ey; //============================================================= //============================================================= //============================================================= int main() { std::ios::sync_with_stdio(0); std::cin.tie(0), std::cout.tie(0); ex['l'] = -1, ex['r'] = 1; ey['f'] = 1, ey['b'] = -1; int n, k; std::cin >> n >> k; while (k --) { int x, y; std::cin >> x >> y; std::string s; std::cin >> s; for (auto ch: s) { int nx = x + ex[ch], ny = y + ey[ch]; if (nx >= 1 && nx <= n && ny >= 1 && ny <= n) x = nx, y = ny; } std::cout << x << " " << y << "\n"; } return 0; } /* 3 2 1 1 ffrrbbll 3 3 frbl */
B 枚举
先不考虑修改操作,记初始时能量的最小值为 ,则若 合法,显然到达第 个位置后,当前的能量将变为:
考虑每次从位置 向下一个位置移动时,需要保证当前能量不小于 ,则 合法当且仅当满足如下限制:
移个项,则可得到 的值:
考虑预处理 ,则 即为答案。
然后考虑修改操作对 ,的影响。考虑代入上式,容易发现修改 后, 不受影响, 全部加上 ,即有:
发现 即 的前缀最大值, 即 的后缀最大值,于是考虑预处理 的前后缀最大值即可根据上式求得所有答案。
总时间复杂度 级别。
#include <bits/stdc++.h> #define LL long long const int kN = 1e5 + 10; const int kInf = 1e9; //============================================================= int n, a[kN], b[kN], ans[kN]; int c[kN], premax[kN], sufmax[kN]; //============================================================= //============================================================= int main() { std::ios::sync_with_stdio(0); std::cin.tie(0), std::cout.tie(0); std::cin >> n; for (int i = 1; i <= n + 1; ++ i) std::cin >> a[i]; for (int i = 1; i <= n; ++ i) std::cin >> b[i]; premax[0] = sufmax[n + 2] = -kInf; for (int i = 1; i <= n + 1; ++ i) { c[i] = c[i - 1] + a[i] - b[i - 1]; premax[i] = std::max(premax[i - 1], c[i]); } for (int i = n + 1; i; -- i) { sufmax[i] = std::max(sufmax[i + 1], c[i]); } for (int i = 1; i <= n; ++ i) { ans[i] = std::max(premax[i], sufmax[i + 1] + b[i]); std::cout << std::max(ans[i], 0) << " "; } return 0; } /* 3 5 5 5 5 0 100 0 3 9 4 6 2 9 4 6 */
C 模拟,STL
会用 STL 就变得很简单的题。
某个内存块对应的缓存块是唯一的,则发现需要支持的操作本质上只有如下几种:
- 查询某个内存块,是否在某个缓存块中;
- 查询某个缓存块中,最早被操作的内存块;
- 向缓存块中插入、删除内存块
- 查询并修改:某个内存块的被操作时间;
- 查询并修改:某个内存块是否被修改过且未保存;
操作 4,5 很容易通过 map 实现,开两个 map 分别维护每个内存块的最近操作时间以及是否被修改过即可;操作 1,2,3 很容易通过 set 实现,仅需对每个缓存块开两个 set:
set<int>
:存缓存块中所有内存块的编号;set <pair <int, int> >
:存缓存块中所有内存块的修改时间以及编号,其中 pair 的第一关键字为内存块修改时间,第二关键字为内存块编号。
则完成操作 1,2 仅需:
- 操作 1:调用
set.count()
查询某个内存块是否在对应的缓存块中即可; - 操作 2:调用
set.begin()
查询缓存块中被操作时间最小的内存块即可; - 操作 3,4:直接在 set 中
insert
和erase
即可。
单次操作时间复杂度 级别,总时间复杂度 级别。
#include <bits/stdc++.h> #define LL long long #define pr std::pair #define mp std::make_pair const int kN = 1e5 + 10; //============================================================= int n, N, q, nowtime; std::map<int, int> tim, modified; std::set<int> have[kN]; std::set<pr <int, int> > lru[kN]; //============================================================= void pop(int id_) { if (have[id_].size() < n) return ; auto it = lru[id_].begin(); if (modified[it->second]) { std::cout << 1 << " " << it->second << "\n"; modified[it->second] = 0; } have[id_].erase(it->second); lru[id_].erase(it); } void insert(int id_, int pos_) { have[id_].insert(pos_); tim[pos_] = nowtime; lru[id_].insert(mp(nowtime, pos_)); std::cout << 0 << " " << pos_ << "\n"; } void solve(int pos_) { ++ nowtime; int id = (pos_ / n) % N; if (have[id].count(pos_)) { lru[id].erase(lru[id].find(mp(tim[pos_], pos_))); tim[pos_] = nowtime; lru[id].insert(mp(nowtime, pos_)); return ; } if (have[id].size() == n) pop(id); insert(id, pos_); } //============================================================= int main() { std::ios::sync_with_stdio(0); std::cin.tie(0), std::cout.tie(0); std::cin >> n >> N >> q; while (q --) { int o, a; std::cin >> o >> a; solve(a); if (o == 1) modified[a] = 1; } return 0; } /* 4 8 8 0 0 0 1 1 2 0 1 1 0 0 32 1 33 0 34 */
D DP,单调性优化,最短路
DP
考虑暴力 DP,设 表示到达位置 所需的最小步数,初始化 ,则有显然的转移:
答案即 ,然而发现上述转移成环了,即会导致某些状态的重复更新,无法直接做,于是寄。
但是容易发现,如果出现成环的转移,这些转移一定是不优的,步数一定不小于第一次更新该状态的步数,则容易发现关键的性质:
- 转移到 的最优决策 ,一定满足 ;
- 进一步地发现 一定是单调不降的;
- 则对于某个状态,能转移到它的最左侧的转移,一定是最优决策,即每个状态只会被转移一次。
于是考虑在 DP 时记 为能转移到的最靠右的位置,然后枚举 并考虑向后转移,每次仅转移位置大于 的状态并更新 即可。 至多增加 次,转移次数至多为 级别,则总时间复杂度为 级别。
#include <bits/stdc++.h> #define LL long long const int kN = 2e5 + 10; int n, a[kN], k[kN], f[kN]; int main() { std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0); std::cin >> n; for (int i = 1; i <= n; ++ i) std::cin >> a[i], f[i] = -1; for (int i = 1; i <= n; ++ i) std::cin >> k[i]; int R = 1; f[1] = 0; for (int i = 1; i <= n; ++ i) { if (f[i] == -1) break; for (int j = std::max(R + 1, i - a[i] + 1); j <= std::min(n, i - a[i] + k[i - a[i]]); ++ j) { f[j] = f[i] + 1; R = j; } } std::cout << f[n] << "\n"; return 0; }
赛时做法
赛时的一个比较天才的最短路做法,考虑分层图最短路,建三层数量为 的节点:
- 第一层:节点 代表从位置 出发;
- 第二层:节点 代表到达位置 ;
- 第三层:用于描述每个位置向之后的位置的区间转移。
然后有如下连边:
- 第一层 向第三层 连边;
- 第二层 向第一层 连边;
- 第三层 向第二层 连边。
- 第三层 向第三层 连边;
求得第一层节点 1 到达第一层节点 的最短路即为答案。
前两层点的正确性容易理解,为什么第三层每个点向前一个点连边,就能解决区间转移问题?同样考虑上述“每个位置仅会被更新一次”的单调性性质,则上述第三层转移仅会更新之前从未被到达过的位置,并不会更新被到达的位置的最短路。
上图中点和边数量均为 级别,Dijkstra 实现,总时间复杂度 级别。
#include <bits/stdc++.h> #define LL long long #define pr std::pair #define mp std::make_pair const int kN = 3e5 + 10; const int kInf = 1e9; //============================================================= int n, a[kN], k[kN], dis[kN]; bool vis[kN]; std::vector<pr <int, int> > edge[kN]; //============================================================= //============================================================= int main() { std::ios::sync_with_stdio(0); std::cin.tie(0), std::cout.tie(0); std::cin >> n; for (int i = 1; i <= n; ++ i) std::cin >> a[i]; for (int i = 1; i <= n; ++ i) std::cin >> k[i]; for (int i = 1; i <= n; ++ i) { edge[i].push_back(mp(2 * n + std::min(i + k[i], n), 1)); edge[n + i].push_back(mp(i - a[i], 0)); if (i > 1) edge[2 * n + i].push_back(mp(2 * n + i - 1, 0)); edge[2 * n + i].push_back(mp(n + i, 0)); } std::priority_queue <pr <int, int> > q; for (int i = 1; i <= 3 * n; ++ i) dis[i] = kInf, vis[i] = 0; q.push(mp(0, 1)); dis[1] = 0; while (!q.empty()) { auto [d, u] = q.top(); q.pop(); if (vis[u]) continue; for (auto [v, w]: edge[u]) { if (dis[v] > dis[u] + w) { dis[v] = dis[u] + w; q.push(mp(-dis[v], v)); } } } std::cout << (dis[n] == kInf ? -1 : dis[n]) << "\n"; return 0; } /* 10 0 1 1 1 1 3 1 0 3 0 2 4 5 4 1 4 1 3 5 3 5 0 1 2 3 0 3 4 4 10 15 */
E 笛卡尔树,单调性,DP
想到笛卡尔树其实就秒了,但是要怎么才能想到呢?
发现 太小了, 又太大了,猜测总时间复杂度是 的,即每次询问实际上是完全独立的,都可以直接大力修改并套用 的解法。
考虑初始位置位于间隔 ,记初始力量的最小值为 ,则显然有 ,即保证至少打败相邻的怪物中较弱的。
考虑记该怪物左侧第一个 大于它的怪物为 ,右侧第一个为 ,则打败该怪物后,力量将增加 ,然后仅需再考虑如何打败 这两个怪物中较弱的一个即可,则此时应有:……不断重复上述过程,直至将所有怪物全部打败即可。
发现上述不断寻找“最近的最弱怪物,并增加力量”的过程,就是在笛卡尔树上,以相邻的较弱怪物为起点,不断跳父亲直至根,到达某个节点 后获得的力量值即为子树 中所有节点 的 。记先后跳到的节点为 则若初始力量 合法,则有:
同样移项后,有:
建立笛卡尔树后,考虑进行两次 dfs,第一次 dfs 预处理 ,第二次 dfs 预处理从笛卡尔树根到所有节点的上式即可。
求答案时考虑枚举所有相邻两个怪物,记两者中较弱怪物为起点 ,其答案即为 ;对于多组询问,每组询问大力修改后独立处理即可,总时间复杂度 级别。
#include <bits/stdc++.h> #define LL long long const int kN = 2e6 + 10; const int kInf = 1e9 + 2077; //============================================================= int n, a[kN], b[kN], aa[kN], bb[kN]; LL sumb[kN], ans[kN]; int rt, son[kN][2], top, st[kN], fa[kN]; //============================================================= void build() { st[top = 0] = 0; for (int i = 1; i <= n; ++ i) { while (top && aa[st[top]] < aa[i]) -- top; son[i][0] = son[st[top]][1], son[st[top]][1] = i; st[++ top] = i; } rt = st[1]; } void dfs1(int u_, int fa_) { sumb[u_] = bb[u_]; fa[u_] = fa_; if (son[u_][0]) dfs1(son[u_][0], u_), sumb[u_] += sumb[son[u_][0]]; if (son[u_][1]) dfs1(son[u_][1], u_), sumb[u_] += sumb[son[u_][1]]; } void dfs2(int u_, int fa_, LL maxd_) { ans[u_] = std::max(1ll * aa[u_], maxd_); if (son[u_][0]) dfs2(son[u_][0], u_, std::max(maxd_, 1ll * aa[u_] - sumb[son[u_][0]])); if (son[u_][1]) dfs2(son[u_][1], u_, std::max(maxd_, 1ll * aa[u_] - sumb[son[u_][1]])); } //============================================================= int main() { std::ios::sync_with_stdio(0); std::cin.tie(0), std::cout.tie(0); std::cin >> n; a[0] = a[n + 1] = kInf; for (int i = 1; i <= n; ++ i) std::cin >> a[i]; for (int i = 1; i <= n; ++ i) std::cin >> b[i]; int q; std::cin >> q; while (q --) { int k; std::cin >> k; for (int i = 0; i <= n + 1; ++ i) { aa[i] = a[i], bb[i] = b[i], sumb[i] = 0; son[i][0] = son[i][1] = fa[i] = 0; ans[i] = 0; } while (k --) { int p, x, y; std::cin >> p >> x >> y; aa[p] = x, bb[p] = y; } build(), dfs1(rt, 0); dfs2(rt, 0, 0); LL out = 0; // for (int i = 1; i <= n; ++ i) std::cout << sumb[i] << " "; // for (int i = 1; i <= n; ++ i) std::cout << ans[i] << " "; for (int i = 2; i <= n; ++ i) { int p = (aa[i - 1] < aa[i] ? i - 1 : i); out ^= ans[p]; } std::cout << out << "\n"; } return 0; } /* 6 4 9 3 1 7 7 4 2 1 3 1 4 2 2 3 8 1 5 4 3 0 6 1 2 3 4 5 6 0 2 0 0 1 0 1 0 */
写在最后
Update on 2024.12.13:代码已替换为赛时 AC 代码。
Update on 2025.01.26:稿子已提交,要上电视啦~
唉唉退役了什么也没学到了,直接进入夹带私货环节:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix