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 的其实挺烂的,最近感觉写题动力不足呢,水平也上不去,感觉现在已经是半退役状态了。。。