[ABC335F] Hop Sugoroku 【根号分治】

发布时间 2024-01-07 11:06:11作者: 固态H2O

[ABC335F] Hop Sugoroku 【根号分治】

\(\mathtt {TAGS}\): 根号分治 DP

\(\mathtt {APPRAIS}\): 很优美的暴力 DP

First. 朴素 DP

这里做一个转化:求不同集合的数量相当与求走到所有点的不同方案数之和。

\(dp_i\) 表示走到 \(i\) 号的方案数。

显然, \(dp_i\) 可以去更新所有 \(dp_{i + a_i \times x}[i + a_i \times x \le n]\)

即:

for (int i = 1; i <= n; i ++) {
	for (int j = i + a[i]; j <= n; j += a[i]) {
		dp[j] += dp[i], dp[j] -= (dp[j] >= mod) ? mod : 0;
	}
}

时间复杂度:\(\text{O}(n ^ 2)\)

Second. 优化

\(su_{i,x, r}\) 为所有小于 \(i\) 的数中模 \(x\)\(r\)\(dp\) 值之和。

对于 \(a_i\) 的大小不同,我们尝试去做不同的解决方案。

设阈值为 \(x\),对于 \(i \le x\) 的部分:

此时枚举所有 \(p \le x\)\(dp_i = \sum su_{p, i \bmod x}\)。这相当枚举所有与 \(i\)\(p\) 同余的所有 \(j\) 的位置的 \(dp\) 值之和。

剩余部分已经由 \(a_i > x\)\(i\) 更新过了。

更新时,对于 \(a_i \le x\)

\(su_{a_i, i \bmod a_i}\) 上累加即可实现向后更新的转移。

对于 \(a_i > x\)

此时直接暴力更新 \(dp_i + a_i \times j\) 最多更新 \(\frac{n}{x}\) 次。

所以总复杂度为 \(\text{O}(x + \frac{n}{x})\)

\(x\)\(\sqrt{n}\) 时得到最优。

傻缺代码
#include <bits/stdc++.h>
using namespace std;
// 根号分治
// 1: a <= sqrt(n)
// 记 sum[x][y] 表示 % x = y 的 i 的 dp[i] 之和。
// 2: a > sqrt(n)
// 暴力更新,次数不会超过 sqrt(n) 次
/*
for (int i = 1; i <= n; i ++) {
	for (int j = i + a[i]; j <= n; j += a[i]) {
		dp[j] = (dp[j] + dp[i]) % mod;
	}
}
*/
const int N = 2e5 + 10, M = 500 + 10;
const int mod = 998244353;
int n, b, a, ans;
int dp[N], su[M][M];

signed main() {
	ios::sync_with_stdio(0);
	cin >> n;
	b = sqrt(n);
	dp[1] = 1;
	for (int  i = 1; i <= n; i ++) {
		cin >> a;
		for (int j = 1; j <= b; j ++) dp[i] = (dp[i] + su[j][i % j]) % mod;
		if(a >= b) for (int j = i + a; j <= n; j += a) dp[j] = (dp[j] + dp[i]) % mod;
		else su[a][i % a] = (su[a][i % a] + dp[i]) % mod;
		(ans += dp[i]) %= mod;
	}
	cout << ans << endl;
	return 0;
}