1. 程式人生 > 其它 >Educational Codeforces Round 124 (CF1651) 簡要題解

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

暫時看不懂

我的解法

觀察發現,塔的回覆十分的麻煩,於是我們考慮分塊

\(\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;
}