ICPC World Finals 2019 题解

【A】Azulejos

题意简述:

有两排瓷砖,每排都有 \(n\) 个,每个瓷砖有高度 \(h_i\) 和价格 \(p_i\) 两种属性。

你需要分别重新排列这两排瓷砖,使得同一排的瓷砖满足价格不降,后一排的瓷砖的高度严格大于前一排对应瓷砖的高度。

判断无解或输出一种合法方案。

题解:

首先要满足价格不降,那么先把两排瓷砖分别按照 \(p_i\) 排序。

如果同一排中的两个瓷砖 \(p_i\) 不同,那么顺序已经确定。但是如果 \(p_i\) 相同,就可以任意交换顺序。

也就是说,前后两排中的瓷砖都被划分为了若干区间,每个区间中的瓷砖的 \(p_i\) 都相同,而且可以任意交换顺序。

让我们从两排的第一个区间开始考虑,这时有两种情况。

第一种是前一排的区间长度较短,那么此时前排的区间中的每个瓷砖都需要与后排的区间中的某个瓷砖配对。
那么在保证尽量能构造出解的前提下,最好把更多更优(\(h_i\) 尽量大)的后排的瓷砖留给后续考虑。
这引出一个贪心策略,使用 set 按照 \(h_i\) 为关键字维护瓷砖,
对于前排的每个瓷砖,在后排中寻找一个 \(h_i\) 尽量小,但是比当前瓷砖大的瓷砖与其配对,这样可以保证留给后面的瓷砖尽量优。

第二种是后一排的区间长度较短,相反地,我们对于后排的每个瓷砖寻找前排中 \(h_i\) 尽量大,但是比当前瓷砖小的瓷砖与其配对即可。

这种策略可以保证留给后续考虑的瓷砖尽量优。那么我们只需要对还未配对的瓷砖继续考虑即可。

不难写出代码,时间复杂度为 \(\mathcal{O}(n\log n)\)

#include <cstdio>
#include <algorithm>
#include <set>

const int MN = 500005;

int N, Ans1[MN], Ans2[MN];
struct dat{ int p, h, id; dat() {} dat(int h, int id) : h(h), id(id) {} } a1[MN], a2[MN];
inline bool operator <(dat i, dat j) { return i.h == j.h ? i.id < j.id : i.h < j.h; }

std::set<dat> s1, s2;

int main() {
    scanf("%d", &N);
    for (int i = 1; i <= N; ++i) scanf("%d", &a1[i].p);
    for (int i = 1; i <= N; ++i) scanf("%d", &a1[i].h);
    for (int i = 1; i <= N; ++i) scanf("%d", &a2[i].p);
    for (int i = 1; i <= N; ++i) scanf("%d", &a2[i].h);
    for (int i = 1; i <= N; ++i) a1[i].id = a2[i].id = i;
    std::sort(a1 + 1, a1 + N + 1, [](dat i, dat j) { return i.p < j.p; });
    std::sort(a2 + 1, a2 + N + 1, [](dat i, dat j) { return i.p < j.p; });
    int cnt = 0;
    for (int i = 0; i <= N; ++i) {
        if (a1[i].p != a1[i + 1].p || a2[i].p != a2[i + 1].p) {
            if (s1.size() < s2.size()) {
                for (auto j : s1) {
                    auto it = s2.lower_bound(dat(j.h, 1));
                    if (it != s2.begin()) {
                        --it, ++cnt;
                        Ans1[cnt] = j.id;
                        Ans2[cnt] = it->id;
                        s2.erase(it);
                    }
                    else return puts("impossible"), 0;
                }
                s1.clear();
            }
            else {
                for (auto j : s2) {
                    auto it = s1.upper_bound(dat(j.h, N));
                    if (it != s1.end()) {
                        ++cnt;
                        Ans2[cnt] = j.id;
                        Ans1[cnt] = it->id;
                        s1.erase(it);
                    }
                    else return puts("impossible"), 0;
                }
                s2.clear();
            }
            if (a1[i].p != a1[i + 1].p)
                for (int j = i + 1; j <= N && a1[j].p == a1[i + 1].p; ++j)
                    s1.insert(a1[j]);
            if (a2[i].p != a2[i + 1].p)
                for (int j = i + 1; j <= N && a2[j].p == a2[i + 1].p; ++j)
                    s2.insert(a2[j]);
        }
    }
    for (int i = 1; i <= N; ++i) printf("%d ", Ans1[i]); puts("");
    for (int i = 1; i <= N; ++i) printf("%d ", Ans2[i]); puts("");
    return 0;
}

【B】Beautiful Bridges

题意简述:

给定一个地形剖面图,用 \(n\)\(n\le 10^4\))个点描述,点 \(i\) 和点 \(i+1\) 之间有直线连接的地面。

你需要建一座拱桥,连接点 \(1\) 和点 \(n\),桥面的高度为 \(h\)

你可以在桥中间建若干个柱子,以分配重量,柱子只能恰好建在给出的 \(n\) 个点上(点 \(1\) 和点 \(n\) 上必须有柱子)。

相邻的两根柱子之间需要建一个半圆形的拱,准确地说,拱的半径为两根柱子之间的距离的一半,并且与两根柱子和桥面相切。

拱可以与地面相切,但不能相交。

同时,桥的花费与柱子高度和拱面积有关,具体地,给出两个参数 \(\alpha\)\(\beta\)
则花费为 \(\alpha\sum_{i=1}^{k}h_i+\beta\sum_{i=1}^{k-1}d_i^2\),其中 \(k\) 为柱子数量,\(h_i\) 为第 \(i\) 个柱子的高度,\(d_i\) 为第 \(i\) 个柱子到第 \(i+1\) 个柱子的距离。

问是否可以建出桥,若可以,问最小花费。

题解:

因为 \(n\) 不大,我们猜测正解是 \(\mathcal{O}(n^2)\) 的。

考虑一个简单的 DP:\(\mathrm{f}[i]\) 表示从 \(1\) 号点连接到 \(i\) 号点的最小花费,若不可行则为 \(\infty\)

那么 \(\mathrm{f}[i]\) 可以从任意一个合法的 \(\mathrm{f}[j]\)\(1\le j<i\))转移而来,多出的花费为 \(\alpha(h-y_i)+\beta(x_i-x_j)^2\)

这样总转移数就是 \(\mathcal{O}(n^2)\) 的,但是要判断一个转移是否合法还需要 \(\mathcal{O}(i-j)\) 的时间,总复杂度提高到 \(\mathcal{O}(n^3)\)

这时有两种优化思路,从转移数或判断合法性入手均可,但是观察发现合法转移点无明显规律,所以考虑如何优化判断合法性。

一个 \(j\)\(i\) 的转移是否合法,取决于 \(j\)\(i\) 之间的每个点是否和拱形相交。

注意到 \(i\) 固定,考虑利用这个性质,对每个 \(j\) 判断转移是否合法。

可以发现,对于每个 \(j\) 点,拱形的右端点固定时,此点不会与拱形相交的左端点形成一个区间,而区间本身可以通过解方程得到。

从右往左考虑每个 \(j\) 点,将它们对应的区间并起来便可以得到合法区间,对与每个转移只要判断 \(x_j\) 是否在合法区间内部即可。

据此写出代码,复杂度 \(\mathcal{O}(n^2)\)

#include <cstdio>
#include <cmath>
#include <algorithm>

typedef long long LL;
const LL Inf = 0x3f3f3f3f3f3f3f3f;
const int MN = 10005;

inline LL MySqrt(LL x) {
    LL y = sqrt(x);
    while (y * y > x) --y;
    while ((y + 1) * (y + 1) <= x) ++y;
    return y;
}

int N;
LL H, Alpha, Beta;
LL px[MN], py[MN];
LL f[MN];

int main() {
    scanf("%d%lld%lld%lld", &N, &H, &Alpha, &Beta);
    for (int i = 1; i <= N; ++i) scanf("%lld%lld", &px[i], &py[i]);
    f[1] = Alpha * (H - py[1]);
    for (int i = 2; i <= N; ++i) {
        f[i] = Inf;
        LL Lb = px[i] - 2 * (H - py[i]), Rb = px[i];
        for (int j = i - 1; j >= 1; --j) {
            LL C1 = px[i] - px[j], C2 = H - py[j];
            LL Sqrt = MySqrt(8 * C1 * C2);
            LL MIN = px[i] - 2 * (C1 + C2) - Sqrt;
            LL MAX = px[i] - 2 * (C1 + C2) + Sqrt;
            if (px[i] - px[j] <= 2 * (H - py[j])) MAX = px[j];
            Lb = std::max(Lb, MIN);
            Rb = std::min(Rb, MAX);
            if (Lb <= px[j] && px[j] <= Rb)
                f[i] = std::min(f[i], f[j] + Alpha * (H - py[i]) + Beta * (px[i] - px[j]) * (px[i] - px[j]));
        }
    }
    if (f[N] != Inf) printf("%lld\n", f[N]);
    else puts("impossible");
    return 0;
}

【C】Checks Post Facto

待补


【D】Circular DNA

题意简述:

一个长度为 \(n\) 的环形 DNA 序列,以顺时针顺序给出,其中每个基因有类型和编号两个属性,类型是 s(头)或 e(尾)中的一种,而编号是 \(1\)\(10^6\) 中的整数。
你需要在某个地方切断,按照顺时针顺序拉成链后,最大化能够完美匹配的基因编号个数。

一个基因编号 \(i\) 是能够完美匹配的,当且仅当它在链中对应的所有基因,将 s 看作左括号,e 看作右括号,可以匹配成非空的合法括号序列。

如果有多个位置满足最大化的条件,输出最小的位置。

题解:

考虑每个基因编号,首先左括号个数要等于右括号个数,然后把左括号看作 \(+1\),右括号看作 \(-1\),在环上走一遍形成一个折线图,只有从其最低点开始,才能形成合法括号序。

那么把所有最低点(可能有多个)左右两边的 es 之间的位置都是可行的。

也就是区间加 \(1\),最后查询全局,差分-前缀和即可。

不难写出代码,复杂度 \(\mathcal{O}(n+m)\)\(m\) 为值域范围:

#include <cstdio>
#include <vector>

inline void getStr(int &Typ, int &Idt) {
    char ch; Idt = 0;
    while ((ch = getchar()) != 'e' && ch != 's') ;
    Typ = ch == 's' ? 1 : -1, ch = getchar();
    while (Idt = Idt * 10 + (ch ^ '0'), (ch = getchar()) >= '0' && ch <= '9') ;
}

const int MN = 1000005;
const int M = 1000000;

int N;
int Ty[MN], Id[MN], S[MN], Ans[MN];
std::vector<int> G[MN];

int main() {
    scanf("%d", &N);
    for (int i = 1; i <= N; ++i)
        getStr(Ty[i], Id[i]),
        G[Id[i]].push_back(i);
    for (int id = 1; id <= M; ++id) {
        int Sum = 0, Mn = 0;
        for (auto i : G[id]) {
            Sum += Ty[i];
            S[i] = Sum;
            if (Mn > Sum) Mn = Sum;
        }
        if (Sum) continue;
        for (int i = 0; i < (int)G[id].size(); ++i) {
            if (S[G[id][i]] == Mn) {
                if (i < (int)G[id].size() - 1)
                    ++Ans[G[id][i] + 1], --Ans[G[id][i + 1] + 1];
                else {
                    ++Ans[G[id][i] % N + 1], --Ans[G[id][0] + 1];
                    if (G[id][i] != N) ++Ans[1];
                }
            }
        }
    }
    for (int i = 1; i <= N; ++i) Ans[i] += Ans[i - 1];
    int Ai = 1, Av = Ans[1];
    for (int i = 2; i <= N; ++i)
        if (Ans[i] > Av) Ai = i, Av = Ans[i];
    printf("%d %d\n", Ai, Av);
    return 0;
}

【E】Dead-End Detector

题意简述:

有一张 \(n\) 个点 \(m\) 条边的简单无向图。

如果走过一条边 \(u\to v\) 后,不掉头无法返回到 \(u\),这条边就是对 \(u\) 来说的“死路”。

你需要对每个死路标记路标,但是有的路标是多余的。

如果从一个死路 \(u\to v\) 开始可以不掉头地走到另一个死路 \(u'\to v'\),那么后者 \(u'\to v'\) 就是多余的。

最后问要标记多少路标,输出每对 \(u\to v\),按照 \(u\) 为第一关键字,\(v\) 为第二关键字排序。

题解:

对每个连通块分别考虑。

如果当前连通块无环,也就是一棵树,那么所有方向的边都是死路,但是为了防止多余,应该在所有叶子(度数为 \(1\) 的点)处放置路标。

如果当前连通块有环(这里的环指的是不掉头,并且首尾相接的路径),那么环内的边都不是死路,指向环的边也不是死路,只有远离环的边才是死路,为了防止多余,应该在所有离开环的边处放置路标。

维护一个队列,每次删除度数为 \(1\) 的点,不难区分这些情况。

不难写出代码,复杂度 \(\mathcal{O}(n+m)\)

#include <cstdio>
#include <algorithm>
#include <vector>

const int MN = 500005;

int N, M;
std::vector<int> G[MN];
int d[MN];

int vis[MN], que[MN], l, r;

int Ans, A1[MN], A2[MN];

int main() {
    scanf("%d%d", &N, &M);
    for (int i = 1; i <= M; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G[u].push_back(v);
        G[v].push_back(u);
        ++d[u], ++d[v];
    }
    for (int i = 1; i <= N; ++i)
        std::sort(G[i].begin(), G[i].end());
    l = 1, r = 0;
    for (int i = 1; i <= N; ++i)
        if (d[i] == 1) vis[i] = 1, que[++r] = i;
    while (l <= r) {
        int u = que[l++];
        for (auto v : G[u]) {
            if (!vis[v] && --d[v] == 1)
                vis[v] = 1, que[++r] = v;
        }
    }
    l = 1, r = 0;
    for (int i = 1; i <= N; ++i) vis[i] = 0;
    for (int i = 1; i <= N; ++i)
        if (d[i] > 1) vis[i] = 1, que[++r] = i;
    while (l <= r) {
        int u = que[l++];
        for (auto v : G[u]) {
            if (!vis[v])
                vis[v] = 1, que[++r] = v;
        }
    }
    for (int u = 1; u <= N; ++u) {
        for (auto v : G[u]) {
            if (!vis[u] && G[u].size() == 1)
                A1[++Ans] = u, A2[Ans] = v;
            if (vis[u] && d[u] > 1 && d[v] == 1)
                A1[++Ans] = u, A2[Ans] = v;
        }
    }
    printf("%d\n", Ans);
    for (int i = 1; i <= Ans; ++i)
        printf("%d %d\n", A1[i], A2[i]);
    return 0;
}

【F】 Directing Rainfall

待补


【G】First of Her Name

题意简述:

给定一个 \(n+1\) 个点的 trie 树,根节点表示空串,非根节点表示的字符串为其父节点表示的字符串在前端加上父节点到它的边上的字符。

给定 \(k\) 个询问,每次给出一个字符串,询问 trie 树中 \(n\) 个非根节点表示的字符串中有多少个以询问串作为前缀。

题解:

对询问串的反串建立 AC 自动机,把原 trie 树在自动机上跑,每次跑到的位置在 \(\mathrm{fail}\) 树上的权值加 \(1\)。每个询问的答案就是 \(\mathrm{fail}\) 树上子树权值和。

注意 AC 自动机需要拓展转移,否则不断跳 \(\mathrm{fail}\) 会让复杂度失去保证。

子树权值和可以直接最后统计,但是我使用了树状数组,复杂度 \(\mathcal{O}(n\log n)\)

据此写出代码,复杂度 \(\mathcal{O}(n\Sigma+n\log n)\)

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>

const int MN = 1000005;

int N, Q;
int h[MN], nxt[MN], to[MN], w[MN], tot;
inline void Ins(int x, int y, int z) {
    nxt[++tot] = h[x], to[tot] = y, w[tot] = z, h[x] = tot;
}

char str[MN];
int ch[MN][26], fail[MN], cnt;
inline int Insert(char *str) {
    int now = 0;
    for (int i = 0; str[i]; ++i) {
        int c = str[i] - 'A';
        if (!ch[now][c]) ch[now][c] = ++cnt;
        now = ch[now][c];
    } return now;
}

std::vector<int> G[MN];
int que[MN], l, r;
void BuildAC() {
    fail[0] = -1;
    que[l = r = 1] = 0;
    while (l <= r) {
        int u = que[l++];
        for (int j = 0; j < 26; ++j) {
            if (ch[u][j]) {
                int to = fail[u];
                while (~to && !ch[to][j]) to = fail[to];
                fail[ch[u][j]] = ~to ? ch[to][j] : 0;
                que[++r] = ch[u][j];
            }
            else ch[u][j] = ~fail[u] ? ch[fail[u]][j] : 0;
        }
    }
    for (int i = 1; i <= cnt; ++i) G[fail[i]].push_back(i);
}

int ldf[MN], rdf[MN], dfc;
void DFS0(int u) {
    ldf[u] = ++dfc;
    for (auto v : G[u]) DFS0(v);
    rdf[u] = dfc;
}

int b[MN];
inline void Mdf(int i) { for (; i <= dfc; i += i & -i) ++b[i]; }
inline int Qur(int i) { int a = 0; for (; i; i -= i & -i) a += b[i]; return a; }
void Solve(int u, int now) {
    Mdf(ldf[now]);
    for (int i = h[u]; i; i = nxt[i])
        Solve(to[i], ch[now][w[i]]);
}

int Pos[MN];

int main() {
    scanf("%d%d", &N, &Q);
    for (int i = 1; i <= N; ++i) {
        int f; char ch[3];
        scanf("%s%d", ch, &f);
        Ins(f, i, *ch - 'A');
    }
    for (int i = 1; i <= Q; ++i) {
        scanf("%s", str);
        std::reverse(str, str + strlen(str));
        Pos[i] = Insert(str);
    }
    BuildAC();
    DFS0(0);
    Solve(0, 0);
    for (int i = 1; i <= Q; ++i)
        printf("%d\n", Qur(rdf[Pos[i]]) - Qur(ldf[Pos[i]] - 1));
    return 0;
}

【H】Hobson's Trains

题意简述:

给定一个 \(n\) 个点的图,每个点 \(i\) 都有且仅有一条连向 \(d_i\) 的有向边,即一个基环内向森林。

给定 \(k\),对于每个点,回答沿着有向边走不超过 \(k\) 步能到达这个点的点数。

题解:

找出环,对于环里面的树和环本身分开考虑。

对于树的部分,可以树上差分,DFS 解决。

对于环的部分,考虑每个点对环的贡献,每个点影响到的点是环上的一个连续段,具体位置和长度有关它到达环上的第一个点和到环的距离。
也用差分-前缀和方法统计即可。

虽然思路很简单,细节还是有一些的。

据此写出代码,复杂度 \(\mathcal{O}(n)\)

#include <cstdio>
#include <vector>

const int MN = 500005;

int N, K, d[MN];
int inc[MN], vis[MN], ist[MN], stk[MN], tp;

int cid, Len[MN], Id[MN];
std::vector<int> C[MN], sum[MN];

void Circ(int u) {
    stk[++tp] = u, vis[u] = tp, ist[u] = 1;
    if (!vis[d[u]]) Circ(d[u]);
    else if (ist[d[u]]) {
        ++cid;
        for (int i = vis[d[u]]; i <= tp; ++i)
            C[cid].push_back(stk[i]),
            inc[stk[i]] = cid,
            Id[stk[i]] = (int)C[cid].size() - 1;
        Len[cid] = C[cid].size();
        sum[cid].resize(Len[cid]);
    }
    --tp, ist[u] = 0;
}

std::vector<int> G[MN];
int tc[MN], dep[MN], S[MN];

void DFS(int u) {
    stk[++tp] = u, dep[u] = tp;
    tc[u] = tc[d[u]];
    ++S[u];
    if (tp > K + 1) --S[stk[tp - K - 1]];
    for (auto v : G[u]) DFS(v), S[u] += S[v];
    --tp;
}

int main() {
    scanf("%d%d", &N, &K);
    for (int i = 1; i <= N; ++i) scanf("%d", &d[i]);
    for (int i = 1; i <= N; ++i) if (!vis[i]) Circ(i);
    for (int i = 1; i <= N; ++i) if (!inc[d[i]]) G[d[i]].push_back(i);
    for (int i = 1; i <= N; ++i) if (inc[i]) tc[i] = i;
    for (int i = 1; i <= N; ++i) if (!inc[i] && inc[d[i]]) DFS(i);
    for (int i = 1; i <= N; ++i) {
        if (dep[i] > K) continue;
        if (K - dep[i] + 1 >= Len[inc[tc[i]]])
            ++sum[inc[tc[i]]][0];
        else {
            ++sum[inc[tc[i]]][Id[tc[i]]];
            int ed = (Id[tc[i]] + K - dep[i] + 1) % Len[inc[tc[i]]];
            --sum[inc[tc[i]]][ed];
            if (ed < Id[tc[i]]) ++sum[inc[tc[i]]][0];
        }
    }
    for (int id = 1; id <= cid; ++id) {
        S[C[id][0]] = sum[id][0];
        for (int i = 1; i < Len[id]; ++i)
            sum[id][i] += sum[id][i - 1],
            S[C[id][i]] = sum[id][i];
    }
    for (int i = 1; i <= N; ++i) printf("%d\n", S[i]);
    return 0;
}

【I】Karel the Robot

待补


【J】Miniature Golf

待补


【K】Traffic Blights

待补

posted @ 2019-04-05 15:51 粉兔 阅读(...) 评论(...) 编辑 收藏