【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(3)

比赛链接
本文发布于博客园,会跟随补题进度实时更新,若您在其他平台阅读到此文,请前往博客园获取更好的阅读体验。
跳转链接:https://www.cnblogs.com/TianTianChaoFangDe/p/18786128

开题 + 补题情况

很菜的一把,就开了三个签到题,1001 Lucas 定理花了好久才看出来,明明前两周 CF div3 才遇到过,1010 那么明显的曼哈顿距离拆绝对值想了八百年才想出来,明明寒假集训才学过的,真的感觉自己基础太不牢固了,很多很基本的东西用不好。
image

1005 - 修复公路

非常明显的并查集,答案就是连通块个数 \(-1\)

点击查看代码
#include <bits/stdc++.h>
#define inf32 1e9
#define inf64 2e18
#define ls o << 1
#define rs o << 1 | 1

using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned int;

const int N = 3e5 + 9;
int a[N], fa[N];

int root(int x) {
    return (fa[x] == x) ? x : fa[x] = root(fa[x]);
}

void merge(int u, int v) {
    fa[root(u)] = root(fa[v]);
}

void solve()
{
    int n;std::cin >> n;

    for(int i = 1;i <= n;i ++) {
        std::cin >> a[i];
    }

    for(int i = 1;i <= n;i ++) {
        fa[i] = i;
    }

    for(int i = 1;i <= n;i ++) {
        if(i - a[i] > 0) {
            merge(i, i - a[i]);
        }

        if(i + a[i] <= n) {
            merge(i, i + a[i]);
        }
    }

    int sum = 0;
    for(int i = 1;i <= n;i ++) {
        sum += (root(i) == i);
    }

    std::cout << sum - 1 << '\n';
}

1001 - 数列计数

乘积为奇数,那就是每一项都是奇数,运用 Lucas 定理,一个组合数 \(C_n^m\) 为奇数,当且仅当 \(n \& m = m\)
若我们先忽略掉题目给的 \(L_i\) 的限制,则答案为 \(2^{sum_i}\)\(sum_i\)\(a_i\) 中为 \(1\) 的位数,很好理解,也就是对于这些位,\(b_i\) 可以取 \(0\) 也可以取 \(1\),总的组合数。
但是题目有一个 \(L_i\) 的限制,这个如何考虑呢?

  • 如果 \(L_i \geq a_i\),则无需考虑。
  • 如果 \(L_i < a_i\),那么我们从高位到低位考虑。
    • 如果 \(a_i\) 这一位为 \(1\),但是 \(L_i\) 这一位为 \(0\),则我们减掉第 \(i\) 位右边所有 \(a_i\)\(1\) 的位的组合数,相当于就是减掉了这一位取 \(1\) 的情况,因为此时大于了 \(L_i\),不合法;如果 \(a_i\) 这一位。
    • 如果 \(a_i\) 这一位为 \(0\),但是 \(L_i\) 这一位为 \(1\),则我们直接跳出循环,因为后面的答案都是合法的。

对每一个数的答案乘起来就是最终答案了。

点击查看代码(省略了取模类)
const long long M = 998244353;
using Z = ModNum<M>;

const int N = 2e5 + 9;

void solve()
{
    int n;std::cin >> n;
    std::vector<i64> a(n), l(n);

    for(auto &i : a)std::cin >> i;
    for(auto &i : l)std::cin >> i;

    Z ans(1);
    Z t(2);

    for(int i = 0;i < a.size();i ++) {
        int sum = 0;
        for(int j = 0;j < 32;j ++) {
            if(a[i] & (1 << j)) {
                sum ++;
            }
        }
        Z tmp;
        tmp = t.Pow(sum);
        if(l[i] < a[i]) {
            for(int j = 31;j >= 0;j --) {
                if(a[i] & (1 << j))sum --;
                int x = (a[i] >> j & 1);
                int y = (l[i] >> j & 1);
                if(x == 1 && y == 0) {
                    tmp -= t.Pow(sum);
                }
                if(x == 0 && y == 1) {
                    break;
                }
            }
        }
        ans *= tmp;
    }

    std::cout << ans << '\n';
}

1010 - 选择配送

寒假集训的时候才学过的东西,竟然想了这么久,真的不应该。
对曼哈顿距离拆掉绝对值,可以得到 \((x_1, y_1)\)\((x_2, y_2)\) 的曼哈顿距离为:

  • \(\max((x_1 - y_1) - (x_2 - y_2),(x_1 + y_1) - (x_2 + y_2),(x_2 - y_2) - (x_1 - y_1),(x_2 + y_2) - (x_1 + y_1))\)
    \(x_2 - y_2\)\(x_2 + y_2\) 各自维护一下最大最小值,然后对每个待选点查询一下即可。
点击查看代码
#include <bits/stdc++.h>
#define inf32 1e9
#define inf64 2e18
#define ls o << 1
#define rs o << 1 | 1

using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned int;

const int N = 2e5 + 9;

struct Node {
    i64 x, y;
};

void solve()
{
    int n, m;std::cin >> n >> m;
    std::vector<Node> a(n), b(m);
    
    for(auto &[x, y] : a) {
        std::cin >> x >> y;
    }

    for(auto &[x, y] : b) {
        std::cin >> x >> y;
    }

    i64 admx = -inf64, admi = inf64, demx = -inf64, demi = inf64; 
    for(int i = 0;i < n;i ++) {
        admx = std::max(admx, a[i].x + a[i].y);
        admi = std::min(admi, a[i].x + a[i].y);
        demx = std::max(demx, a[i].x - a[i].y);
        demi = std::min(demi, a[i].x - a[i].y);
    }

    i64 ans = inf64;
    for(auto &[x, y] : b) {
        i64 tmp = -inf64;
        tmp = std::max(tmp, x + y - admi);
        tmp = std::max(tmp, admx - (x + y));
        tmp = std::max(tmp, x - y - demi);
        tmp = std::max(tmp, demx - (x - y));
        ans = std::min(ans, tmp);
    }
    std::cout << ans << '\n';
} 

1007 - 宝石商店(补题)

这题有一说一确实不难,赛时想到了正解的,但是因为时间不够没能码出来,感觉榜被开得有点歪。
首先题目给出的那个式子,枚举一下每一位的 \(0,1\) 情况不难发现,不就是异或吗,整这么一大坨来吓唬人。
那么问题就转化为了,对于给定的数,在给定区间里面找另一个数,和这个数构成一个最大异或值。
由于不带修改,我们可以考虑主席树,而对于这些数的二进制情况,可以使用 01trie 树来维护,那么,这个题的解题思路就出来了:把 01trie 树挂在主席树上,查找时贪心地查,尽可能让高位更大。
主要是没怎么写过 01trie 树挂主席树上面,所以最后没有及时码出来。

顺便复习一下 trie 树吧,感觉好久没写了不太熟悉了,trie 树上面每一个结点代表的是一个前缀,从根走到这个结点形成的前缀,因此,只要一个结点有出现次数,就代表这个前缀是存在的,那么我们在查找时,只需要尽可能贪心地往让当前位更大的那个结点走就行了。

(还尝试了一下莫队,但是莫队的复杂度是 \(O(n\sqrt n)\),而这个题的 \(n\) 的和达到了 \(10^6\),因此哪怕是给了 10s 也难以通过)

点击查看代码
#include <bits/stdc++.h>
#define inf32 1e9
#define inf64 2e18
#define ls o << 1
#define rs o << 1 | 1

using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned int;

const int N = 2e5 + 9;

struct trie {
    std::vector<std::array<int, 2>> tr;
    std::vector<int> vis;
    int tot;

    void init(int n) {
        tr = std::vector<std::array<int, 2>>(n * 32);
        vis = std::vector<int>(n * 32);
        tot = 0;
    }

    void clear() {
        for(int i = 0;i <= tot;i ++) {
            vis[i] = 0;
            tr[i][0] = tr[i][1] = 0;
        }
        tot = 0;
    }

    void insert(int &p, int pre, int now, int x) {
        p = ++ tot;
        tr[p] = tr[pre];
        vis[p] = vis[pre];
        vis[p] ++;

        if(now < 0)return;

        int c = (x >> now & 1);
        insert(tr[p][c], tr[pre][c], now - 1, x);
    } 

    int query(int lp, int rp, int x) {
        int res = 0;
        for(int i = 30;i >= 0;i --) {
            int c = (x >> i & 1 ^ 1);
            if(vis[tr[rp][c]] - vis[tr[lp][c]]) {
                res |= (1 << i);
                lp = tr[lp][c];
                rp = tr[rp][c];
            } else {
                lp = tr[lp][c ^ 1];
                rp = tr[rp][c ^ 1];
            }
        }
        return res;
    }
}te;

void solve()
{
    int n, q;std::cin >> n >> q;
    std::vector<int> a(n + 1), rt(n + 1);

    te.clear();

    for(int i = 1;i <= n;i ++) {
        std::cin >> a[i];
    }

    te.insert(rt[0], 0, 30, 0);

    for(int i = 1;i <= n;i ++) {
        te.insert(rt[i], rt[i - 1], 30, a[i]);
    }

    while(q --) {
        int l, r, x;
        std::cin >> l >> r >> x;
        std::cout << te.query(rt[l - 1], rt[r], x) << '\n';
    }
} 

int main()
{
    std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);

    te.init(N);

    int t = 1;std::cin >> t;
    while(t --)solve();

    return 0;
}

1009 - 部落冲突(补题)

听说是前几场 ABC 的原题,有点后悔没打 ABC 了。
对于这个题,若没有第三个操作,则很明显就是一个并查集,用并查集来维护一下各个部落的归属情况,查询的时候查一下野人对应的部落所在的连通块即可。
但是操作 3 让这个变得难了一点,操作 3 我们需要互换两个部落,若暴力求解显然会 TLE。
我们看一下上面没有操作 3 的思路,对于合并操作,我们并没有修改野人对应的部落,而是修改部落相互之间的归属关系,查询时直接查询即可。
那么类似的,我们也想一下能不能维护。
我们把这个问题转化一下,初始的时候有 \(n\) 个部落,放在 \(n\) 个位置中,那么对于操作 \(3\) 的交换,就可以翻译为“交换位置 \(a\) 和位置 \(b\) 的部落”,而对于最后的查询,我们也是查询一个野人所在部落的当前位置,这样就无需修改野人的位置信息了,而对于题目中的输入,我们输入的都是位置,而不是部落,因此此做法成立!
我们用 \(f_i\) 表示位置 \(i\) 的部落,用 \(g_i\) 表示部落 \(i\) 所在的位置,用 \(h_i\) 表示野人 \(i\) 所在的部落,那么就有以下解决方法:

  • 操作 \(1\): 获取一下位置 \(a\) 和位置 \(b\) 当前的部落 \(f_a\)\(f_b\),使用并查集进行合并。
  • 操作 \(2\): 获取一下位置 \(b\) 对应的部落 \(f_b\),修改一下野人 \(a\) 当前的部落。
  • 操作 \(3\): 获取一下位置 \(a\) 和位置 \(b\) 当前的部落 \(f_a\)\(f_b\),同时交换 \(f_a,f_b\)\(g_{f_a},g_{f_b}\),表示交换位置 \(a\) 和 位置 \(b\) 对应的部落,同时交换部落 \(f_a\) 和部落 \(f_b\) 对应的位置。
  • 操作 \(4\): 由于我们对于野人存储的是部落信息 \(root_{h_a}\),但答案需要的是位置信息,因此应该输出 \(g_{root_{h_a}}\)

很好的一个题吧,算是把离散数学的函数应用得淋漓尽致了。

点击查看代码
#include <bits/stdc++.h>
#define inf32 1e9
#define inf64 2e18
#define ls o << 1
#define rs o << 1 | 1

using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned int;

const int N = 2e5 + 9;

struct BCJ {
    std::vector<int> fa;

    BCJ(int n) {
        fa = std::vector<int>(n + 1);
        std::iota(fa.begin() + 1, fa.begin() + n + 1, 1);
    }

    int root(int x) {
        return (fa[x] == x) ? x : (fa[x] = root(fa[x]));
    }

    void merge(int u, int v) {
        fa[root(u)] = root(fa[v]);
    }
};

void solve()
{
    int n, q;std::cin >> n >> q;

    std::vector<int> f(n + 1), g(n + 1), h(n + 1);

    std::iota(f.begin() + 1, f.begin() + n + 1, 1);
    std::iota(g.begin() + 1, g.begin() + n + 1, 1);
    std::iota(h.begin() + 1, h.begin() + n + 1, 1);
    
    BCJ bcj(n);

    while(q --) {
        int op;std::cin >> op;

        if(op == 1) {
            int a, b;std::cin >> a >> b;
            bcj.merge(f[b], f[a]);
        } else if(op == 2) {
            int a, b;std::cin >> a >> b;
            h[a] = f[b];
        } else if(op == 3) {
            int a, b;std::cin >> a >> b;
            a = f[a];
            b = f[b];
            std::swap(f[g[a]], f[g[b]]);
            std::swap(g[a], g[b]);
        } else {
            int a;std::cin >> a;
            std::cout << g[bcj.root(h[a])] << '\n';
        }
    }
}

1003 - 拼尽全力(补题)

充分体现出数据范围和时间复杂度分析的一道题。
这个题,首先 \(nm \leq 3 \times 10 ^ 6\),因此可以考虑 \(O(nm)\) 的做法或者再乘一个 \(\log\)
思考一下,什么样的公司可以通过面试,很简单,题目已经说过了,所有能力都大于等于公司要求就能通过。
那么什么样的要求可以通过,大于等于公司要求的能力可以通过。
那么,要通过公司要求,需要做什么?需要让我们的能力值尽可能的大。
嗯,上面三句话看似都是题目直接告诉我们的废话(没错我赛时也是这么觉得的),但是却是我们此题贪心的关键!
我们把上面三句话倒过来看一遍呢?
我们要让能力值尽可能大,那就要在达不到的公司之前尽可能多地提升。
我们要想尽可能多地提升,我们就要尽可能多地通过一些公司来提升能力。
我们要想尽可能多地通过一些公司来提升能力,那就要尽可能把能通过的都通过了。
能通过的,一定是能力要求越小的,因为越容易通过。
那么,答案就出来了。
对于这 \(m\) 种能力,我们分别放进 \(m\) 个小根堆中,然后对于每一个堆,只要堆顶能打过,就打,并对这个公司的能力总数 \(-1\),表示通过了这一项能力,当一个公司所有能力都通过后,我们就可以用这个公司来提升自己,提升后再像上述进行贪心,直到公司全通过,那答案就是 YES,如果某一轮没有新通过的公司,那答案就是 NO
时间复杂度 \(O(nm\log m)\),因为最坏的情况就是每一轮只通过一家公司,那一共就是 \(n\) 轮。

复盘一下,这个题,赛时过于关注题目所描述的“顺序”了,从而导致一直想着有没有一种排序方式,可以直接全部满足,而忽略了这一题的贪心性质,即能打则打。

点击查看代码
#include <bits/stdc++.h>
#define inf32 1e9
#define inf64 2e18
#define ls o << 1
#define rs o << 1 | 1

using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned int;

const int N = 2e5 + 9;

struct Node {
    int ix;
    i64 val;
    bool operator < (const Node &v) const {
        return val > v.val;
    }
};

void solve()
{
    int n, m;std::cin >> n >> m;
    std::vector<i64> a(m);

    std::vector<std::vector<i64>> b(n, std::vector<i64>(m)), w(n, std::vector<i64>(m));

    for(auto &i : a) {
        std::cin >> i;
    }

    for(int i = 0;i < n;i ++) {
        for(int j = 0;j < m;j ++) {
            std::cin >> b[i][j];
        }

        for(int j = 0;j < m;j ++) {
            std::cin >> w[i][j];
        }
    }

    std::vector<std::priority_queue<Node>> pq(m);

    for(int i = 0;i < n;i ++) {
        for(int j = 0;j < m;j ++) {
            pq[j].push({i, b[i][j]});
        }
    }

    int sum = n;
    std::vector<int> cnt(n);
    while(true) {
        int pre = sum;
        std::vector<int> tmp;
        for(int i = 0;i < m;i ++) {
            while(pq[i].size() && pq[i].top().val <= a[i]) {
                cnt[pq[i].top().ix] ++;
                if(cnt[pq[i].top().ix] == m) {
                    tmp.push_back(pq[i].top().ix);
                    sum --;
                }
                pq[i].pop();
            }
        }

        for(auto &i : tmp) {
            for(int j = 0;j < m;j ++) {
                a[j] += w[i][j];
            }
        }

        if(sum == 0)break;

        if(sum == pre) {
            std::cout << "NO\n";
            return;
        }
    }

    std::cout << "YES\n";
}

1004 - 弯曲筷子(补题)

看到题很明显是一个 DP,但由于有必选的筷子,因此 DP 状态和转移方程不是很好想。
DP 其实有一个很好的思考方向,定义一个满足题目要求的状态,在状态转移的过程中,始终满足此要求,让所有 DP 值变得合法。
那么对于这个题,我们首先不管筷子是否是必选,对于筷子 \(i\)\(dp_{i, 0/1}\) 表示在 \(i\) 之前的必选筷子均已选的情况下,一个筷子是选用还是不选用的最优解。
首先不难想到,我们可以对筷子按照弯曲值进行排序,越靠近的一双筷子对答案的贡献肯定越小,那么一双筷子,要么间隔 \(0\) 根筷子,要么间隔 \(1\) 根筷子。
间隔 \(0\) 根筷子很好理解,那为什么可能间隔 \(1\) 根筷子呢?
这里简单定性分析理解一下,假设 \(x \leq a \leq b \leq c \leq y\)\(a\)\(c\) 均为必选,不考虑间隔一根的情况,那么会出现以下三种选法:

  • \(x, a\)\(c, y\)
  • \(x, a\)\(b, c\)
  • \(a, b\)\(c, y\)

\((a - x) ^ 2\) 特别大并且 \((y - c) ^ 2\) 特别大,那么以上三种分法均会有一项变得特别大而导致答案特别大,但如果 \((a - c) ^ 2\) 很小,比如 \(a - c = 0\),那么此时 \(a, c\) 一起选就会很优秀,因此可能会间隔 \(1\) 根筷子。

为什么不会间隔 \(2\) 根筷子?
官方题解没有证明,这里来证明一下。

该问题也就也就转化为证明:对于四个正整数 \(a \leq b \leq c \leq d\),一定有 $$(d - a) ^ 2 \geq (b - a) ^ 2 + (d - c) ^ 2$$设 \(x = b - a, y = c - b, z = d - a\),那么证明转化为:$$(x + y + z) ^ 2 \geq x ^ 2 + z ^ 2$$令 \(f = (x + y + z) ^ 2 - x ^ 2 + z ^ 2\),将平方展开可以得到:$$f = x ^ 2 + y ^ 2 + z ^ 2 + 2xy + 2yz + 2xz - x ^ 2 - z ^ 2$$继续消元可以得到:$$f = y ^ 2 + 2xy + 2yz + 2xz$$此时可以发现,\(f\) 中所有项均为非负整数,因此 \(f\) 为非负整数,当且仅当 \(a = b = c = d\) 时为 \(0\),因此:$$(x + y + z) ^ 2 \geq x ^ 2 + z ^ 2$$证毕。

那么对于 \(3\) 根及以上,自然也不可能了,因为还不如间隔 \(2\) 根。
得到了这个事实后,我们就可以用 DP 来维护答案了,对于第 \(i\) 根筷子:

  • 如果第 \(i - 1\) 根筷子必选,那么:$$dp_{i, 0} = dp_{i - 1, 1}$$ $$dp_{i, 1} = dp_{i - 1, 0} + (c_i - c_{i - 1}) ^ 2$$
  • 如果第 \(i - 1\) 根筷子非必选,那么:$$dp_{i, 0} = \min(dp_{i - 1, 0}, dp_{i - 1,1})$$ $$dp_{i, 1} = \min(dp_{i - 1, 0} + (c_i - c_{i - 1}) ^ 2, dp_{i - 2, 0} + (c_i - c_{i - 2}) ^ 2)$$

由于我们在 DP 过程中用到了 \(dp_{i - 2}\),因此我们要对前两项进行初始化:

  • 对于第 \(1\) 根筷子,\(dp_{1, 0} = 0\)\(dp_{1, 1} = inf\),因为我们维护 DP 维护的是到第 \(i\) 根筷子为止的选法,但是第一根筷子是不能单选的,因此 \(dp_{1, 1} = inf\)
  • 对于第 \(2\) 根筷子,\(dp_{2, 1} = (c_2 - c_1) ^ 2\),如果第 \(1\) 根筷子必选,那么 \(dp_{2, 0} = inf\),否则 \(dp_{2, 0} = 0\),还是因为我们维护 DP 维护的是到第 \(i\) 根筷子为止的选法,而我们要满足 \([1, i - 1]\) 都是合法的选法,因此如果第一根筷子必选,则 \(dp_{2, 0}\) 不合法。

最后取答案,如果第 \(n\) 根筷子必选,则答案为 \(dp_{n, 1}\),否则答案为 \(\min(dp_{n, 1}, dp_{n, 0})\)

点击查看代码(以 0 为下标起始点)
#include <bits/stdc++.h>
#define inf32 1e9
#define ls o << 1
#define rs o << 1 | 1

using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned int;

const int N = 2e5 + 9;
const i64 inf64 = 2e18;

void solve()
{
    int n, m;std::cin >> n >> m;
    std::vector<i64> a(n);
    std::vector<bool> vis(n);
    std::vector<std::pair<i64, int>> c(n);

    for(auto &i : a) {
        std::cin >> i;
    }

    for(int i = 0;i < n;i ++) {
        c[i] = {a[i], i};
    }

    sort(c.begin(), c.end());

    for(int i = 1;i <= m;i ++) {
        int x;std::cin >> x;
        x --;
        vis[x] = true;
    }

    std::vector<std::array<i64, 2>> dp(n, {inf64, inf64});

    auto power = [](i64 x) -> i64 {
        return x * x;
    };

    dp[0][0] = 0;
    dp[1][1] = power(c[0].first - c[1].first);
    if(!vis[c[0].second])dp[1][0] = 0;

    for(int i = 2;i < n;i ++) {
        if(vis[c[i - 1].second]) {
            dp[i][0] = std::min(dp[i][0], dp[i - 1][1]);
            dp[i][1] = std::min(dp[i][1], dp[i - 1][0] + power(c[i - 1].first - c[i].first));
        } else {
            dp[i][0] = std::min(dp[i][0], std::min(dp[i - 1][0], dp[i - 1][1]));
            dp[i][1] = std::min(dp[i][1], dp[i - 1][0] + power(c[i - 1].first - c[i].first));
            dp[i][1] = std::min(dp[i][1], dp[i - 2][0] + power(c[i - 2].first - c[i].first));
        }
    }

    i64 ans = inf64;
    if(vis[c[n - 1].second])ans = std::min(ans, dp[n - 1][1]);
    else ans = std::min(ans, std::min(dp[n - 1][0], dp[n - 1][1]));

    std::cout << ans << '\n';
}
posted @ 2025-03-22 01:09  天天超方的  阅读(928)  评论(2)    收藏  举报