CF EDU 133 D - Chip Move
前缀和优化DP
题意
给定 n,k (1<= n, k <= 2e5), 若当前位置是 i,已经走了 j 次,则下一次只能走 j + k - 1 的倍数步;
求从 0 开始,走到 [1, n] 中每个位置的方案数
思路
先想一个朴素dp
设 \(f[i][j]\) 为从 0 开始,用了 j 步走到 i 的方案数
最后一步是走了 d = k + j - 1 步, 枚举最后一步的可能情况,即 \(f[i][j]=f[i-d][j-1]+f[i-2*d][j-1]+...\)
因为 \(j\) 是 \(sqrt(i)\) 级别的;
根据调和级数,总转移是 \(n*logn*\sqrt n\) ,因此总时间复杂度为 \(O(n*logn*\sqrt n)\)
状态数是 \(n*\sqrt n\), 空间复杂度是 \(O(n\sqrt n)\)
考虑优化时空复杂度
空间
显然可以用滚动数组优化掉 \(j\) 那一维,若当前枚举到第 j 步,用 \(f[i]\) 表示 j 步走到 i 的方案数,\(tmp[i]\) 表示 j - 1 步走到 i 的方案数
时间
\(f[i]=tmp[i-d]+tmp[i-2*d]+tmp[i-3*d]+tmp[i-4*d]+...\)
用前缀和优化转移即可
也可以用 \(f[i]=tmp[i-d]+f[i-d]\)
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
#include <cmath>
using namespace std;
#define endl "\n"
typedef long long ll;
typedef pair<int, int> PII;
const int N = 2e5 + 10;
const int mod = 998244353;
int n, k;
int f[N], ans[N], tmp[N];
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> k;
f[0] = 1;
for (int j = 1; j < sqrt(2 * n) + 2; j++)
{
memset(tmp, 0, sizeof tmp);
for (int i = 1; i <= n; i++)
{
int now = j + k - 1;
if (i - now >= 0)
tmp[i] = (tmp[i-now] + f[i-now]) % mod;
ans[i] = (ans[i] + tmp[i]) % mod;
}
memcpy(f, tmp, sizeof f);
}
for (int i = 1; i <= n; i++)
cout << ans[i] << " ";
cout << endl;
return 0;
}
PS
我自己想的dp是先枚举 i,再从 \([1,\sqrt{2*n}]\) 枚举 j
\(f[i][j]=f[i+1-k-j][j-1]+f[i+1-k-j][j]\)
第 j 次跳的步数是 d = k + j - 1
意思是
- i 是第 j 次跳了 1 个 d 步过来的
- i 是跳了 t 次 d 步来的,但是这个方案数可以由用 j 次到位置 i - d 的方案数继承而来(如果第 j 次能到 i - d,那多跳一个 d 的方案数是一样的)
这样的时空复杂度都是 \(O(n*\sqrt n)\)
f[0][0] = 1;
for (int i = 1; i <= n; i++)
f[i].resize(sqrt(2*i) + 1);
for (int i = 1; i <= n; i++)
{
for (int j = 1; j < f[i].size(); j++)
{
if (i + 1 - k - j < 0)
break;
if (f[i+1-k-j].size() <= j - 1)
break;
add(f[i][j], f[i+1-k-j][j-1]);
if (f[i+1-k-j].size() > j)
add(f[i][j], f[i+1-k-j][j]);
}
}
但不太能用滚动数组优化空间,就寄了。。。
而且常数略大可能跑不出来,本地跑 n = 2e5, k = 1 用 4s,正解只用 800ms