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

分类 笔记 下的文章


date: '2023-10-12'
title: 'Solution -「ABC 313F」Flip Machines'


Desc.

Link.

有 $N$ 张卡片,第 $i$ 张卡片正面印着一个数 $A_i$,反面印着一个数 $B_i$。一开始所有数正面朝上。

有 $M$ 种操作,第 $i$ 种操作表示为:

  • 有 $50\%$ 的概率将卡片 $X_i$ 翻转,否则将 $Y_i$ 翻转。

要求你求一个集合 $S\subseteq \mathbb{N} \bigcap [1,m]$,使得进行了集合中所有的编号的操作之后正面朝上的所有数的和的期望最大。输出这个最大值。

Sol.

容易发现有概率被任意正数台机器翻转的卡片对答案的贡献是 $\frac{a_i+b_i}{2}$, 其他卡片的贡献是 $a_i$. 将卡片分为两个集合 $A$ 和 $B$, 其中 $\forall i \in A$ 有 $a_i > b_i$, $\forall i \in B$ 有 $a_i \leqslant b_i$. 把一台机器的操作 $(x_i, y_i)$ 分为三个集合 $P, Q$ 和 $X$, 其中 $P$ 中的边只涉及 $A$ 内的卡片, $Q$ 中的边只涉及 $B$ 中的卡片, $X$ 中的边同时涉及两边的卡片. $P$ 中的边一定不会连, $Q$ 中的边一定会连, 留给我们决策的是 $X$ 中的边.

分类讨论 $|A|$ 和 $|B|$ 的相对大小:

  • $|A| > |B|$:

此时 $|B|<\frac n2$, 采取以下的动态规划, 设 $f_{i, S}$ 表示 $A$ 中的前 $i$ 个点与 $B$ 的点集 $S$ 有连边的最大期望答案. 有转移

$$ f_{i, S}\rightarrow f_{i+1, S\cup\{j\}} $$

  • $|A| \leqslant |B|$:

朴素的应用搜索即可.

代码有点脑溢血, 抄了官方题解的实现.

int main()
{
    ios::sync_with_stdio(0);
    cin.tie(nullptr);
    int n, m; cin >> n >> m;
    vi a(n), b(n), u(m), v(m);
    for (int i=0;i<n;++i) cin >> a[i] >> b[i];
    for (int i=0;i<m;++i) cin >> u[i] >> v[i];
    for (int i=0;i<m;++i) u[i]--, v[i]--;
    for (int i=0;i<m;++i) {
        if (u[i] == v[i] && a[u[i]] < b[u[i]]) swap(a[u[i]], b[u[i]]);
    }
    int ans = 0;
    vi A, B, id(n);
    for (int i=0;i<n;++i) {
        ans += a[i]*2;
        if (a[i] >= b[i]) id[i] = A.size(), A.pb(a[i]-b[i]);
        else id[i] = B.size(), B.pb(b[i]-a[i]);
    }
    int nA = A.size(), nB = B.size();
    vector<ll> ls(nA);
    const int INF = 1e9;
    for (int i=0;i<m;++i) {
        bool xp = a[u[i]] >= b[u[i]],
             yp = a[v[i]] >= b[v[i]];
        if (xp == yp) {
            if (!xp) {
                ans += B[id[u[i]]]+B[id[v[i]]];
                B[id[u[i]]] = B[id[v[i]]] = 0;
            }
               
        } else {
            if (!xp) swap(u[i], v[i]);
            ls[id[u[i]]] |= 1ll<<id[v[i]];
        }
    }
    if (nA <= nB) {
        int opt = 0;
        for (int i=0;i<1<<nA;++i) {
            int cur = 0; ll st = 0;
            for (int j=0;j<nA;++j) if (i&(1<<j)) cur -= A[j], st |= ls[j];
            for (int j=0;j<nB;++j) if (st&(1ll<<j)) cur += B[j];
            chkmax(opt, cur);
        }
        ans += opt;
    } else {
        vector dp(nA+1, vi(1<<nB, -INF));
        dp[0][0] = 0;
        for (int i=0;i<nA;++i) {
            for (int j=0;j<1<<nB;++j) {
                chkmax(dp[i+1][j], dp[i][j]);
                chkmax(dp[i+1][j|ls[i]], dp[i][j]-A[i]);
            }
        }
        int mx = 0;
        for (int j=0;j<1<<nB;++j) {
            int cur = dp[nA][j];
            for (int i=0;i<nB;++i) if (j&(1<<i)) cur += B[i];
            chkmax(mx, cur);
        }
        ans += mx;
    }
    cout << fixed << ans / 2. << "\n";
}

/ 当最后一缕余温消亡 /

/ 空躯壳何处能够安放 /

/ 睁眼醒于温暖的慌 /

/ 一笑而过继续坚强 /

—— 孤寂code - 逆光 ft. 洛天依


date: '2023-10-11'
title: 'Solution -「CF 1610G」AmShZ Wins a Bet'


Desc.

Link.

给出一个括号序列, 在一次操作中, 你可以:

  • 选择一对匹配的括号, 将他们从原序列中删去.

请你规划每次操作, 使得若干次操作后得到的序列字典序最小.

Sol.

结论: 如果删除了 $i, j$, 那么 $(i, j)$ 一定被删除了.

证明考虑对 $s_{i+1}$ 分类讨论即可.

设 $f_i$ 表示从 $i$ 出发的最近合法括号序列的右端点, $g_i$ 表示考虑 $[i, n]$ 的答案. 转移有

$$ g_i = \begin{cases} \mathrm{CONCAT}(s_i, g_{i+1}) \\ \min(\mathrm{CONCAT}(s_i, g_{i+1}), g_{f_i+1}) \end{cases} $$

直接 DP 由于值域的处理需要 $\mathcal O(n)$ 的时间, 所以是 $\mathcal O(n^2)$ 的. 考虑用数据结构处理, 就是要支持如下的问题:

给定一个字符串, 支持

  • 新增版本, 并在旧版本字符串的前面添加一个字符;
  • 比较版本 $i, j$ 的字典序大小.

其中比较操作可以转化为求 LCP (Longest Common Prefix). 求解 LCP 的一个常见方式是二分 + Hash. 然而这样我们需要维护一个字符串的所有前缀 Hash 值, 显然不如直接比较. 我们是否可以不维护所有前缀呢? 当然可以, 我们仅存储 $h_{s, j}$ 表示字符串 $s$ 前 $2^j$ 位的 Hash 值即可. 那么二分就变成了倍增. 具体来说, 维护 $g_i$ 第 $2^j$ 位在原串中的位置和前缀 Hash 值即可.

/*=cirnovsky=*/
#include <bits/stdc++.h>
using namespace std;
using ull = unsigned long long;
const int N = 3e5;
const ull BASE = 1331;
int f[N + 5], g[N + 5], pos[21][N + 5], h[21][N + 5];
ull pw[N + 5];
static void* __tmp = ([]() {
    pw[0] = 1; for (int i=1;i<N+5;++i) pw[i] = pw[i-1]*BASE;
    return pw;
 })();
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(nullptr);
    string s; cin >> s;
    int n = s.length();
    stack<int> stk;
    for (int i=0;i<n;++i) {
        if (s[i] == '(') stk.push(i);
        else if (!stk.empty()) f[stk.top()] = i+1, stk.pop();
    }
    pos[0][n] = g[n] = n;
    for (int i=n-1;i>=0;--i) {
        pos[0][i] = g[i+1], h[0][i] = s[i], g[i] = i;
        for (int j=1;j<=18;++j) pos[j][i] = pos[j-1][pos[j-1][i]];
        for (int j=1;j<=18;++j) h[j][i] = h[j-1][i]+h[j-1][pos[j-1][i]]*pw[1<<(j-1)];
        if (f[i]) {
            int x = i, y = g[f[i]];
            for (int j=18;j>=0;--j) if (h[j][x] == h[j][y]) x = pos[j][x], y = pos[j][y];
            if (y == n || s[x] > s[y]) g[i] = g[f[i]];
        }
    }
    for (int i=g[0];i<n;i=g[i+1]) cout << s[i];
    cout << "\n";
}

/ 将 诗句通通掰碎 捡一双含情眉 /

/ 如何形容她?讽刺来得卑微 歌颂不够尖锐 /

/ 然后看向她 红唇正在颓废 墓志太过宏伟 /

/ 最后告别她 用尽言辞虚伪 发酵一滴眼泪 /

/ 再忘却 她是谁 /

—— 阿良良木键 - 乌云 ft. 乐正绫


date: '2023-10-09'
title: 'Solution -「CF 710F」String Set Queries'


Desc.

Link.

维护一个字符串集合,支持三种操作:

  1. 加字符串;
  2. 删字符串;
  3. 查询集合中的所有字符串在给出的模板串中出现的次数.

强制在线.

Sol.

アメリカ民謡研究会非常🐂🍺. 开始理解 AC 自动机了...

题面容易让人想到根号分治, 然而实际上本题可以进行二进制拆分. 这样我们一共维护了 $\mathcal O(log_2 n)$ 个 AC 自动机, 查询的总复杂均摊应该比较明显是 $\mathcal O(m\log_2n)$, 而修改操作, 每个点只会被合并不超过 $\mathcal O(log_2n)$ 次, 因此也是对的.

const int N = 3e5;
int q, tot, totNode = 1, rt[N + 5], nxt[N + 5][26], cntEnd[N + 5], fail[N + 5], cntIns[N + 5], sum[N + 5];
bool isChild[N + 5][26];
int merge(int u, int v) {
    if (!u || !v) return u+v;
    cntEnd[u] += cntEnd[v];
    for (int i=0;i<26;++i) {
        nxt[u][i] = merge(isChild[u][i]*nxt[u][i], isChild[v][i]*nxt[v][i]);
        if (nxt[u][i]) isChild[u][i] = 1;
    }
    return u;
}
void build(int st) {
    queue<int> que;
    fail[st] = st;
    for (int i=0;i<26;++i) if (isChild[st][i]) fail[nxt[st][i]] = st, que.push(nxt[st][i]);
        else nxt[st][i] = st;
    while (!que.empty()) {
        int u = que.front(); que.pop();
        sum[u] = sum[fail[u]]+cntEnd[u];
        for (int i=0;i<26;++i) if (isChild[u][i]) fail[nxt[u][i]] = nxt[fail[u]][i], que.push(nxt[u][i]);
            else nxt[u][i] = nxt[fail[u]][i];
    }
}
void ins(const string& s, int dlt) {
    cntIns[tot] = 1;
    int u = rt[tot] = totNode++;
    for (int i=0,iend=s.length();i<iend;++i) {
        if (!nxt[u][s[i]-'a']) nxt[u][s[i]-'a'] = totNode++;
        isChild[u][s[i]-'a'] = 1;
        u = nxt[u][s[i]-'a'];
    }
    cntEnd[u] += dlt;
    while (tot >= 1 && cntIns[tot] == cntIns[tot-1]) {
        rt[tot-1] = merge(rt[tot], rt[tot-1]);
        cntIns[tot-1] *= 2;
        tot--;
    }
    build(rt[tot++]);
}
int Ask(int st, const string& s) {
    int res = 0, u = st;
    for (int i=0,iend=s.size();i<iend;++i) u = nxt[u][s[i]-'a'], res += sum[u];
    return res;
}
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(nullptr);
    cin >> q;
    while (q--) {
        int opt; string s; cin >> opt >> s;
        if (opt == 1) ins(s, 1);
        else if (opt == 2) ins(s, -1);
        else {
            int ans = 0;
            for (int i=0;i<tot;++i) ans += Ask(rt[i], s);
            cout << ans << "\n";
            cout.flush();
        }
    }
}


date: '2023-10-09'
title: 'Solution -「BZOJ 4231」回忆树'


Desc.

Link.

树 $T=(V,E)$, 询问 $Q=\{q_i\}$,每次询问:

  • $u~v~s$, 问从 $u$ 到 $v$ 的简单路径上的字符拼接起来字符串中, $s$ 出现了多少次.

Sol.

真毒瘤... 😅

$s$ 对询问 $q_i$ 的贡献可以分为三种:

  • 在 $\lang u,lca(u,v)\rang$ 上出现;
  • 在 $\lang lca(u,v),v\rang$ 上出现;
  • 跨过 $lca(u, v)$.

其中第三种贡献可能的情况不超过 $\mathcal O(2|s|)$ 种, 拉出来跑哈希或者 KMP 即可.

对于剩下两种的情况, 我们以模式串的正反串建立 ACAM, 然后对原树 DFS, 那么从根节点到当前节点的树链上组成的字符串即为文本串.

代码太难打了, 打了一半就跑路了.

using pii = pair<int, int>;
using vvp = vector<vector<pii>>;
const int SZ = 3e5;
typedef struct AhoCorasickAutomaton {
    int tot = 1, nxt[SZ + 5][26], fail[SZ + 5], dfn[SZ + 5], out[SZ + 5], num;
    vvi grp;
    void insert(const bsi& s, int& pos) {
        int u = 0;
        for (int i=0;i<(int)s.length();++i) {
            if (!nxt[u][s[i]-'a']) nxt[u][s[i]-'a'] = tot++;
            u = nxt[u][s[i]-'a'];
        }
        pos = u;
    }
    void build() {
        queue<int> que;
        for (int v : nxt[0]) if (v) que.push(v);
        while (!que.empty()) {
            int u = que.front();
            for (int i=0;i<26;++i) if (nxt[u][i]) fail[nxt[u][i]] = nxt[fail[u]][i], que.push(nxt[u][i]);
                else nxt[u][i] = nxt[fail[u]][i];
        }
        grp = vvi(tot);
        for (int i=1;i<tot;++i) grp[fail[i]].pb(i);
        dfs(0);
    }
    void dfs(int u) {
        dfn[u] = num++;
        for (int v:grp[u]) dfs(v);
        out[u] = num;
    }
    int bit[SZ + 5];
    void upd(int p, int d) {
        for (p=dfn[p]+1;p<=tot;p+=p&-p) bit[p] += d;
    }
    int Ask(int p) {
        int res = 0;
        for (p=dfn[p];p;p-=p&-p) res += bit[p];
        return res;
    }
    int Ask(int l, int r) { return Ask(r)-Ask(l); }
} ACAM;
ACAM acam[2];
const int N = 1e5;
int n, q, fa[23][N + 5], ht, dep[N + 5], to[N + 5];
vvp grp;
void dfs(int u, int Fu) {
    fa[0][u] = Fu; dep[u] = dep[Fu] + 1;
    for (int i=1;i<=ht;++i) fa[i][u] = fa[i-1][fa[i-1][u]];
    for (const auto& [v, ch] : grp[u]) if (v != Fu) dfs(v, u), to[v] = ch-'a';
}
int getLca(int u, int v) {
    if (dep[u] < dep[v]) swap(u, v);
    for (int i=ht;i>=0;--i) if (dep[fa[i][u]] >= dep[v]) u = fa[i][u];
    if (u == v) return u;
    for (int i=ht;i>=0;--i) if (fa[i][u] != fa[i][v]) u = fa[i][u], v = fa[i][v];
    return fa[0][u];
}
struct Query {
    int u, v, lca, pos[2];
};
int jump(int u, int d) {
    for (int i=0;i<=ht;++i) if (d&(1<<i)) u = fa[i][u];
    return u;
}
bsi extract(int u, int v, int lca, int len) {
    bsi res, tmp;
    u = jump(u, dep[u]-dep[lca]-len);
    v = jump(v, dep[v]-dep[lca]-len);
    while (u != lca) res.pb(to[u]), u = fa[0][u];
    while (v != lca) tmp.pb(to[v]), v = fa[0][v];
    reverse(allu(tmp));
    return res.append(tmp);
}
int solve_passing(const bsi& text, const bsi& pat) {
    static const ull BASE = 1331;
    static ull pw[SZ + 5], h[SZ + 5];
    static void* __tmp = ([]() {
        pw[0] = 1;
        for (int i=1;i<SZ+5;++i) pw[i] = pw[i-1]*BASE;
        return pw;
    })();
    auto get_hash = [&](int l, int r) { return h[r]-h[l-1]*pw[r-l+1]; };
    int m = text.length(), k = pat.length();
    memset(h, 0, m*8);
    ull H = 0;
    for (int x:pat) H = H*BASE+x;
    for (int i=0;i<m;++i) h[i+1] = h[i]*BASE+text[i];
    int res = 0;
    for (int i=1;i<=m-k+1;++i) res += get_hash(i, i+k-1) == H;
    return res;
}
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(nullptr);
    cin >> n >> q; ht = ceil(log2(n));
    grp = vvp(n);
    for (int i=1,u,v;i<n;++i) {
        char c; cin >> u >> v >> c; u--; v--;
        grp[u].eb(v, int(c-'a')); grp[v].eb(u, int(c-'a'));
    }
    dfs(0, n);
    vector<Query> queries(q);
    vector<vvp> mnt(2, vvp(n));
    for (auto& [u, v, lca, pos] : queries) {
        static int qid = 0;
        string _let;
        cin >> u >> v >> _let; u--; v--;
        int len = _let.length();
        bsi let;
        for (int i=0;i<len;++i) let.pb(_let[i]-'a');
        lca = getLca(u, v);
        acam[0].insert(let, pos[0]); reverse(allu(let)); acam[1].insert(let, pos[1]);
        if (lca != u && lca != v) {
            bsi s = extract(u, v, lca, len);
            cout << solve_passing(s, let) << "\n";
        } else {
            if (dep[u]-dep[lca] >= len) mnt[0][u].eb(qid, 1), mnt[0][jump(u, dep[u]-dep[lca]-len+1)].eb(qid, -1);
            if (dep[v]-dep[lca] >= len) mnt[1][v].eb(qid, 1), mnt[1][jump(v, dep[v]-dep[lca]-len+1)].eb(qid, -1);
        }
    }
    acam[0].build(), acam[1].build();
    static int ans[N + 5];
    auto dfs2 = [&](auto self, int u, int Fu, vi cur) {
        for (int i:{0,1}) acam[i].upd(cur[i], 1);
        for (const auto& [v, ignore] : grp[u]) {
        }
    };
    dfs2(dfs2, 0, n, {0, 0});
    for (int i=0;i<q;++i) cout << ans[i] << "\n";
}


date: '2023-10-08'
title: 'Solution Set -「LOC 4293」'


Link.

A. 特种训练 (train)

写不来这种 Ad-hoc 题.

B. 红色改造 (modification)

可以参考论文 万成章 - Astral Birth 解题报告

把相同连续段合并起来, 将原 $0/1$ 序列转化成一个由二元组 $(0/1, c_i)$ 组成的长度为 $m$ 的序列, 问题有转化一:

  • 选出尽量长的子序列, 其段数不超过 $k+1$ ($+1$ 的原因是贡献分为三种, 打过朴素的 DP 都知道), 则子序列长度和为答案.

倒过来考虑, 我们需要从中选择一些元素删去, 有以下的 Observation:

  • Observation 1: 我们不会删除相邻的元素.

那么有转化二:

  • 对于每个 $k$, 选择一些价值之和为 $k$ 的元素并将它们删除, 要求元素两两不相邻, 且长度之和最小. 元素的价值就是 $1$ 或 $2$, 第一个和最后一个元素的价值为 $1$, 其余为 $2$.

然后就是一个经典的反悔贪心问题.

D. 车站爆破 (bomb)

居然可以维护操作序列... 还是见识浅薄...