FCOI 2^2^2 总结 & 题解

· · 题解

话在前面:

我没有收到任何广告费。

返回比赛链接

FCOI 2^2^2 总结

统计信息

本场比赛共 91 人报名,44 位有效参加选手(#44 及以前),平均分为 2985.7 分,通过率如下:

题目 A B C D E F G
通过率 97\% 61\% 45\% 15.9\% 9.1\% 13.6\% 6.8\%
预期通过率 95\% 70\% 65\% 20\% 10\% 15\% 5\%
得分率 97\% 61\% 47\% 21.8\% 10.1\% 17.3\% 9.1\%

奖励

奖励细则详见比赛公告。

为了保护选手隐私,我们将会采取以下方式进行颁奖:

在 10/18 00:00 之前,若榜单有出入,请及时联系本人作修改。过期后的更改请求不会受理。

ChatSheep 将会在 10/18 00:00 ~ 10/31 23:59 内按照最终榜单颁奖,请在此期间及时联系 ChatSheep 处理。

致歉与未来

很抱歉这场比赛我们没有考虑原 G 题过高的实现难度和代码难度,为大家带来了非常不好的参赛体验,在此磕头。我们将会保证,我们团队的任何系列比赛都不会再出类似这场 G 无聊且 gaoshai 的题目。

下一次的 FCOI 我也不知道什么时候,这可能要大量的时间来沉淀出那些高质量的题目,希望大家可以谅解(其实是没选进公开赛还不小心造了数据写了题解的题目

如果您不想要做唐题了,您也可以关注我们的团队,我们会预计在明年年初举办我们的第二场公开赛(FCRT R2),欢迎大家支持!

FCOI 2^2^2 题解

A - 对联

因为题解不能只有一个代码,所以我还额外写了这一行文本。

:::info[点击查看代码]

#include <bits/stdc++.h>
using namespace std;

signed main() {
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    string a, b, c;
    cin >> a >> b >> c;
    cout << a + b + c + "," + c + b + a + "," + c + c + b + b + c + b + a + "."
  + c + b + a + "," + a + b + c + "," + b + b + c + c + a + b + c + ".";
    return 0;
}

:::

B - 构造题

考虑证明答案上界,首先我们发现 \lceil\frac{n}{2}\rceil 这个数字一定会出现在这个排列中,而这个数字产生的对答案的贡献至多是 \lfloor\frac{n}{2}\rfloor,所以答案上界就是 \lfloor\frac{n}{2}\rfloor

注意到排列 (\lceil\frac{n}{2}\rceil, n, \lceil\frac{n}{2}\rceil-1, n-1, \lceil\frac{n}{2}\rceil-2, n-2,\dots) 是可以达到这个上界的,因此这道题就做完了。

:::info[代码]

#include <bits/stdc++.h>
using namespace std;
#define int long long

signed main() {
    int n;
    cin >> n;
    for (int i = (n + 1) / 2, j = n; i >= 1; --i, --j) {
        cout << i << " ";
        if (j != (n + 1) / 2) cout << j << " ";
    }
}

:::

C - 无处存储

首先发现 1\sim x 这个路径的点很少,不会超过 n 个,所以本质还是单点查询。

考虑经典差分操作,记录 cnt_x 表示 x 这个子树被加了多少的权值,如果要查询 a 这个点的权值,直接跳父亲跳上去累加 cnt 即可。

然后我们会发现 x 点的 k 级祖先会对答案产生 k 次贡献,这样就可以优化到不需要对于每一个节点查权值了,如果我们用哈希表来实现 cnt,复杂度就是 O(nm)

:::info[代码]

#include <bits/stdc++.h>
using namespace std;
#define int long long

signed main() {
    int n, q;
    unordered_map<int, int> cnt;
    cin >> n >> q;
    while (q--) {
        int x, w, tot = 0;
        cin >> x >> w;
        cnt[x] += w; w = 0; 
        while (x) {
            w += cnt[x] * (++tot);
            x >>= 1;
        }
        cout << w << "\n";
    }
}

:::

可以使用值域线段树(或者 01-Trie)来做到严格 O(nm),但是为了能有足够的签到题就没卡。

D - 关于打表

一道推式子题,通过神秘方法得知选手可能要推 1.5h(?)。

首先考虑这个 \operatorname{SubSum} 究竟在干啥,我们考虑把传入的参数 N 拆成一个一个数位,从高位到低位排列在一起,就像这样:

1 4 7 7 4 4 1 5 1

然后我们考虑每一个数位的贡献是什么,比如说第 i 位,考虑计算所有包含了 i 的区间,i 位对其的贡献。

容易发现向左拓展不会改变 i 这个位在解析成十进制后的权值,而向右拓展可能就形如一堆 111111\dots 的贡献。

所以,第 i 位能贡献的次数就是 i\times \dfrac{(10^{n-i+1}-1)}{9},其中 n 是数位长度。

那这样答案就很明显了,对于最高位,对答案的贡献为 5 \times (10^{n}-1)\times10^{n-1},其中 5\frac{45}{9}45 是这一位能填的所有数位的总和,9 是原本式子带的分母,10^{n-1} 是其他位任意填的方案。

仿照上述把非最高位的推出来,答案就是:

5 \times (10^{n}-1)\times10^{n-1}+\sum_{i=2}^n i\times (10^{n-i+1}-1)\times5\times9\times10^{n-2}

这里给出 Python3 的示例代码:

:::info[示例代码]

n = int(input())
p = 998244353;
ans = 5 * (pow(10, n, p) - 1) % p * pow(10, n - 1, p) % p
for i in range(2, n + 1):
    ans += i * (pow(10, n - i + 1, p) - 1) * 45 * pow(10, n - 2, p)
    ans %= p
print(ans)

:::

接下来我们需要引入一个定理:

p \in P$,$a^b\equiv a^{b\bmod p-1} \pmod p

如果您不知道,您可以理解为他能处理在乘方运算中,指数较大的情况。

然后 5 \times (10^{n}-1)\times10^{n-1} 这一坨就可以直接算了,接下来我们把重点移动到后面的求和式。

5\times9\times10^{n-2}\times\sum_{i=2}^n i\times (10^{n-i+1}-1)

为了简洁这里把 5\times9\times10^{n-2} 去掉。

\sum_{i=2}^n (i\times 10^{n-i+1}-i) (\sum_{i=2}^n i\times 10^{n-i+1})-(2+n)\times(n-1)\times\frac{1}{2}

那前面这一坨怎么计算呢,我们考虑把这个写成递推的样子,即让 S(n) = \displaystyle\sum_{i=2}^n i\times 10^{n-i+1},不难发现以下递推式:

S(n)=S(n-1)\times10 + n\times10

这样就能轻松 50 分了。

:::info[50 分]

n = int(input())
p = 998244353;
ans = 5 * (pow(10, n, p) - 1) % p * pow(10, n - 1, p) % p
ans2 = 0;
for i in range(2, n + 1):
    ans2 = ans2 * 10 + i * 10;
    ans2 %= p;
ans += (ans2 - (2 + n) * (n - 1) // 2) * 45 * pow(10, n - 2, p)
print(ans % p)

:::

现在我们的目标显然是要求出 S(n) 的通项,本人太菜了只会待定系数法,于是让 DeepSeek 来推了一下,最终式子就是 S(n) = \dfrac{1.9\cdot 10^{n-1} - 0.9n - 1}{0.81}(具体怎么推的直接用 DeepSeek V3 生成即可)。

以下是鄙人的推导,仅供参考。

:::info[点击展开] 设 S(n)=a\times 10^{n-1}+ b\times n + c,不难得到:

S(n) = S(n-1)\times10 +n\times10\newline= \Bigl(a\times 10^{n-2}+ b\times (n-1) + c\Bigr)\times10+n\times10\newline=a\times 10^{n-1}+10\times b\times(n-1)+10\times c+n\times10\newline=a\times 10^{n-1}+(10\times b+10)\times n+10\times (c-b)\newline = a\times 10^{n-1}+ b\times n + c

因此:

c=-\frac{100}{81}\newline a+b+c=0\newline a=\frac{190}{81}

综上,S(n) = \dfrac{19}{81} \times 10^n-\dfrac{10}{9}n-\dfrac{100}{81}

:::

于是就可以写出这样的代码:

:::info[70 分]

n = int(input())
p = 998244353;
ans = 5 * (pow(10, n, p) - 1) % p * pow(10, n - 1, p) % p
ans2 = (19 * pow(81, p - 2, p) % p * pow(10, n, p) % p - 10 * pow(9, p - 2, p) * n - 100 * pow(81, p - 2, p)) % p;
ans += (ans2 - (2 + n) * (n - 1) // 2) * 45 * pow(10, n - 2, p)
print(ans % p)

:::

如果想要得到 100 分,只需要把 n 给降幂即可,不过空间限制不足以读入 n,所以就边读入 n 的每一个字符边将 n\bmod pn\bmod (p-1) 的结果求出来即可。

这里给出 C++ 的参考实现。

:::info[代码]

#include <bits/stdc++.h>
using namespace std;
#define int long long

const int p = 998244353;
int n, inv9 = 443664157, inv2 = 499122177;

int qpow(int a, int b) {
    b = (b % (p - 1) + p - 1) % (p - 1);
    int res = 1;
    while (b) {
        if (b & 1) res = res * a % p;
        b >>= 1;
        a = a * a % p;
    }
    return res;
}

signed main() {
    int ve = 0, v = 0;
    char ch = getchar();
    while (ch >= '0' && ch <= '9') {
        ve = (ve * 10 + ch - '0') % (p - 1);
        v = (v * 10 + ch - '0') % (p);
        ch = getchar();
    }
    int ans = 5ll * (qpow(10, ve) - 1) % p * qpow(10, ve - 1) % p;
    int ans2 = (19ll * inv9 % p * inv9 % p * qpow(10, ve) % p - 10 * inv9 * v % p - 100 * inv9 % p * inv9 % p);
    ans += (ans2 - (2 + v) * (v - 1) % p * inv2 % p) * 45 % p * qpow(10, ve - 2) % p;
    cout << (ans % p + p) % p << "\n";
    return 0;
}

:::

E - 听不清楚

看上去很骇人,其实有点板子。

首先要理解 B_i + B_{i+1} = B_i \oplus B_{i+1} 这个条件,不难证明,B_i \operatorname{and} B_{i+1} = 0 是上者的充要条件。

接下来,我们考虑把所有 128 个整数的每一位都抽出来,假如说我们按照顺序将所有 128 个整数的第 i 位抽出来组成了一个 01 序列 A,那么我们需要做的就是把这个 01 序列 A 映射到一个没有两个连续 1 的序列 B,并且还能映射回来。

没有两个连续的 1 自然可以想到斐波那契编码,具体的,我们把这个序列 A 看成一个二进制数字 a,那么我们每一次执行以下操作:

解码是简单的,直接把对应位置的斐波那契数累加即可。

这样做显然是对的,编码后的序列绝对不会出现两个 1,假设存在两个 1,他们是 f_nf_{n-1},但由于 f_n+f_{n-1}=f_{n+1},所以这个数字应该在第 n+1 个斐波那契数被减掉,而不分别在 f_nf_{n-1} 这两个数字被减掉。

不难发现 f_{185}\ge 2^{128},所以编码序列长度一定是 185,可以获得 100 分。

:::info[代码]


#include <bits/stdc++.h>
using namespace std;
#define ul unsigned int
#define lll unsigned __int128 
const int N = 128, M = 185; 
int init = 0;

lll f[M+2];

void get_f() {
    init = 1; f[0] = 1; f[1] = 2;
    for (int i = 2; i <= M; i++) {
        f[i] = f[i - 1] + f[i - 2];
    }
}

vector<ul> encode(const vector<ul>& a) {
    if (!init) get_f();
    vector<lll> num(32);
    for (int j = 0; j < 32; j++) for (int k = 0; k < N; k++)
        if ((a[k] >> j) & 1u) num[j] += ((lll)(1) << k);
    vector<ul> b(M, 0);
    for (int j = 0; j < 32; j++) for (int i = 0; i < M; i++)
        if (num[j] >= f[M - i - 1]) {
            b[i] |= (1u << j);  
            num[j] -= f[M - i - 1];
        }
    return b;
}

vector<ul> decode(const vector<ul>& b) {
    if (!init) get_f();
    vector<lll> num(32);
    for (int j = 0; j < 32; ++j) for (int i = 0; i < M; ++i)
        if ((b[i] >> j) & 1u) num[j] += f[M - i - 1];
    vector<ul> a(N, 0);
    for (int k = 0; k < N; ++k) for (int j = 0; j < 32; j++)
        if ((num[j] >> k) & 1) a[k] |= (1u << j);
    return a;
}

// 省去 main 部分

:::

F - 列车号

很板的题以至于可以一句话题解。

我们考虑这个 p\bmod i,他是 p-\lfloor \dfrac{p}{i} \rfloor i,而看见这个 \dfrac{p}{i},就不难想到整除分块,整除分块后,每一段的转移都是 f_i=a'f_{i-1}+b'i + c' 的形式,直接用矩阵快速幂推到下一段即可,时间复杂度 O(TnB^3\sqrt{p}\log p),其中 B=3,表示矩阵大小。

如果你有耐心推式子求通项可以做到 O(Tn\sqrt{p}\log p),当然出题人很善良并且很懒所以没卡。

:::info[代码]

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int p = 998244353;

struct Mat {
    int m[4][4];
    Mat operator * (const Mat &mt) const {
        Mat d = {};
        for (int k = 1; k <= 3; ++k)
            for (int j = 1; j <= 3; ++j)
                if (mt.m[k][j])
                    for (int i = 1; i <= 3; ++i)
                        if (m[i][k])
                            (d.m[i][j] += m[i][k] * mt.m[k][j] % p) %= p;
        return d;
    }
};

int n, a, b, c, d;

void solve() {
    scanf("%lld%lld%lld%lld%lld", &n, &a, &b, &c, &d);
    int lastf = 1;
    for (int l = 2; l <= n; ++l) {
        int val = d / l, r = min(n, val ? d / val : 1ll << 60);
        val %= p;
        Mat res = {}, tmp = {};
        res.m[1][1] = res.m[2][2] = res.m[3][3] = 1;
        tmp.m[1][1] = a; tmp.m[1][2] = (b - c * val % p) % p;
        tmp.m[2][2] = tmp.m[2][3] = tmp.m[3][3] = 1;
        tmp.m[1][3] = c * (d % p) % p;
        int pows = r - l + 1;
        while (pows) {
            if (pows & 1) res = tmp * res;
            tmp = tmp * tmp;
            pows >>= 1;
        }
        lastf = ((lastf * res.m[1][1]) % p + (l % p * res.m[1][2])  % p + res.m[1][3]) % p;
        l = r;
    }
    printf("%lld\n", (lastf + p) % p);
}

signed main() {
    int tc; scanf("%lld", &tc);
    while (tc--) solve();
    return 0;
}

:::

G - 数位 DP

Sub 2

不妨直接枚举 m 会出现的位置,例如 n=1373377,m=37,列出来的表如下:

位置 1 3 7 3 3 7 7
1 3 7
2 3 7
3 3 7
4 3 7
5 3 7
6 3 7

观察上表,我们分讨出了一下几种情况:

这时候,对于情况 1,我们就注意到,在 m 的前面,一定要是严格小于原串的,后面则随便填写,因为前面都已经由一个小于的位置了,这样的方案数就比较好求解了,就是前面的部分乘一个以 10 为底数,后面剩余长度的指数的数值,例如对于位置 4,答案就是 (137-1)\times 10^{2}

同理可推情况二,前面部分一定不能大于原串,后面部分随便填写,同理得到位置 3 的方案数为 13\times10^{3}

而情况三相当于就是把中间扣掉的这个数字。

特别的,当 m=0 的时候,对于前半部分的计算都需要减一个一,即前面都是零的情况,前面是零自己就也不会出现了。

于是就显然有了 O(\log_{10}{nm+n^2}) 的算法,期望 28 分。

Sub 4

前缀和优化即可,对于情况三无疑就是类似于哈希的思想,记录前缀后缀和 10 的各种幂次即可。

被优化至 O(\log_{10}{nm}),期望 50 分。

Sub 5

瓶颈就在于如何判断当前的位置相对于原串是大还是小的关系了。

其实上,这个问题的本质就是问 m 与每一个长度相同子段的最长公共前缀,因为只要找到了这个前缀,那么前缀的后一个位置就是判断大小的关键。

因此就不难设计二分哈希做法,期望 72 分。

Sub 6

其实算法四所说的就是 Z 函数,直接 O(\log_{10} n + \log_{10} m) 求就完了。

std 常数一坨,因为是远古题所以懒得改了。

:::info[代码]

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define N 3000000

int n, m, pre[N+5], suf[N+5], pw10[N+5], z[(N+5) << 1];
const int p = 998244353;
char s[N+5], t[N+5];

void zfunc(string s, string t) {
    memset(z, 0, sizeof(z));
    string ts = t + s;
    int len = ts.size(), l = 0;
    for (int i = 1; i < len; ++i) {
        if (l + z[l] > i) z[i] = min(z[i - l], l + z[l] - i);
        while (i + z[i] < len && ts[z[i]] == ts[i + z[i]]) z[i]++;
        if (i + z[i] > l + z[l]) l=i;
    }
}

int count_number(string s, string t) {
    zfunc(s, t);
    n = s.size(); m = t.size();
    s = " " + s;
    t = " " + t;
    for (int i = 1; i <= n; ++i) {
        pre[i] = (pre[i - 1] * 10 + (s[i] - '0')) % p;
    }
    suf[n + 1] = 0;
    for (int i = n; i >= 1; --i) {
        suf[i] = (suf[i + 1] + pw10[n - i] * (s[i] - '0')) % p;
    }
    int iszero = (t[1] == '0' && m == 1);
    int ans = 0;
    for (int i = 1 + iszero; i <= n - m + 1; ++i) {
        int l = min(z[i + m - 1], m);
        if (l == m) {
            (ans += ((pre[i - 1] - iszero) * pw10[n - (i + m - 1)]) + suf[i + m] + 1) %= p;
        } else {
            (ans += (pre[i - 1] + (s[i + l] > t[l + 1]) - iszero) * pw10[n - (i + m - 1)]) %= p;
            assert(s[i + l] != t[l + 1]);
        }
    }
    return ans + iszero;
}

signed main() {
    ios::sync_with_stdio(0);
    int tc; cin >> tc;
    pw10[0] = 1;
    for (int i = 1; i <= N; ++i) {
        pw10[i] = pw10[i - 1] * 10 % p;
    }
    while (tc--) {
        string n, m;
        cin >> n >> m;
        cout << count_number(n, m) << "\n";
    }
}

:::

G - Parity(原本被换掉的题)

:::info[题解] 考虑暴力怎么做,我们把这个 N 数字给他求解出来,那么这就是一个经典的数位 dp 题目,我们设计 dp_{i,lim,xor} 表示低 i 位是否触碰上界(lim),目前的异或和是 xor

然后直接把这个塞进矩阵里面,所以就是 32 大小的矩阵,直接用线段树维护,重复 r 次就是矩阵快速幂。

时间复杂度 O((q+n)(\log n+\log r) \times 32^3),需要一定卡常。

具体的,我们发现这个矩阵的左半部分几乎都是零,而对于左上角来说,每行每列都只有一个 1,因此我们既可以优化矩阵乘法部分,使其砍掉 \dfrac{3}{4} 的常数,就可以愉快地通过了。

:::info[代码]

#include <bits/stdc++.h>
using namespace std;
#define int long long

int n, q;
char s[50004];
const int p = 2000003;

struct Mat {
    int mt[32][32];
    inline Mat operator * (const Mat &m) const {
        Mat res = {};
        int f2[16] = {};
        for (int i = 0; i < 16; ++i) {
            for (int j = 0; j < 16; ++j) {
                if (!m.mt[i][j]) continue;
                f2[i] = j;
                break;
            }
        }
        for (int i = 0; i < 16; ++i) {
            for (int j = 0; j < 16; ++j) {
                if (!mt[i][j]) continue;
                res.mt[i][f2[j]] += 1;
                for (int k = 16; k < 32; ++k) {
                    res.mt[i][k] += m.mt[j][k];
                }
                break;
            }
        }

        for (int i = 0; i < 32; ++i) {
            for (int k = 16; k < 32; ++k) {
                for (int j = 16; j < 32; ++j) {
                    res.mt[i][j] += mt[i][k] * m.mt[k][j];
                }
            }
        }
        for (int i = 0; i < 32; ++i) {
            for (int j = 16; j < 32; ++j) {
                res.mt[i][j] %= p;
            }
        }
        return res;
    }
};
const int B = 1000;
Mat ns[10][B+2], I;
inline Mat qpow(Mat a, int b) {
    Mat res = I;
    while (b) {
        if (b & 1) res = a * res;
        b >>= 1;
        a = a * a;
    }
    return res;
}

struct SGT {
    int tag[32769];
    Mat res[32769];
#define ls(p) (p << 1) 
#define rs(p) (p << 1 | 1)
    inline void push_up(int i) {
        res[i] = res[ls(i)] * res[rs(i)];
    }
    inline void build(char *s, int i, int l, int r) {
        if (l == r) {
            res[i] = ns[s[l] - '0'][1];
            return;
        }
        int mid = (l + r) / 2;
        build(s, ls(i), l, mid);
        build(s, rs(i), mid + 1, r);
        push_up(i);
    }
    inline void settag(int i, int l, int r, int v) {
        if (r - l + 1 <= B) res[i] = ns[v][r - l + 1];
        else res[i] = qpow(ns[v][1], r - l + 1);
        tag[i] = v;
    }
    inline void push_down(int i, int l, int r) {
        if (tag[i] == -1) return;
        int mid = (l + r) / 2;
        settag(ls(i), l, mid, tag[i]);
        settag(rs(i), mid + 1, r, tag[i]);
        tag[i] = -1;
    }
    inline void update(int i, int l, int r, int ql, int qr, int v) {
        if (l > qr || r < ql) return;
        if (l >= ql && r <= qr) return settag(i, l, r, v);
        int mid = (l + r) / 2; push_down(i, l, r);
        update(ls(i), l, mid, ql, qr, v);
        update(rs(i), mid + 1, r, ql, qr, v);
        push_up(i);
    }
    inline void query(int i, int l, int r, int ql, int qr, Mat &fi) {
        if (l > qr || r < ql) return;
        if (l >= ql && r <= qr) return fi = fi * res[i], void();
        int mid = (l + r) / 2; push_down(i, l, r);
        query(ls(i), l, mid, ql, qr, fi);
        query(rs(i), mid + 1, r, ql, qr, fi);
    }
} t;

signed main() {
    ios::sync_with_stdio(0); cin.tie(0);
    int tc; cin >> tc >> n >> q >> (s + 1);
    memset(t.tag, 255, sizeof(t.tag));

    for (int i = 0; i < 10; ++i) {
        for (int x = 0; x < 16; ++x) {
            for (int l = 0; l < 2; ++l) {
                for (int j = 0; j <= max(i, l * 9); ++j) {
                    ns[i][1].mt[x + 16 * l][(j ^ x) + 16 * (l || (j != i))]++;
                }
            }
        }
        for (int t = 2; t <= B; ++t) ns[i][t] = ns[i][t - 1] * ns[i][1];
    }
    for (int i = 0; i < 32; ++i) I.mt[i][i] = 1;
    t.build(s, 1, 1, n);
    while (q--) {
        int o, x, y, r;
        cin >> o >> x >> y >> r;
        if (o == 1) {
            Mat fi = I;
            t.query(1, 1, n, x, y, fi);
            fi = qpow(fi, r);
            int res = 0;
            for (int i = 0; i < 16; ++i) {
                res += i * i * (fi.mt[0][i]);
                res += i * i * (fi.mt[0][16 + i]);
            }
            cout << res % p << "\n";
        } else {
            t.update(1, 1, n, x, y, r);
        }
    }
    return 0;
}

:::

题目总结

有强烈的主观意见,仅供参考。

题目 难度^1 算法标签 思考难度 / 实现难度*
A - 对联 红- 模拟 800 / 800
B - 构造题 构造,贪心 1100 / 800
C - 无处存储 黄- 哈希,树 1300 / 900
D - 关于打表 绿 数学 1900 / 1200
E - 听不清楚 构造 2600 / 1700
F - 列车号 矩阵,数论分块 1800 / 2200
G - 数位 dp 蓝+ 字符串,前缀和 2300 / 2200
G2 - Parity 紫- 矩阵,线段树,数位 DP 1600 / 2900

所有题目均被加入了 fishc 竞赛团副团,可以在赛后查看补题。