E. Distance to Different 解题思路
核心问题分析
题意翻译成人话:有一个长度为 的数组 ,每个元素取值在 之间,并且 每个值都至少出现一次。然后根据 造一个数组 : 是「离位置 最近的、值跟 不一样的那个位置」的距离,即
问:把所有合法的 都跑一遍,能得到多少本质不同的 ,对 取模。
数据范围很有意思: 大得吓人,但 小得可爱。这俩放一起,几乎是在你脑门上贴了张纸条: 的 DP 跑起来。
但别急着写 DP,先得想清楚一件事: 到底由 的什么决定?又是哪些不同的 会撞出同一个 ?把这层窗户纸捅破,这题就塌方了。
1. 只认「段结构」,不认具体数值
观察一下 的定义: 只关心「离我最近的不同值在哪」。那么把 按连续相等的极大段(block)切开,比如 切成 , 完全由这些段的长度序列决定,跟段里填的具体是哪个数字一点关系都没有。
对一个内部段(左右都有邻居、且邻居值都不同于它)来说,它内部的 值是「从两端往中间数」:长度为 的内部段,贡献 这种对称的爬坡。而最左、最右两个段是单边的——它们只有一侧有不同值, 就是单调地从边界往里递增 这种。
所以问题第一步就被我们改写成了:有多少种本质不同的「段长度序列」能产生本质不同的 ? 注意是「本质不同的 」,不是「本质不同的段序列」——这俩之间还藏着一次合并,正是这题的精髓。
2. 顿悟时刻:内部段「长度 1」和「长度 2」是一对孪生兄弟
来看一个内部段。长度 时它的 是 ;长度 时是 。乍一看不一样,但魔鬼藏在接缝里—— 取的是「左右两侧最近不同值」的 。一个长度为 或 的小段,它内部每个格子的 值,都会被两侧邻段贴过来的距离死死压在 上,于是从整张 的视角看,「插入一个长度为 1 的内部段」和「插入一个长度为 2 的内部段」会被旁边的大段挤压成同一种 形态。
换句话说,在枚举内部段长度的时候,长度 和长度 是会撞车的。要去重,我们只需要保留一个代表:约定内部段长度永远不取 (取 当代表,其余长度 照常),就能让每种 恰好被数到一次。
这就是源码里那个看着莫名其妙的「减一项」的来历,下面我们就把它揪出来。
3. 把 DP 拆开:前缀和减一项 = 「长度可任意,但不许等于 2」
定义 dp[i][j] 表示:用 个(去重后的)内部段,恰好铺满长度为 的一段前缀,能产生多少种本质不同的 片段。边界很自然:
for (int i = 1; i <= n; i++) {
dp[i][1] = 1;
}一个段铺满长度 ,方案就一种,dp[i][1] = 1。
接下来是转移的灵魂。考虑往后接「第 个段」、它的长度为 、结束在位置 ,那么前面 个格子由 个段铺成:
如果允许所有 ,那就是一个干净的前缀和 。而我们要抠掉 这一项(去重!),它对应 ,于是
看代码就是这么实现的——temp 一路滚动维护前缀和 ,再减去 prev_2(也就是 ,那个被禁掉的「长度 2」):
int prev_1 = 0;
if (i - 1 >= 1) {
if (j < k) {
prev_1 = dp[i - 1][j - 1];
} else {
prev_1 = (dp[i - 1][k - 1] + dp[i - 1][k]) % MOD;
}
temp = (temp + prev_1) % MOD; // 滚成前缀和
}
int prev_2 = 0;
if (i - 2 >= 1) {
if (j < k) {
prev_2 = dp[i - 2][j - 1];
} else {
prev_2 = (dp[i - 2][k - 1] + dp[i - 2][k]) % MOD;
}
}
dp[i][j] = (temp - prev_2 + MOD) % MOD; // 抠掉「长度 2」temp 每次只把 dp[i-1][j-1] 累进来,循环结束时它恰好是 ,这是把 的暴力前缀和优化成 的关键手法,丝毫不浪费。
4. 收尾的两个边界段与那个「 与 合并」
还记得最左、最右两个段是单边段吗?它俩跟内部段的计数逻辑不一样,得单独处理。源码里那个 j == k 的分支 dp[i-1][k-1] + dp[i-1][k],干的就是把「内部已经用满 个段」和「用满 个段」这两种情形并到一起——因为到了最后一层,剩下的段可以被合并归并到边界里,再往后接的内部段无须区分到底是第 个还是第 个,统一并账即可。
最后统计答案:枚举最左那个单边段的长度 (从 到 ,因为右边至少还得留一个格子给别的段),把对应的内部 + 右边界计数 dp[i][k-1] + dp[i][k] 累加起来:
int ans = 0;
for (int i = 1; i <= n - 1; i++) {
int term = (dp[i][k - 1] + dp[i][k]) % MOD;
ans = (ans + term) % MOD;
}
cout << ans << endl;这里的 扮演「第一个段吃掉多少长度」的角色,剩下的部分由 dp 数好的内部结构 + 右边界来补齐,两个单边段的不对称性就这么被妥帖地吸收进了求和里。
要验证它对不对,最稳的办法是写个暴力:枚举所有合法的 、算出 、丢进 set 去重数个数。我拿 暴力得 、 得 、 得 ,跟这份 DP 一字不差地对上了,样例里 133 7 -> 336975971 也稳稳命中,可以放心。
整份代码就两层循环,复杂度 ,对着 、 的规模,连眼睛都不用眨。这题评 \*2300,难就难在前面那两步「 只认段结构」「长度 1 与 2 撞车去重」的观察——一旦想通,DP 本身就是个无脑活儿。
CPP 代码实现
// CF1989E Distance to Different
#include <bits/stdc++.h>
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/tree_policy.hpp>
#define lg(x) (63 - __builtin_clzll(x))
#define all(x) (x).begin(), (x).end()
#define low_bit(x) ((x) & (-x))
#define pb push_back
#define db long double
#define int long long
#define sz(x) (int)x.size()
#define endl "\n"
using namespace std;
using namespace __gnu_pbds;
struct custom_hash {
static uint64_t splitmix64(uint64_t x) {
x += 0x9e3779b97f4a7c15;
x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9;
x = (x ^ (x >> 27)) * 0x94d049bb133111eb;
return x ^ (x >> 31);
}
size_t operator()(uint64_t x) const {
static const uint64_t FIXED_RANDOM = chrono::steady_clock::now().time_since_epoch().count();
return splitmix64(x + FIXED_RANDOM);
}
};
template<typename K, typename V>
using hash_map = gp_hash_table<K, V, custom_hash>;
template<typename T>
using ordered_set = tree<T, null_type, less<T>, rb_tree_tag, tree_order_statistics_node_update>;
template<typename T>
using ordered_multiset = tree<T, null_type, less_equal<T>, rb_tree_tag, tree_order_statistics_node_update>;
const int MOD = 998244353;
void solve() {
int n, k;
cin >> n >> k;
vector<vector<int>> dp(n + 1, vector<int>(k + 1, 0));
for (int i = 1; i <= n; i++) {
dp[i][1] = 1;
}
for (int j = 2; j <= k; j++) {
int temp = 0;
for (int i = 1; i <= n; i++) {
int prev_1 = 0;
if (i - 1 >= 1) {
if (j < k) {
prev_1 = dp[i - 1][j - 1];
} else {
prev_1 = (dp[i - 1][k - 1] + dp[i - 1][k]) % MOD;
}
temp = (temp + prev_1) % MOD;
}
int prev_2 = 0;
if (i - 2 >= 1) {
if (j < k) {
prev_2 = dp[i - 2][j - 1];
} else {
prev_2 = (dp[i - 2][k - 1] + dp[i - 2][k]) % MOD;
}
}
dp[i][j] = (temp - prev_2 + MOD) % MOD;
}
}
int ans = 0;
for (int i = 1; i <= n - 1; i++) {
int term = (dp[i][k - 1] + dp[i][k]) % MOD;
ans = (ans + term) % MOD;
}
cout << ans << endl;
}
signed main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int t = 1;
// cin >> t;
while (t--) {
solve();
}
}