第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(0\le i\le n)\) 个位置后,当前的能量将变为:

\[w +(- a_0 + b_1) +(- a_1 + b_2) + \cdots + (-a_{i - 1} + b_i) \]

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

\[\forall 0\le i\le n,\ \left(w - \sum_{j=0}^{i - 1}a_i + \sum_{j=1}^{i} b_j \right)\ge a_{i} \]

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

\[w= \max_{0\le i\le n} \left( \sum_{j=0}^{i} a_j - \sum_{j=1}^{i} b_j\right) \]

考虑预处理 \(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}\),即有:

\[w(p) = \max\left( \max_{0\le i\le p - 1} c_i, b_p + \max_{p\le i\le n} c_i \right) \]

发现 \(\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 就变得很简单的题。

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

  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(\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_{i} + 1 \rightarrow f_{j} \left(j \in\left[i-a_i+1, i - \min(a_i + k_{i - a_i}, n) \right]\right) \]

答案即 \(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)\) 合法,则有:

\[\begin{cases} w(u_1) \ge a_{u_1}\\ \forall 1\le i< k,\ w(u_1) + \sum_{v\in \operatorname{subtree}(u_i)} b_v \ge a_{\operatorname{fa}_{u_i}} \end{cases} \]

同样移项后,有:

\[w(u_1) = \max\left\{a_{u_1}, \max_{1\le i< k} \left(a_{\operatorname{fa}_{u_i}} - \sum_{v\in \operatorname{subtree}(u_i)} b_v\right) \right\} \]

建立笛卡尔树后,考虑进行两次 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 代码。

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

posted @ 2024-12-09 23:39  Luckyblock  阅读(321)  评论(0编辑  收藏  举报