C - Not So Consecutive

C - Not So Consecutive

Problem Statement

You are given an integer N. An integer sequence x=(x1,x2,,xN) of length N is called a good sequence if and only if the following conditions are satisfied:

  • Each element of x is an integer between 1 and N, inclusive.
  • For each integer i (1iN), there is no position in x where i appears i+1 or more times in a row.

You are given an integer sequence A=(A1,A2,,AN) of length N. Each element of A is 1 or an integer between 1 and N. Find the number, modulo 998244353, of good sequences that can be obtained by replacing each 1 in A with an integer between 1 and N.

Constraints

  • 1N5000
  • Ai=1 or 1AiN.
  • All input values are integers.

Input

The input is given from Standard Input in the following format:

N
A1 A2 AN

Output

Print the answer.


Sample Input 1

2
-1 -1

Sample Output 1

3

You can obtain four sequences by replacing each 1 with an integer between 1 and 2.

A=(1,1) is not a good sequence because 1 appears twice in a row.

The other sequences A=(1,2),(2,1),(2,2) are good.

Thus, the answer is 3.


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

 

解题思路

  纯动态规划优化题,硬是从 O(n4) 优化到 O(n2) 甚至是 O(n2logn)超有意思的说

  状态还是很容易想到的,定义 f(i,j) 表示由前 i 个数构成且第 i 个数是 j 的所有合法方案的数量。根据序列最后一段有多少个连续 j(假设有 k 个),以及第 ik 个数是哪个数(假设是 u,需满足 uj)进行状态划分,状态转移方程就是f(i,j)=k=1min{i,j}u=1ujnf(ik,u)

  实际上这个状态转移方程是有问题的,因为默认了 ai1,ai2,,aimin{i,j} 都是 1 的情况。考虑 a1ai1,如果这些数中存在某些 av1avj,不妨假设 v 是这些数中的最大下标。如果不存在这样的 v,即该范围内的数均是 1j,则令 v=0,同时规定 a0=0。分情况讨论,如果 ivj,那么很明显最后一段最多只能有 iv 个连续的 j,且第 v 个数 av 是固定的。否则连续一段 j 的最大长度就是 j。另外如果存在 aik=j 的情况,跳过即可。因此正确的状态转移方程应该是

f(i,j)={(k=1aikjiv1u=1ujnf(ik,u))+f(v,av),ivjk=1aikjju=1ujnf(ik,u),others

  同时规定 f(0,0)=0,这样对于序列前 i 个数都是 j 的状态可以从 f(0,0) 转移得到。

  容易知道整个 dp 的时间复杂度是 O(n4),不过一个很明显可以优化的地方是 u=1ujnf(ik,u) 这部分。本质是累加所有第一维是 ik 的状态 f(ik,),然后减去 f(ik,j),而 f(ik,) 在之前就已经全部求出来了。所以定义 si=k=1nf(i,k),那么 u=1ujnf(ik,u) 就可以等价成 sikf(ik,j),而 si 只需在计算完 f(i,) 时进行累加即可,这样时间复杂度就降到了 O(n3)

  对应的状态转移方程如下:

f(i,j)={(k=1iv1sikf(ik,j))+f(v,av),ivjk=1jsikf(ik,j),others

  对于 aik=j 的情况原本是要跳过的,但对于这种情况必然有 sik=f(ik,j),这是因为 aik 是定值,f(ik,u)=0,uj,因此 sikf(ik,j)=0,并没有影响。

  先放出 TLE 代码,时间复杂度为 O(n3)

#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;
}

  上述的代码是在枚举 k 的过程中找到 v 的。很明显如果还要优化的话那么就应该继续把求和符号去掉,求和的部分本质也是对第一维某个已求得区间的 sif(i,j) 进行累加,因此可以用前缀和进行优化。

  定义 Si=k=1isig(i,j)=k=1if(k,j)

  那么 k=1iv1sikf(ik,j) 就等价于 Si1Sv(g(i1,j)g(v,j))

  同理 k=1jsikf(ik,j) 就等价于 Si1Sij1(g(i1,j)g(ij1,j))

  状态转移方程变成了

f(i,j)={Si1Sv(g(i1,j)g(v,j))+f(v,av),ivjSi1Sij1(g(i1,j)g(ij1,j)),others

  现在关键的问题对于每个状态 f(i,j) 如何快速确定对应的 v。本质是在 a0ai1 中找到同时满足 av1avj 的最大下标 v,所以可以用 std::set<std::pair<int, int>> 来动态维护 0n 每个值出现的最大下标,其中第一个关键字是下标,第二个关键字是值,按第一个关键字降序排序。另外开一个数组 p 表示每个值对应的最大下标。

  当枚举到 j 时,查看 st.begin()->second,如果不等于 j,则对应的 v 就是 st.begin()->first,否则就是 next(st.begin())->first

  当枚举到的 ai 是一个定值,那么只需从 std::set 中删除原本的数对 (pai,ai),并重新插入 (i,ai),同时更新 paii

  AC 代码如下,时间复杂度为 O(n2logn)

#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;
}

  其实 O(n2logn) 的复杂度已经可以过了,实际上还可以优化到 O(n2),如果有兴趣可以继续往下看。

  上面的 pi 表示值 i 的最大下标,可以反过来考虑,变成对于值不为 i 的最大下标。那么对于 f(i,j),对应的 v 就直接等于 pj。另外可以发现只有 ai1 的情况才需要更新 p 数组,只需暴力枚举 k,令 pk=i,kj 即可。

  AC 代码如下,时间复杂度为 O(n2)

#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

posted @   onlyblues  阅读(23)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
Web Analytics
点击右上角即可分享
微信分享提示