2024ICPC 全国邀请赛(武汉)题解 更新至 8 题

Preface

这场比赛是在今年上半年 vp 过一次的,当时是过了五个题,近两天打算重新拿出来单挑一把,结果成功的在前面的签到题卡住了,搞了老半天,最后勉勉强强还是五题,罚时直接吃屎了。

所有代码前面的火车头

#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <vector>
#include <set>
#include <queue>
#include <map>
#include <iomanip>
#define endl '\n'
#define int long long
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define rep2(i,a,b) for(int i=(a);i>=(b);i--)
using namespace std;
template<typename T>
void cc(vector<T> tem) { for (auto x : tem) cout << x << ' '; cout << endl; }
void cc(int a) { cout << a << endl; }
void cc(int a, int b) { cout << a << ' ' << b << endl; }
void cc(int a, int b, int c) { cout << a << ' ' << b << ' ' << c << endl; }
void fileRead() { freopen("D:\\AADVISE\\cppvscode\\CODE\\in。txt", "r", stdin); }
void kuaidu() { ios::sync_with_stdio(false), cin。tie(0), cout。tie(0); }
inline int max(int a, int b) { if (a < b) return b; return a; }
inline int min(int a, int b) { if (a < b) return a; return b; }
void cmax(int& a, const int b) { if (b > a) a = b; }
void cmin(int& a, const int b) { if (b < a) a = b; }
using PII = pair<int, int>;
using i128 = __int128;


//--------------------------------------------------------------------------------
const int N = 1e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;

//--------------------------------------------------------------------------------

Problem I. 循环苹果串

题意就是能够任意挪动子串任意次,让原本的字符串(只有 0,1)挪动之后变成有序的,求出来最少的挪动次数。
例如 00110101,我们显而易见肯定是每一次都把隔开的 1 给合并到一起,这样操作的次数最后一定会是最少的。
所以我们只需要找到 1 的联通块个数就好了。
有个小细节就是如果字符串的末尾是 1,那这个联通块就不算入计数。


signed main() {
    kuaidu();
    T = 1;
    //cin >> T;
    while (T--) {
        string s; cin >> s;
        s = '0' + s;
        s = s + '0';
        int len = s。size();
        int cnt = 0;
        rep(i, 1, len - 1) {
            if (s[i] != s[i - 1] and s[i] == '0' and s[i - 1] == '1')
                cnt++;
        }
        if (s[len - 2] == '1')
            cout << cnt - 1 << endl;
        else cout << cnt << endl;

    }
    return 0;
}
/*


*/

Problem K. 派对游戏

题意大致如下:
有 n 个整数 1, 2, 3, ... , n 从左到右顺序排成一行,某个我和萍琪派将依次尝试进行如下操作:
• 若剩下的整数的异或和不为 0,移走这一行整数中最左边的数或最右边的数,并不改变其余数字的
顺序。若当前行动者无法操作,那么其输掉游戏。

对于这种,直接打表即可,(以下用 0 和 1 代表自己是输,赢)通过打表后我们发现,
n=1,2,3,4,5,6,7,8 时,答案依次是 1,0,0,1,1,0,0,1,1...
除去第一个是 1 之外是一个 0,0,1,1 的循环节,根据这个直接输出就好了。

signed main() {
    kuaidu();
    T = 1;
    cin >> T;
    while (T--) {
        cin >> n;
        if (n == 1) {
            cout << "Fluttershy" << endl;
            continue;
        }
        n -= 1;
        n %= 4;
        if (n == 2 or n == 1) cout << "Pinkie Pie" << endl;
        else cout << "Fluttershy" << endl;
    }
    return 0;
}
/*


*/

Problem B. 无数的我

稍微吃屎的一集,一开始想的直接取最小和最大的执行 n 次操作,并且非常弱智的直接交了一发 WA 了,之后改变做法,觉得这种没有任何道理的绝对不对,之后想了想,操作应该是可以转化成总和 sum 一定,去分配给 n 个数字。所以我们就直接二进制从高位到低位枚举 i,当这些 n 个数字的第 i 位全是 0 的时候,后面全是 1 的和是大于 sum,则说明可以在当前 i 位都不放 1,否则就说明当前这一位必须要放 1,既然放了 1,我们就不妨 i 位能放多少 1 就放多少 1,按照这个思路贪下去就好了。

int kuai(int a, int b) {
    int l = 1;
    while (b) { if (b % 2)l = l * a; a = a * a; b /= 2; }
    return l;
}
signed main() {
    kuaidu();
    T = 1;
    //cin >> T;
    while (T--) {
        cin >> n;
        int sum = 0;
        rep(i, 1, n) {
            int a; cin >> a; sum += a;
        }
        int ans = 0;
        rep2(i, 32, 0) {
            if (i == 0) {
                if (sum) ans |= 1ll;
                break;
            }
            if (sum > kuai(2, i) * n - n) {
                //如果大于,则说明当前这一位必须要有1,就尽量取满,sum / kuai(2, i)和 n要取一个min
                sum -= min(sum / kuai(2, i), n) * kuai(2, i);
                ans |= 1ll << i;
            }
            if (!sum) break;
        }
        cout << ans << endl;
    }
    return 0;
}
/*


*/

Problem F. 订制服装

最吃屎的地方来了,这个题当时是队友写的,自己没太怎么思考,当时直接去开 E 了,结果自己现在被这题差点直接卡死。

首先我们可以在 log(n*n)时间内算出来某个点是谁,这个是 20 次。

当时写了一个巨假的做法,是去找到左下角的点是谁,然后逐渐往右上走,这样可以划分一半的区域是大于还是小于,剩下的一半区域依旧这样做,这样算下来大概是 log(n*n)log(n*n)2*n ,(这里还算错了,实际是 8e5,算成了 8e4)所以开始赌徒模式了。但是在实现的时候由于我依赖于左下角点的权值,导致往右上走实现不太科学,再加上次数可能会超,所以卡死了。

下面是正解,我们应该二分权值,然后在这个地图上找有多少个大于他(小于他),这样子时间复杂度还会少了一个 log,这样才是正解。

//找x,y这个点和val的关系,最后返回的是个数
int dfs(int x, int y, int val) {
    if (x <= 0 || y > n) return 0;
    cout << "? " << x << " " << y << " " << val << endl; cout。flush();
    int t; cin >> t;
    if (t == 1) {
        return x + dfs(x, y + 1, val);
    }
    return dfs(x - 1, y, val);
}

int check(int val) {
    int tem = dfs(n, 1, val);
    return tem;
}

signed main() {
    kuaidu();
    T = 1;
    //cin >> T;
    while (T--) {
        cin >> n >> k;
        k = n * n - k + 1;
        int l = 0, r = n * n + 1;
        while (l + 1 != r) {
            int mid = l + r >> 1;
            if (check(mid) < k) l = mid;
            else r = mid;
        }
        cout << "! " << r << endl; cout。flush();
    }
    return 0;
}
/*


*/

Problem E. 回旋镖

这个题比较的有意思,首先我们能想到这个题一定和直径有关,我们假设如果$ k$ 是$ 1$ 的时候,那最后一定是选择树上的直径中心,当$ k$ 稍微大的时候,那比较倾向选择 $t0 \(时间生成的树的直径中心,不难想到找的直径和时间是有关系的,而且大概是满足某种单调性,当\) k\(逐渐大的时候,\)t $会逐渐变小。

之后可以想出来,我们可以枚举$ k$ 从$ n$ 到$ 1\(,\)t \(当前是\) t0$,当前的树是 $t0 \(时刻的树。如果当前的\) k$ 选择当前树的直径中心在(t-t0)的时间可以覆盖掉,那 \(ans[k]=t,k--。\)
直到无法覆盖的时候,那么 \(t++\),树的直径(可能)变化,再判断能不能覆盖。

中间有一个 \(trick\) 是关于直径的更新,\(t++\)之后,我们会多更新一层节点,仔细想想会发现,在一棵树上如果多了一个点 \(x\),对于直径的影响就是 $x $对直径的两个端点的距离可能会比直径大,所以我们只需要维护直径的两个端点就好了。

下面是 AC 代码加上少许注释

//--------------------------------------------------------------------------------
const int N = 2e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
vector<int> A[N];
int r, t0;
vector<int> ceng[N];
int dep[N];
int fa[N][27];
int ans[N];
//--------------------------------------------------------------------------------

void dfs(int x, int pa) {
    dep[x] = dep[pa] + 1;
    ceng[dep[x]]。push_back(x);
    fa[x][0] = pa;
    rep(i, 1, 20) fa[x][i] = fa[fa[x][i - 1]][i - 1];
    for (auto y : A[x]) {
        if (y == pa) continue;
        dfs(y, x);
    }
}
//求x和y点的lca
int lca(int x, int y) {
    if (x == y) return x;
    if (dep[x] < dep[y]) swap(x, y);
    rep2(i, 20, 0) {
        if (dep[fa[x][i]] < dep[y]) continue;
        x = fa[x][i];
    }
    if (x == y) return x;
    rep2(i, 20, 0) {
        if (fa[x][i] == fa[y][i]) continue;
        x = fa[x][i], y = fa[y][i];
    }
    return fa[x][0];
}
//求两点之间距离
int dis(int x, int y) {
    int t = lca(x, y);
    return dep[x] + dep[y] - dep[t] - dep[t];
}
signed main() {
    kuaidu();
    T = 1;
    //cin >> T;
    while (T--) {
        cin >> n;
        //存图
        rep(i, 1, n - 1) {
            int a, b; cin >> a >> b;
            A[a]。push_back(b);
            A[b]。push_back(a);
        }
        cin >> r >> t0;
        //一开始先选择r去预处理每一层的节点,方便下面更新
        dfs(r, 0);
        //q1和q2是直径端点
        int q1 = r, q2 = r;
        rep(i, 1, t0 + 1) {
            if (i + 1 <= n)
                for (auto x : ceng[i + 1]) {
                    if (dis(x, q1) > dis(q1, q2)) {
                        q2 = x;
                    }
                    if (dis(x, q2) > dis(q1, q2)) {
                        q1 = x;
                    }
                }
        }
        // cout << lca(6, 7) << endl;
        // cout << dis(6, 7) << endl;
        // cc(dep[6], dep[7], dep[2]);
        int ned = 1;//ned是需要的时间
        rep2(i, n, 1) {//i是枚举的k
            if (i * ned >= (dis(q1, q2) + 1) / 2) {
                ans[i] = ned;
                continue;
            }

            while (i * ned < (dis(q1, q2) + 1) / 2) {
                ned++;
                if (t0 + 1 + ned <= n)
                    for (auto x : ceng[t0 + 1 + ned]) {
                        if (dis(x, q1) > dis(q1, q2)) {
                            q2 = x;
                        }
                        if (dis(x, q2) > dis(q1, q2)) {
                            q1 = x;
                        }
                    }
            }
            ans[i] = ned;//ans是比t0多的时间,最后还会再加上t0

        }

        rep(i, 1, n) {
            cout << ans[i] + t0 << " ";
        }
        cout << endl;

    }
    return 0;
}
/*
8
1 2
1 4
1 5
3 6
2 3
4 7
7 8
2 1

*/

Problem D. 国际大胃王锦标赛

这个题说难不难,但说简单也不简单,我没做出来

首先根据题目的问法,我们显然是需要有一个 n 方的做法可以直接把 F 数组求出来。但是如果单纯的去想 dp 式子个人感觉不是太好想出来。这个题最后是通过前后缀优化做出来的。

我们显然想到最优的做法一定是要么不拐弯,要么就拐一次弯。
我们先设 g[s][t]数组是在 s 坐标走不超过 t 秒的最大值(不拐弯)
F[s][t]是由以下 g 数组推出来的
max{g[s][t],g[s-1][t-1],g[s-2][t-2],g[s-3][t-3],。。。,g[s+1][t-1],g[s+2][t-2],。。。}

因此 F[s][t]可以直接由 F[s-1][t-1]和 g[s][t]取 max
同时再倒着再来一遍就好了。

//--------------------------------------------------------------------------------
const int N = 5e3 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
int A[N];
int g[N][N + N];
int sum[N];
int dp[N][N + N];
//--------------------------------------------------------------------------------
// 1 2 3 4 5 6 7 8 9

signed main() {
    // freopen("D:\\AADVISE\\cppvscode\\CODE\\in。txt", "r", stdin);
    // freopen("D:\\AADVISE\\cppvscode\\CODE\\out。txt", "w", stdout);
    kuaidu();
    T = 1;
    //cin >> T;
    while (T--) {
        cin >> n;
        rep(i, 1, n) {
            cin >> A[i];
            sum[i] = sum[i - 1] + A[i];
        }
        rep(i, 1, n) {
            rep(j, 1, n + n) {
                int l = max(1ll, i - j);
                int r = min(n, i + j);
                g[i][j] = max(sum[i] - sum[l - 1], sum[r] - sum[i - 1]);
            }
        }

        rep(i, 1, n) {
            rep(j, 1, n + n) {
                cmax(dp[i][j], dp[i - 1][j - 1]);
                // cmax(dp[i][j], dp[i][j - 1]);
                cmax(dp[i][j], g[i][j]);
                // cmax(dp[i][j], suf[i][j]);
                // cmax(dp[i][j], pre[i][j]);
            }
        }

        rep2(i, n, 1) {
            rep(j, 1, n + n) {
                cmax(dp[i][j], dp[i + 1][j - 1]);
                cmax(dp[i][j], g[i][j]);
            }
        }

        int ans = 0;

        rep(i, 1, n) {
            int tem = 0;
            rep(j, 1, n + n) {
                tem ^= (j * dp[i][j]);
            }
            ans ^= (i + tem);
        }

        cc(ans);
    }
    return 0;
}
/*
6
7 2 1 3 0 8


*/

Problem M. 合并

这个题其实想到了一点就比较的 ez 了,首先能够感觉到的是,合并的次数绝对不会很多,其实后来我算是误打误撞想到的思路,当时在发呆想如果合并的数字需要差值是 2 会怎么样,然后就发现这样的话奇数就都没有用了。然后就莫名其妙联想到差值为 1 的话,那么合并出来的新数将会是奇数。于是我们的思路便大差不差了。

可以去找当前序列中最大的偶数 x,找 x+1 存不存在,x-1 存不存在,存在就可以合并,不存在就找下一个偶数接着进行这样的操作。

找 x+1 存不存在,因为 x+1 是奇数,所以他是可以合并操作得到的,去找寻 x/2 和 x/2+1,这样子是 log 级别的。

其实思路不算太难,但是代码中间有小细节问题需要处理好才可以。时间复杂度上,会有 log(1e18)*n,在加上map会再多出来一个log,会爆掉,得卡常。 我们直接先找 x/2 存不存在,因为他一定会是偶数,所以我们直接 o1 判断出来,如果他不存在的话就直接 return false 了,这样说不定就不用找(x/2+1),会更快。这个地方也是卡住我了好久的地方之一。另一个地方就是代码细节处理,在下方会有注释说明。


//--------------------------------------------------------------------------------
const int N = 2e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
vector<int> ans;
//mp是存数用的,mp3用来撤回的。
map<int, int> mp3, mp;
int A[N];
//--------------------------------------------------------------------------------

bool dfs(int x) {
    if (mp[x] > 0) {
        mp[x]--;
        mp3[x]++;
        return 1;
    }
    if ((x % 2 == 0) || (x == 1)) return 0;
    return (dfs(x / 2) and dfs(x / 2 + 1));
}

signed main() {
    // 记得注释
    // fileRead();
    kuaidu();
    T = 1;
    // cin >> T;
    while (T--) {
        cin >> n;
        rep(i, 1, n) {
            int a; cin >> a;
            A[i] = a;
            mp[a]++;
            // mp2[a]++;
        }
        sort(A + 1, A + n + 1, [&](int a, int b) {return a > b; });
        rep(i, 1, n) {
            int a = A[i];
            if (mp[a] <= 0) continue;
            //要先减去,否则会对dfs函数产生影响,这是第二个卡住了我n年的地方。
            mp[a]--;
            mp3。clear();
            if (dfs(a + 1)) {
                mp[a + a + 1]++;
                continue;
            }
            //如果return false的话就把之前删除的恢复回来
            for (auto k : mp3) mp[k。first] += k。second;
            mp3。clear();
            if (dfs(a - 1)) {
                mp[a + a - 1]++;
                continue;
            }
            for (auto k : mp3) mp[k。first] += k。second;
            mp3。clear();
            mp[a]++;
        }

        for (auto [a, b] : mp) {
            if (a <= 0) continue;
            while (b > 0) {
                b--;
                ans。push_back(a);
            }
        }

        // sort(ans。begin(), ans。end(), [&](int a, int b) { return a > b; });
        cout << ans。size() << endl;
        for (auto x : ans) { cout << x << " "; }
    }
    return 0;
}

Problem C. 书包与最长上升子序列

这个题是想不出来看着蒋老师的 vp 录像的代码搞明白的,在此膜拜一下,太厉害啦。

首先小于 10000 的时候直接特判就好了。当比较大的时候,我们采取用 012 去给他补满,假设这个输入的数 x 是一个 12 的倍数,我们如果能用最短的长度凑满呢?
首先是基本的
012012012012012012012012012012
但是在这其中每多一个 012,增长的贡献就会很大,所以我们会在中间插入若干个 2,例如变成
01201201201222012012201222201201201222
至于该怎么插入,插入多少,就写在代码里了。
那如果 x 不是 12 的倍数呢?
前面暴力枚举三个数字,使得 x 减去他们之后是 12 的倍数就好了。


signed main() {
    //记得注释
    // fileRead();
    kuaidu();
    T = 1;
    //cin >> T;
    while (T--) {
        cin >> n;
        if (n == 0) {
            cout << 0 << endl;
            continue;
        }
        if (n <= 100000) {
            rep(i, 1, n) cout << 1;
            continue;
        }
        string s = "";
        rep(i, 3, 9) rep(j, i + 1, 9) rep(p, j + 1, 9) {
            int val = i * 100 + j * 10 + p;
            if (n % 12 != 0 and (n >= val) and (n - val) % 12 == 0) {
                s += i + '0';
                s += j + '0';
                s += p + '0';
                n -= val;
            }
        }
        int num = n / 12;

        if (num == 1) {
            s = s + "012";
        }
        else {

            m = 1;
            //这是所有的012产生贡献的式子表达
            while (m * (m + 1) * (m + 2) / 6 <= num) m++;
            m -= 1;
            num -= m * (m + 1) * (m + 2) / 6;
            //要倒着枚举,这样更大。从大往下处理。
            rep2(i, m, 1) {
                //这个式子是第i位的2能够提供的贡献
                int x = i * (i + 1) / 2;
                用num除以x就是我们贪心个数的极限
                A[i] = num / x;
                num -= x * A[i];
            }

            rep(i, 1, m) {
                s += "012";
                rep(j, 1, A[i]) s += "2";
            }
        }

        cout << s << endl;

    }
    return 0;
}
/*


*/

PostScript

这场自己 v 的其实挺烂的,最近感觉写题动力不足呢,水平也上不去,感觉现在已经是半退役状态了。。。

posted @ 2024-11-11 20:32  AdviseDY  阅读(102)  评论(0编辑  收藏  举报