第36次 CCF CSP 计算机软件能力认证 题解(民间版)

写在前面

你说得对,但是 I AK 36th CSP。

6baa5f28c0e08e27c07a6e2ae275dcfd.png

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 枚举

先不考虑修改操作,记初始时能量的最小值为 w,则若 w 合法,显然到达第 i(0in) 个位置后,当前的能量将变为:

w+(a0+b1)+(a1+b2)++(ai1+bi)

考虑每次从位置 i 向下一个位置移动时,需要保证当前能量不小于 ai,则 w 合法当且仅当满足如下限制:

0in, (wj=0i1ai+j=1ibj)ai

移个项,则可得到 w 的值:

w=max0in(j=0iajj=1ibj)

考虑预处理 ci=j=0iajj=1ibj,则 maxci 即为答案。

然后考虑修改操作对 ci,的影响。考虑代入上式,容易发现修改 bp:=0 后,c0cp1 不受影响,cpcn 全部加上 bp,即有:

w(p)=max(max0ip1ci,bp+maxpinci)

发现 max0ip1ci 的前缀最大值,maxpinci 的后缀最大值,于是考虑预处理 ci 的前后缀最大值即可根据上式求得所有答案。

总时间复杂度 O(n) 级别。

#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 就变得很简单的题。

某个内存块对应的缓存块是唯一的,则发现需要支持的操作本质上只有如下几种:

  1. 查询某个内存块,是否在某个缓存块中;
  2. 查询某个缓存块中,最早被操作的内存块;
  3. 向缓存块中插入、删除内存块
  4. 查询并修改:某个内存块的被操作时间;
  5. 查询并修改:某个内存块是否被修改过且未保存;

操作 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 中 inserterase 即可。

单次操作时间复杂度 O(logn) 级别,总时间复杂度 O(qlogn) 级别。

#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,设 fi 表示到达位置 i 所需的最小步数,初始化 f1=0,则有显然的转移:

fi+1fj(j[iai+1,imin(ai+kiai,n)])

答案即 fn,然而发现上述转移成环了,即会导致某些状态的重复更新,无法直接做,于是寄。

但是容易发现,如果出现成环的转移,这些转移一定是不优的,步数一定不小于第一次更新该状态的步数,则容易发现关键的性质:

  • 转移到 fj 的最优决策 fi,一定满足 i<j
  • 进一步地发现 fi 一定是单调不降的;
  • 则对于某个状态,能转移到它的最左侧的转移,一定是最优决策,即每个状态只会被转移一次。

于是考虑在 DP 时记 R 为能转移到的最靠右的位置,然后枚举 1n 并考虑向后转移,每次仅转移位置大于 R 的状态并更新 R 即可。R 至多增加 n 次,转移次数至多为 O(n) 级别,则总时间复杂度为 O(n) 级别。

#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;
}

赛时做法

赛时的一个比较天才的最短路做法,考虑分层图最短路,建三层数量为 n 的节点:

  • 第一层:节点 i 代表从位置 i 出发;
  • 第二层:节点 i 代表到达位置 i
  • 第三层:用于描述每个位置向之后的位置的区间转移。

然后有如下连边:

  • 第一层 i 向第三层 i+ki 连边;
  • 第二层 i 向第一层 iai 连边;
  • 第三层 i 向第二层 i 连边。
  • 第三层 i 向第三层 i1 连边;

求得第一层节点 1 到达第一层节点 n 的最短路即为答案。

前两层点的正确性容易理解,为什么第三层每个点向前一个点连边,就能解决区间转移问题?同样考虑上述“每个位置仅会被更新一次”的单调性性质,则上述第三层转移仅会更新之前从未被到达过的位置,并不会更新被到达的位置的最短路。

上图中点和边数量均为 O(n) 级别,Dijkstra 实现,总时间复杂度 O(nlogn) 级别。

#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

想到笛卡尔树其实就秒了,但是要怎么才能想到呢?

发现 q20 太小了,n2×106 又太大了,猜测总时间复杂度是 O(nq) 的,即每次询问实际上是完全独立的,都可以直接大力修改并套用 O(n) 的解法。

考虑初始位置位于间隔 (i1,i),记初始力量的最小值为 w,则显然有 wmin(ai1,ai),即保证至少打败相邻的怪物中较弱的。

考虑记该怪物左侧第一个 a 大于它的怪物为 L,右侧第一个为 R,则打败该怪物后,力量将增加 L<j<Rbj,然后仅需再考虑如何打败 L,R 这两个怪物中较弱的一个即可,则此时应有:w+L<j<Rbjmin(aL,aR)……不断重复上述过程,直至将所有怪物全部打败即可。

发现上述不断寻找“最近的最弱怪物,并增加力量”的过程,就是在笛卡尔树上,以相邻的较弱怪物为起点,不断跳父亲直至根,到达某个节点 u 后获得的力量值即为子树 u 中所有节点 vbv。记先后跳到的节点为 u1,u2,,uk 则若初始力量 w(u1) 合法,则有:

{w(u1)au11i<k, w(u1)+vsubtree(ui)bvafaui

同样移项后,有:

w(u1)=max{au1,max1i<k(afauivsubtree(ui)bv)}

建立笛卡尔树后,考虑进行两次 dfs,第一次 dfs 预处理 vsubtree(u)bv,第二次 dfs 预处理从笛卡尔树根到所有节点的上式即可。

求答案时考虑枚举所有相邻两个怪物,记两者中较弱怪物为起点 u1,其答案即为 w(u1);对于多组询问,每组询问大力修改后独立处理即可,总时间复杂度 O(nq) 级别。

#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:稿子已提交,要上电视啦~

唉唉退役了什么也没学到了,直接进入夹带私货环节:

posted @   Luckyblock  阅读(1441)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示