In mathematics you don't understand things, you just get used to them.

标签 dp 下的文章

Desc.

Link.

求出满足如下条件的 $1$ 到 $n$ 的排列数量 (记排列为 $P = (p_1, \dots, p_n))$:

  • $\forall i \in [1, n]$, 有 $\mid p_i-i\mid \geqslant X$.

$1 \leqslant n \leqslant 100$, $1 \leqslant X \leqslant 5$.

Sol.

$X$ 很小, 从这里入手. 注意到 $\geqslant X$ 的状态可能有很多, 但 $< X$ 的状态比较少, 因此计算补集. 考虑容斥, 设 $g_i$ 表示有 $i$ 个数不合法, 剩余数随意放置的方案数, $f_i$ 为恰好 $i$ 个不合法, 剩余数均合法的方案数. 有:

$$ g_i = \sum_{j=i}^n f_j \times \binom ji $$

由二项式反演:

$$ f_i = \sum_{j=i}^n (-1)^{j-i} \times \binom ji \times g_j $$

答案即为 $\displaystyle f_0 = \sum_{i=0}^n (-1)^i \times g_i$. 考虑怎么求 $g_i$. 设 $f_{i, j, s}$ 表示前 $i$ 个位置有 $j$ 个不合法, 其余位置不纳入考虑1, 且第 $i$ 个数的不合法区间当前的被占用情况为 $s$ 的方案数. 转移分当前数是否加入不合法讨论即可, 具体可以看代码.

int main() {
    ios::sync_with_stdio(0);
    cin.tie(nullptr);
    const int MOD = 998244353;
    using mint = modint<MOD>;
    int n, X; cin >> n >> X;
    const int LIM = 1<<(2*X-1), U = LIM-1;
    vector f(n+1, vector(n+1, vector<mint>(LIM)));
    f[0][0][0] = 1;
    for (int i=0;i<n;++i) {
        for (int j=0;j<=i;++j) {
            for (int s=0;s<LIM;++s) {
                f[i+1][j][s>>1] += f[i][j][s];
                for (int p=0;p<2*X-1;++p)
                    if (!((s>>1)&(1<<p)) && i+2-X+p >= 1 && i+2-X+p <= n)
                        f[i+1][j+1][(s>>1)|(1<<p)] += f[i][j][s];
            }
        }
    }
    vector<mint> g(n+1), facs(n+1);
    facs[0] = 1;
    for (int i=1;i<=n;++i)
        facs[i] = facs[i-1]*i;
    for (int i=0;i<=n;++i)
        g[i] = accumulate(allu(f[n][i]), mint(0));
    mint ans = 0;
    for (int i=0;i<=n;++i)
        ans += (i&1?MOD-1:1)*g[i]*facs[n-i];
    cout << ans << "\n";
}

/ 来不及拒绝 /

/ 黑与白的纯洁 /

/ 视而不见 /

/ 假装不见 /

/ 凝望成永别 /

—— Zeno - D!SCOLOЯ ft. 星尘


  1. 为什么其余位置不纳入考虑? 因为随意放置的方案数可以通过乘阶乘计算, 放入 DP 的计算里会很麻烦!

Link.

今天的题解写得有点水, 没什么参考价值.

A. 出关 (laozi)

可以写出朴素的方程式:

$$ f_{i}+A_{s_i} \rightarrowtail f_{i+1} \\ f_i+B+k \times C \rightarrowtail f_{2i-k} $$

$\mathcal O(\frac {n^2}T)$. 其中 $T$ 是某个类似周期的数. 注意字符串是 0-indexed, 而 DP 数组是 1-indexed 的. 对第二个方程换元 $f_i + B + 2i \times C -(2i-k) \times C \rightarrowtail f_{2i-k}$, 令 $v_i = B + 2i \times C + f_i$, 则 $v_i - j \times C \rightarrow f_j$, 那么用线段树维护区间取 min, 单点求值, 标记永久化即可. 求范围需要求解 Z function.

B. 补天 (nvwa)

有点恶心. 首先, 合法的方案一共两种, 取决于第一个放置的颜色. 那么要统计的就是 $(i+j)\bmod 2 = 0$ 的位置数或 $(i+j)\bmod 2 = 1$ 的位置数. 先将所有区间离散化, 使用并查集维护连续段. 并查集的节点上再挂载一个 $cnt_{0/1}$, 分别表示连续段内符合上述两种条件的位置数. 再使用线段树维护区间内... [咕]

C. 理水 (dayu)

发现以前根本没做过正经换根 DP.

先对原图进行边双缩点, 得到一棵树. 如果一个边双连通分量里面存在一条 $1$ 边, 则经过该点的路径均为合法路径. 对于固定的根节点, 设 $f_u$ 表示从 $u$ 出发, $u$ 的子树里的合法节点数. 然后换根即可.

D. 非攻 (mozi)

高一学弟的做法非常简洁, 很有水平! 以下就是他的做法.

考虑固定排列 $P = (p_1, \dots, p_n)$, 连边 $i \rightarrow p_i$, 解决交换次数最小的限制很容易, 注意到每次操作最多让一个环里面的节点变成自环即可. 然后是代价最小的限制, 一个下界是 $\displaystyle \sum m_i(s_i-m_i)$ 其中 $m_i$ 和 $s_i$ 分别表示第 $i$ 个环的最小值和权值和. 手玩发现, 一次交换使一个点变成自环, 剩下的点构成一个新环, 因此这个下界总是取得到.

接下来解决计数. 对一对数 $(i, j)$, 考虑 $i \times j$ 对答案的贡献次数, 这对数对答案产生贡献当且仅当:

  • $i$ 是环中最小值;
  • $i$ 和 $j$ 在同一环里.

进行构造:

  1. 把前 $i-1$ 个数放进图里, 这部分可以随意放, 方案数 $(i-1)!$;
  2. 把 $i$ 放进来, 由于 $i$ 是最小值, 因此 $i$ 只能构成一个新环, 方案数 $1$;
  3. 把 $j$ 放进和 $i$ 同一环里, 因为此时环里仅有 $i$ 一个元素, 方案数 $1$;
  4. 把 $(i, j)$ 和 $(j, n]$ 中的数放进来, 这部分可以随意放, 方案数 $\frac{n!}{(i+1)!}$.

整理以下, 答案就是 $\displaystyle \sum_i \sum_j ij\times \frac{n!(i-1)!}{(i+1)!}$, 预处理阶乘计算即可.


/ 如在黑夜中 被熄灭了星空 /

/ 荒原上看不到尽头 /

/ 只有这一路相随的孤独 /

/ 是我黑暗中唯一的盟友 /

—— Kide - ft. 星尘

Desc.

Link.

定义好串为一个或多个偶回文串拼接的结果. 给出一个字符串, 求其为好串的子串数.

Sol.

考试的时候犯病了, 求 $f_i$ 居然没有想到任何字符串算法, 而去连边写成了图论题... 有点离谱. 🤔

容易想到一个 DP, 设 $f_i$ 表示在 $i$ 结尾的极短偶回文串的长度, $g_i$ 表示在 $i$ 结尾的好串数量, 有 $g$ 的转移:

$$ g_i = g_{i-f_i}+1 $$

若 $f_i$ 不存在则为 $0$. 接下来考虑怎么求 $f_i$.

我们先求出 $rad_i$ 表示以间隔 $i$ 为中心的回文串长度, 这个可以用各种姿势求, 比如二分 & 哈希, PAM, Manacher etc. 然后发现 $f_i$ 可以用一个 $rad_j$ 来更新, 其中 $j < i, j+rad_j \geqslant i$. 我们肯定希望 $i$ 和 $j$ 靠得越近越好. 于是倒着扫描, 用并查集维护连续段, 以 $rad_i$ 更新连续段. 这个有点没说清楚, 具体可以看代码, 代码应该会更好理解.

void solve() {
    int n; string tmp, s; cin >> n >> tmp;
    for (int i=0;i<n;++i) s.pb(tmp[i]), s.pb('|');
    s.pop_back();
    const int m = 2*n-1;
    const ull BASE = 1331;
    vector<ull> pw(m);
    vector hs(2, vector<ull>(m+1));
    pw[0] = 1;
    for (int i=1;i<m;++i) pw[i] = pw[i-1]*BASE;
    for (int i=0;i<m;++i) hs[0][i+1] = hs[0][i]*BASE+s[i]-'a'+1;
    for (int i=m-1;i>=0;--i) hs[1][i] = hs[1][i+1]*BASE+s[i]-'a'+1;
    const auto getHash = [&](int l, int r, int idx) {
        if (idx == 0) return hs[0][r]-hs[0][l]*pw[r-l];
        return hs[1][l]-hs[1][r]*pw[r-l];
    };
    vi rad(m);
    for (int i=0;i<m;++i) {
        int l = 0, r = min(i, m-i-1), res = 0;
        while (l <= r) {
            int mid = (l+r)/2;
            if (getHash(i-mid, i, 0) == getHash(i+1, i+mid+1, 1)) l = mid+1, res = mid;
            else r = mid-1;
        }
        rad[i] = res;
    }
    vi fa(m);
    iota(allu(fa), 0);
    const auto find = [&](int u) {
        while (u != fa[u]) u = fa[u] = fa[fa[u]];
        return u;
    };
    vi f(n);
    for (int i=m-2;i>=0;i-=2) {
        for (int j;(j=find(i+rad[i]))>=i;) {
            if (j%2 == 0) f[j/2] = (j-i+1)/2;
            fa[j] = j-1;
        }
    }
    vll g(n);
    for (int i=0;i<n;++i)
        if (f[i]) {
            if (i/2 >= f[i]) g[i] = g[i-2*f[i]]+1;
            else g[i] = 1;
        }
    cout << accumulate(allu(g), 0ll) << "\n";
}

/ 光与影 是否不该在这一刻相逢 /

/ 而你和我从来谁也不是谁的附庸 /

/ 我放逐自己残缺的魂魄 /

/ 在镜子背面那一国 /

/ 一路上从没有挽留过 /

/ 那些徒劳奔波 却不曾完整的我 /

—— Kide - 零和 Zero-Sum ft. Minus


date: '2023-10-16'
title: 'Solution Set -「LOC 4326」'


Link.

A. 路径 (path)

设 $f_u$ 表示到 $u$ 时凑出的路径节点个数. 以拓扑序转移即可.

B. 异或 (xor)

先差分, 将区间操作转化为双点 (或单点) 操作. 把双点操作抽象成连边, 每个连通块的步数下界是连通块大小减一, 上界是连通块大小.

结论: 取得下界当且仅当连通块异或和为零.

证明:

  • 必要性: 因为是一个连通块, 又只进行了大小减一次操作, 因此每次操作都是双点. 因此连通块异或和不变, 因此连通块异或和是 $0$;
  • 充分性: 当连通块异或和为 $0$, 设最后一个元素为 $x$, 则前面元素的异或和也为 $x$, 每次操作选择一个未被选择过的非最后一个元素的元素, 令其与最后一个元素均异或其, 操作次数为连通块大小减一.

答案即为 $n$ 减去最大的异或和为零的子序列划分数. 状压即可.

C. 距离 (distance)

我甚至没学过点分树...

考虑单点. 建出点分树, 令 $d(x, y)$ 表示原树上 $x, y$ 的距离, $f(p)$ 表示目前已经插入点集且在点分树中 $p$ 的子树里的点中, 距离 $p$ 最近的点. 那么对于一个询问 $a$, 枚举 $a$ 和 $a$ 在点分树上的祖先 $p$, 答案就是 $d(a, p) + \min(f(p))$, 注意我们不需要排除 $a$ 本身在的这棵子树, 因为 $p$ 是 $a$ 和 $x$ 的 LCA 的祖先, 若 $x$ 和 $a$ 在 $p$ 的同一棵子树里, 答案一定更劣.

再考虑点对. 对于一个询问 $(a, b)$, 把路径拆成 $x \rightarrow p \rightarrow a$ 和 $y \rightarrow q \rightarrow b$. 设 $f(p, q)$ 表示与 $f(p)$ 类似的含义, 预处理出来我们就可以回答询问了. 但是 $\mathcal O(n^2)$ 的空间并不能承受, 于是把询问挂载在 $a$ 到根的路径上, 求 $f_p(q)$ 即可.

说着简单, 代码还是有点难度的, 尤其我对点分树完全不熟悉...

D. 花之舞 (dance)

不会.


/ 即使我们分隔 在遥远的两端 /

/ 我会始终凝望 你存在的方向 /

/ 很感谢你曾经赋予给我的最特殊的意义 /

/ 为我描画翅膀 去向蔚蓝之上 /

/ 闪烁最动人的星光 /

—— Evalia - 星辰 ft. 星尘


date: '2023-10-15'
title: 'Solution -「JOISC 2022」コピー & ペースト 3'


Desc.

Link.

你有两个字符串 $S$ 和 $T$, 初始为空. 每次你可以进行以下三种操作中的一种:

  1. 选定小写字母 $c$, 令 $S := S+c$;
  2. 令 $T := S$, 令 $S:=\texttt{""}$;
  3. 令 $S := S+T$.

三种操作分别花费 $A, B, C$, 现要求你将 $S$ 转化为目标串 $X$, 求最小花费.

$1\leqslant |X| \leqslant 2.5\times 10^{3}$, $\Sigma = \{\texttt{a}, \dots, \texttt{z}\}$.

Sol.

今天真调了一天 RE, 人麻了. 😅

考虑区间 DP, 设 $f_{l, r}$ 表示达成 $X[l, r)$ 的最小花费. 操作 1 的转移很简单:

$$ f_{l, r}+A \rightarrow f_{l-1, r} \\ f_{l, r}+A \rightarrow f_{l, r+1} $$

其中 $\rightarrow$ 表示更新, 即取最小值. 可以发现操作 2 比较有性质, 因为每次执行操作 2 都可以「刷新」当前字符串的状态. 设当前这次操作 2 后, $T$ 变成了 $Y$, 那么后面若干次操作中, 我们会使用操作 2&3, 来使 $S$ 变成类似 $\texttt{...}Y\texttt{...}YY\texttt{...}Y\texttt{...}$ 的形状, 其中省略号代表操作 1 插入的字母. 所以如果考虑从 $f_{l, r}$ 转移到 $f_{x, y}$ ($x \leqslant l <r \leqslant y$), 那么有 $X[x, x+r-l)=X_[y-r+l, y) = X[l, r)$. 并且这三个部分无交. 我们可以这样钦定 $X[x, y)$ 的两端一定等于 $X[l, r)$ 的原因是, 上述形式两边多出来的字母可以通过第一种操作转移到.

具体转移的路径是从 $f_{l, r}$ 转移到 $f_{i, r}$, 其中 $X[i, i+r-l) = X[l, r]$ 且 $i+r-l \leqslant l$. 令 $cnt$ 表示 $X[i, r)$ 中最多可选出的子串个数, 使得这些子串都等于 $X[l, r)$ 且互相不交, 则转移的方程可以写为:

$$ f_{l, r} + B + cnt \times C + (r - i - cnt \times (r - l)) \times A \rightarrow f_{i, r} $$

这样的复杂度最坏可以达到 $\mathcal O(n^3)$. 接下来引出一个优化复杂度的重要性质:

  • 若存在两个下标 $i$ 和 $j$, 满足可以从 $f_{l, r}$ 转移到 $f_{i, r}$ 和 $f_{j, r}$, 并且 $i+r-l > j$ (即有交), 那么我们只需要转移到 $j$ 即可!

为什么? 因为 $f_{i, r}$ 可以被 $f_{j, r}$ 以同样的代价更新 (注意从 $f_{j, r}$ 到 $f_{i, r}$ 的更新能且仅能使用操作 1)! 比如 $X = \texttt{ababaccaba}$, 取最右边的 $\texttt{aba}$ 作为当前的模式串, 花费 $\texttt{ba}$ 到 $\texttt{aba}$ 和从 $\texttt{aba}$ 花费 $\texttt{ab}$ 都可以以同样的代价到达 $\texttt{aba}$.

那么我们每次转移就只剩下 $\mathcal O(\frac{l}{r-l})$ 次了, 加起来就是调和级数. 预处理 $p_{l, r}$ 表示 $X[0, l)$ 中最后出现的 $X[l, r)$ 位置即可.

int main() {
    ios::sync_with_stdio(0);
    cin.tie(nullptr);
    const ull BASE = 1331;
    int n; cin >> n;
    vector<ull> pw(n+1), hs(n+1);
    pw[0] = 1;
    for (int i=1;i<=n;++i) pw[i] = pw[i-1]*BASE;
    string s; cin >> s;
    ll A, B, C; cin >> A >> B >> C;
    for (int i=0;i<n;++i) hs[i+1] = hs[i]*BASE+s[i]-'a'+1;
    const auto get_hash = [&](int l, int r) { return hs[r]-hs[l]*pw[r-l]; };
    vector pos(n, vi(n+1)); // pos[l][r] = Latest @i such that i+r-l <= l, s[i, i+r-l) == s[l, r)
    unordered_map<ull, int> latest;
    for (int d=1;d<=n;++d) {
        latest.clear();
        for (int r=d;r<=n;++r) {
            int l = r-d;
            if (l-d >= 0) latest[get_hash(l-d, l)] = l-d;
            auto h = get_hash(l, r);
            if (latest.find(h) != latest.end()) pos[l][r] = latest[h];
            else pos[l][r] = -1;
        }
    }
    const ll INF = 1.01e18;
    vector dp(n, vector(n+1, INF));
    for (int i=0;i<n;++i) dp.at(i).at(i+1) = A;
    for (int d=1;d<n;++d) {
        for (int l=0,r=d;r<=n;++l,++r) {
            if (l > 0) chkmin(dp.at(l-1).at(r), dp.at(l).at(r)+A);
            if (r < n) chkmin(dp.at(l).at(r+1), dp.at(l).at(r)+A);
            int p = pos[l][r], cnt = 2;
            while (p >= 0) {
                chkmin(dp.at(p).at(r), dp.at(l).at(r)+B+cnt*C+(r-p-cnt*(r-l))*A);
                cnt++;
                p = pos[p][p+r-l];
            }
        }
    }
    cout << dp.at(0).at(n) << "\n";
}

/ With a handshake /

/ Or an embrace /

/ Or a kiss on the cheek /

/ Possibly, all three /

—— American Football - The Summer Ends