COCI19-20#6 Trener & COI2020 题解

题外话

给大家讲个笑话,这四题写一起是因为别人送了一场校内模拟赛,结果赛后发现 Subtask 还要自己加,T3 的 SPJ RE 了,T4 的数据包还和 T3 发重了

COCI 2019-2020 #6 Trener

题目里有一个关键条件:前一个字符串必须是后一个字符串的子串。

这意味着,只要后一个字符串确定了,那么前一个字符串至多只有两种可能。

所以我们有一个初步的想法:确定后面的即可缩小前面的范围。于是有一个暴力的想法,就是枚举最后的选谁,然后往前搜索,逐层确定。

这个东西的复杂度我不会分析(想了一下,暴力是指数复杂度,记搜和下面的 DP 似乎是一个做法),考场上实现了一下也不大好写(因为我比较菜)。但是我们能从这个想法里感觉出来两个点:

  1. 往前搜索的过程中每个点只能从长的字符串往短的字符串走,也就是不能形成环
  2. 一个合法的方案就是把搜索过程中经过的点倒序输出

发现了什么?如果我们把搜索图画出来,那么这就是一个反向的 DAG,求答案的过程实际上就是在求路径的条数!

于是本题的做法就呼之欲出了:考虑两个长度差为 \(1\) 的字符串 \(a,b(|a| < |b|)\),如果 \(a\)\(b\) 的子串,那么就在图中加一条 \(a\) 的编号到 \(b\) 的编号的点,最终形成一个 DAG,在 DAG 上跑 DP 求所有起点(长度为 \(1\) 的串代表的点)到每个点的路径条数,把所有终点(长度为 \(n\) 的串代表的点)的答案加起来即可。

复杂度分析?

我也不大会分析这个复杂度,但是有两点是知道的:

  1. 总点数为 \(NK\)
  2. 每个点至多会连进来两条入边,比如 abcde 这个字符串,两个可能的子串是 abcdbcde,从这两个点连边到这个字符串

实现起来跑得飞快,截止到发表时最优解第二名。

const int MAXN = 50 + 10;
const int MAXK = 1500 + 10;
const int MAXNODE = MAXN * MAXK;
const int HA = 1e9 + 7;

std::map<std::string, int> mp[MAXNODE], idx[MAXNODE];

int n, k;

struct E {int v, w;};
int cnt = 0;
std::vector<E> G[MAXNODE];
int ind[MAXNODE];

void addEdge(int u, int v, int w) {
    // DEBUG(u); DEBUG(v); DEBUG(w);
    G[u].push_back({v, w});
    ++ind[v];
}

lli dp[MAXNODE];

void DP() {
    std::queue<int> q;
    // for (int i = 1; i <= cnt; ++i) {
    //     if (!ind[i]) {
    //         dp[i] = 1; q.push(i);
    //     }
    // }
    for (auto ids : idx[1]) {
        dp[ids.second] = 1; q.push(ids.second);
    }
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (auto e : G[u]) {
            int v = e.v; lli w = e.w;
            dp[v] += dp[u] * w % HA;
            dp[v] %= HA;
            --ind[v];
            if (!ind[v]) q.push(v);
        }
    }
}

int main() {
    std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    cin >> n >> k;
    rep (i, 1, n) rep (j, 1, k) {
        std::string ss; cin >> ss;
        mp[i][ss]++;
        idx[i][ss] = 0;
    }
    rep (i, 1, n) {
        for (auto it = idx[i].begin(); it != idx[i].end(); it++) {
            it->second = ++cnt;
            // DEBUG(it->first); DEBUG(cnt);
        }
    }
    for (int len = 2; len <= n; ++len) {
        for (auto strp : mp[len]) {
            // DEBUG(strp.first);
            std::string sub = strp.first.substr(0, len - 1);
            if (mp[len - 1].count(sub)) {
                // DEBUG(sub);
                // DEBUG(mp[len - 1][sub]);
                addEdge(idx[len - 1][sub], idx[len][strp.first], mp[len - 1][sub]);
            }
            if (sub == strp.first.substr(1, len - 1)) continue;
            sub = strp.first.substr(1, len - 1);
            if (mp[len - 1].count(sub)) {
                // DEBUG(sub);
                // DEBUG(mp[len - 1][sub]);
                // G[idx[len - 1][sub]].push_back({idx[len][strp.first], mp[len - 1][sub]});
                addEdge(idx[len - 1][sub], idx[len][strp.first], mp[len - 1][sub]);
            }
        }
    }
    DP();
    lli ans = 0;
    for (auto strp: mp[n]) {
        ans += dp[idx[n][strp.first]] * 1ll * strp.second;
        ans %= HA;
    }
    cout << ans << endl;
    return 0;
}

COI 2020 Pastiri 题解翻译

前言

定义点集为 \(V\)

首先可以观察到这个问题相当于是在大量集合里选一些集合,使得它们并起来是全集。要选的集合记为 \(S_v\) 表示将一个牧羊人放在 \(v\) 点能保护哪些羊。这个问题是 NPC 的,但是本题的树形结构有一些特殊性质可以帮助解题。

Task 1

链的情况很简单,从左到右贪心,如果当前的羊和下一只羊可以被同一个牧羊人管就放一个,否则就只在当前点放牧羊人,下一只羊和再下只羊一起考虑,重复这个过程。

Task 2

在羊很少的情况下我们可以使用 BFS 来计算 \(S_v\),然后考虑状压 DP:设 \(f(mask)\) 表示至少(不是恰好)覆盖 \(mask\) 这个集合所需要的最少牧羊人数。

转移很显然,枚举子集 $submask = $ 某个 \(S_v\) 然后 \(f(mask) \leftarrow f(mask \oplus submask) + 1\) 即可。为了完成转移,我们还要让所有满足 \(submask \subseteq mask\)\(f(submask) \leftarrow f(mask)\)

空间复杂度 \(O(N+2^K)\),时间复杂度 \(O(NK+3^K)\),因为要枚举子集。

Task 3

首先随便找一个点提起来当根。

接下来我们考虑反过来做,刚刚是对每个点处理出覆盖的羊,这次是对所有羊处理出 \(T_v\) 表示对于一个在 \(v\) 点的羊,在哪些点放置牧羊人能覆盖到它,我们称之为它的领地

想法很简单,贪心地放置一个牧羊人使得它能尽可能多地覆盖羊,显然这样的位置存在于一堆 \(T\) 集合的交集,我们再定义两只羊 \(u,v\)朋友当且仅当 \(T_u \bigcap T_v \neq \varnothing\)。于是这个想法可以描述成,每次贪心地针对某个羊放置一个牧羊人,使得这个牧羊人可以管住该羊和其他所有(未被保护的)朋友

有一条性质:

对于某只羊 \(x\),令 \(a(x)\) 表示在 \(x\) 领地里的深度最浅的祖先,那么 \(S_{a(x)}\) 包含了 \(x\)朋友中所有深度不比 \(x\) 更深的。

证明:考虑任意一个不比 \(x\) 更深的 \(x\)朋友 \(y\)。如果 \(y\)\(a(x)\) 的子树里则结论显然(不比 \(x\) 更深保证了它不超出 \(a(x)\) 的覆盖范围),考虑不在子树中的情况。

任意选取一个在 \(x\)\(y\) 领地外的点 \(z\),取 \(x \longleftrightarrow y\) 的路径中点 \(w\),可以证明对于任意羊 \(t\)

\[\mathrm{dist}(w, x) \leq \mathrm{dist}(w, t), \mathrm{dist}(w, y) \leq \mathrm{dist}(w, t) \]

显然 \(w \in S_x \bigcup S_y\),又因为 \(x\) 的深度不小于 \(y\) 所以 \(w\) 即是 \(x\) 的一个祖先,夹在 \(x \longleftrightarrow a(x)\) 的路径之间。因为 \(y\) 不在 \(a(x)\) 子树中,所以有

\[\mathrm{dist}(a(x), y) \leq \mathrm{dist}(w, y) = \mathrm{dist}(w, x) \leq \mathrm{dist}(a(x), x) \]

得证。

所以我们有一个贪心策略:不断选取最深的没有被保护的羊 \(x\),在 \(a(x)\) 上放一个牧羊人,直到所有羊都被保护。

时间复杂度 \(O(N(N+K))\)

满分做法

考虑优化这个想法。

快速求出 \(a(x)\)

\(dep(v)\) 表示节点的深度,\(ds(v)\) 表示节点到最近羊的距离,这个东西可以靠一次 BFS 预处理出来(初始时把所有羊加入队列)。

可以发现另一个性质:

如果 \(v\)\(x\) 的祖先,那么 \(ds(v) \leq dep(x) - dep(v)\),等号成立当且仅当 \(v \in T_x\)

这个东西比较显然,实际上就是 \(T_x\) 的定义。

从这里我们可以看出,\(a(x)\) 就是在 \(x\) 到根的路径上第一个 \(dep(v) + ds(v)\) 最大的点。这个东西一遍 DFS 搞定。

快速维护没被保护的最深的羊

按深度降序排序,问题转化成维护哪些羊被保护了。

然后考虑建一棵完全相同的树,在这个新图上给边定向,边 \((u, v)\) 定向成 \((u \rightarrow v)\) 当且仅当 \(ds(v) = ds(u) + 1\)

这个图有一个性质:

对于任意点 \(v\),一只羊 \(x\) 属于 \(S_v\) 当且仅当从 \(x\)\(v\) 是联通的。

所以我们可以在添加牧羊人的时候从牧羊人的点开始做反图的 DFS,能遍历到的所有点就是 \(S_v\)。注意到已经被访问过的点可以直接忽略,这样每个点至多会被访问一次,时间复杂度 \(O(N)\)

到此,整道题就解决了。时间复杂度 \(O(N + K \log K)\)\(\log\) 来源于排序,但如果你使用桶排则可以做到 \(O(N + K)\)

posted @ 2021-10-29 17:20  Handwer  阅读(143)  评论(0编辑  收藏  举报