2022年东北赛VP/补题记录 (10/13) (BCDEFGIJKL) (附有AHM官方题解)

这场比赛当年是用牛客竞赛平台线上比赛的,比赛链接:https://ac.nowcoder.com/acm/contest/35146

在这里插入图片描述

三人VP(xht,mqy,我),评价为全部唐完了,尤其是前两个小时更是唐中唐。

L. Polygon

还以为是什么计算几何题目呢,兴冲冲打开,发现是签到,白高兴一场。

所有边可以组成一个多边形,当且仅当除去一条最长边后剩余边的长度和大于最长边。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) (x).begin(),(x).end()
typedef long long i64;
typedef pair<int, int> pii;

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    i64 n, x; cin >> n;
    i64 sum = 0, m = 0;
    while(n--) {
        cin >> x;
        m = max(m, x);
        sum += x;
    }
    sum -= m;
    if(m >= sum) cout << "NO\n";
    else cout << "YES\n";
    return 0;
}

E. Plus

诈骗题,一开始就怀疑只有很少的答案,推导了一下发现确实如此。

p , q p, q p,q 均为奇素数,则 p q + q p p^q + q^p pq+qp 是一个大于 2 2 2 的偶数,一定不是奇数。显然 p = 2 p = 2 p=2 时才可能有解。若 q = 2 q = 2 q=2 p q + q p = 4 p^q + q^p = 4 pq+qp=4,舍去。当 q = 3 q = 3 q=3 时,合法, p q + q p = 17 p^q + q^p = 17 pq+qp=17;其余情况, q q q 是一个奇质数,且 q q q 不是 3 3 3 的倍数, 2 q m o d    3 = 1 , q 2 m o d    3 = 2 2^q \mod 3 = 1, q^2 \mod 3 = 2 2qmod3=1,q2mod3=2 p q + q p p^q + q^p pq+qp 3 3 3 的倍数,舍去。答案最多只有一对。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) (x).begin(),(x).end()
typedef long long i64;
typedef pair<int, int> pii;

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    i64 n; cin >> n;
    if(n <= 2) cout << "0\n";
    else cout << "1\n2 3\n";
    return 0;
}

D. Game

此时 I 题搞得我和 xht 很红温,mqy 大致想到了如何判断必胜方,xht 完善了思路然后写了个莫队。第一发交上去 TLE 了,然后发现排序的 cmp 函数里不是比较两个区间的 l B \frac{l}{B} Bl,而是直接比较了 l l l,相当于分块分了 n n n 块,难怪会超时。改了一下就过了,过的第三题居然是这个,很神奇。

这题相当于我完全没参与,赛后自己想和写了一遍。手摸发现设 f ( x ) f(x) f(x) 表示数字 x x x 在区间 [ l , r ] [l,r] [l,r] 的出现次数。先手必胜当且仅当 ∃ x , f ( x ) ≡ 1 ( m o d 2 ) \exist x,f(x) \equiv 1 \pmod 2 x,f(x)1(mod2)。详细证明题解中有叙述,直接截图放在底下。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) (x).begin(),(x).end()
typedef long long i64;
typedef pair<int, int> pii;

const int N = 1e5 + 10;
struct {
    int cnt[N], oddcnt;
    void insert(int x) {
        if(cnt[x] & 1) oddcnt--;
        else oddcnt++;
        cnt[x]++;
    }
    void erase(int x) {
        if(cnt[x] & 1) oddcnt--;
        else oddcnt++;
        cnt[x]--;
    }
} mp;

int n, m, ans[N], a[N], B;
struct question {
    int index, l, r;
    bool operator < (const question &o) {
        int b = l / B, bo = o.l / B;
        if(b == bo) {
            if(b & 1) return r > o.r;
            else return r < o.r;
        } else return b < bo;
    }
};
vector<question> q;

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    cin >> n >> m;
    B = floor(sqrt(n));
    for(int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    for(int i = 1; i <= m; i++) {
        int l, r; cin >> l >> r;
        q.push_back({i, l, r});
    }
    sort(all(q));
    int nl = 1, nr = 1;
    mp.insert(a[1]);
    for(auto [i, l, r] : q) {
        while(nr < r) mp.insert(a[++nr]);
        while(nl > l) mp.insert(a[--nl]);
        while(nr > r) mp.erase(a[nr--]);
        while(nl < l) mp.erase(a[nl++]);
        if(mp.oddcnt) ans[i] = 1;
    }
    for(int i = 1; i <= m; i++) {
        if(ans[i]) cout << "Alice\n";
        else cout << "Bob\n";
    }
    return 0;
}

I. Generator

这题是我们全场表现最唐的一道题目,干的事情包括但不限于:

  • 用 Python 打表尝试算出前 x x x 项的调和级数和,但是用的是分数类,根本跑不动
  • const int eu = 0.57721566540153L;,不知道这个 int 怎么写出来的
  • 以为用 C++ 的 long double 计算出的数的精度不够,所以想用 Python 打表,后来发现实际上精度非常够用

手推公式 + 打表发现输入对于 n n n ,答案是 1 + ∑ k = 1 n − 1 1 k 1 + \sum_{k = 1}^{n - 1} \frac{1}{k} 1+k=1n1k1 。显然直接暴力计算实在是太慢了。有一个小知识: lim ⁡ n → ∞ ∑ k = 1 n 1 k − ln ⁡ n = γ \lim_{n \rightarrow \infty} \sum_{k = 1}^{n} \frac{1}{k} - \ln n = \gamma limnk=1nk1lnn=γ,这里 γ \gamma γ 是欧拉常数。

用 long double 计算出前 1 0 9 10^9 109 项调和级数的和,再减去 ln ⁡ 1 0 9 \ln 10^9 ln109 就得到了一个可用的欧拉常数的估计值。虽然有浮点误差和截断误差,但也应该大致够用。当 m m m 不够大的时候, ln ⁡ m + γ \ln m + \gamma lnm+γ 作为前 m m m 项的调和级数的和的估计值精确度是欠缺的,因此直接暴力计算即可。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) (x).begin(),(x).end()
typedef long long i64;
typedef pair<int, int> pii;

const auto eu = 0.57721566540153L;

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    i64 n; cin >> n;
    long double ans = 0;
    if(n <= 100'000'000) {
        for(int i = 1; i < n; i++) {
            ans += 1.0L / i;
        }
    } else {
        ans = logl(n - 1) + eu;
    }
    cout << fixed << setprecision(10) << ans + 1 << endl;
    return 0;
}

K. Maze

直接搜就行,单个测试点复杂度为 O ( 4 n 2 m ) O(4n^2m) O(4n2m),没什么好说的。注意代码写法不要太繁琐。里面那一句 memset 实际上不是正确复杂度,但是考虑到 t t t 较小且 memset 较快,为了少写一点就直接这么做了。如果 t t t 较大就只能初始化范围内的。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'

const int N = 102;
char mp[N][N];
int n, m, vis[N][N][N][4];
const int dx[] = {0, 0, 1, -1};
const int dy[] = {1, -1, 0, 0};
struct state {int x, y, c, d;};
inline int &v(state &A) {
    return vis[A.x][A.y][A.c][A.d];
}

void solve() {
    cin >> n >> m;
    for(int i = 1; i <= n; i++) {
        mp[0][i] = mp[n + 1][i] = '*';
        mp[i][0] = mp[i][n + 1] = '*';
    }
    for(int i = 1; i <= n; i++) {
        string s; cin >> s;
        for(int j = 1; j <= n; j++)
            mp[i][j] = s[j - 1];
    }
    memset(vis, -1, sizeof(vis));

    queue<state> q;
    for(int i = 0; i < 4; i++) {
        state t = {1, 1, 0, i};
        q.push(t); v(t) = 0;
    }
    while(!q.empty()) {
        for(int i = 0; i < 4; i++) {
            auto p = q.front();
            p.c = (i == p.d ? p.c + 1 : 1);
            p.x += dx[i], p.y += dy[i], p.d = i;
            if(p.c <= m && v(p) == -1 && mp[p.x][p.y] != '*') {
                v(p) = v(q.front()) + 1;
                q.push(p);
                if(p.x == n && p.y == n) {
                    cout << v(p) << endl;
                    return;
                }
            }
        }
        q.pop();
    }
    cout << -1 << endl;
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int T; cin >> T;
    while(T--) solve();
    return 0;
}

G. Hot Water Pipe

可以一眼发现,对于同样温度的一整个连续段,应当绑在一起维护。可以发现,推进管子的段的数量是 O ( n ) O(n) O(n) 的,因此使用的段也是 O ( n ) O(n) O(n) 的,直接暴力模拟即可。

一开始读错题目了,以为是降低到 T min ⁡ T_{\min} Tmin 时瞬间变成 T max ⁡ T_{\max} Tmax,后来发现是降低到 T min ⁡ − 1 T_{\min} - 1 Tmin1 时瞬间加热。调试发现这个问题后直接在读入 T min ⁡ T_{\min} Tmin 后将其减一就解决了。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) (x).begin(),(x).end()
typedef long long i64;
typedef pair<int, int> pii;

i64 n, m, tmin, tmax;
i64 nowtime;

struct block {
    i64 length;
    i64 pushtime;
    i64 temperature;
    block(i64 l, i64 t, i64 tp) {
        length = l;
        pushtime = t;
        temperature = tp;
    }
    i64 getnowtemp() {
        i64 ret = temperature;
        i64 pt = nowtime - pushtime;
        i64 mod = tmax - tmin;
        ret = ret - tmin;
        ret = ret - pt;
        ret = (ret % mod + mod) % mod;
        if(ret == 0) ret = mod;
        ret += tmin;
        return ret;
    }
};

deque<block> hotpipe;
i64 pipelength = 0;

i64 solve(i64 len) {
    i64 ans = 0;
    while(!hotpipe.empty() && len > 0) {
        auto b = hotpipe.back();
        hotpipe.pop_back();
        pipelength -= b.length;
        if(b.length <= len) {
            ans += b.length * b.getnowtemp();
            len -= b.length;
        } else {
            ans += len * b.getnowtemp();
            b.length -= len;
            hotpipe.push_back(b);
            pipelength += b.length;
            len = 0;
        }
    }
    if(len > 0) {
        ans += tmax * len;
        len = 0;
    }
    len = n - pipelength;
    if(len > 0) {
        hotpipe.push_front(block(len, nowtime, tmax));
        pipelength = n;
    }
    return ans;
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    cin >> n >> m >> tmin >> tmax;
    tmin--;
    pipelength = n;
    for(int i = 1; i <= n; i++) {
        int a; cin >> a;
        hotpipe.push_back(block(1, 0, a));
    }
    while(m--) {
        i64 t, k; cin >> t >> k;
        nowtime += t;
        cout << solve(k) << endl;
    }
    return 0;
}

B. Capital Program

本题可参考原题洛谷 P5536,做法很多样。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) (x).begin(),(x).end()
typedef long long i64;
typedef pair<int, int> pii;

const int N = 1e5 + 10;
int n, k, dep[N], deepest, fa[N], maxdep[N];
vector<int> edge[N];

void dfs(int u, int f) {
    dep[u] = dep[f] + 1;
    fa[u] = f;
    maxdep[u] = dep[u];
    if(dep[u] > dep[deepest]) {
        deepest = u;
    }
    for(int x : edge[u]) {
        if(x == f) continue;
        dfs(x, u);
        maxdep[u] = max(maxdep[u], maxdep[x]);
    }
}

int check(int ans) {
    int ret = 0;
    for(int i = 1; i <= n; i++) {
        if(maxdep[i] - dep[i] >= ans) {
            ret++;
        }
    }
    return ret;
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    cin >> n >> k; 
    for(int i = 1; i < n; i++) {
        int u, v; cin >> u >> v;
        edge[u].push_back(v);
        edge[v].push_back(u);
    }
    dep[0] = -1;
    deepest = 0; dfs(1, 0);
    int dpt = deepest;
    deepest = 0; dfs(dpt, 0);
    int len = dep[deepest];
    for(int i = 1; i <= len / 2; i++) {
        deepest = fa[deepest];
    }
    dpt = deepest;
    deepest = 0; dfs(dpt, 0);
    int L = 0, R = (len + 1) / 2, mid;
    while(L < R) {
        mid = (L + R) / 2;
        if(check(mid) <= k) {
            R = mid;
        } else L = mid + 1;
    }
    cout << L << endl;
    return 0;
}

C. Segment Tree

我读了伪代码发现是一个动态开点线段树,把题意给了队友,顺便给出了我的观察:如果线段树的区间 [ 1 , m ] [1, m] [1,m] 中的 m = 2 x m = 2^x m=2x,可以有一个贪心做法,每一层的建出的点的数量就是 min ⁡ ( q , 2 i ) \min(q, 2^i) min(q,2i)

一般的线段树的子节点会在 k k k k + 1 k + 1 k+1 层。去掉这两层,前面的部分还是一样。最后的这两层有一些单独的点,也有一些“人”字形的三个点的树。三点树的根显然应该尽可能多取。最后再加上 q q q 个叶子节点即可。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) (x).begin(),(x).end()
typedef long long i64;
typedef pair<int, int> pii;

void solve() {
    i64 m, q; cin >> m >> q;
    if(q >= m) q = m;
    i64 x = 0, ans = 0;
    while((1LL << (x + 1)) <= m) x++;
    for(int i = 0; i < x; i++)
        ans += min(1LL << i, q);
    ans += min(m - (1LL << x), q);
    cout << ans + q << endl;
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int T; cin >> T;
    while(T--) solve();
    return 0;
}

F. Tree Path

我和 xht 很快想到了树链剖分。我们意识到,我们可以在基于 DFS 序建立的线段树上,维护经过某个点 x x x 的所有路径的集合。虽然涉及到线段树的区间修改,但只有单点查询,所以不用标记下传,直接用类似标记永久化的手段即可。因此,被添加到线段树内的数据量总共是 O ( n log ⁡ 2 n ) O(n \log^2 n) O(nlog2n) 的。然后我们就陷入了思考的停滞,因为要查询的是不经过 x x x 的路径,而上述维护的是经过 x x x 的路径。

赛后发现,树链剖分后,一根链 ( u , v ) (u, v) (u,v) 可以被拆分成 O ( log ⁡ n ) O(\log n) O(logn) 段连续 DFS 序的区间,这也就意味着,这棵树剩余的所有部分,虽然可能看上去零碎,但本质上只是一个补集,因此剩余部分也是 O ( log ⁡ n ) O(\log n) O(logn) 个区间,上述结构维护的就是不经过 x x x 的路径了。

顺便一说,出题人的两个解法都与树链剖分无关,但是赛场上的四个 AC 似乎都是树链剖分,这是怎么回事呢。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) (x).begin(),(x).end()
typedef long long i64;
typedef pair<int, int> pii;

const int N = 1e5 + 10;
int dfsn[N], dfso[N], dfsc, fa[N];
int hson[N], top[N], dep[N], sz[N];
vector<int> edge[N], son[N];
int n, k, m, nowmin;
const int inf = 1e9;

void dfs1(int u, int f) {
    sz[u] = 1, fa[u] = f;
    dep[u] = dep[f] + 1;
    hson[u] = 0;
    for(int x : edge[u]) {
        if(x != f) {
            dfs1(x, u);
            sz[u] += sz[x];
            if(sz[x] > sz[hson[u]]) {
                hson[u] = x;
            }
        }
    }
    for(int x : edge[u]) {
        if(x != f && x != hson[u]) {
            son[u].push_back(x);
        }
    }
}

void dfs2(int u, int f) {
    dfsn[u] = ++dfsc;
    dfso[dfsc] = u;
    if(u == hson[f]) {
        top[u] = top[f];
    } else top[u] = u;
    if(hson[u]) dfs2(hson[u], u);
    for(int x : son[u]) {
        dfs2(x, u);
    }
}

void split(int u, int v, vector<pii> &vec) {
    vec.clear();
    while(top[u] != top[v]) {
        if(dep[top[u]] > dep[top[v]]) swap(u, v);
        vec.push_back({dfsn[top[v]], dfsn[v]});
        v = fa[top[v]];
    }
    if(dep[u] > dep[v]) swap(u, v);
    vec.push_back({dfsn[u], dfsn[v]});
    sort(all(vec));
}

vector<int> st[N << 2];

void update(int l, int r, int x, int p = 1, int L = 1, int R = n) {
    if(l <= L && R <= r) {
        st[p].push_back(x);
        return;
    } else {
        int mid = (L + R) / 2;
        if(l <= mid) update(l, r, x, p << 1, L, mid);
        if(r > mid) update(l, r, x, p << 1 | 1, mid + 1, R);
    }
}

int query(int x, int p = 1, int L = 1, int R = n) {
    while(!st[p].empty() && st[p].back() < nowmin) st[p].pop_back();
    int ret = st[p].empty() ? inf : st[p].back();
    if(L == R) return ret;
    int mid = (L + R) / 2;
    if(x <= mid) return min(ret, query(x, p << 1, L, mid));
    else return min(ret, query(x, p << 1 | 1, mid + 1, R));
}


int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    cin >> n >> k >> m;
    for(int i = 1; i < n; i++) {
        int u, v; cin >> u >> v;
        edge[u].push_back(v);
        edge[v].push_back(u);
    }
    dfs1(1, 0);
    dfs2(1, 0);
    vector<pair<int, pii>> paths;
    for(int i = 1; i <= k; i++) {
        int p, q, v;
        cin >> p >> q >> v;
        paths.push_back({v, {p, q}});
    }
    sort(all(paths));
    reverse(all(paths));
    for(auto [val, pa] : paths) {
        auto [u, v] = pa;
        vector<pii> vec;
        split(u, v, vec);
        vector<pii> rvec;
        if(vec[0].first > 1) {
            update(1, vec[0].first - 1, val);
        }
        for(int i = 1; i < vec.size(); i++) {
            int L = vec[i - 1].second + 1;
            int R = vec[i].first - 1;
            if(L <= R) update(L, R, val);
        }
        if(vec.back().second < n) {
            update(vec.back().second + 1, n, val);
        }
    }
    int last = 0, op, x;
    while(m--) {
        cin >> op;
        if(op == 0) {
            paths.pop_back();
            nowmin = paths.empty() ? inf : paths.back().first;
        } else {
            cin >> x; x ^= last;
            last = query(dfsn[x]);
            if(last == inf) last = -1;
            cout << last << endl;
        }
    }
    return 0;
}

J. Papers

非常好题目,我连基础性质都没发现,直接死在了第一步。

官方题解:一定存在一种最优方案,使得其中没有任何一件 paper 是在做其他的 paper 中间开始的(中间的含义里不包括开始和结束)。可以假设存在这样的 paper,找到最后一个在其他 paper 中间开始的,把它开始时所有的 paper 向后平移,不影响总时间,甚至可能变得更优。

于是这个问题就被转化为了一个背包问题:已知有 m m m 个物品,第 i i i 个物品占据空间为 i i i ,开销为 a i a_i ai ,每次询问给出一个 n n n ,求装满大小正好为 n n n 的背包的最小开销。不过这里有一个问题: n n n 太大了,没办法直接做。不过由于物品的占据空间最大为 m m m ,所以当 n > m 2 n > m^2 n>m2 时可以贪心地选取性价比最高的那个,只需要背包做出 m 2 m^2 m2 内的答案即可。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) (x).begin(),(x).end()
typedef long long i64;
typedef pair<int, int> pii;

i64 dp[160410], a[410], n, m, q;

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    cin >> m;
    for(int i = 1; i <= m; i++) {
        cin >> a[i];
    }
    memset(dp, 0x3f, sizeof(dp));
    dp[0] = 0;
    for(int i = 0; i < m * m; i++) {
        for(int j = 1; j <= m; j++) {
            dp[i + j] = min(dp[i] + a[j], dp[i + j]);
        }
    }
    int mi = 1;
    for(int i = 1; i <= m; i++) {
        if(a[i] / (double)i < a[mi] / (double)mi) mi = i;
    }
    cin >> q;
    while(q--) {
        i64 n; cin >> n;
        i64 ans = 0;
        if(n > m * m) {
            i64 t = ((n - (m * m + 1)) / mi) + 1;
            ans += t * a[mi];
            n -= t * mi;
        }
        ans += dp[n];
        cout << ans << endl;
    }
    return 0;
}

AHM 官方题解

这场比赛的题解不太容易找到,我把剩下三题 (AHM) 的题解截图附在底下,不过我自己不打算写了。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值