ABC335

基本情况

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

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

C - 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';
		}
	}
}

E - Non-Decreasing Colorful Path

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

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

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

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

考虑以下动态规划:

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

实际上,以下内容成立:

\(G\)是通过保留\(A_u = A_v\)的边\(u - v\)得到的图。然后,属于同一连通分量的顶点可以视为单个顶点。

例如,如果顶点 \(p,q,r\)\(s\) 满足:

  • \(A_p < A_q = A_r < A_s\)
  • 存在边 \(p - q\)
  • \(q\)\(r\)\(G\) 上属于同一连通分量
  • 存在边 \(r - s\)

则可以沿着 \(p \rightarrow q \rightarrow \dots \rightarrow r \rightarrow s\) 路径遍历,而不会破坏 \(S\) 的单调性。

\(q\)\(r\) 出发时,只需采用一棵生成树并使用其中的边。

在识别顶点后,所有剩余的边 \(u - v\) 满足 \(A_u < A_v\) 。在这种情况下,上述描述的 \(DP\) 可以通过以 \(A_u\) 的升序进行边 \(u - v\) 的转换来进行(要解决的问题是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;
}

F - Hop Sugoroku

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

根号分治

先考虑最暴力的 \(DP\)

\(dp_j\) 表示以 \(j\) 结尾的方案数。

答案就是 \(\sum^n_{i=1}dp_i\)

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';

然而这个算法的可行性取决于 \(A_i\) 的大小

如果 \(A_i\) 过小,这就是一个 \(O(n^2)\) 超时算法。

我们找一个 \(A_i\) 的阈值,这里直接设成 \(W = \sqrt{n}\) 就行。

  • \(A_i > W\)
    • 沿用上述暴力转移即可,\(O(n\times \sqrt{n})\)
  • \(A_i \leq W\)
    • 设计状态 \(dp_{a_i, i\mod a_i}\)
      • 表示对于终点 \(a_i, j\),(这里 \(j\mod a_i\) 也包含于 \(i\mod a_i\) 所以可行)的方案数
    • 考虑 \(i\) 能到的位置 \(i\mod a_i + a_i\times k\)
    • 因为 \(a_i\) 小,所以开一个二维数组 \(g_{a_i, i\mod a_i}\),维护所有以 \(a_i\) 为模数, \(j(j\mod a_i = i\mod a_i)\)​ 为起点的方案数。
      • 为什么这样设计?
        • 因为 \(j\) 能包含所有当前 \(i\) , \(A_i\) 能到达的地点。
        • \(j = i\mod a_i + a_i\times k\)
        • \(j\mod a_i = i\mod a_i\)
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;
}
posted @ 2024-01-07 11:42  加固文明幻景  阅读(71)  评论(0编辑  收藏  举报