2024ICPC武汉邀请赛复盘/补题记录 (7/13) (BDEFIKM)

Codeforces gym 链接 : https://codeforces.com/gym/105143
在这里插入图片描述
最意难平的一次经历,详见 https://www.zhihu.com/question/648600132/answer/3485823785

人生就是不断喜悦与不断遗憾的交替啊。

一些总结与技巧

  • 树的直径的一些性质:
    • 直径两端点一定是两个叶子节点
    • 若树上所有边边权均为正,则树的所有直径中点重合
    • 有两颗分离的树 A , B A, B A,B ( u , v ) (u, v) (u,v) 是树 A 的一条直径, ( x , y ) (x, y) (x,y) 是树 B B B 的一条直径,在 A , B A, B A,B 间任意连一条边, ( u , v , x , y ) (u, v, x, y) (u,v,x,y) 两两组成的 6 6 6 条路径中必有至少一条是新树的一条直径。
      • 特别地,若树 B B B 只有一个点 w w w ( u , v ) , ( u , w ) , ( v , w ) (u, v), (u, w), (v, w) (u,v),(u,w),(v,w) 中至少有一个是新树的一条直径。

I. Cyclic Apple Strings

给定一个长度为 n n n 的仅由 0 0 0 1 1 1 组成的字符串 s s s 。我可以进行如下操作任意次:选择 s s s 的一个子串和一个正整数 k k k ,并将子串向左循环移动 k k k 位。需要求出将 s s s 变为有序的所需的最小操作次数。

统计不在开头的连续的 0 0 0 的段数即可。

B. Countless Me

一个大小为 n n n 的数组,在不改变其总和、所有数字都是非负整数的前提下,随意进行调整,求 a 1   ∣   a 2   ∣   …   ∣   a n a_1\,|\,a_2\,|\,\dots\,|\,a_n a1a2an 的最小值。

考虑从高位向低位遍历:若遍历到了代表 2 i 2^i 2i 的这一位,考虑剩下的 s u m sum sum 能不能全部放到更低的那些位去,如果可以,答案的 2 i 2^i 2i 这一位为 0 0 0 ,否则设为 1 1 1 ,并将 s u m sum sum 尽可能多地填到这一位。

#include<bits/stdc++.h>
using namespace std;
long long x, ans, sum;
int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    long long n; cin >> n;
    for(int i = 0; i < n; i++)
        cin >> x, sum += x;
    for(int i = (1 << 30); i > 0; i >>= 1)
        if(sum > (i - 1) * n)
            ans |= i, sum -= i * min(n, sum / i);
    cout << ans << endl;
    return 0;
}

K. Party Games

n n n 个整数 1 , 2 , 3 , … , n 1, 2, 3, \dots, n 1,2,3,,n 从左到右顺序排成一行,若剩下的整数的异或和不为 0,当前操作者移走这一行整数中最左边的数或最右边的数,并不改变其余数字的顺序。若当前行动者无法操作,那么其输掉游戏。

我们知道
⊕ i = 1 n i = { n n   m o d   4 = 0 1 n   m o d   4 = 1 n + 1 n   m o d   4 = 2 0 n   m o d   4 = 3 \mathop\oplus\limits_{i = 1}^{n} i = \begin{cases} n & n \bmod 4 = 0 \\ 1 & n \bmod 4 = 1 \\ n + 1 & n \bmod 4 = 2 \\ 0 & n \bmod 4 = 3 \end{cases} i=1ni= n1n+10nmod4=0nmod4=1nmod4=2nmod4=3

异或和为 1 1 1 n n n 时,先手拿走 1 1 1 n n n ,先手胜;异或和为 0 0 0 时,后手胜;异或和 n + 1 n + 1 n+1 时, n n n 是偶数, n + 1 = n ⊕ 1 n + 1 = n \oplus 1 n+1=n1 ,先手和后手拿走两端的 1 1 1 n n n ,后手胜。

F. Custom-Made Clothes

谭老师看了看题:这个东西我在 leetcode 上见过,秒切。WA 了两次,具体记不得了,似乎是因为下标起点搞错了。

有一个 n × n n \times n n×n ( 1 ≤ n ≤ 1000 ) (1 \le n \le 1000) (1n1000) 的正整数方阵,所有元素 a i , j a_{i,j} ai,j 满足 1 ≤ a i , j ≤ n × n 1 \le a_{i,j} \le n \times n 1ai,jn×n ,且每一个元素都大于等于其上方元素与左方元素(如果有的话)。每次可以询问某个位置的 a i , j a_{i,j} ai,j 是否不大于 x x x 。需要使用不超过 50000 50000 50000 次询问找出矩阵中 n × n n \times n n×n 个元素从大到小排序后的第 k k k 个元素的值。

考虑二分。 50000 50000 50000 大约等于 2.5 n log ⁡ ( n × n ) 2.5 n \log(n \times n) 2.5nlog(n×n) ,其中 n × n n \times n n×n 是二分的值域。有没有什么办法用 2.5 n 2.5n 2.5n 次询问得到小于 m i d mid mid 的数字个数呢?以 m i d mid mid 为界,把矩阵分成两块,沿着分割线有大概 2 n 2n 2n 个元素,因此只要沿着分割线询问即可。

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

// return aij <= x
bool ask(int i, int j, int x) {
    if(j == 0) return 1;
    cout << "? " << i << ' ' << j << ' ' << x << '\n';
    cout.flush(); cin >> x; return x;
}

int main() {
    int n, k; cin >> n >> k;
    int L = 1, R = n * n;
    while(L < R) {
        int mid = (L + R) / 2, cnt = 0;
        // cnt counts how many aij > mid
        for(int i = 1, j = n; i <= n;) {
            int x = ask(i, j, mid);
            if(!x) {j--;}
            else {cnt += n - j; i++;}
        }
        if(cnt < k) R = mid;
        else L = mid + 1;
    }
    cout << "! " << L << '\n';
    return 0;
}

D. ICPC

谭老师很快就出了思路,但好像是有些不完全,我参与讨论了一会儿之后谭老师完善了思路。样例解释非常好,调了一会儿就过了。赛后写复盘的时候我发现我不会做这题了,这也是这篇记录2025年才发出的重要原因。

在长桌上有 n n n 个座位排成一排,其中从左至右的第 i i i 个座位上放有一道份量为 a i a_i ai 的菜品。幽幽子初始位于第 s s s 个座位,并且在每秒结束时能够移动至任意一个与当前座位相邻的座位(她也可以待在原座位不动)。任何幽幽子到达过的座位上的菜品都会被她吃掉。对于所有满足 1 ≤ s ≤ n , 1 ≤ t ≤ 2 n 1 \le s \le n, 1 \le t \le 2n 1sn,1t2n 的正整数 s , t s, t s,t,求出幽幽子从第 s s s 个座位出发移动 t t t 秒时能吃掉的菜品的份量总和的最大值。 1 ≤ n ≤ 5000 1 \le n \le 5000 1n5000

需要求的矩阵的大小为 2 n 2 2n^2 2n2 ,也就是说只能 O ( s i z e ) O(size) O(size) 地求出这个矩阵。

先假设最后一步幽幽子是向右移动的,令 d i , j d_{i, j} di,j 代表从位置 i i i 出发走最多 j j j 步且最后一步是向右移动的条件下可以获得的最大价值。显然中途改变方向超过一次是不优的,那么总共只有两种情况:先向左再向右,或者一直向右。一直向右的情况可以用前缀和 O ( 1 ) O(1) O(1) 求出。如果先向左 k k k 步再向右 j − k j - k jk 步,那么实际上这和从 i − k i - k ik 点出发向右走 j − k j - k jk 步没有区别。但是需要注意一点,上述 k k k 不可大于 j 2 \frac{j}{2} 2j,否则从 i − k i - k ik 点出发向右走 j − k j - k jk 步吃到的食物是先向左 k k k 步再向右 j − k j - k jk 步吃到食物的子集,即,会让答案偏小。但是你会发现,这种情况会在假设最后一步向左移动时更好地覆盖到。所以,可以大胆地写下 d i , j = max ⁡ ( d i − 1 , j − 1 , ∑ t = i i + j a t ) d_{i, j} = \max(d_{i - 1, j - 1}, \sum_{t = i}^{i + j}a_t) di,j=max(di1,j1,t=ii+jat) 的转移式。这里有一个前缀和一样的东西,可能成为 d i , j d_{i, j} di,j 答案的段的集合是 S i , j S_{i, j} Si,j 实际是 S i − 1 , j − 1 ∪ { [ i , i + j ] } S_{i - 1, j - 1} \cup \{[i, i + j]\} Si1,j1{[i,i+j]},因此可以 O ( 1 ) O(1) O(1) 递推。

然后假设最后一步幽幽子是向左移动的,转移式为 d i , j = max ⁡ ( d i + 1 , j − 1 , ∑ t = i − j i a t ) d_{i, j} = \max(d_{i + 1, j - 1}, \sum_{t = i - j}^{i}a_t) di,j=max(di+1,j1,t=ijiat)

上述转移式需要额外定义 a i = 0 a_i = 0 ai=0 i < 1 i < 1 i<1 i > n i > n i>n 时,这可以在前缀和查询中处理。

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

const int N = 5050;
int a[N], n;
i64 p[N], f1[N][N << 1], f2[N][N << 1];

inline i64 query(int L, int R) {
    if(R > n) R = n; if(L < 1) L = 1;
    return p[R] - p[L - 1];
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    cin >> n;
    for(int i = 1; i <= n; i++) {
        cin >> a[i];
        p[i] = p[i - 1] + a[i];
    }

    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= 2 * n; j++) {
            f1[i][j] = max(f1[i - 1][j - 1], query(i, i + j));
        }
    }
    for(int i = n; i >= 1; i--) {
        for(int j = 1; j <= 2 * n; j++) {
            f2[i][j] = max(f2[i + 1][j - 1], query(i - j, i));
        }
    }

    i64 ans = 0;
    for(int i = 1; i <= n; i++) {
        i64 temp = 0;
        for(int j = 1; j <= 2 * n; j++) {
            temp ^= (j * max(f1[i][j], f2[i][j]));
        }
        ans ^= (i + temp);
    }
    cout << ans << endl;

    return 0;
}

E. Boomerang

最后两个半小时,双开 E 题和 M 题,双双遗憾落败。E 题思考了很久,谭老师提出了一个想法:在一棵树上加上一个点之后,树的直径要么不变,要么就是新点和原来直径的一个端点。赵老师尝试证明了这个结论,于是开写。事实上这个结论是对的,见本文开头。中途写挂了一些东西,debug 很久才发现。最后一个 bug 是 4 小时 58 分发现的爆 int,谭老师改了一下,但是因为服务器崩溃未能交上,痛失金牌,万分遗憾。

在本题中,真相能够反驳谣言的充分必要条件是,在某一时刻 t t t,真相树的直径开始大于等于谣言树的直径。这样,你就可以选定辟谣开始点 r ′ r' r t t t 时刻的谣言树的直径中点。即,只要求出每一时刻谣言树的直径即可。每一时刻的谣言树的直径不会减少,只会增加,因此可以二分/双指针求出从 t 0 t_0 t0 开始辟谣的最短时间。

#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 = 2e5 + 10;
vector<int> edge[N], add[N];
i64 r, t0, n, d[N * 2], dep[N], fa[N][20];

void dfs(int u, int f) {
    dep[u] = dep[f] + 1;
    add[dep[u]].push_back(u);
    fa[u][0] = f;
    for(int i = 1; i < 20; i++) {
        fa[u][i] = fa[fa[u][i - 1]][i - 1];
    }
    for(int x : edge[u]) {
        if(x != f) {
            dfs(x, u);
        }
    }
}

int lca(int u, int v) {
    if(dep[u] > dep[v]) swap(u, v);
    for(int i = 0, t = dep[v] - dep[u]; i < 20; i++) {
        if((1 << i) & t) v = fa[v][i];
    }
    if(u == v) return u;
    for(int i = 19; i >= 0; i--) {
        if(fa[u][i] != fa[v][i]) {
            u = fa[u][i];
            v = fa[v][i];
        }
    }
    return fa[u][0];
}

i64 dis(int u, int v) {
    return dep[u] + dep[v] - 2 * dep[lca(u, v)];
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    cin >> n;
    for(int i = 1; i < n; i++) {
        int u, v; cin >> u >> v;
        edge[u].push_back(v);
        edge[v].push_back(u);
    }
    cin >> r >> t0;
    dep[0] = -1; dfs(r, 0);
    pii p = {r, r}; i64 nowd = 0;
    for(int i = 1; i <= n; i++) {
        for(int x : add[i]) {
            i64 w1 = dis(x, p.first);
            i64 w2 = dis(x, p.second);
            if(w1 > nowd) {
                p = {x, p.first};
                nowd = w1;
            }
            if(w2 > nowd) {
                p = {x, p.second};
                nowd = w2;
            }
        }
        d[i] = nowd;
    }
    for(int i = n + 1; i <= 2 * n; i++) {
        d[i] = d[n];
    }
    for(i64 k = 1, ans = 2 * n; k <= n; k++) {
        while(2LL * k * (ans - t0 - 1) >= d[ans - 1]) ans--;
        cout << ans << ' ';
    }
    return 0;
}

M. Merge

因为只能将两个相差 1 1 1 的数字合并,因此偶数一定不可能合成,非 1 1 1 的奇数可以合成。一个奇数可以拆成一个奇数和一个偶数,奇数可以再拆分。但因为只有一个可以递归拆分,因此只会递归 O ( log ⁡ x ) O(\log x) O(logx) 次数。可以发现,由于拆分约等于除以 2 2 2,一个数字拆分到底后,其组成部分几乎各不相同,除了一个例外: 5 5 5 可以拆分成 2 + 2 + 1 2 + 2 + 1 2+2+1,因此在尝试递归拆分时,对于这种情况需要特殊判定。赛场上我们一直没过这题就是这个原因。

但这个问题其实比较好发现,只要测试一下一个 1 1 1 和一个 2 2 2 就可以测试出程序的问题。另外就是第三个样例也算是一个提示。我们看见是 TLE 判定就一直在测试大数据,没有想到是递归出了问题导致死循环。总之,赛场上不能急,大数据和简单数据都测一测才对。

下面的程序中,我为了保险, x = 1 , 3 , 5 x = 1, 3, 5 x=1,3,5 的情况都特判了一下。

#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;

unordered_map<i64, int> mp;
#define mycount(x) (mp.count(x) && mp[x] != 0)
i64 a[200005];
vector<i64> ans;

bool find(i64 x) {
    if(x & 1) {
        if(x == 5) {
            if(mp[5]) return true;
            else if(mp[3] && mp[2]) return true;
            else if(mp[1] && mp[2] > 1) return true;
            return false;
        } else if(x == 3) {
            if(mp[3]) return true;
            else if(mp[2] && mp[1]) return true;
            return false;
        } else if(x == 1) {
            return (mp[1] != 0);
        }
        if(mycount(x)) return true;
        i64 t1 = x / 2, t2 = x / 2 + 1;
        return find(t1) && find(t2);
    } else {
        return mycount(x);
    }
}

void erase(i64 x) {
    if(x & 1) {
        if(x == 5) {
            if(mp[5]) mp[5]--;
            else if(mp[3] && mp[2]) mp[3]--, mp[2]--;
            else if(mp[1] && mp[2] > 1) mp[2] -= 2, mp[1]--;
            return;
        } else if(x == 3) {
            if(mp[3]) mp[3]--;
            else if(mp[2] && mp[1]) mp[2]--, mp[1]--;
            return;
        } else if(x == 1) {
            mp[1]--;
            return;
        }
        if(mycount(x)) {
            mp[x]--;
            return;
        }
        i64 t1 = x / 2, t2 = x / 2 + 1;
        erase(t1); erase(t2);
        return;
    } else {
        mp[x]--;
        return;
    }
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int n; cin >> n;
    for(int i = 0; i < n; i++) {
        cin >> a[i];
        mp[a[i]]++;
    }
    sort(a, a + n, greater<i64>());
    for(int i = 0; i < n; i++) {
        if(!mycount(a[i])) continue;
        else if(find(a[i] * 2 + 1)) {
            ans.push_back(a[i] * 2 + 1);
            erase(a[i] * 2 + 1);
        } else if(find(a[i] * 2 - 1)) {
            ans.push_back(a[i] * 2 - 1);
            erase(a[i] * 2 - 1);
        } else {
            ans.push_back(a[i]);
            erase(a[i]);
        }
    }
    cout << ans.size() << endl;
    for(i64 x : ans) cout << x << ' ';
    return 0;
}
<think>嗯,用户想要找2024ICPC昆明邀请赛的题目解析,但根据提供的引用内容,现有的资料只覆盖到2022年昆明的题目,比如引用[1]提到2020、2021年的ICPC和CCPC题目,以及引用[4]详细解析了2022昆明站的G题。不过用户询问的是2024年的,这可能还没有公开的解析,或者可能还没有举办。需要先确认是否存在2024年的比赛信息,或者是否用户可能记错了年份?比如,2024年的比赛可能尚未举行,或者相关信息还未被整理发布。 首先,检查提供的引用内容,发现最新的昆明站信息是2022年的,引用[4]讨论的是2022昆明G题。用户提到的2024年可能是个错误,或者他们希望提前获取信息。但通常比赛结束后才会有解析,所以需要告知用户目前没有相关资料,并建议他们关注官方渠道或后续的社区分享。同时,可以推荐现有的2022年昆明题解作为参考,帮助用户了解题型和考点,比如动态规划、期望计算等常见问题。 另外,用户可能对如何准备比赛感兴趣,可以建议他们分析历年题目类型,比如引用[1]提到的区域赛常考题型,或者引用[3]中提到的训练经验。需要确保回答准确,避免提供不存在的资源,同时提供有用的替代信息,帮助用户找到最近的可用资料。</think>关于2024ICPC昆明邀请赛题目解析,目前尚未有官方或主流平台发布完整的题目解析报告。根据历史信息推测,ICPC区域赛的题目解析通常会在赛后由参赛队伍、算法社区或教练团队逐步整理发布。以下是可参考的备赛建议和现有资源: --- ### 一、近期ICPC题型趋势参考 1. **动态规划与组合数学** 如2022年昆明站G题考察了**概率期望与递推关系**,需掌握马尔可夫链、状态转移方程的建立[^4] $$E = \sum_{i=1}^n \left( \frac{p_i}{1-p_i} \cdot \sum_{j \neq i} \frac{p_j}{1-p_i} \right)$$ 此类问题常需分析极限状态下的数学期望。 2. **数据结构优化** 近年区域赛常出现需要**线段树/树状数组维护区间性质**的题目,例如区间最值、历史版本查询等。 3. **图论与网络流** 包括最小割建模、分层图最短路等高级技巧,如2021年沈阳站曾出现网络流与二分答案结合的题目[^2]。 --- ### 二、获取解析的途径建议 1. **官方渠道** 关注ICPC官网及昆明站承办院校的赛事公告,解析可能通过**赛后题解报告会**发布。 2. **算法社区** - **Codeforces**:搜索标签`[ICPC Kunming 2024]` - **知乎/掘金**:技术博主常撰写详细题解(例:2022年G题推导过程) 3. **训练平台** 尝试在**Codeforces Gym**或**牛客竞赛**题库中查找昆明站模拟赛题。 --- ### 三、历届昆明站真题参考 若需练习类似题型,可参考2022年昆明站题目: - **G题(豆子操作期望)**:结合概率论与递推,需推导稳定状态下的位置关系 - **B题(几何构造)**:通过坐标系变换简化多边形切割问题 ```python # 示例:概率期望计算的代码框架 def calculate_expected_value(probabilities): n = len(probabilities) expected = 0.0 for i in range(n): pi = probabilities[i] term = pi / (1 - pi) if pi != 1 else 0 sum_other = sum(pj / (1 - pi) for j, pj in enumerate(probabilities) if j != i) expected += term * sum_other return expected ``` ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值