ABC446E Multiple-Free Sequences 题解

原题链接
题目翻译

问题形式化

定义状态图 \(G = (V, E)\)

  • 顶点集 \(V := \left \lbrace v \in \mathbb{Z}^2 \mid 0\le x_{v},y_{v} \le M-1 \right \rbrace\),约定顶点 \(v\) 的两个分量用 \(x_{v}\)\(y_{v}\) 表示;
  • 边集 \(E := \left \lbrace (u,v) \mid x_{v}=y_{u},\ y_{v}=(A \times y_{u}+B \times x_{u}) \bmod M \right \rbrace\)

\(R_{\text{in}}(v):=\{u \in V \mid u \rightsquigarrow v\}\)\(R_{\text{out}}(v):=\{u \in V \mid v \rightsquigarrow u\}\)

定义判定函数 \(J : V \to \{\text{LEGAL}, \text{ILLEGAL}\}, \; J(v) :=\begin{cases} \text{LEGAL} & x_{u} \neq 0 \wedge y_{u} \neq 0, \forall u \in R_{\text{out}}(v) \\ \text{ILLEGAL} & x_{u} = 0 \vee y_{u}=0, \exists u \in R_{\text{out}}(v) \end{cases}\)

求出 \(\operatorname{Card}\{v\in V \mid J(v) = \text{LEGAL}\}\)

重要性质

  1. 由判定函数定义可知,如果 \(J(v) = \text{LEGAL}\),那么 \(\forall u \in R_{\text{out}}(v), \text{LEGAL}\);如果 \(J(v) = \text{ILLEGAL}\),那么 \(\forall u \in R_{\text{in}}(v), J(u)=\text{ILLEGAL}\)

  2. \(G\) 是有限图,且所有顶点恰有一条出边,因此从 \(G\) 中任一顶点开始沿出边行进,最终必定会陷入某个环。

求解过程

根据上述两条性质,我们可以采用记忆化方法。首先定义一个二维标记数组 mem 记录 \(J(v)\),全部初始化为 \(\text{UNKNOWN}\);同时定义 ans 记录答案,初始化为 \(0\)。然后遍历每一个顶点 \(v=(x,y)\)

  • 如果 mem[x][y] 不是 \(\text{UNKNOWN}\),说明该点之前已被计算过,直接遍历下一个顶点。
  • 如果 mem[x][y]\(\text{UNKNOWN}\),那么以 \(v\) 为起点开始沿出边行进,将途经的顶点放入 visited 集合中,直至下面三种情况中的一种发生:
    • 遇到两分量中至少一分量为 \(0\) 的顶点:这说明该点是 \(\text{ILLEGAL}\) 的,进而此前途经的顶点均是 \(\text{ILLEGAL}\) 的。将相应顶点标记为 \(\text{ILLEGAL}\),清空 visited,遍历下一个顶点。
    • 遇到曾被计算过的顶点:如果该点是 \(\text{ILLEGAL}\) 的,那么此前途经的顶点均是 \(\text{ILLEGAL}\) 的;如果该点是 \(\text{LEGAL}\) 的,那么此前途经的顶点均是 \(\text{LEGAL}\) 的。打上相应标记,ans加上相应值,清空 visited,遍历下一个顶点。
    • 陷入循环:之后不会遇到新的顶点,所以不可能再遇上 \(\text{ILLEGAL}\) 的点。将途经的所有点标记为 \(\text{LEGAL}\)ans加上相应值,清空 visited,遍历下一个顶点。

实现

有几个优化技巧

  • 可以将顶点的两个分量压成一个值存入 visited 集合中,减小使用 pair 的性能开销。
  • 可以将途经的顶点预先标记为 \(\text{LEGAL}\),这样做的好处是可以将“陷入循环”和“遇到曾被计算过的顶点”合并为一种情况讨论,而且避免了哈希开销,还优化了常数。
#include <bits/stdc++.h>
using namespace std;
typedef unsigned u32;
u32 visited[1000005];
enum STATE { UNKNOWN, LEGAL, ILLEGAL } mem[1000][1000];
u32 M, A, B, ans, cnt;
int main() {
    scanf("%u%u%u", &M, &A, &B);
    for (u32 x = 1; x < M; ++x)
        for (u32 y = 1; y < M; ++y) {
            if (mem[x][y] != UNKNOWN)
                continue;
            u32 xx = x, yy = y, k = cnt;
            while (mem[xx][yy] == UNKNOWN) {
                if (xx == 0) 
                    { mem[xx][yy] = ILLEGAL; break; }
                visited[cnt++] = (xx << 16) + yy;
                mem[xx][yy] = LEGAL;
                int next = (A * yy + B * xx) % M;
                xx = yy, yy = next;
            }
            if (mem[xx][yy] == ILLEGAL)
                for (u32 i = k; i < cnt; ++i)
                    mem[visited[i] >> 16][visited[i] & 0xFFFF] = ILLEGAL;
            else
                ans += cnt - k;
        }
    printf("%u", ans);
    return 0;
}
// 12ms 11672KiB

用递归也能实现,可以节省一些空间。

#include <bits/stdc++.h>
using namespace std;
typedef unsigned u32;
enum STATE { UNKNOWN, LEGAL, ILLEGAL } mem[1000][1000];
u32 M, A, B, ans;
STATE calcState(int x, int y) {
    if (mem[x][y] != UNKNOWN)
        return mem[x][y];
    if (x == 0 || y == 0)
        return mem[x][y] = ILLEGAL;
    mem[x][y] = LEGAL;
    return mem[x][y] = calcState(y, (A * y + B * x) % M);
}
int main() {
    scanf("%u%u%u", &M, &A, &B);
    for (u32 x = 1; x < M; ++x)
        for (u32 y = 1; y < M; ++y)
            if (calcState(x, y) == LEGAL)
                ++ans;
    printf("%u", ans);
    return 0;
}
// 11ms 7888KiB
posted @ 2026-03-31 11:16  SHUddol  阅读(4)  评论(0)    收藏  举报