The 2024 CCPC National Invitational Contest (Changchun) , The 17th Jilin Provincial Collegiate Programming Contest 题解

B

动态规划

解题思路

一眼 dp,主要就是怎么去转移。

我们定义 \(dp[u][0/1]\) 为在整个 dfs 序中节点 \(u\) 在偶数位/奇数位时,子树 \(u\) 的最大价值。

为了简便描述,我们将 “\(u\) 在偶数位” 记作 “从 0 进入 \(u\)”,将 “\(u\) 在奇数数位” 记作 “从 1 进入 \(u\)”。

显然的我们要额外维护出每颗子树的价值,因为对于大小为奇数的子树,若我们从 0 进入这颗子树,则就要从 1 进入下一颗子树;对于大小为偶数的子树,若我们从 0 进入这颗子树,则就要从 0 进入下一颗子树。

假设 \(u\) 有儿子节点 \(v\), 且 儿子节点的大小是 \(siz(v)\)

考虑特殊情况:当所有 \(siz(v)\) 都是偶数时,有

\[dp[u][bit] = \sum dp[v][1 - bit] \]

其余的情况就是存在 \(siz(v)\) 是奇数的情况,此时对于所有 \(siz(v)\) 为偶数的子树,我们可以任意从 0 或者从 1 进入。对 \(u\) 的贡献是:

\[\sum_{siz(v) \equiv 0 \pmod{2}} \max (dp[v][0], dp[v][1]) \]

对于 \(siz(v)\) 是奇数的子树,应该选择一半从 0 进入,另一半从 1 进入,若有奇数个这样的子树,则剩下的一颗从哪里进入取决于从哪里进入 \(u\)(如果从 0 进入 \(u\) 就从 \(1\) 进入剩下的那颗)。假设我们要对 \(dp[u][bit]\) 进行转移,且根据上面的分析我们知道了要从 1 进入 \(x\) 颗大小为奇数的子树,要从 0 进入 \(y\) 颗大小为偶数的子树,则我们要考虑那些子树从 1 进入和那些子树从 0 进入(因为从 1 进入就取 \(dp[v][1]\),从 0 进入就取 \(dp[v][0]\))。
这就像问你有一个二元组数组 \(A\),从每个二元组里选数,要求有 \(x\) 个二元组选前面一个数, \(y\) 个二元组选后面一个,能够选到的最大的和是多少。按二元组内的差排序然后贪心地求就好了。解决了这个问题我们就解决了大小为奇数地子树的转移,也就解决了问题。

CODE
std::vector<int> g[N + 1], a(N + 1, 0);

std::array<i64, 3> dfs(int cur, int fa) {
    int siz = 1;

    std::vector<std::array<i64, 2>> obb, eve;
    for (auto &to : g[cur]) {
        if (to == fa) {
            continue;
        }

        auto [v0, v1, sizt] = dfs(to, cur);
        if (sizt & 1) {
            obb.push_back({ v0, v1 });
        }
        else {
            eve.push_back({ v0, v1 });
        }
        siz += sizt;
    }

    i64 v0 = 0, v1 = 0;
    if (obb.size() == 0) {
        for (auto &[vv0, vv1] : eve) {
            v0 += vv1, v1 += vv0;
        }
    }
    else {
        // 大小为偶数子树可以任选两个值
        for (auto &[vv0, vv1] : eve) {
            v0 += std::max(vv0, vv1);
        }
        v1 = v0;

        i64 tmp = 0;
        for (auto &[vv0, vv1] : obb) {
            tmp += vv0;
        }
        std::sort(obb.begin(), obb.end(), [](auto &u, auto &v) {
            return u[1] - u[0] > v[1] - v[0];
        });

        for (int i = 0; i + i < obb.size(); i++) {
            tmp += obb[i][1] - obb[i][0];
        }
        
        v1 += tmp, v0 += tmp;
        if (obb.size() & 1) {
            int m = obb.size() >> 1;
            v1 += obb[m][0] - obb[m][1];
        }
    }
    
    return { v0 + a[cur], v1, siz };
}

void solve() {
    int n = 0;
    std::cin >> n;
    for (int i = 1; i <= n; i++) {
        std::cin >> a[i];
        g[i].clear();
    }
    for (int i = 0; i < n - 1; i++) {
        int u = 0, v = 0;
        std::cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }

    std::cout << dfs(1, 0)[1] << '\n';
}

D

计算几何 扫描线

解题思路

题解是说随机选两个点重复 \(k\) 遍就好了,我一开始是这么想的但是概率算错了以为是不可行的。于是自己想的是遍历凸包上的边,将这些边的斜率视为 \(k\) 条线的斜率看合不合法,具体的方法是,对于一个点,根据斜率算截距,所有截距相同的点就认为在一条直线上:

CODE
std::array<int, 2> v(std::array<int, 3> u, std::array<int, 3> v) {
    return { v[0] - u[0], v[1] - u[1] };
}

i64 cross(std::array<int, 2> u, std::array<int, 2> v) {
    // x1y2 - x2y1
    return 1ll * u[0] * v[1] - 1ll * v[0] * u[1];
}

void solve() {
    int n = 0, k = 0;
    std::cin >> n >> k;
    std::vector p(n, std::array<int, 3>{});
    std::unordered_map<double, std::vector<int>> mp;
    int idx = 0;
    for (auto &[x, y, id] : p) {
        std::cin >> x >> y;
        id = idx++;
        mp[x].push_back(id);
    }
    
    auto out = [](std::unordered_map<double, std::vector<int>> &mp) -> bool {
        bool ok = true;
        for (auto &[b, v] : mp) {
            if (v.size() == 1) {
                ok = false;
                break;
            }
        }
        if (ok) {
            for (auto &[b, v] : mp) {
                std::cout << v.size() << ' ';
                for (auto &id : v) {
                    std::cout << id + 1 << ' ';
                }
                std::cout << '\n';
            }
        }
        return ok;
    };

    // 特殊情况,k 条竖线x
    if (mp.size() == k && out(mp)) {
        return;
    }
    
    std::sort(p.begin(), p.end(), [](auto &u, auto &v) {
        if (u[0] == v[0]) {
            return u[1] < v[1];
        }
        else {
            return u[0] < v[0];
        }
    });
    
    // 求凸包
    std::vector stk(2 * n + 5, 0);
    int top = 0;
    for (int i = 0; i < n; i++) {
        while (top >= 2 && cross(v(p[stk[top - 1]], p[i]), v(p[stk[top - 2]], p[stk[top - 1]])) >= 0) {
            top--;
        }
        stk[top++] = i;
    }
    int siz = top;
    for (int i = n - 2; i >= 0; i--) {
        while (top > siz && cross(v(p[stk[top - 1]], p[i]), v(p[stk[top - 2]], p[stk[top - 1]])) >= 0) {
            top--;
        }
        stk[top++] = i;
    }
    
    for (int i = 1; i < top; i++) {
        auto [x1, y1, id1] = p[stk[i - 1]];
        auto [x2, y2, id2] = p[stk[i]];
        
        if (x1 == x2) {
            continue;
        }
        
        double kk = (1.0 * (y1 - y2)) / (1.0 * (x1 - x2));
        mp.clear();
        for (int i = 0; i < n; i++) {
            double b = 1.0 * p[i][1] - 1.0 * p[i][0] * kk;
            mp[b].push_back(p[i][2]);
        }

        if (mp.size() == k && out(mp)) {
            break;
        }
    }

    return;
}

E

观察 二维偏序

解题思路

首先根据题目的式子,我们可以将两点之间有边的条件变为:\((a_i - i) \lesseqgtr (a_j - j) \And (i - b_i) \lesseqgtr (j - b_j)\)。于是问题就变成了二维偏序问题:

CODE
void solve()
{
    int n = 0;
    std::cin >> n;
    std::vector a(n, std::array<int, 2>{});
    for (int i = 0; i < n; i++) {
        std::cin >> a[i][0] >> a[i][1];
        a[i][0] -= i, a[i][1] = i - a[i][1];
    }
    std::sort(a.begin(), a.end(), [](auto &u, auto &v) { return u[0] != v[0] ? u[0] < v[0] : u[1] < v[1]; });
    
    std::stack<int> stk;
    for (auto &[A, B] : a) {
        if (stk.empty() || B < stk.top()) {
            stk.push(B);
        }
        else {
            int mn = stk.top();
            while (not stk.empty() && B >= stk.top()) {
                stk.pop();
            }
            stk.push(mn);
        }
    }

    std::cout << stk.size() << '\n';
}

F

解题思路

对于特定的玩家 \(i\),我们只用看当局面对他最有利时能不能当冠军就可以了,最有利的局面是指,对于他能拿的分最多的场次,让他全部拿完,其他的场就尽量平局分配。但是注意存在一些场,使得 \(i\) 就算拿完了所有的分也仍然超越不了对面,这样的场即使 \(i\) 拿的分是最多的,我们也不去用这个分更新最大值。

于是我们只需要用一个 multiset 来维护所有分数平均分配的场。在确定 \(i\) 时,我们枚举他参与的所有场,并且在 multiset 里删去大于等于当前最大值的场,具体看代码就好了:

CODE
void solve()
{
    int n = 0, m = 0;
    std::cin >> n >> m;
    std::vector py(n + 1, std::vector<int>{});
    std::vector con(m + 1, std::array<int, 5>{});
    std::multiset<int> s;
    for (int i = 1; i <= m; i++) {
        int a = 0, b = 0, x = 0, y = 0, z = 0;
        std::cin >> a >> b >> x >> y >> z;
        con[i] = { a, b, x, y, z };
        s.insert(std::max({ x, y, x + y + z + 1 >> 1 }));

        py[a].push_back(i);
        py[b].push_back(i);
    }

    std::vector<int> ans;
    for (int i = 1; i <= n; i++) {
        int sc = -1;
        std::vector<int> val;
        for (int j = 0; j < py[i].size(); j++) {
            auto &[a, b, x, y, z] = con[py[i][j]];
            // 这一场在 s 中的值是 v
            int v = std::max({ x, y, x + y + z + 1 >> 1 });
            if (i == a) {
                if (x + z > y) {
                    sc = std::max(sc, x + z);
                    val.push_back(v);
                    // 注意不能写 s.erase(v)
                    // 区别是,上面是删去所有值为 v 的,下面是只删一个
                    s.erase(s.find(v));
                }
            }
            else {
                if (y + z > x) {
                    sc = std::max(sc, y + z);
                    val.push_back(v);
                    s.erase(s.find(v));
                }
            }
        }

        if (s.empty() || sc > *s.rbegin()) {
            ans.push_back(i);
        }

        for (auto v : val) {
            s.insert(v);
        }
    }

    std::cout << ans.size() << '\n';
    for (auto &i : ans) {
        std::cout << i << ' ';
    }
    std::cout << '\n';
    return;
}

GIL 略

posted @ 2025-03-10 21:52  Young_Cloud  阅读(222)  评论(0)    收藏  举报