2024 ICPC National Invitational Collegiate Programming Contest, Wuhan Site

写在前面

补题地址:https://codeforces.com/gym/105143

正式赛全程犯大病打铜了呃呃,以下按个人向难度排序。

AIEEEEE!忍者为何!队长=san 实际战犯!罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罪罚罚罪罚罪罚罪罚罪罚罪罚罪罚

南无阿弥陀佛……不由得土下座……只得切断谢罪!庸碌大半生,邀请赛取我性命,再无意难平。撒由那拉!

「2024·ICPC·National·Invitational·Collegiate·Programming·Contest, Wuhan·Site 电子狂言俳句乱舞·殒命珞珈山上」#1 完

I

签到。

场上 dztle 大神一眼秒了。

首先忽略初始时就位于原串最右侧的 1。

发现最优的策略是每次将不位于字符串最右侧的、最右侧的连续一段 1 调整到字符串最右侧,于是仅需统计此时字符串中有多少段连续的 1 即可。

复制复制
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
//=============================================================
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::string s; std::cin >> s;
while (s.back() == '1') s.pop_back();
int cnt = 0;
for (int i = 0, lst = -2, len = s.length(); i < len; ++ i) {
if (s[i] == '1') {
if (lst != i - 1) ++ cnt;
lst = i;
}
}
std::cout << cnt;
return 0;
}

B

贪心。

赛时这题一直想怎么均分所有数想了一堆假做法挂了三发纯战犯。然后 dztle 看了一眼秒了,紫砂了。

首先发现 n 次操作实际上可以将整个数列调整成任意形态,于是仅需考虑如何分配 ai 得到最终答案的形态即可。

考虑枚举二进制位从高位向低位贪心,一个显然的想法是尽可能不让高位在答案中出现,于是考虑答案中是否可以不出现某位。

对于第 i(0ilogv) 位,若可以仅通过之后的若干位构造出 n 个和为 ai 的数,即满足 ain×(2i1),则该位是不必要的;否则必定有一个数该位为 1,则应尽可能地令所有数该位均为 1,从而减少使用之后若干位构造的负担。

按照上述思路贪心地分配即可。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
//=============================================================
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int n; std::cin >> n;
LL sum = 0, ans = 0;
for (int i = 1; i <= n; ++ i) {
int x; std::cin >> x;
sum += x;
}
for (LL i = 30; i >= 0; -- i) {
LL j = (1ll << i);
if (sum <= 1ll * n * (j - 1)) continue;
ans += j;
sum -= 1ll * std::min(1ll * n, sum / j) * j;
}
std::cout << ans;
return 0;
}

K

找规律。

打表题,场上替 dztle 大神打了个表大神一眼秒了,本场唯一的贡献。

发现有:

i=1ni={n(nmod4=0)1(nmod4=1)n+1(nmod4=2)0(nmod4=3)

喜欢证明可以参考:https://www.cnblogs.com/Mychael/p/8633365.html

则当 n=0 时先手拿 n 必胜,n=1 时先手拿 1 必胜。

n=3 时先手无法操作必败,n=4 时先手无论如何操作,后手取走另一侧均会获胜,则先手必败。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
//=============================================================
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
// for (int i = 1, s = 0; i <= 100; ++ i) {
// s ^= i;
// std::cout << i << " " << s << "\n";
// }
int T; std::cin >> T;
while (T --) {
int n; std::cin >> n;
if (n % 4 == 0 || n % 4 == 1) std::cout << "Fluttershy\n";
else std::cout << "Pinkie Pie\n";
}
return 0;
}

F

交互,单调性。

这个矩阵的形态学名叫杨氏矩阵,在算法作业上见过并且当时发现了下述结论于是跟队友说了下,可能对场上出了本题有了一点贡献,本场唯 1.0001 的贡献。

发现对于某个权值 v,其在每一行的所有出现位置一定构成一段连续的区间。且其在第 i 行与第 i+1 行中出现的位置构成的区间 [li,ri],[li+1,ri+1] 一定满足:

li+1liri+1ri

即有:

(li+1li)(ri+1ri)

发现 l1ln 和数列 r1rn 均为非递减数列,构成了一个阶梯形,以该阶梯形为分界,左上为小于 v 的所有位置,右下为大于 v 的所有位置,则权值 v 的排名即为右下角位置的数量。

由阶梯形这个性质,求每种权值 v 的排名,可以考虑维护一个初始位于最后一行左侧的指针,倒序枚举每行,并维护指针使之指向第一个大于 v 的元素,即可统计每行有多少数大于 v 从而得到排名。发现该过程中指针仅会单调右移和上移 O(n) 次。

解决了对任意权值求排名的问题,仅需在外层加个二分答案枚举权值即可解决本题。值域为 O(n2) 级别,则总时间复杂度 O(nlogn) 级别。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
int n, k, ans;
//=============================================================
int query(int x_, int y_, int val_) {
std::cout << "? " << x_ << " " << y_ << " " << val_ << "\n";
std::cout.flush();
int ret; std::cin >> ret;
return ret;
}
bool check(int lim_) {
int s = 0;
for (int x = n, y = 1; x; -- x) {
while (y <= n && query(x, y, lim_) == 1) ++ y;
s += n - y + 1;
}
return s < k;
}
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
// std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> n >> k;
for (int l = 1, r = n * n; l <= r; ) {
int mid = (l + r) >> 1;
if (check(mid)) {
r = mid - 1;
ans = mid;
} else {
l = mid + 1;
}
}
std::cout << "! " << ans;
return 0;
}

D

枚举,递推。

赛时转移顺序反了多一个 log 而且难写的一批,输!

发现对于所有移动方案,至多仅会在途中进行一次转向。考虑记 fs,t,0/1 表示从 s 开始移动朝向左侧/右侧经过了 t 时间可以获得的最大价值,仅需枚举起点大力移动将经过的位置求和即可。

然后考虑转向。考虑记 gs,t,0/1 表示从 s 开始移动,经过了 t 时间,结束移动时朝向左侧/右侧时,可以获得的最大价值,则题目所求 Fi,j 即为 max(gi,j,0,gi,j,1)。先考虑转向后朝向右侧的情况,设出发后向左移动 t 秒进行转向,发现此时获得的贡献实际上即为 fst,tt,1。即有:

gs,t,1=max0t2×n(fst,tt,1)

类似地,有:

gs,t,0=max0t2×n(fs+t,tt,0)

发现上式相当于枚举每条对角线,在对角线上对 f 取前缀最大值即为 g。又发现在对角线上的转移均是从 t 较小的状态转移到 t 较大的状态,于是直接按列枚举进行转移即可。

总时间复杂度 O(n2) 级别。

代码中并没有显式地定义 g 数组而是直接对 f 进行了前缀最大值的 DP。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 5e3 + 10;
//=============================================================
int n, a[kN];
LL f[kN][2 * kN][2];
//=============================================================
void Init() {
for (int s = 1; s <= n; ++ s) {
f[s][0][0] = f[s][0][1] = a[s];
for (int t = 1, p = s - 1, q = s + 1; t <= 2 * n; ++ t) {
f[s][t][0] = f[s][t - 1][0], f[s][t][1] = f[s][t - 1][1];
if (p >= 1) f[s][t][0] += a[p], -- p;
if (q <= n) f[s][t][1] += a[q], ++ q;
f[s][t][0] = f[s][t][0], f[s][t][1] = f[s][t][1];
}
}
}
void DP() {
for (int t = 1; t <= 2 * n; ++ t) {
for (int s = 1; s <= n; ++ s) {
f[s][t][0] = std::max(f[s][t][0], f[s + 1][t - 1][0]);
f[s][t][1] = std::max(f[s][t][1], f[s - 1][t - 1][1]);
}
}
}
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> n;
for (int i = 1; i <= n; ++ i) std::cin >> a[i];
Init();
DP();
LL ans = 0;
for (int s = 1; s <= n; ++ s) {
LL sum = 0;
for (int t = 1; t <= 2 * n; ++ t) {
sum ^= (1ll * t * std::max(f[s][t][0], f[s][t][1]));
// std::cout << " (" << f[s][t][0] << " " << f[s][t][1] << ") ";
// std::cout << std::max(f[s][t][0], f[s][t][1]) << " ";
}
ans ^= 1ll * s + sum;
// std::cout << "\n";
}
std::cout << ans;
return 0;
}
/*
5
6 1 0 2 4
g 3 4 1
g 2 3 1
g 1 2 1
g 3 4 0
g 4 3 0
g 5 2 0
*/

E

树。

赛时居然他妈忘了怎么维护直径,鉴定结果为被车撞了纯纯失忆症。

对于某个确定的速度,为了最快地覆盖一棵树,一个显然的想法是辟谣的根节点应当放在直径的中点上,使之与所有节点距离的最大值最小。

于是一个显然的想法是从 t0 开始枚举辟谣完成的时刻 t,并确定此时树的直径长度 len,则所满足 (tt0)×vlen2 的速度 v 进行辟谣均可以覆盖所有谣言。满足上述不等式的 v 是不增的,则仅需考虑如何快速维护直径,即可双指针枚举得到所有速度的最早辟谣完成时刻。

发现 t 时刻时树中新增节点即为深度为 t 的所有节点,考虑枚举所有节点 x 并依次将它们加入树中并检查是否会替代直径 (u,v),即找到距离 fax 最远的节点 y,比较路径 (x,y)(u,v) 的长度。众所周知的结论是距离树上某节点最远的点一定为当前直径的端点,即加入 x 后新的直径仅可能是 (u,v),(u,x),(x,v) 三者之一,套路地求 lca 即可得到路径长度。

总时间复杂度 O(nlogn) 级别。

如果 WA18:完全覆盖整棵树的时刻可能大于 n,上界大概是 O(1.5n),在树为一条链且 t0=n 时达到。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2e5 + 10;
//=============================================================
int n, m, rt, t0;
int edgenum, head[kN], v[kN << 1], ne[kN << 1];
int fa[kN], sz[kN], dep[kN], son[kN], top[kN];
std::vector<int> nodes[kN];
int nowlen, diameter[2], ans[kN];
//=============================================================
inline int read() {
int f = 1, w = 0; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Add(int u_, int v_) {
v[++ edgenum] = v_;
ne[edgenum] = head[u_];
head[u_] = edgenum;
}
namespace Cut {
void Dfs1(int u_, int fa_) {
sz[u_] = 1;
fa[u_] = fa_;
dep[u_] = dep[fa_] + 1;
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
if (v_ == fa_) continue;
Dfs1(v_, u_);
sz[u_] += sz[v_];
if (sz[v_] > sz[son[u_]]) son[u_] = v_;
}
}
void Dfs2(int u_, int top_) {
top[u_] = top_;
if (son[u_]) Dfs2(son[u_], top_);
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
if (v_ == fa[u_] || v_ == son[u_]) continue;
Dfs2(v_, v_);
}
}
int Lca(int u_, int v_) {
for (; top[u_] != top[v_]; u_ = fa[top[u_]]) {
if (dep[top[u_]] < dep[top[v_]]) std::swap(u_, v_);
}
return dep[u_] < dep[v_] ? u_ : v_;
}
}
void AddNode(int u_) {
int len[2] = {0};
for (int i = 0; i < 2; ++ i) {
int lca = Cut::Lca(u_, diameter[i]);
len[i] = dep[u_] + dep[diameter[i]] - 2 * dep[lca] + 1;
}
if (std::max(len[0], len[1]) <= nowlen) return ;
diameter[len[0] >= len[1]] = u_;
nowlen = std::max(len[0], len[1]);
}
void Init() {
n = read();
for (int i = 1; i < n; ++ i) {
int u_ = read(), v_ = read();
Add(u_, v_), Add(v_, u_);
}
rt = read(), t0 = read();
Cut::Dfs1(rt, 0), Cut::Dfs2(rt, rt);
for (int i = 1; i <= n; ++ i) nodes[dep[i] - 1].push_back(i);
diameter[0] = diameter[1] = rt;
nowlen = 1;
for (int i = 1; i <= t0 - 1; ++ i) {
for (auto u_: nodes[i]) AddNode(u_);
}
}
bool check(int speed_, int time_) {
return 1ll * speed_ * (time_ - t0) >= nowlen / 2ll;
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
Init();
for (int i = t0, k = n; i <= 2 * n; ++ i) {
if (i <= n) for (auto u_: nodes[i]) AddNode(u_);
while (k && check(k, i)) ans[k --] = i;
}
for (int i = 1; i <= n; ++ i) std::cout << ans[i] << " ";
return 0;
}

M

贪心。

因为是字典序比较,于是仅需关注在当前数列中如何合成出最大的一个数。

发现每次合并都需要一个奇数和一个偶数并合成一个新的奇数,则考虑将所有偶数降序排序,枚举当前合成最大的数的最后一步中所需的偶数 x,检查能否合成出奇数 x+1x1 即可。显然,若无法得到这两个奇数,说明无法合成大于等于 2×x1 的数,则偶数 x 一定不会参与之后的合并。

又发现合并得到一个数 v 所需的原数列中的数实际上可以递归求得,且个数至多为 O(logv) 级别,于是可以考虑递归地检查能否得到某个数。具体地:

  • 若原数列中存在 v,则合法。
  • 否则若 v 为偶数或 v 为 1,则 v 无法被合成,非法。
  • 否则若 v 为奇数,递归地检查能否同时得到 v2v2+1 即可。

特别注意上述第三条中的条件同时,即最终递归求得的所需的各种数的数量,不得大于原数列中对应数的数量。于是需要在上述过程中哈希表维护原数列中每种权值的出现次数,在上述检查过程中需要动态维护每个数出现次数,若检查失败需要还原哈希表。最后输出答案时仅需输出哈希表中所有元素即可。

偷懒写个 unordered_map,总时间复杂度 O(nlogv) 级别。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2e5 + 10;
//=============================================================
int n;
std::unordered_map<LL, int> cnt;
std::vector<LL> even, modified, ans;
//=============================================================
int count(LL val_) {
if (!cnt.count(val_)) return 0;
return cnt[val_];
}
void add(LL val_) {
if (!cnt.count(val_)) cnt[val_] = 0;
++ cnt[val_];
}
void sub(LL val_) {
-- cnt[val_];
if (cnt[val_] == 0) cnt.erase(val_);
modified.push_back(val_);
}
bool get(LL val_) {
if (count(val_)) return sub(val_), true;
if (val_ == 1 || val_ % 2 == 0) return false;
return (get(val_ / 2ll) && get(val_ / 2ll + 1));
}
bool check(LL val_) {
modified.clear();
if (get(val_)) return true;
for (auto x: modified) add(x);
return false;
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> n;
for (int i = 1; i <= n; ++ i) {
LL a; std::cin >> a;
if (a % 2 == 0) even.push_back(a);
add(a);
}
std::sort(even.begin(), even.end(), std::greater<LL>());
for (auto x: even) {
if (!count(x)) continue;
sub(x);
if (check(x + 1)) {
add(2ll * x + 1);
} else if (check(x - 1)) {
add(2ll * x - 1);
} else {
add(x);
}
}
for (auto x: cnt) {
for (int i = 1; i <= x.second; ++ i) {
ans.push_back(x.first);
}
}
std::cout << ans.size() << "\n";
for (std::vector<LL>::reverse_iterator it = ans.rbegin(); it != ans.rend(); ++ it) {
std::cout << *it << " ";
}
return 0;
}
/*
4
1 1 2 2 4
*/

C

构造。

一个显然的想法是先构造全 1 串,然后在其中插入若干 0 构造若干最长上升子序列 0 1,发现这样做在 n105 限制下最多只能处理 x(5×104)2<3×109 的情况,挺烂的。

上述想法太暴力了,但是为了便于调整达到 x,保留子序列 0 1 有贡献的想法是可取的。于是想到能否限定最长上升子序列长度为 2,即构造如下形式:

8899778811220011

然而发现上述构造最多仅能处理 x89×(5×104)2<3×1011,还是不太行。

这下不得不用长度不小于 3 的上升子序列了。同样考虑构造上述形式,但是考虑在上述每一段前插入若干 0 从而将上述各段的贡献翻倍。此时仅需将较大的几个段的长度调大即可很容易获得很大贡献,保留较小的段长度均为 2 即可。但是发现此时失去了子序列 0 1 的贡献,贡献最少的子序列变为 0 1 2,即在最后一段 12 前插入一个 0 至少会获得 12 的贡献,凑不出来小于 12 的贡献了,坏!

但是发现在倒数第二段 23 之前插入一个 0 会获得 23+12=35 的贡献,记在倒数第一段前插入 0 的数量为 x,在倒数第二段前插入 0 的数量为 y,则可获得的贡献可表示为 35x+12y=c,可知该不定方程对于所有 c35×12 均有正整数解,于是在之前的位置插入 0 时保留至少 12×35 的剩余量,再通过 exgcd 解出凑出剩余量的方案即可。

代码中分治了 x 较大较小的情况,对于较小情况采用了上述第二种构造。对于较大情况时上述代码中取了:

length[89] = 10000, length[78] = 1000, length[67] = 100, length[56] = 10;
length[45] = length[34] = length[23] = length[12] = 1;

完整代码:

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const LL kLim = 2e10;
const LL kRemain = 12 * 35;
//=============================================================
LL lim;
LL length[110], delta[110];
std::string ans;
//=============================================================
void SolveLess1(int x_, int y_) {
char cx = (char) (x_ + '0'), cy = (char) (y_ + '0');
int len = 0, d = 10 * x_ + y_;
while (1ll * len * len * d <= lim) ++ len;
-- len;
for (int i = 1; i <= len; ++ i) ans += cx;
for (int i = 1; i <= len; ++ i) ans += cy;
lim -= 1ll * len * len * d;
}
void SolveLess() {
for (int i = 8; i; -- i) SolveLess1(i, i + 1);
for (int i = 1; i <= lim; ++ i) ans += '0';
ans += '1';
}
//=============================================================
void SolveGreater1(int x_, int y_) {
char cx = (char) (x_ + '0'), cy = (char) (y_ + '0');
int len = 0, now = 10 * x_ + y_;
while (1ll * len * delta[now] <= lim - kRemain) ++ len;
-- len;
for (int i = 1; i <= len; ++ i) ans += '0';
for (int i = 1; i <= length[now]; ++ i) ans += cx;
for (int i = 1; i <= length[now]; ++ i) ans += cy;
lim -= 1ll * len * delta[now];
}
LL exgcd(LL a_, LL b_, LL &x_, LL &y_) {
if (!b_) {
x_ = 1, y_ = 0;
return a_;
}
LL d_ = exgcd(b_, a_ % b_, y_, x_);
y_ -= a_ / b_ * x_;
return d_;
}
void SolveGreater2() {
LL a = 35, b = 12, c = lim, x, y;
LL d = exgcd(a, b, x, y);
x *= c / d, y *= c / d;
LL p = b / d, q = a / d, k;
k = ceil((1.0 - x) / p), x += p * k, y -= q * k;
// std::cout << "---" << x << " " << y << "\n";
for (int i = 1; i <= x; ++ i) ans += '0';
ans += "23";
for (int i = 1; i <= y; ++ i) ans += '0';
ans += "12";
}
void SolveGreater() {
length[89] = 10000, length[78] = 1000, length[67] = 100, length[56] = 10;
length[45] = length[34] = length[23] = length[12] = 1;
for (int i = 1; i <= 8; ++ i) {
int now = 10 * i + i + 1, pre = 10 * (i - 1) + i;
delta[now] = 1ll * delta[pre] + length[now] * length[now] * now;
}
for (int i = 8; i >= 3; -- i) SolveGreater1(i, i + 1);
SolveGreater2();
}
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> lim;
if (lim == 0) {
ans = "0";
} else if (lim <= kLim) {
SolveLess();
} else {
SolveGreater();
}
// std::cout << ans.length() << "\n";
std::cout << ans;
return 0;
}
/*
88889999 77778888 66667777 666654321 00001111
0 888889 777778 666667 555556 45 34 23 12
8...9... 7...8... 6...7... 0...1...
*/

写在最后

妈的前八道这不都是水题吗感觉没一道能超过 2000 的我赛时到底他妈在干什么啊呃呃呃呃

给 dztle 大神磕四个四题全是大神出的我纯拖后腿的飞舞一个。

撒由那拉!

参考:


  1. https://oi-wiki.org/math/young-tableau/ ↩︎

  2. https://www.cnblogs.com/rainycolor/p/18080157 ↩︎

posted @   Luckyblock  阅读(338)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示