题解:CF1716D Chip Move

本文参考了题解区的部分做法,综合了自己的理解,力求解释得更清楚,也为了加深自己对题目得理解。

题目

link

给定两个数 \(n,k\),问从 \(0\) 开始,第 \(i\) 步只能走 \((k + i - 1)\) 的正倍数(即不能走 \(0\)),问分别走到 \(x\in[1,n]\) 的方案数,对 \(998244353\) 取模。

解题

首先考虑对这些位置做 \(\bf DP\)。设 f[i][j] 表示走到第 \(j\) 步时走到第 \(i\) 个位置的方案数。

考虑最坏情况下,\(k = 1\),从 \(1\) 的步长开始走,每多一步,步长只加 \(1\)。所以 \(1 + 2 + \dots + m = n\)\(m\) 为最多的步数。

\(\therefore \frac{(m + 1) m}{2} = n\)

\(又 \because \frac{(\sqrt{2n} + 1) \sqrt{2n}}{2} = \frac{2n + \sqrt{2n}}{2} = n + \frac{\sqrt{2n}}{2} \gt n\)

\(\therefore m \lt \sqrt{2n}\)

枚举步数复杂度 \(O(\sqrt{n})\) 枚举每个位置复杂度 \(O(n)\) 所以 \(\bf DP\) 外面两层循环的复杂度为 \(O(n \sqrt{n})\)

不难写出以下暴力:(暂不在意时间、空间复杂度的错误)

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;

const int NR = 2e5 + 10;
const int MR = sqrt(2 * NR);
const long long mod = 998244353;
long long f[NR][MR];

int main()
{
	int n, k;
	scanf("%d%d", &n, &k);
	f[0][0] = 1;
	for (int j = 1; j <= sqrt(2 * n); j ++)
	{
		for (int i = 1; i <= n; i ++)
		{
			if (i - (k + j - 1) < 0) continue;
			for (int o = 1; ; o ++)
			{
				int l = i - o * (k + j - 1);
				if (l < 0) break;
				f[i][j] += f[l][j - 1];
				f[i][j] %= mod;
			}
		}
	}
	for (int i = 1; i <= n; i ++)
	{
		long long sum = 0;
		for (int j = 1; j <= sqrt(2 * n); j ++)
		{
			sum += f[i][j];
			sum %= mod;
		}
		printf("%lld ", sum);
	}
	puts("");
	return 0;
}

显然发现,\(\bf DP\) 双重循环内部的复杂度要尽量小。

还是考虑使用暴力的部分思路。

f[0][0] = 1;
for (int j = 1; j <= sqrt(2 * n); j ++)
{
    for (int i = 1; i <= n; i ++)
    {
        if (i - (k + j - 1) < 0) continue;
        for (int o = 1; ; o ++)
        {
            int l = i - o * (k + j - 1);
            if (l < 0) break;
            f[i][j] += f[l][j - 1];
            f[i][j] %= mod;
        }
    }
}

注意到,这里枚举的 o 费了时间复杂度。又考虑更新 i 的位置 li 位置的差都是这一步步长的倍数,因此这两个位置对步长取余的余数相等。

所以可以开一个数组 mstep 记录对步长取余余数相同的位置的方案数和,更新每个位置时,直接加上这个数组的对应值即可。

也要注意更新 i 的位置 l 必须在 i 之前,所以对每个步长都要重置一下 mstep,然后按位置从小到大在计算新答案的同时将上一步这个位置的方案数放入 mstep。这样既保证遍历到后面的位置时使用的 mstep 值是之前更小可行的位置转移来的;又保证计算的新答案的值是由上一步得来的,不会出现跳步的情况。

在对特定步长下每个位置计算的时候,我们可以直接将这一步长的答案值存入每个位置的最终答案。

最后,如果开 \(O(n \sqrt{n})\)\(\bf DP\) 数组会炸空间,所以考虑滚动数组,滚掉步数这一维。

\(Code\)

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;

const int NR = 2e5 + 10;
const int MR = sqrt(2 * NR);
const long long mod = 998244353;
long long f[NR][2]; // f[i][j] 表示走到第 j 步,第 i 个位置的方案数,这里将 j 这维滚动
long long mstep[NR]; // 对步长取余余数相同的位置的方案数和
long long ans[NR];

int main()
{
	int n, k;
	scanf("%d%d", &n, &k);
	for (int j = 1; j <= sqrt(2 * n); j ++)
	{
		int step = (k + j - 1); // 步长
		for (int i = 0; i <= step; i ++)
		{
			mstep[i] = 0;
		}
		if (j == 1) mstep[0] = 1; // 走第一步时可从 0 走过来
		for (int i = 1; i <= n; i ++)
		{
			f[i][j % 2] = mstep[i % step]; // 由上一步,位置在 i 之前,这一步可以走到 i 的位置的方案和更新而来
			f[i][j % 2] %= mod;
			mstep[i % step] += f[i][(j % 2) ^ 1]; // 在这一位置对步长取余余数的下标上,放入这个位置上一步的结果
			mstep[i % step] %= mod;
			ans[i] += f[i][j % 2];
			ans[i] %= mod;
		}
	}
	for (int i = 1; i <= n; i ++)
	{
		printf("%lld ", ans[i]);
	}
	puts("");
	return 0;
}
posted @ 2025-02-08 11:44  hsy8116  阅读(16)  评论(0)    收藏  举报