T1. 虚张声势

考虑分治

我们设 \(f(l, r)\) 为所有 \([l, r]\) 的子区间对答案的贡献。即,所有满足 \((l \leqslant x \leqslant y \leqslant r)\)\(y-x+1 \leqslant k\) 的子区间 \([x, y]\) 的乘积之和。

\([l, r]\) 的中点为 \(\text{mid}\),那么答案分为三个部分:

  • \([l, \text{mid}]\) 内的答案,即 \(f(l, \text{mid})\)
  • \([\text{mid}+1, r]\) 内的答案,即 \(f(\text{mid}+1, r)\)
  • 跨过 \(\text{mid}\) 的答案。
    我们可以把 \([x, y]\) 拆成 \([x, \text{mid}]\)\([\text{mid}+1, y]\),即一个 \(a[l \cdots \text{mid}]\) 的后缀和一个 \(a[\text{mid}+1 \cdots r]\) 的前缀。
    \(p_i\)\(a[\text{mid}-i+1 \cdots \text{mid}]\) 的乘积,\(q_i\)\(a[\text{mid}+1 \cdots \text{mid}+i]\) 的乘积。那么我们要求的其实是 \(\displaystyle\sum_{i=1}^{k-1}p_i\sum_{j=1}^{k-i} q_j\)
    可以通过对 \(q\) 做前缀和线性求出。

总复杂度为 \(\mathcal{O}(n\log n)\)

代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;
using ll = long long;

int mod;
//const int mod = 998244353;
//const int mod = 1000000007;
struct mint {
    ll x;
    mint(ll x=0):x((x%mod+mod)%mod) {}
    mint operator-() const {
        return mint(-x);
    }
    mint& operator+=(const mint a) {
        if ((x += a.x) >= mod) x -= mod;
        return *this;
    }
    mint& operator-=(const mint a) {
        if ((x += mod-a.x) >= mod) x -= mod;
        return *this;
    }
    mint& operator*=(const mint a) {
        (x *= a.x) %= mod;
        return *this;
    }
    mint operator+(const mint a) const {
        return mint(*this) += a;
    }
    mint operator-(const mint a) const {
        return mint(*this) -= a;
    }
    mint operator*(const mint a) const {
        return mint(*this) *= a;
    }
    mint pow(ll t) const {
        if (!t) return 1;
        mint a = pow(t>>1);
        a *= a;
        if (t&1) a *= *this;
        return a;
    }

    // for prime mod
    mint inv() const {
        return pow(mod-2);
    }
    mint& operator/=(const mint a) {
        return *this *= a.inv();
    }
    mint operator/(const mint a) const {
        return mint(*this) /= a;
    }
};
istream& operator>>(istream& is, mint& a) {
    return is >> a.x;
}
ostream& operator<<(ostream& os, const mint& a) {
    return os << a.x;
}

void solve() {
    int n, k;
    cin >> n >> k >> mod;
    
    vector<int> a(n);
    rep(i, n) cin >> a[i];
    
    mint ans;
    auto f = [&](auto& f, int l, int r) -> void {
        if (l+1 == r) {
            ans += a[l];
            return;
        }
        
        int c = (l+r)/2;
        f(f, l, c);
        f(f, c, r);
        
        int m = r-c;
        vector<mint> q(m);
        q[0] = a[c];
        for (int i = 1; i < m; ++i) {
            q[i] = q[i-1]*a[c+i];
        }
        rep(i, m-1) q[i+1] += q[i];
        
        mint p = 1;
        for (int i = c-1; i >= l; --i) {
            p *= a[i];
            if (i+k-1 >= c) ans += p*q[min(i+k-1-c, m-1)];
        }
    };
    f(f, 0, n);
    
    cout << ans << '\n';
}

int main() {
    int t;
    cin >> t;
    
    while (t--) solve();

    return 0;
}

T2. 简单路径

\(M=|S|\)\(S\) 的长度

回顾一下,经典的两个字符串的最长公共子序列问题可用动态规划在 \(\mathcal{O}(NM)\) 时间内解决。
若输入树退化为一条链,我们所需解决的问题正是此简化版本。因为原题显然是更难的问题变种 \(--\) 并且需要使用动态规划求解。
我们只需设计出状态定义与状态转移即可。

考虑树上一条从 \(u\)\(v\) 的路径。
把树以点 \(1\) 为根,令 \(L\) 表示 \(u\)\(v\) 的最近公共祖先。注意字符串 \(\text{str}(u, v)\) 是先从 \(u\) 向上走到 \(L\),再从 \(L\) 向下走到 \(v\) 得到的。
更一般地,任何路径都可看作一条向上路径与一条向下路径的组合。

注意到:

  • 向上的字符串会与 \(S\) 的某个前缀有公共子序列。
  • 向下的字符串会与 \(S\) 的某个后缀有公共子序列。

这应该会让你想起在经典最长公共子序列问题中我们如何处理两个字符串的前缀(或后缀,取决于实现方式)。
事实上,我们可以用这个来定义动态规划的状态。

\(\text{up}[u][i]\) 表示满足以下条件的最长公共子序列的长度:

  • 所考虑的路径从 \(u\) 的子树内某点开始,向上走直到到达 \(u\)
  • 只与 \(S\) 的前 \(i\) 个字符进行匹配。

为此构造转移并不难。
\(v\)\(u\) 的一个孩子,\(c\) 是连接它们的边上的字符。
那么,我们得到:

  • 如果 \(u\)\(v\) 之间的边没有匹配到任何字符,则长度就是 \(\text{up}[v][i]\),因为所有匹配都必须来自 \(v\) 的子树本身。

  • 否则,该边匹配到某个字符。存在两种情况:

    • \(S_i = c\) 。此时最优为 \(1 + \text{up}[v][i-1]\),通过在 \(v\) 的子树中匹配前 \(i-1\) 个字符得到。
    • \(S_i \neq c\) 。此时最优为 \(up[u][i-1]\),因为 \(S\) 的第 \(i\) 个字符并未被匹配。
  • 于是,\(\text{up}[u][i]\) 即为 \(u\) 的所有子节点 \(v\) 对应值的最大值。

你可能会注意到,这再次与经典最长公共子序列问题的转移方程极为相似。
其时间复杂度为 \(O(NM)\),因为我们共有 \(N(M+1)\) 个状态,且每个状态只需 \(O(1)\) 次转移。

类似地,可以计算 \(\text{down}[u][i]\),它表示从 \(u\) 出发并向下进入其子树的路径与 \(S\) 的最后 \(i\) 个字符匹配时的最长公共子序列长度。

最后,为了得到最终答案,我们需要将向上路径与向下路径进行组合。
具体做法是:固定点 \(u\),考察 \(\text{up}[u][i]\) 的值。
这代表一条与字符串 \(S\)\(i\) 个字符匹配的向上路径,因此最佳策略是将其与某个与 \(S\) 的后 \(M-i\) 个字符匹配的向下路径相结合。

按定义这是 \(\text{down}[u][M-i]\) \(--\) 但有一个问题:\(\text{up}\)\(\text{down}\) 是在处理 \(u\) 的子节点时计算的,我们需要确保所选的向上路径和向下路径来自不同的子节点,否则它们的最近公共祖先就不是 \(u\)

有几种方法可以处理这个问题。最简单的方法是在执行转移时即时更新答案。也就是说,假设你正在处理 \(u\) 的一个新子节点 \(v\)
注意此时,\(\text{up}[u][i]\)\(\text{down}[u][i]\) 仅对迄今为止已考虑的子节点是正确的;特别地,不包括 \(v\)
因此,对于每个 \(i\),查看 \(v\)\(\text{up}/\text{down}\) 值以及它们如何扩展到 \(u\),并用 \(\text{up}[u][M-i]\)\(\text{down}[u][M-i]\) 来更新整体答案。
全部处理完后,再用 \(v\) 更新 \(u\)\(dp\) 值。

代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
#define rep1(i, n) for (int i = 1; i <= (n); ++i)

using namespace std;
using P = pair<int, char>;

inline void chmax(int& a, int b) { if (a < b) a = b; }

void solve() {
    int n, m;
    cin >> n >> m;
    
    vector<vector<P>> g(n);
    rep(i, n-1) {
        int a, b; char c;
        cin >> a >> b >> c;
        --a; --b;
        g[a].emplace_back(b, c);
        g[b].emplace_back(a, c);
    }
    
    string s;
    cin >> s;
    
    vector up(n, vector<int>(m+1));
    vector down(n, vector<int>(m+1));
    
    int ans = 0;
    auto dfs = [&](auto& f, int v, int p=-1) -> void {
        for (auto [u, c] : g[v]) {
            if (u == p) continue;
            f(f, u, v);
            rep1(i, m) {
                int now = up[u][i];
                if (c == s[i-1]) chmax(now, up[u][i-1]+1);
                now += down[v][m-i];
                chmax(ans, now);
                
                now = down[u][i];
                if (c == s[m-i]) chmax(now, down[u][i-1]+1);
                now += up[v][m-i];
                chmax(ans, now);
            } 
            rep1(i, m) {
                int now = up[u][i];
                if (c == s[i-1]) chmax(now, up[u][i-1]+1);
                chmax(up[v][i], now);
                
                now = down[u][i];
                if (c == s[m-i]) chmax(now, down[u][i-1]+1);
                chmax(down[v][i], now);
            } 
            
            rep1(i, m) chmax(up[v][i], up[v][i-1]), chmax(down[v][i], down[v][i-1]);
        }
    };
    dfs(dfs, 0);
    
    cout << ans << '\n';
}

int main() {
    int t;
    cin >> t;
    
    while (t--) solve();
    
    return 0;
}

T3. 函数

原题是PKUSC2022D2T1
可以参考 题解