CF848E Days of Floral Colours 题解

Problem - 848E - Codeforces

首先,由于整个图是对称的,所以我们将其沿直径分为两半,在算一半答案时把每一段的贡献平方再相加即可。(因为对面也有一段相同长度的也要计入贡献)

现在我们的问题转化为了对于一个长为 \(n\) 的环,你可以给一个点连出一个线头(即在原图中连向对面的边)将其余点进行配对,每一对的距离不能超过 \(2\)。求总权值和。

对于环上问题,我们先破环为链,我们若随机指定一个点作为破开的起点则可能出现一段甚至一个配对横跨首尾的情况,不可取。我们选择指定一个线头,枚举线头向后一段的长度,最终再乘上这个贡献即可。

现在我们需要考虑一个序列问题:对于每个 \(n\) ,求一个长为 \(n\) 的序列,在上面留线头或者配对的总权值和。

由于要求出每个 \(n\) 的答案,考虑 DP。

为了方便计算,我们先不管线头,先算出来一个长为 \(n\) 的配对方案数,记为 \(x_n\)

对于一个状态,我们可以向后放一个长度为 \(1\) 的配对或者两个长度为 \(2\) 的配对交叉起来,所以:

\[x_n=x_{n-2}+x_{n-4} \]

好了,现在进入本题的核心。


我们发现我们上面所考虑的序列问题是不严谨的。

因为可能左侧和右侧会伸出去一个长度为 \(2\) 的配对,这一点在破环为链中很重要,因为计算答案的系数不同。

所以我们现在需要计算一个序列向左右都不伸出去配对,向左伸出 \(1\) 个配对和左右都伸出配对的权值和。(向左和向右对称)

我们分别记他们为 \(f_n,g_n,h_n\)

接下来开始暴力推式子环节,推式子的过程本身小学生都可以完成,这里不放一坨坨 \(\LaTeX\) 了。

结论:

\[f_n=(n-1)^2x_{n-1}+\sum_{i=1}^{n-1}f_{n-i}(i-1)^2x_{i-1}+\sum_{i=2}^{n-2}g_{n-i}(i-1)^2x_{i-2} \]

\[g_n=(n-1)^2x_{n-2}+\sum_{i=2}^{n-1}f_{n-i}(i-1)^2x_{i-2}+\sum_{i=3}^{n-2}g_{n-i}(i-1)^2x_{i-3} \]

\[h_n=(n-1)^2x_{n-3}+\sum_{i=2}^{n-2}g_{n-i}(i-1)^2x_{i-2}+\sum_{i=3}^{n-3}h_{n-i}(i-1)^2x_{i-3} \]

很好,现在我们已经做完了

我们发现这个式子直接进行递推是 \(\Theta(n^2)\) 的,显然无法通过此题。

但是我们注意到对于每个 \(i\) 的系数在对应的方程位置里是固定的。

我们使用分治 FFT 进行计算即可。


实现细节:

可以设立一个 update 函数进行计算一个式子对后面的贡献,利用另外三个数组来表示转移方程中的系数,即可优雅地实现这个原本代码实现难度相当高的题目。

详细看代码。

/*
 * @Author: LightningCreeper l_ghtn_ngcr__p_r@outlook.com
 * @Date: 2024-12-30 21:01:12
 * @LastEditors: LightningCreeper l_ghtn_ngcr__p_r@outlook.com
 * @LastEditTime: 2024-12-30 21:46:22
 * @FilePath: /i.省队训练/C.cpp
 * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 */
#include <bits/stdc++.h>
#pragma GCC optimize(3, 2, 1, "Ofast")
using namespace std;

#define int long long
#define endl '\n'
#define debug(x) cerr << #x << " = " << x << endl
#define rep(i, a, b) for (int i = (a); i <= (b); i++)
#define per(i, a, b) for (int i = (a); i >= (b); i--)
#define gn(u, v) for (int v : G.G[u])
#define pb push_back
#define mp make_pair
#define fi first
#define se second
#define sz(x) (int)(x).size()
#define pii pair<int, int>
#define vi vector<int>
#define vpii vector<pii>
#define vvi vector<vi>
#define no cout << "NO" << endl
#define yes cout << "YES" << endl
#define all(x) x.begin(), x.end()
#define rall(x) x.rbegin(), x.rend()
#define tomin(x, y) ((x) = min((x), (y)))
#define tomax(x, y) ((x) = max((x), (y)))
#define ck(mask, i) (((mask) >> (i)) & 1)
#define pq priority_queue
#define FLG (cerr << "Alive!" << endl);

constexpr int MAXN = 2e5 + 5;

constexpr int MOD = 998244353;
constexpr int G = 3;
constexpr int GINV = 332748118;

int qpow(int x, int y) {
    int ans = 1;
    while (y) {
        if (y & 1)
            ans = ans * x % MOD;
        x = x * x % MOD;
        y >>= 1;
    }
    return ans;
}

int rt[MAXN];

void NTT(int x[], int len, bool t) {
    for (int i = 0; i < len; i++)
        if (i < rt[i])
            swap(x[i], x[rt[i]]);
    for (int i = 1; i < len; i *= 2) {
        int n;
        if (t)
            n = G;
        else
            n = GINV;
        n = qpow(n, (MOD - 1) / (i * 2));

        for (int j = 0; j < len; j += (i * 2)) {
            for (int k = 0, t = 1; k < i; k++, t = (t * n) % MOD) {
                int p = x[j + k];
                int q = t * x[j + k + i] % MOD;
                x[j + k] = (p + q);
                x[j + k + i] = p - q + MOD;
                if (x[j + k] >= MOD)
                    x[j + k] -= MOD;
                if (x[j + k + i] >= MOD)
                    x[j + k + i] -= MOD;
            }
        }
    }
}

void Times(int a[], int b[], int len) {
    int l = __lg(len);
    for (int i = 0; i < len; i++)
        rt[i] = 0;
    for (int i = 0; i < len; i++)
        rt[i] = (rt[i >> 1] >> 1) | ((i & 1) << (l - 1));

    NTT(a, len, true);
    NTT(b, len, true);
    for (int i = 0; i < len; i++)
        a[i] = a[i] * b[i] % MOD;
    NTT(a, len, false);
    NTT(b, len, false);
    int inv = qpow(len, MOD - 2);

    for (int i = 0; i < len; i++) {
        a[i] = a[i] * inv % MOD;
        b[i] = b[i] * inv % MOD;
    }
}

int n;
int tmp[MAXN], f0[MAXN], g0[MAXN], f1[MAXN], g1[MAXN], f2[MAXN], g2[MAXN], g[MAXN];

void update(int f[], int g[], int h[], int len) { // f = gh, h on the left
    for (int i = 0; i < len * 2; i++)
        tmp[i] = 0;
    for (int i = 0; i < len / 2; i++)
        tmp[i] = h[i];
    Times(tmp, g, len * 2);

    for (int i = len / 2; i < len; i++)
        (f[i] += tmp[i]) %= MOD;
}

void CDQ(int l, int r) {
    if (l + 1 == r) {
        (f0[l] += g0[l]) %= MOD;
        (f1[l] += g1[l]) %= MOD;
        (f2[l] += g2[l]) %= MOD;
        return;
    }

    int mid = l + r >> 1;
    int len = r - l;
    CDQ(l, mid);

    update(f0 + l, g0, f0 + l, len);
    update(f0 + l, g1, f1 + l, len);
    update(f1 + l, g1, f0 + l, len);
    update(f1 + l, g2, f1 + l, len);
    update(f2 + l, g1, f1 + l, len);
    update(f2 + l, g2, f2 + l, len);

    CDQ(mid, r);
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);

    cin >> n;
    g[0] = g[2] = 1;
	for (int i = 4; i <= n; ++i)
        g[i] = (g[i - 2] + g[i - 4]) % MOD;
	for (int i = 1; i <= n; ++i)
        g0[i] = (i - 1) * (i - 1) * g[i - 1] % MOD;
	for (int i = 2; i <= n; ++i)
        g1[i] = (i - 1) * (i - 1) * g[i - 2] % MOD;
	for (int i = 3; i <= n; ++i)
        g2[i] = (i - 1) * (i - 1) * g[i - 3] % MOD;
    
    int len = (1 << (__lg(n) + 1));
    CDQ(0, len);
    // FLG
    int ans = n * (g0[n] + g2[n]) % MOD;
    for (int i = 2; i < n - 1; i++)
        (ans += i * (g0[i] * f0[n - i] % MOD + 2 * g1[i] * f1[n - i] % MOD + g2[i] * f2[n - i] % MOD) % MOD) %= MOD;
    cout << ans << endl;

    return 0;
}

经验总结:
对于一类看起来很唬人的计数题,先做出一个多项式做法的 DP,再考虑优化,且在转移方程中出现在固定位置系数不变的情况可以使用分治 FFT 优化。以及,在分治 FFT 中将多个求和放在一起算可大大提升常数。

posted @ 2025-01-01 21:00  LightningCreeper  阅读(13)  评论(0编辑  收藏  举报