第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 枚举
先不考虑修改操作,记初始时能量的最小值为 \(w\),则若 \(w\) 合法,显然到达第 \(i(0\le i\le n)\) 个位置后,当前的能量将变为:
考虑每次从位置 \(i\) 向下一个位置移动时,需要保证当前能量不小于 \(a_{i}\),则 \(w\) 合法当且仅当满足如下限制:
移个项,则可得到 \(w\) 的值:
考虑预处理 \(c_i = \sum\limits_{j=0}^{i} a_j - \sum\limits_{j=1}^{i} b_j\),则 \(\max c_i\) 即为答案。
然后考虑修改操作对 \(c_i\),的影响。考虑代入上式,容易发现修改 \(b_p:=0\) 后,\(c_0\sim c_{p - 1}\) 不受影响,\(c_{p}\sim c_{n}\) 全部加上 \(b_{p}\),即有:
发现 \(\max_{0\le i\le p-1}\) 即 \(c_i\) 的前缀最大值,\(\max_{p\le i\le n}\) 即 \(c_i\) 的后缀最大值,于是考虑预处理 \(c_i\) 的前后缀最大值即可根据上式求得所有答案。
总时间复杂度 \(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 就变得很简单的题。
某个内存块对应的缓存块是唯一的,则发现需要支持的操作本质上只有如下几种:
- 查询某个内存块,是否在某个缓存块中;
- 查询某个缓存块中,最早被操作的内存块;
- 向缓存块中插入、删除内存块
- 查询并修改:某个内存块的被操作时间;
- 查询并修改:某个内存块是否被修改过且未保存;
操作 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
即可。
单次操作时间复杂度 \(O(\log n)\) 级别,总时间复杂度 \(O(q\log n)\) 级别。
#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,设 \(f_{i}\) 表示到达位置 \(i\) 所需的最小步数,初始化 \(f_{1} = 0\),则有显然的转移:
答案即 \(f_{n}\),然而发现上述转移成环了,即会导致某些状态的重复更新,无法直接做,于是寄。
但是容易发现,如果出现成环的转移,这些转移一定是不优的,步数一定不小于第一次更新该状态的步数,则容易发现关键的性质:
- 转移到 \(f_j\) 的最优决策 \(f_i\),一定满足 \(i<j\);
- 进一步地发现 \(f_i\) 一定是单调不降的;
- 则对于某个状态,能转移到它的最左侧的转移,一定是最优决策,即每个状态只会被转移一次。
于是考虑在 DP 时记 \(R\) 为能转移到的最靠右的位置,然后枚举 \(1\sim n\) 并考虑向后转移,每次仅转移位置大于 \(R\) 的状态并更新 \(R\) 即可。\(R\) 至多增加 \(n\) 次,转移次数至多为 \(O(n)\) 级别,则总时间复杂度为 \(O(n)\) 级别。
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e5 + 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];
for (int i = 1; i <= n; ++ i) std::cin >> k[i];
int R = 1;
for (int i = 1; i <= n; ++ i) {
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 << (R < n) ? -1 : f[n];
return 0;
}
赛时做法
赛时的一个比较天才的最短路做法,考虑分层图最短路,建三层数量为 \(n\) 的节点:
- 第一层:节点 \(i\) 代表从位置 \(i\) 出发;
- 第二层:节点 \(i\) 代表到达位置 \(i\);
- 第三层:用于描述每个位置向之后的位置的区间转移。
然后有如下连边:
- 第一层 \(i\) 向第三层 \(i+k_i\) 连边;
- 第二层 \(i\) 向第一层 \(i-a_i\) 连边;
- 第三层 \(i\) 向第二层 \(i\) 连边。
- 第三层 \(i\) 向第三层 \(i-1\) 连边;
求得第一层节点 1 到达第一层节点 \(n\) 的最短路即为答案。
前两层点的正确性容易理解,为什么第三层每个点向前一个点连边,就能解决区间转移问题?同样考虑上述“每个位置仅会被更新一次”的单调性性质,则上述第三层转移仅会更新之前从未被到达过的位置,并不会更新被到达的位置的最短路。
上图中点和边数量均为 \(O(n)\) 级别,Dijkstra 实现,总时间复杂度 \(O(n\log n)\) 级别。
#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
想到笛卡尔树其实就秒了,但是要怎么才能想到呢?
发现 \(q\le 20\) 太小了,\(n\le 2\times 10^6\) 又太大了,猜测总时间复杂度是 \(O(nq)\) 的,即每次询问实际上是完全独立的,都可以直接大力修改并套用 \(O(n)\) 的解法。
考虑初始位置位于间隔 \((i - 1, i)\),记初始力量的最小值为 \(w\),则显然有 \(w\ge \min(a_{i - 1}, a_i)\),即保证至少打败相邻的怪物中较弱的。
考虑记该怪物左侧第一个 \(a\) 大于它的怪物为 \(L\),右侧第一个为 \(R\),则打败该怪物后,力量将增加 \(\sum_{L <j < R} b_j\),然后仅需再考虑如何打败 \(L, R\) 这两个怪物中较弱的一个即可,则此时应有:\(w + \sum_{L<j<R} b_j\ge \min(a_{L}, a_{R})\)……不断重复上述过程,直至将所有怪物全部打败即可。
发现上述不断寻找“最近的最弱怪物,并增加力量”的过程,就是在笛卡尔树上,以相邻的较弱怪物为起点,不断跳父亲直至根,到达某个节点 \(u\) 后获得的力量值即为子树 \(u\) 中所有节点 \(v\) 的 \(\sum b_v\)。记先后跳到的节点为 \(u_1, u_2, \cdots, u_k\) 则若初始力量 \(w(u_1)\) 合法,则有:
同样移项后,有:
建立笛卡尔树后,考虑进行两次 dfs,第一次 dfs 预处理 \(\sum\limits_{v\in \operatorname{subtree}(u)} b_v\),第二次 dfs 预处理从笛卡尔树根到所有节点的上式即可。
求答案时考虑枚举所有相邻两个怪物,记两者中较弱怪物为起点 \(u_1\),其答案即为 \(w(u_1)\);对于多组询问,每组询问大力修改后独立处理即可,总时间复杂度 \(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 代码。
唉唉退役了什么也没学到了,直接进入夹带私货环节: