C - Not So Consecutive
C - Not So Consecutive
Problem Statement
You are given an integer . An integer sequence of length is called a good sequence if and only if the following conditions are satisfied:
- Each element of is an integer between and , inclusive.
- For each integer (), there is no position in where appears or more times in a row.
You are given an integer sequence of length . Each element of is or an integer between and . Find the number, modulo , of good sequences that can be obtained by replacing each in with an integer between and .
Constraints
- or .
- All input values are integers.
Input
The input is given from Standard Input in the following format:
Output
Print the answer.
Sample Input 1
2
-1 -1
Sample Output 1
3
You can obtain four sequences by replacing each with an integer between and .
is not a good sequence because appears twice in a row.
The other sequences are good.
Thus, the answer is .
Sample Input 2
3
2 -1 2
Sample Output 2
2
Sample Input 3
4
-1 1 1 -1
Sample Output 3
0
Sample Input 4
20
9 -1 -1 -1 -1 -1 -1 -1 -1 -1 7 -1 -1 -1 19 4 -1 -1 -1 -1
Sample Output 4
128282166
解题思路
纯动态规划优化题,硬是从 优化到 甚至是 ,超有意思的说。
状态还是很容易想到的,定义 表示由前 个数构成且第 个数是 的所有合法方案的数量。根据序列最后一段有多少个连续 (假设有 个),以及第 个数是哪个数(假设是 ,需满足 )进行状态划分,状态转移方程就是
实际上这个状态转移方程是有问题的,因为默认了 都是 的情况。考虑 ,如果这些数中存在某些 且 ,不妨假设 是这些数中的最大下标。如果不存在这样的 ,即该范围内的数均是 或 ,则令 ,同时规定 。分情况讨论,如果 ,那么很明显最后一段最多只能有 个连续的 ,且第 个数 是固定的。否则连续一段 的最大长度就是 。另外如果存在 的情况,跳过即可。因此正确的状态转移方程应该是
同时规定 ,这样对于序列前 个数都是 的状态可以从 转移得到。
容易知道整个 dp 的时间复杂度是 ,不过一个很明显可以优化的地方是 这部分。本质是累加所有第一维是 的状态 ,然后减去 ,而 在之前就已经全部求出来了。所以定义 ,那么 就可以等价成 ,而 只需在计算完 时进行累加即可,这样时间复杂度就降到了 。
对应的状态转移方程如下:
对于 的情况原本是要跳过的,但对于这种情况必然有 ,这是因为 是定值,,因此 ,并没有影响。
先放出 TLE 代码,时间复杂度为 :
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 5010, mod = 998244353;
int a[N];
int f[N][N];
int s[N];
int main() {
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", a + i);
}
f[0][0] = 1;
for (int i = 1; i <= n; i++) {
if (a[i] == -1) {
for (int j = 1; j <= n; j++) {
for (int k = 1; k <= j && k <= i; k++) {
if (a[i - k] != -1 && a[i - k] != j) {
f[i][j] = (f[i][j] + f[i - k][a[i - k]]) % mod;
break;
}
f[i][j] = ((LL)f[i][j] + s[i - k] - f[i - k][j] + mod) % mod;
}
}
}
else {
int j = a[i];
for (int k = 1; k <= j && k <= i; k++) {
if (a[i - k] != -1 && a[i - k] != j) {
f[i][j] = (f[i][j] + f[i - k][a[i - k]]) % mod;
break;
}
f[i][j] = ((LL)f[i][j] + s[i - k] - f[i - k][j] + mod) % mod;
}
}
for (int j = 1; j <= n; j++) {
s[i] = (s[i] + f[i][j]) % mod;
}
}
int ret = 0;
for (int i = 1; i <= n; i++) {
ret = (ret + f[n][i]) % mod;
}
printf("\n%d", ret);
return 0;
}
上述的代码是在枚举 的过程中找到 的。很明显如果还要优化的话那么就应该继续把求和符号去掉,求和的部分本质也是对第一维某个已求得区间的 进行累加,因此可以用前缀和进行优化。
定义 ,。
那么 就等价于 。
同理 就等价于 。
状态转移方程变成了
现在关键的问题对于每个状态 如何快速确定对应的 。本质是在 中找到同时满足 且 的最大下标 ,所以可以用 std::set<std::pair<int, int>>
来动态维护 每个值出现的最大下标,其中第一个关键字是下标,第二个关键字是值,按第一个关键字降序排序。另外开一个数组 表示每个值对应的最大下标。
当枚举到 时,查看 st.begin()->second
,如果不等于 ,则对应的 就是 st.begin()->first
,否则就是 next(st.begin())->first
。
当枚举到的 是一个定值,那么只需从 std::set
中删除原本的数对 ,并重新插入 ,同时更新 。
AC 代码如下,时间复杂度为 :
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
const int N = 5010, mod = 998244353;
int a[N], p[N];
int f[N][N], g[N][N];
int s[N];
int main() {
int n;
scanf("%d", &n);
set<PII> st({{0, 0}});
for (int i = 1; i <= n; i++) {
scanf("%d", a + i);
st.insert({0, i});
}
f[0][0] = 1;
for (int i = 1; i <= n; i++) {
if (a[i] == -1) {
for (int j = 1; j <= n; j++) {
int x = -st.begin()->first, y = st.begin()->second;
if (st.begin()->second == j) x = -next(st.begin())->first, y = next(st.begin())->second;
if (x < i - j) f[i][j] = ((LL)s[i - 1] - s[max(0, i - j - 1)] - g[i - 1][j] + g[max(0, i - j - 1)][j]) % mod;
else f[i][j] = ((LL)s[i - 1] - s[x] - g[i - 1][j] + g[x][j] + f[x][y]) % mod;
}
}
else {
int j = a[i];
int x = -st.begin()->first, y = st.begin()->second;
if (st.begin()->second == j) x = -next(st.begin())->first, y = next(st.begin())->second;
if (x < i - j) f[i][j] = ((LL)s[i - 1] - s[max(0, i - j - 1)] - g[i - 1][j] + g[max(0, i - j - 1)][j]) % mod;
else f[i][j] = ((LL)s[i - 1] - s[x] - g[i - 1][j] + g[x][j] + f[x][y]) % mod;
st.erase({-p[j], j});
st.insert({-i, j});
p[j] = i;
}
s[i] = s[i - 1];
for (int j = 1; j <= n; j++) {
s[i] = (s[i] + f[i][j]) % mod;
g[i][j] = (g[i - 1][j] + f[i][j]) % mod;
}
}
int ret = 0;
for (int i = 1; i <= n; i++) {
ret = (ret + f[n][i]) % mod;
}
ret = (ret + mod) % mod;
printf("%d", ret);
return 0;
}
其实 的复杂度已经可以过了,实际上还可以优化到 ,如果有兴趣可以继续往下看。
上面的 表示值 的最大下标,可以反过来考虑,变成对于值不为 的最大下标。那么对于 ,对应的 就直接等于 。另外可以发现只有 的情况才需要更新 数组,只需暴力枚举 ,令 即可。
AC 代码如下,时间复杂度为 :
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
const int N = 5010, mod = 998244353;
int a[N];
int f[N][N], g[N][N];
int s[N];
int p[N];
int main() {
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", a + i);
}
f[0][0] = 1;
for (int i = 1; i <= n; i++) {
if (a[i] == -1) {
for (int j = 1; j <= n; j++) {
if (p[j] < i - j) f[i][j] = ((LL)s[i - 1] - s[max(0, i - j - 1)] - g[i - 1][j] + g[max(0, i - j - 1)][j]) % mod;
else f[i][j] = ((LL)s[i - 1] - s[p[j]] - g[i - 1][j] + g[p[j]][j] + f[p[j]][a[p[j]]]) % mod;
}
}
else {
int j = a[i];
if (p[j] < i - j) f[i][j] = ((LL)s[i - 1] - s[max(0, i - j - 1)] - g[i - 1][j] + g[max(0, i - j - 1)][j]) % mod;
else f[i][j] = ((LL)s[i - 1] - s[p[j]] - g[i - 1][j] + g[p[j]][j] + f[p[j]][a[p[j]]]) % mod;
for (int k = 1; k <= n; k++) {
if (k != j) p[k] = i;
}
}
s[i] = s[i - 1];
for (int j = 1; j <= n; j++) {
s[i] = (s[i] + f[i][j]) % mod;
g[i][j] = (g[i - 1][j] + f[i][j]) % mod;
}
}
int ret = 0;
for (int i = 1; i <= n; i++) {
ret = (ret + f[n][i]) % mod;
}
ret = (ret + mod) % mod;
printf("%d", ret);
return 0;
}
参考资料
Editorial - estie Programming Contest 2023 (AtCoder Regular Contest 169):https://atcoder.jp/contests/arc169/editorial/7911
AtCoder Regular Contest 169(A~D):https://zhuanlan.zhihu.com/p/671467218
本文来自博客园,作者:onlyblues,转载请注明原文链接:https://www.cnblogs.com/onlyblues/p/17900946.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效