Educational Codeforces Round 124 (CF1651) 简要题解

A ~ C

由于是简要题解,所以没有 A ~ C

CF1651D Nearest Excluded Points

题意

\(n\) 个二维点,对每个二维点求距离(曼哈顿距离)它最近的没有给出的二维点,如果答案有多个输出任意一个即可

题解

假设与点 \((x, y)\) 最近的没有给出的点与 \((x, y)\) 的距离为 \(f(x, y)\)

不难发现,\(f(x, y) = min(f(x - 1, y),f(x + 1, y), f(x, y - 1), f(x, y + 1)) + 1\),其中 \((x, y)\) 为已经给出的点

我们认为,若 \((x, y)\) 没有给出,则 \(f(x, y) = 0\)

简单的证明:

首先,由于曼哈顿距离满足 \(dist(a, b) \le dist(a, c) + dist(c, b)\),所以 \(f(x, y) \le min(f(x - 1, y),f(x + 1, y), f(x, y - 1), f(x, y + 1)) + 1\)

然后,我们假设距离 \((x, y)\) 最近的点是 \((x_0, y_0)\)(如果有多个任取一个)

不妨假设有 \(x_0 < x\),那么就一定有 \(f(x, y) = f(x - 1, y) + 1\)

这里用到了曼哈顿距离的另一个性质:

若点 \(A, B, C\) 满足 \(C\) 在以 \(AB\) 为对角线的矩形内部或边上,则一定有 \(dist(A, B) = dist(A, C) + dist(B, C)\) (这个应该显然吧)

那么因为 \((x, y)\) 为已经给出的点,所以 \((x_0, y_0)\)\((x, y)\) 一定不是同一个点

所以一定有 \(x_0 < x\)\(x_0 > x\)\(y_0 < y\)\(y_0 > y\)

其他的情况与 \(x_0 < x\) 的类似

有了上面的结论,我们就只要从外向内枚举每个点就行了,因为要求输出点,所以我们对于每个点 \(i\) 记录一下距离它最近的没有给出的点 \(ans_i\)

\(ans_i\) 就是 \(i\) 四周的点的 \(ans\) 中距离 \(i\) 点最近的

至于从外向内枚举点,我们考虑使用 BFS 算法,先找出最外面的点(也就是四周有一个点没有给出的点),然后 BFS 即可

复杂度 \(O(n)\) (如果使用哈希的话)

代码

注:使用 STL map,复杂度为 \(O(n \log n)\)

#include <bits/stdc++.h>
using namespace std;

const int N = 200010;

struct node{
    int x, y;
    bool operator <(const node a) const{
        return x == a.x ? y < a.y : x < a.x;
    }
}a[N], b[N];

int n, vis[N], dx[4] = {0, 0, -1, 1}, dy[4] = {1, -1, 0, 0};

map <node, int> mp;

queue <int> q;

int main()
{
    cin >> n;
    for(int i = 1; i <= n; i++)
        cin >> a[i].x >> a[i].y, mp[(node){a[i].x, a[i].y}] = i;
    for(int i = 1; i <= n; i++)
    for(int j = 0; j < 4; j++){
        int nx = a[i].x + dx[j], ny = a[i].y + dy[j];
        if(!mp.count((node){nx, ny})){
            b[i] = (node){nx, ny};
            q.push(i), vis[i] = 1;
            break;
        }
    }

    while(!q.empty()){
        int u = q.front(); q.pop();
        for(int i = 0; i < 4; i++){
            int nx = a[u].x + dx[i], ny = a[u].y + dy[i];
            if(!mp.count((node){nx, ny})) continue;
            else{
                int v = mp[(node){nx, ny}];
                if(!vis[v]) b[v] = b[u], vis[v] = 1, q.push(v);
            }
        }
    }

    for(int i = 1; i <= n; i++) cout << b[i].x << " " << b[i].y << endl;
    return 0;
}

CF1651E Sum of Matchings

题意

给一张左右各 \(n\) 个点的二分图,左右结点编号分别为 \(1\) ~ \(n\)\(n + 1\) ~ \(2n\),且满足每个点度数为 \(2\),求

\[\sum_{1 \le l \le r \le n \le L \le R \le 2n} MM(l, r, L, R) \]

其中 \(MM(l, r, L, R)\) 为左部保留 \([l, r]\) 中结点,右部保留 \([L, R]\) 中结点的子图的最大匹配

题解

显然这道题每个点度数为 \(2\) 是重点

因为每个点度数为 \(2\) 且有偶数个点,所以原图一定由若干个偶环构成

考虑子图,子图中每个点度数一定小于等于 \(2\),观察发现,这样的二分图一定由一些环和链构成

注意这里的构成指的是每个点只属于一个环或一条链

因为原图只有偶环,所以子图中必然也只有偶环,而偶环的匹配是满的(每个点都能匹配上)

再考虑链的情况,偶链中每个点也都能匹配上,而奇链中会有一个点匹配不上

那我们不妨假设每个子图中每个点都匹配上了,这样算到的答案再减去所有子图中奇链的个数和,最后再除以 \(2\) 就是答案了

由于每个点度数为 \(2\),所以奇链最多只有 \(O(n^2)\)

时间复杂度 \(O(n^2)\)

代码

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;

const int N = 3010;

int n, m, vis[N], st[N], top;

vector <int> to[N];

LL ans;

LL f(LL n) { return n * (n + 1) / 2; }

int nxt(int x) { return vis[to[x][0]] ? to[x][1] : to[x][0]; }

LL g(int l, int r, vector <int> &fo)
{
    if(l > r){
        int mx = *max_element(fo.begin(), fo.end()),
            mn = *min_element(fo.begin(), fo.end());
        return f(mn - 1) + f(mx - mn - 1) + f(n - mx);
    }
    int minl = 1, maxl = l, minr = r, maxr = n;
    for(int i = 0; i < fo.size(); i++){
        int x = fo[i];
        if(l <= x && x <= r) return 0;
        else if(x < l) minl = max(minl, x + 1);
        else maxr = min(maxr, x - 1);
    }
    return (maxl - minl + 1ll) * (maxr - minr + 1ll);
}

LL calc(int x)
{
    top = 0;
    while(!vis[x])
        st[++top] = x, vis[x] = 1, x = nxt(x);
    LL res = 0;
    for(int i = 1; i <= top; i++){
        int now = st[i] > n ? st[i] - n : st[i];
        res += f(n) * (f(now - 1) + f(n - now));
        int l = n, r = 1, L = n, R = 1, pl = i, pr = i;
        for(int j = 0; (j << 1) < top; j++){
            if(st[pl] <= n) l = min(l, st[pl]), r = max(r, st[pl]);
            else L = min(L, st[pl] - n), R = max(R, st[pl] - n);
            if(st[pr] <= n) l = min(l, st[pr]), r = max(r, st[pr]);
            else L = min(L, st[pr] - n), R = max(R, st[pr] - n);

            vector <int> f, F;
            pl = (pl == 1) ? top : pl - 1;
            pr = (pr == top) ? 1 : pr + 1;
            if(st[pl] <= n) f.push_back(st[pl]), f.push_back(st[pr]);
            else F.push_back(st[pl] - n), F.push_back(st[pr] - n);
            res += g(l, r, f) * g(L, R, F);
        }
    }
    return res;
}

int main()
{
    cin >> n, m = n << 1;
    for(int i = 1, u, v; i <= m; i++){
        cin >> u >> v;
        to[u].push_back(v), to[v].push_back(u);
    }

    LL ans = 2 * n * f(n) * f(n);
    for(int i = 1; i <= m; i++)
        if(!vis[i]) ans -= calc(i);
    cout << (ans >> 1) << endl;
    return 0;
}

CF1651F Tower Defense

题意

有一个一维的塔防游戏,在 \(1\)~\(n\) 的每个整数坐标上有一个塔,在 \(i\) 坐标上的塔最大魔法值为 \(c_i\),每秒回复魔法值为 \(r_i\)

另有 \(m\) 只怪物,第 \(i\) 只怪物有 \(h_i\) 的血量,并且他在第 \(t_i\) 秒出现在 \(0\) 坐标,每只怪物的速度都是 \(1\) 单位长度每秒

怪物经过一个塔时,设怪物剩下的血量为 \(h\), 塔当前的魔法值为 \(c\),则二者同时减去 \(\min(h, c)\)

怪物血量为 \(0\) 会死,但是塔魔法值为 \(0\) 依然能活(也就是还能回复魔法值)

问攻破了所有塔的怪物剩下的血量之和是多少

其中 \(n, m, t \le 2 \times 10^5\)

题解

std

很神奇的解法

考虑每个怪物走过会发生什么

  1. 有一个前缀被推平了
  2. 有一个塔的魔法值减少了一部分

当一个怪物出发时,我们让他一段一段打(这个段指的是被同一个怪物推平的连续的塔)

假设我们可以在 \(O(T)\) 的时间内考虑一段,那么总复杂度就是 \(O(nT)\)

简单的证明:

显然,每个怪物只会产生 \(O(1)\) 个段,所以总段数是 \(O(n)\)

考虑一个段会出现的情况

  1. 怪物的血量比这个段内魔法值之和高,那么这个段就消失了
  2. 怪物的血量比这个段内魔法值之和低,那么会新建 \(O(1)\) 个段,并且怪物不能继续前进了

对于第一部分,每个段只会出现一次这样的情况(因为出现了就说明这段要消失了),总复杂度是 \(O(nT)\)

对于第二部分,每个怪物只会出现一次这样的情况(因为出现了就说明怪物要死了),总复杂度是 \(O(nT)\)

所以总复杂度是 \(O(nT)\)

这个部分和下面的分块做法的复杂度证明有异曲同工之妙

那怎么快速考虑一段呢

我们发现,一段有一个非常明显的性质:它是被同一个怪物推平的

我们不妨假设他被 \(t\) 时刻出发的怪物推平,那么对于 \(s\) 时刻出发的怪物,他经过这个段的每一个塔时,这个塔都回了 \(s - t\) 秒的魔法值

那就是考虑一段从 \(0\) 开始回复 \(x\) 秒后的魔法值之和

不难发现这是一个分 \(O(n)\) 段的一次函数

对于一个塔,当 \(x \le \lfloor \frac{c_i}{r_i} \rfloor\) 时,魔法值为 \(r_ix\),当 \(x > \lfloor \frac{c_i}{r_i} \rfloor\) 时,魔法值为 \(c_i\),也就是一个拐点的一次分段函数

显然 \(n\) 个塔的魔法值之和就最多有 \(n\) 个拐点

线段树维护区间和,可持久化每个拐点之后的版本,查询时找到最近的拐点后线段树上二分即可,\(T = O(\log n)\)

复杂度 \(O(n \log n)\)

我的解法

观察发现,塔的回复十分的麻烦,于是我们考虑分块

\(\sqrt n\) 个塔分成一块,每一块维护一个长度为 \(t\) 的数组 \(v\)\(v_i\) 表示这一个块的所有塔从魔法值为 \(0\) 开始,回复 \(i\) 秒之后总魔法值是多少

计算 \(v\) 数组可以考虑将块内的塔按照 \(\frac{c_i}{r_i}\) 排序,也就是按一个塔回满魔法值所需时间排序,然后每一秒考虑当且没回完的塔即可

这部分的时间复杂度是 \(O(n\sqrt n)\)

然后再对每个块维护一个时间戳表示上一个打到这个块的怪物是第几秒出发的

依次考虑每个怪物

让他一个一个块去打,找到第一个他过不去的块(如果都过去了就还要加答案),假设是第 \(i\) 个块

对于前 \(i - 1\) 个块,对他们打上时间戳,并打上标记,表示他们在这个时间戳被完全推平了

对于第 \(i\) 个块,我们需要维护每个塔在这次进攻之后剩下的魔法值,假设这个数组为 \(rest\)

我们让怪物再一个一个塔去打,找到第一个他打不过的塔 \(j\)

对于第 \(i\) 个块在 \(j\) 之前的塔,将他们的 \(rest\) 设为 \(0\)

对于第 \(j\) 个塔以及其之后的塔,计算出他的 \(rest\)

最后打上时间戳,并打上标记,表示他们在这个时间戳没有被推平

对于 \(i\) 之后的块,不需要处理

考虑这样做的复杂度

首先,对于被推平的块,由于我们维护了 \(v\) 数组,所以查询块魔法值之和与查询单点魔法值都是 \(O(1)\) 的,这部分总复杂度为 \(O(n\sqrt n)\)

之后,对于没有被推平的块,查询魔法值之和是 \(O(\sqrt n)\) 的,查询单点魔法值依然是 \(O(1)\)

似乎这里的复杂度是 \(O(n^2)\)

但是我们注意到,一个怪物只能产生一个没有被推平的块,而如果我们在某个怪物前进时查询了这个块的和,那会有如下两种情况

  1. 这个怪物剩下的血量比这个块魔法值之和大(或者相等),这个块被推平了
  2. 这个怪物剩下的血量比这个块魔法值之和小,这个块依然没有被推平,但是这个怪物也不能再产生新的没有被推平的块了
    所以我们可以理解为这个块先被标记为推平,再被标记为没有被推平

也就是说,我们查询一次没有被推平的块之后,这个块就被标记为推平了,所以这部分复杂度依然是 \(O(n \sqrt n)\)

总时间复杂度 \(O(n\sqrt n)\),总空间复杂度 \(O(n\sqrt n)\)\(v\) 数组)

代码(我的解法)

因为是\(O(n \sqrt n)\) 的空间,所以 512MB 有点不够用,我这里块大小调大了一点以平衡空间复杂度

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;

const int N = 200010;
const int Bs = 310;
const int Mx = 200000;

LL n, m, B, num[N], bel[N], cnt = 0;
LL ans, v[Bs][N], r[N], c[N], rest[N], used[Bs], tim[Bs];

inline LL read()
{
    LL x = 0, f = 1;
    char c = getchar();
    while(c < '0' || c > '9') { if(c == '-') f = -1; c = getchar(); }
    while(c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
    return x * f;
}

bool cmp(int a, int b) { return c[a] / r[a] < c[b] / r[b]; }

void init() // 计算 v 数组
{
    for(int i = 1; i <= n; i++)
        c[i] = read(), r[i] = read(), bel[i] = (i - 1) / B + 1, num[i] = i;
    for(int i = 1; i <= bel[n]; i++){
        sort(num + (i - 1) * B + 1, num + min(i * B, n) + 1, cmp);
        LL now = 0, mx = 0;
        for(int j = (i - 1) * B + 1; j <= min(n, i * B); j++) now += r[j], mx += c[j];
        v[i][Mx + 1] = mx;
        for(int j = 1, k = (i - 1) * B + 1; j <= Mx; j++){
            LL val = 0, cur = num[k];
            while(k <= min(i * B, n) && c[cur] / max(r[cur], 1ll) == j - 1)
                now -= r[cur], val += c[cur] - ((c[cur] / r[cur]) * r[cur]), cur = num[++k];
            v[i][j] = v[i][j - 1] + now + val;
        }
    }
}

LL calc(LL u, LL t) // 第 u 个块,在被 t 时刻出发的怪物经过时的魔法值之和
{
    if(!used[u]) return (t - tim[u] > Mx + 1) ? v[u][Mx + 1] : v[u][t - tim[u]];
    LL res = 0;
    for(int i = B * (u - 1) + 1; i <= min(n, B * u); i++)
        res += min(rest[i] + (t - tim[u]) * r[i], c[i]);
    return res;
}

int main()
{
    n = read(), B = 670;
    init();
    m = read();
    for(int i = 1; i <= bel[n]; i++) tim[i] = -Mx - 1;
    while(m--){
        LL t = read(), h = read(), cur = 1, o;
        while(h >= (o = calc(cur, t)) && cur <= bel[n])
            h -= o, used[cur] = 0, tim[cur] = t, cur++;
        if(!h || cur > bel[n]){
            ans += h;
            continue;
        }
        if(used[cur]){
            for(int i = B * (cur - 1) + 1; i <= min(n, B * cur); i++)
                if(h >= min(c[i], rest[i] + (t - tim[cur]) * r[i]))
                    h -= min(c[i], rest[i] + (t - tim[cur]) * r[i]), rest[i] = 0;
                else
                    rest[i] = min(c[i], rest[i] + (t - tim[cur]) * r[i]),
                    rest[i] -= h, h = 0;
            tim[cur] = t;
        }
        else{
            for(int i = B * (cur - 1) + 1; i <= min(n, B * cur); i++)
                if(h >= min(c[i], (t - tim[cur]) * r[i]))
                    h -= min(c[i], (t - tim[cur]) * r[i]), rest[i] = 0;
                else
                    rest[i] = min(c[i], (t - tim[cur]) * r[i]),
                    rest[i] -= h, h = 0;
            tim[cur] = t, used[cur] = 1;
        }
    }
    printf("%lld\n", ans);
    return 0;
}
posted @ 2022-04-07 15:41  sgweo8ys  阅读(64)  评论(0)    收藏  举报