ABC335

1|0基本情况

ABD秒了,C卡了一会,空间换时间然后爆内存,最后交了个100多行的逆天模拟终于+4过。

赛后发现其实是手写了双端队列。

2|0C - Loong Tracking

C - Loong Tracking

思路很明显,空间换时间,把每个状态用数组全记录下来。

但是纯这样写数组会开的巨大,所以得让后面没用的状态出去。

用双端队列来实现就贼直接。

void solve() { cin >> n >> Q; pair<int, int> move[200]; move['R'] = make_pair(1, 0);move['L'] = make_pair(-1, 0); move['U'] = make_pair(0, 1);move['D'] = make_pair(0, -1); deque<pair<int, int> > q; for (int i = 1; i <= n; i++) q.push_back({i, 0}); int x = 1, y = 0; while(Q--) { int opt; cin >> opt; if (opt == 1) { char c; cin >> c; q.pop_back(); x += move[c].first, y += move[c].second; q.push_front({x, y}); } else { int x; cin >> x; cout << q[x - 1].first << ' ' << q[x - 1].second << '\n'; } } }

3|0E - Non-Decreasing Colorful Path

https://atcoder.jp/contests/abc335/tasks/abc335_e

首先,由于图是连通的,从 1N 必然存在一条路径。因此,最大得分至少为 0

因此,我们需要考虑的是具有正分数的路径。因此,我们得出了以下关于连接顶点u和顶点v的观察结果。

  • 如果Au<Av,我们只考虑在 uv 方向上使用此边的情况。
  • 如果Au>Av,我们可以考虑与上述情况相同的情况(交换 uv )。
  • 如果Au=Av,则此边可以用于任意方向。

考虑以下动态规划:

dp[v]= {从顶点 1 到顶点 v 的路径的初步最大得分}。

实际上,以下内容成立:

G是通过保留Au=Av的边uv得到的图。然后,属于同一连通分量的顶点可以视为单个顶点。

例如,如果顶点 p,q,rs 满足:

  • Ap<Aq=Ar<As
  • 存在边 pq
  • qrG 上属于同一连通分量
  • 存在边 rs

则可以沿着 pqrs 路径遍历,而不会破坏 S 的单调性。

qr 出发时,只需采用一棵生成树并使用其中的边。

在识别顶点后,所有剩余的边 uv 满足 Au<Av 。在这种情况下,上述描述的 DP 可以通过以 Au 的升序进行边 uv 的转换来进行(要解决的问题是DAG上的最短路径问题)。在过渡时,不要忘记按上述描述识别顶点。可以通过使用 DSU 来管理顶点来实现这一点。

signed main() { std::cin.tie(nullptr)->sync_with_stdio(false); constexpr int M = 2E5 + 5; int n, m; std::cin >> n >> m; std::vector<int> a(n); for (auto& x : a) {std::cin >> x;} std::vector adj(M, std::vector<std::pair<int, int>>());//adj[V]表示权值为V = a[u]的对应的自己的点和比自己大的出点 //即[U,V], a[U] < a[V] //如果等于自己,那就缩成点 DSU dsu(n);//维护a[u] = a[v]的连通块,缩成一个点 for (int i = 0; i < m; i++) { int u, v; std::cin >> u >> v; --u; --v; if (a[u] > a[v]) {std::swap(u, v);} if (a[u] < a[v]) { adj[a[u]].push_back({u, v}); } else { dsu.merge(u, v); } } std::vector<int> dp(M, -1); dp[dsu.find(0)] = 1;//一开始只有0点是1 for (auto& vers : adj) {//对每个点值更新 for (auto&[u, v] : vers) { u = dsu.find(u); v = dsu.find(v); if (dp[u] > 0) { dp[v] = std::max(dp[v], dp[u] + 1); } } } std::cout << std::max(0, dp[dsu.find(n - 1)]) << '\n'; return 0; }

4|0F - Hop Sugoroku

https://atcoder.jp/contests/abc335/tasks/abc335_f

根号分治

先考虑最暴力的 DP

dpj 表示以 j 结尾的方案数。

答案就是 i=1ndpi

std::vector<i64> dp(n); dp[0] = 1; for (int i = 0; i < n; i++) { for (int j = i + A[i]; j < n; j += A[i]) { dp[j] += dp[i]; } } std::cout << std::accumulate(all(dp), 0LL) % mod << '\n';

然而这个算法的可行性取决于 Ai 的大小

如果 Ai 过小,这就是一个 O(n2) 超时算法。

我们找一个 Ai 的阈值,这里直接设成 W=n 就行。

  • Ai>W
    • 沿用上述暴力转移即可,O(n×n)
  • AiW
    • 设计状态 dpai,imodai
      • 表示对于终点 ai,j,(这里 jmodai 也包含于 imodai 所以可行)的方案数
    • 考虑 i 能到的位置 imodai+ai×k
    • 因为 ai 小,所以开一个二维数组 gai,imodai,维护所有以 ai 为模数, j(jmodai=imodai)​ 为起点的方案数。
      • 为什么这样设计?
        • 因为 j 能包含所有当前 i , Ai 能到达的地点。
        • j=imodai+ai×k
        • jmodai=imodai
signed main() { std::cin.tie(nullptr)->sync_with_stdio(false); int n; std::cin >> n; std::vector<int> A(n); for (auto& x : A) {std::cin >> x;} const int BD = (int)std::sqrt(n); std::vector<int> dp1(n); std::vector dp2(BD + 1, std::vector<int>(BD + 1)); dp1[0] = 1; auto add = [&](int& x, int y) { x += y; if (x >= mod) {x -= mod;} }; for (int i = 0; i < n; i++) { for (int d = 1; d <= BD; d++) {//把 dp2[d][i % d] 都转移到 dp1[i] 上,也正是因为BD足够小,这样转移才有效 add(dp1[i], dp2[d][i % d]); } if (A[i] > BD) {//暴力转移即可 for (int j = i + A[i]; j < n; j += A[i]) { add(dp1[j], dp1[i]); } } else {//另一个状态,j % A[i] = i % A[i] 以 j 结尾的方案数可以被 dp1[i] 更新 add(dp2[A[i]][i % A[i]], dp1[i]); } } int res = 0; for (int i = 0; i < n; i++) {add(res, dp1[i]);} std::cout << res << '\n'; return 0; }

__EOF__

本文作者Kdlyh
本文链接https://www.cnblogs.com/kdlyh/p/17949612.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   加固文明幻景  阅读(77)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下
点击右上角即可分享
微信分享提示