Codeforces Round 922 (Div. 2)

A. Brick Wall

因为水平砖块的长度至少为 \(2\),所以一行中水平砖块最多放 \(\lfloor \frac{m}{2} \rfloor\) 块,因此答案不超过 \(n \cdot \lfloor \frac{m}{2} \rfloor\)。如果 \(m\) 是奇数,用长度为 \(\lfloor \frac{m}{2} \rfloor\) 的水平砖块平铺过去,最后一块长度为 \(3\),这样也没有垂直砖块。因此最大稳定性就是 \(n \cdot \lfloor \frac{m}{2} \rfloor\)

参考代码
#include <cstdio>
int main()
{
    int t; scanf("%d", &t);
    while (t--) {
        int n, m; scanf("%d%d", &n, &m);
        printf("%d\n", m / 2 * n);
    }
    return 0;
}

B. Minimize Inversions

注意对 \(a_i\)\(a_j\)\(b_i\)\(b_j\) 的操作是同时进行的,不妨假设我们随意调整 \(a\) 的顺序,则原来的每一对 \(a_i\)\(b_i\) 的匹配关系是不变的。令 \(a\) 变得有序,即没有逆序对,这样的状态是逆序对总数最少的状态。

证明:考虑两对元素 \(a_i\)\(a_j\)\(b_i\)\(b_j \ (i<j)\)。这样数对中每一对的逆序对数可能为 \(0\)\(1\),所以对于两个数对而言,逆序对数可能为 \(0\)\(1\)\(2\)。如果操作前是 \(0\) 对逆序对,那么操作后会变成 \(2\) 对;如果本来是 \(1\) 对,操作后还是 \(1\) 对;如果操作前是 \(2\) 对,操作后是 \(0\) 对。如果将 \(a\) 排成有序,则每一对下标 \(i\)\(j\) 对应的两个数对最多产生 \(1\) 个逆序对,这时不可能通过交换操作将逆序对数再减少了,所以这就是逆序对数量最少的情况。

时间复杂度为 \(O(n)\)

参考代码
#include <cstdio>
const int N = 200005;
int idx[N], b[N];
int main()
{
    int t; scanf("%d", &t);
    while (t--) {
        int n; scanf("%d", &n);
        for (int i = 1; i <= n; i++) {
            int x; scanf("%d", &x); idx[x] = i;
        }
        for (int i = 1; i <= n; i++) scanf("%d", &b[i]);
        for (int i = 1; i <= n; i++) printf("%d%c", i, i == n ? '\n' : ' ');
        for (int i = 1; i <= n; i++) printf("%d%c", b[idx[i]], i == n ? '\n' : ' ');
    }
    return 0;
}

C. XOR-distance

考虑 \(a,b,x\) 的二进制表示。分析 \(a\)\(b\) 中同一个位置上的两个二进制位,如果是相等的,则无论 \(x\) 在这一位上取什么值,\(|(a⊕x)−(b⊕x)|\) 在这一位上的贡献必然为 \(0\)。因此 \(x\) 在这一位上应该选 \(0\),因为我们希望 \(x \le r\),所以既然取值无所谓,则应尽量节省。

不妨设 \(a > b\),则存在第一个(最高)位,那一位上 \(a\) 对应的值为 \(1\)\(b\) 对应的值为 \(0\),考虑对于除了这一位以外的之后每一个这样的位,在 \(x\) 的这一位还有机会取 \(1\) 时(即这一位置 \(1\) 后仍小于等于 \(r\))令其取 \(1\),这样一来 \(a \oplus x\) 在这一位上会变成 \(0\),而 \(b \oplus x\) 在这一位上会变成 \(1\),这样能够使得 \(|(a⊕x)−(b⊕x)|\) 的差值尽可能小。

参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long LL;
const int LOG = 60;
int main()
{
    int t; scanf("%d", &t);
    while (t--) {
        LL a, b, r, x = 0; scanf("%lld%lld%lld", &a, &b, &r);
        if (a < b) swap(a, b);
        bool high = true;
        for (int i = LOG - 1; i >= 0; i--) {
            int ai = (a >> i) & 1, bi = (b >> i) & 1;
            if (ai == 1 && bi == 0) {
                if (!high && r >= 1ll << i) {
                    x += 1ll << i; r -= 1ll << i;
                }
                high = false;
            }
        }
        printf("%lld\n", abs((a ^ x) - (b ^ x)));
    }
    return 0;
}

D. Blocking Elements

考虑二分答案。因为尝试的分隔代价限定得越小,就越难实现,限定得越大则越有可能实现,满足单调性。

当尝试的分隔代价限定为 \(x\) 时,设 \(dp_i\) 表示前 \(i\) 个数,以第 \(i\) 个数作为分隔元素时,在保证每一段的元素和不超过 \(x\) 的情况下,所有的分隔元素之和的最小值,于是 \(dp_i = \min \{ dp_j \} + a_i\),其中 \(j\) 是每一个保证 \([j+1,i-1]\) 的区间和不超过 \(m\) 的位置。由于 \(a_j\) 保证非负,这样的 \(j\) 的取值范围一定是一个连续区间,也就是一个滑动窗口,这个窗口会随着 \(i\) 的右移而右移。因此,这个 \(dp\) 的计算过程可以用单调队列来优化。

时间复杂度为 \(O(n \log \sum a_i)\)

参考代码
#include <cstdio>
#include <deque>
using namespace std;
typedef long long LL;
const int N = 100005;
int a[N], n;
LL dp[N], sum[N];
bool check(LL x) {
    deque<int> dq; dq.push_back(0);
    int idx = 0; 
    for (int i = 1; i <= n + 1; i++) {
        while (idx <= i && sum[i - 1] - sum[idx] > x) idx++;
        while (!dq.empty() && dq.front() < idx) dq.pop_front();
        dp[i] = dp[dq.front()] + a[i];
        while (!dq.empty() && dp[dq.back()] >= dp[i]) dq.pop_back();
        dq.push_back(i);
    }
    return dp[n + 1] <= x;
}
int main()
{
    int t; scanf("%d", &t);
    while (t--) {
        scanf("%d", &n);
        for (int i = 1; i <= n; i++) {
            scanf("%d", &a[i]); sum[i] = sum[i - 1] + a[i];
        }
        a[n + 1] = 0; sum[n + 1] = sum[n];
        LL ans, l = 1, r = sum[n];
        while (l <= r) {
            LL mid = (l + r) / 2;
            if (check(mid)) {
                r = mid - 1; ans = mid;
            } else l = mid + 1;
        }
        printf("%lld\n", ans);
    }
    return 0;
}

F. Caterpillar on a Tree

首先可以发现,要想遍历整棵树,实质上就是遍历到每一个叶子结点,并且将传送门用在非叶子结点以外的地方没有意义。

遍历的最优路径可以被分成三个部分:根结点移动到叶子结点、从一个叶子结点移动到另一个叶子结点、从叶子结点传送到根结点。为了简化操作,不妨认为一开始有 \(k+1\) 次传送机会,最后回到根结点。假如不使用传送的机会,总的花费是 \(2(n-1)\)。当我们处在某个叶子结点时,考虑是否使用传送门回到根结点对总花费的影响,实际上要去下一个叶子结点必然会经过这两个叶子节点的最近公共祖先,而传送门的影响体现在是从当前叶子结点移动过去还是从根结点移动过去,因此如果根结点过去更近,使用一次传送门就可以降低花费。问题可以转化为选择对降低花费影响最大的 \(k+1\) 个叶子结点使用传送门。

那么如何安排遍历叶子结点的顺序呢?这里的策略是对于每个结点按照每棵子树深度大小升序来排序,然后依次遍历。感性理解:让深度最深的叶子结点作为遍历一棵子树的最后一个结点,这样如果有传送机会可以尽可能减少向上返回的步数。

对于每个叶子结点,计算其到下一个叶子结点如果使用传送门可以节省的花费。这里不需要真的去实现 LCA 算法,只需要向上跳,直到跳到某个不作为其父节点下最深子树的根结点的结点即可停下,因为对于这样的结点,它与下一个要去叶子结点之间的最近公共祖先就是此时的父节点。

时间复杂度为 \(O(n \log n)\)

参考代码
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 200005;
int p[N], d[N], h[N], save[N];
vector<int> tree[N];
bool cmp(int x, int y) {
    return d[x] < d[y];
}
void dfs(int cur, int fa) {
    d[cur] = 0; h[cur] = h[fa] + 1;
    for (int child : tree[cur]) {
        dfs(child, cur);
        d[cur] = max(d[cur], d[child]);
    }
    sort(tree[cur].begin(), tree[cur].end(), cmp);
    d[cur]++;
}
int main()
{
    int n, k; scanf("%d%d", &n, &k); 
    for (int i = 2; i <= n; i++) {
        scanf("%d", &p[i]); tree[p[i]].push_back(i);
    }
    dfs(1, 0);
    int len = 0;
    for (int i = 1; i <= n; i++) {
        if (tree[i].size() == 0) {
            int res = 0, cur = i; 
            while (cur != 1) {
                res++;
                if (cur == tree[p[cur]].back()) {
                    cur = p[cur]; 
                } else {
                    save[++len] = res - h[p[cur]] + 1;
                    break;
                }
            }
            if (cur == 1) save[++len] = res;
        }
    }
    sort(save + 1, save + len + 1);
    int ans = 2 * (n - 1);
    k++; // 将遍历到最后一个叶子结点后结束看做是用了一次传送门
    for (int i = len; i >= 1; i--) {
        if (save[i] <= 0 || k == 0) break;
        ans -= save[i]; k--;
    }
    printf("%d\n", ans);
    return 0;
}
posted @ 2024-02-17 21:15  RonChen  阅读(36)  评论(0)    收藏  举报