2023 ICPC Macau

2023 ICPC Macau

ICPC Macau

感觉是一套非常困难的题

A

可以发现选择一个 +1 与去除一个 -1 对行列的效果是一样的,所以我们可以先把所有的 -1 选上。之后改变某个数的选择状态都是对对应的行列和加一。接下来就可以贪心了:一行一行考虑,优先满足缺 +1 多的列:

void solve() {
    int n = 0;
    std::cin >> n;
    std::vector g(n, std::string());
    for (auto &s : g) {
        std::cin >> s;
    }

    std::vector row(n, 0);
    std::vector col(n, std::pair<int, int>());
    for (auto &i : row) {
        std::cin >> i;
    }
    for (int i = 0; i < n; ++i) {
        std::cin >> col[i].first;
        col[i].second = i;
    }

    std::vector ans(n, std::vector(n, 0));
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < n; ++j) {
            if (g[i][j] == '-') {
                row[i] += 1;
                col[j].first += 1;
                ans[i][j] = 1;
            }
        }
    }
    for (int i = 0; i < n; ++i) {
        std::sort(col.begin(), col.end(), std::greater<std::pair<int, int>>());
        for (int j = 0; j < n; ++j) {
            if (row[i] <= 0 || col[j].first <= 0) {
                break;
            }

            row[i] -= 1;
            col[j].first -= 1;
            ans[i][col[j].second] ^= 1;
        }
    }

    bool ok = true;
    for (int i = 0; i < n; ++i) {
        if (row[i] != 0 || col[i].first != 0) {
            std::cout << "NO\n";
            return;
        }
    }

    std::cout << "YES\n";
    for (auto &v : ans) {
        for (auto &i : v) {
            std::cout << i;
        }
        std::cout << '\n';
    }
    return;
}

D

首先需要观察出符合条件的连通块的类型并不多(其实数量也不多)。注意题目给出的条件:最大度不超过 3。这个条件怎么用呢?考虑一个有 \(x\) 个点且满足条件的连通块,这个连通块至少需要 \(2(x - 1)\) 条边,即单独看红边或蓝边都会形成一颗树,而在最大度不超过 3 的条件下,一个有 \(x\) 个点的连通块最多有 \(3x \over 2\),解 \(2(x - 1) \leq {2x \over 2}\)\(x \leq 4\)

接下来就是找符合条件的连通块了。

  • 一个点天然符合条件
  • 两个点符合条件当且仅当两个点之间同时存在两种颜色的边
  • 三个点符合条件,则必然有两个点之间同时存在两种颜色的边,然后找一下这两个点是否同时连一个相同的点(注意处理边的颜色)
  • 四个点符合条件,此时 4 个点的度数都是 3,而且同色边将这四个点连成一条长度为 3 的链。于是我们对一个点,尝试找以它开始的用红色边连起来的长度为 3 的链,若找得到,则记录这四个点,再看蓝色边能不能将这 4 个点连通起来。

后 3 种情况都会将同一个连通块统计两次,所以我们干脆故意将一个点形成的连通块也统计两次,最后求和除 2 就好了:

std::vector<int> adj[2][N + 5];
// i64 ans;

int deg(int idx) {
    return adj[0][idx].size() + adj[1][idx].size();
}

int find(int u, int v) {
    for (auto &to : adj[0][u]) {
        if (to != v) {
            return to;
        }
    }
    return 0;
}

int node[4];

bool vis[N + 5];
// 检查所枚举的连通块通过蓝边是否连通
bool chk(int cnt) {
    for (int i = 0; i < cnt; ++i) {
        vis[node[i]] = false;
    }
    std::queue<int> q;
    q.push(node[0]);
    vis[node[0]] = true;
    while (not q.empty()) {
        int cur = q.front();
        q.pop();

        for (auto &to : adj[1][cur]) {
            if (vis[to]) {
                continue;
            }

            for (int i = 0; i < cnt; ++i) {
                if (to == node[i]) {
                    q.push(to);
                    vis[to] = true;
                    break;
                }
            }
        }
    }

    for (int i = 0; i < cnt; ++i) {
        if (not vis[node[i]]) {
            return false;
        }
    }

    return true;
}

void solve() {
    int n = 0, m = 0;
    std::cin >> n >> m;

    for (int i = 0; i < m; ++i) {
        int u = 0, v = 0, w = 0;
        std::cin >> u >> v >> w;
        adj[w][u].push_back(v);
        adj[w][v].push_back(u);
    }

    std::array<int, 5> cnt{};

    for (int i = 1; i <= n; ++i) {
        // 一个点
        cnt[1] += 2;
        int d = deg(i);
        if (d <= 1) {
            continue;
        }

        // 两个点
        int j = 0;
        bool ok = false;
        for (int &u : adj[0][i]) {
            for (int &v : adj[1][i]) {
                if (u == v) {
                    j = u;
                    ok = true;
                }
            }
        }
        if (ok) {
            cnt[2] += 1;
        }



        // 三个点
        if (deg(j) == 3 && d == 3) {
            // 找第 3 个点
            int k = 0;
            int ic = 0;
            if (adj[ic][i].size() == 1) {
                ic = 1;
            }
            if (adj[ic][i][0] == j) {
                k = adj[ic][i][1];
            }
            else {
                k = adj[ic][i][0];
            }

            bool ok = false;
            for (auto &u : adj[ic ^ 1][j]) {
                if (u == k) {
                    ok = true;
                    break;
                }
            }

            if (ok) {
                cnt[3] += 1;
            }
        }

        // 四个点
        if (adj[0][i].size() == 1) {
            node[0] = i;
            node[1] = adj[0][i][0];
            node[2] = find(node[1], node[0]);
            node[3] = find(node[2], node[1]);
            if (chk(4)) {
                cnt[4] += 1;
            }
        }
    }

    std::cout << (cnt[1] + cnt[2] + cnt[3] + cnt[4]) / 2 << '\n';
    return;
}

H

树形 dp 及其优化

首先要分析合法序列的性质:

一个包含 \(1\)\(n\) 每个数各一次的序列一定是合法的,在这个显然的合法的序列的基础上,考虑如何操作才能让这个序列仍然维持合法或破坏合法性。手玩一下能总结出合法序列满足:对于每个子树,序列中包含的这个子树中的点的个数(可重复)一定要大于等于这个子树的大小。而且序列的合法性与序列顺序无关

于是设计 \(f_{u, i}\) 当有 \(i\) 个点都属于 \(u\) 子树中的点时,这 \(i\) 个点的分法。

为了方便解释,我们定义 \(g_{u, j, i}\) 表示当有 \(i\) 个点都属于 \(u\) 的前 \(j\) 个儿子子树时,这 \(i\) 个点的分法,(这 i 个点不包含 \(u\)

先看朴素的转移:

\[g_{u, j, S} = \sum_{x + y = S \And siz_j \leq y} g_{u, j - 1, x} + f_{v_j, y} \]

\[f_{u, S} = \sum_{x + y = S} {g_{u, m, x} \over y!} \]

第一个转移是从 \(u\) 的儿子转移到 \(u\)\(siz_j \leq y\) 是为了满足序列合法性的必要条件。第二个转移是当儿子都转移完后,加入一些数 \(u\)。除以阶乘的原因是,当我们最后求答案时,我们想直接输出 \(n! \times f_{1, n}\),但是对于相同的数,我们需要除一个阶乘,这个阶乘我们在转移的时候就给他带上。

显然以上转移一次是 \(O(n^2)\) 的,不可接受。

但实际上,第一个转移中的 \(y\) 还有一个上界,即 \(y < siz_{v_j} + depth_{v_j}\),当大于这个值时位置就不够了。结合下届,我们发现每次只用 \(depth_{v_j} \times depth_{u}\) 次转移。由于树是随机的,这个差不多是 \(log_n^2\) 的量级。

然后就是实现了:


i64 fac[N + 5], ifac[N + 5]; // 阶乘和阶乘逆元
std::vector<int> adj[N + 5];
int siz[N + 5], dep[N + 5];
std::vector<i64> f[N + 5];
i64 g[N + 5], h[N + 5]; // 辅助转移

i64 qpow(i64 a, i64 p) {
    i64 res = 1ll;
    while (p) {
        if (p & 1) {
            (res *= a) %= Mod;
        }

        (a *= a) %= Mod;
        p >>= 1;
    }
    return res;
}
i64 inv(i64 a) {
    return qpow(a, Mod - 2);
}
void add(i64 &a, i64 b) {
    a += b;
    if (a >= Mod) {
        a -= Mod;
    }
}

void trans(int x, int y) {
    // 旧区间[l, r]
    // 经过此次转移后得到的新区间 [l + siz[y], r + siz[y]]
    int l = siz[x], r = siz[x] + dep[x];
    for (int i = l; i <= r; ++i) {
        h[i] = g[i];
        g[i] = 0ll;
    }
    for (int i = 1; i <= siz[y]; ++i) {
        g[r + i] = 0;
    }
    for (int i = l; i <= r; ++i) {
        for (int j = siz[y]; i + j <= r + siz[y] && j < siz[y] + dep[y]; ++j) {
            add(g[i + j], h[i] * f[y][j - siz[y]] % Mod);
        }
    }
}

void solve() {
    int n = 0;
    std::cin >> n;
    dep[1] = 1;
    for (int i = 2; i <= n; ++i) {
        int f = 0;
        std::cin >> f;
        adj[f].push_back(i);
        dep[i] = dep[f] + 1;
    }
    for (int x = n; x >= 1; --x) {
        siz[x] = 0;
        for (int i = 0; i <= dep[x]; ++i) {
            g[i] = 0ll;
        }
        g[0] = 1;
        for (auto &y : adj[x]) {
            trans(x, y);
            siz[x] += siz[y];
        }

        // 加入等于 x 的数
        for (int i = siz[x] + dep[x]; i > siz[x]; --i) {
            for (int j = siz[x]; j < i; ++j) {
                add(g[i], g[j] * ifac[i - j] % Mod);
            }
        }
        siz[x] += 1;

        f[x].assign(dep[x], 0ll);
        for (int i = 0; i < dep[x]; ++i) {
            f[x][i] = g[i + siz[x]];
        }
    }

    std::cout << f[1][0] * fac[n] % Mod << '\n';
}
posted @ 2025-10-14 02:27  Young_Cloud  阅读(18)  评论(0)    收藏  举报