ABC335
基本情况
ABD秒了,C卡了一会,空间换时间然后爆内存,最后交了个100多行的逆天模拟终于+4过。
赛后发现其实是手写了双端队列。
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
首先,由于图是连通的,从 \(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\)
- 为什么这样设计?
- 设计状态 \(dp_{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;
}