The 2024 ICPC Asia East Continent Online Contest (II)
Preface
被徐神带飞咯,全程睡觉看队友卡卡过题,最变态的是 K 我上去乱写了个假做法就下机睡觉了,后面徐神反手就改了个正解出来
这场主要是周五晚上无来由地发烧了,第二天比赛的时候头痛的一批,几乎没法集中精力想代码和写题
但没想到这场最后打的还挺好,开局 1h 不到就把 6 个签过了,然后跟徐神讲了下 C 题意徐神表示秒会
徐神上机写 C 的时候我和祁神把 E 的做法讨论出来了,并得到一个较为简单的实现,此时徐神 C 很快写完交上去 WA 了,在写了对拍后发现欠考虑了些就换祁神上去写 E
本来说好是祁神写我在边上看的,结果看着看着给我看红温了直接头也不痛了,抢键盘冲上去乱写一通交上去就过了
此时发现罚时优势挺大,并且看榜发现这题后面题都不简单,遂决定再 all-in 一个过的人比较多的 K
徐神上机改了下就把 C 调出来了,结果交上去竟然 T 了,在本机搞了一堆强数据测试后感觉做法复杂度没问题后,我直接上去抄了个快读然后发现 54ms 过了
在下面的时候和祁神把 K 题分治+卷积+完全二分图组合计数的思路大致搞了出来,但因为没有想清楚就冲上去写了个会计算重复贡献的做法,最后经典没过样例下机反思
此时我的头痛突然加剧,遂只能趴在桌子上开睡,让祁神把做法跟徐神交流下,后面就在我迷迷糊糊中听队友讨论出一个不重不漏计数的方法,徐神上去也是很快敲出来过了
最后 9 题校排 16 终于打了个像样的排名了,那么根据控制变量法之前究竟是谁在演呢,我不好说
A. Gambling on Choosing Regionals
读懂题意后不难发现所谓的最坏情况就是和强队全撞了,因此最优的决策一定是去队伍数最小的赛站
按能力值从大到小排序后对每个学校开一个桶统计下即可,注意当前队伍所在学校对应的值要减去 \(1\)
#include<cstdio>
#include<iostream>
#include<string>
#include<array>
#include<algorithm>
#include<map>
#define RI register int
#define CI const int&
using namespace std;
const int N=100005;
int n,k,c[N],C,idx,bkt[N],ans[N]; array <int,3> a[N]; map <string,int> rst;
int main()
{
ios::sync_with_stdio(0); cin.tie(0);
cin>>n>>k; C=1e9;
for (RI i=1;i<=k;++i) cin>>c[i],C=min(C,c[i]);
for (RI i=1;i<=n;++i)
{
string tmp;
cin>>a[i][0]>>tmp; a[i][2]=i;
if (rst.count(tmp)) a[i][1]=rst[tmp];
else a[i][1]=rst[tmp]=++idx;
}
sort(a+1,a+n+1,greater <array <int,3>>()); int sum=0;
for (RI i=1;i<=n;++i)
{
auto [w,sch,id]=a[i];
if (bkt[sch]<C) ++bkt[sch],++sum;
ans[id]=(sum-bkt[sch])+(bkt[sch]-1)+1;
}
for (RI i=1;i<=n;++i) printf("%d\n",ans[i]);
return 0;
}
B. Mountain Booking
看过题人数是个防 AK,不过这场没过的题好像都是 DS 相关的,看来我的挂机导致我们队没开错题,赢
C. Prefix of Suffixes
string master 专业对口,这么多场网络赛终于有个字符串了
徐神的做法大致就是用 KMP 的 fail 数组等差数列 \(\log\) 级别的性质来做,因为我一点不懂字符串科技具体的也不懂了
#include <bits/stdc++.h>
using llsi = long long signed int;
int n;
int s[300005], a[300005], b[300005];
int fail[300005], top[300005];
llsi ans = 0, bsum = 0;
class FileInputOutput
{
private:
static const int S=1<<21;
#define tc() (A==B&&(B=(A=Fin)+fread(Fin,1,S,stdin),A==B)?EOF:*A++)
char Fin[S],*A,*B;
public:
template <typename T> inline void read(T& x)
{
x=0; char ch; while (!isdigit(ch=tc()));
while (x=(x<<3)+(x<<1)+(ch&15),isdigit(ch=tc()));
}
#undef tc
}F;
int main() {
F.read(n);
for(int i = 1; i <= n; ++i) {
F.read(s[i]); F.read(a[i]); F.read(b[i]);
s[i] = (s[i] + ans) % n;
int j = fail[i - 1];
//int count = 0;
if(i == 1) fail[1] = top[1] = 0;
else {
//count += 1;
while(j && s[j + 1] != s[i]) {
//count += 1;
bsum -= b[i - 1 - j + 1];
j = fail[j];
}
if(s[j + 1] == s[i]) fail[i] = j + 1;
else fail[i] = 0;
if(i - fail[i] == fail[i] - fail[fail[i]]) top[i] = top[fail[i]];
else top[i] = fail[i];
}
for(; j > 0; ) {
//count += 1;
if(s[j + 1] == s[i]) {
if(s[fail[j] + 1] == s[i]) j = top[j];
else j = fail[j];
} else {
bsum -= b[i - 1 - j + 1];
j = fail[j];
}
}
// std::cerr << "top[" << i << "] = " << top[i] << char(10);
// std::cerr << "fail[" << i << "] = " << fail[i] << char(10);
if(s[i] == s[1]) bsum += b[i];
ans += a[i] * bsum;
std::cout << ans << char(10);
}
return 0;
}
D. Query on Tree
不可做的 DS 题,鉴定为弃疗
E. Escape
很经典的一个题,想到了就很简单
考虑玩家最后选择的路径上能不能包含点 \(x\),这就要求当人走到这个位置时不能被机器人抓到
首先若所有机器人到该点的最小距离 \(>d\) 则该点一定可以走;还有一种情况就是人到的比机器人早
由于有走回头路的情况因此套路地发现这和奇偶性有关,考虑将每个点拆成奇偶两个
对奇偶两种点分别判断人是否能先于机器人到达即可,以此可以确定每个点是否可以被走到,最后对所有合法的点求一个最短路即可
#include<bits/stdc++.h>
using namespace std;
const int INF = 1e9+5;
const int N = 5e5+5;
int n, m, d, k, dis1[N], dis2[N], dis3[N], pre[N];
bool valid[N];
vector<int> G[N];
void BFS(queue<int> &Q, int dis[]) {
while (!Q.empty()) {
int x = Q.front(); Q.pop();
for (int v : G[x]) {
if (!valid[v]) continue;
if (dis[v] > dis[x]+1) {
dis[v] = dis[x]+1;
pre[v] = x;
Q.push(v);
}
}
}
}
void solve() {
cin >> n >> m >> d;
for (int i=1; i<=2*n; ++i) G[i].clear(), dis1[i]=dis2[i]=dis3[i]=INF,valid[i]=1;
for (int i=1; i<=m; ++i) {
int x, y; cin >> x >> y;
int x1=2*x-1, y1=2*y-1;
int x2=2*x, y2=2*y;
G[x1].push_back(y2); G[y2].push_back(x1);
G[x2].push_back(y1); G[y1].push_back(x2);
}
cin >> k;
queue<int> Q;
for (int i=1; i<=k; ++i) {
int s; cin >> s;
dis1[2*s-1] = 0;
pre[2*s-1] = -1;
Q.push(2*s-1);
}
BFS(Q, dis1);
queue<int> Q2; Q2.push(1); dis2[1] = 0; pre[1] = -1;
BFS(Q2, dis2);
for (int i=1; i<=2*n; ++i) {
if (dis1[i]>d||dis2[i]<dis1[i]) valid[i]=1; else valid[i]=0;
}
if (!valid[1]) {
puts("-1"); return;
}
queue<int> Q3; Q3.push(1); dis3[1] = 0; pre[1] = -1;
BFS(Q3, dis3);
if (min(dis3[2*n-1],dis3[2*n])==INF) {
puts("-1"); return;
}
printf("%d\n",min(dis3[2*n-1],dis3[2*n]));
int x;
if (dis3[2*n-1]<=dis3[2*n]) x=2*n-1; else x=2*n;
vector <int> path;
while (x!=-1) path.push_back(x),x=pre[x];
reverse(path.begin(),path.end());
for (auto x:path) printf("%d ",(x+1)/2);
putchar('\n');
}
signed main() {
ios::sync_with_stdio(0); cin.tie(0);
int t; cin >> t; while (t--) solve();
return 0;
}
F. Tourist
纯签到,我题都没看
#include <bits/stdc++.h>
int main() {
std::ios::sync_with_stdio(false);
int n; std::cin >> n;
for(int64_t i = 1, rating = 1500, c; i <= n; ++i) {
std::cin >> c;
rating += c;
if(rating >= 4000) {
std::cout << i << char(10);
return 0;
}
}
std::cout << "-1\n";
return 0;
}
G. Game
首先发现平局并没啥用,同时由于赢得的钱不会给胜方因此其实就是个辗转相除的过程,简单模拟一下即可
#include <bits/stdc++.h>
using llsi = long long signed int;
constexpr llsi mod = 998244353;
llsi ksm(llsi a, llsi b) {
llsi c = 1;
while(b) {
if(b & 1) c = c * a % mod;
a = a * a % mod;
b >>= 1;
}
return c;
}
llsi f(llsi x, llsi y, llsi p1, llsi p2) {
if(x == 0) return 0;
if(y == 0) return 1;
return ksm(p1, y / x) * (mod + 1 - f(y % x, x, p2, p1)) % mod;
}
int main() {
std::ios::sync_with_stdio(false);
int t; std::cin >> t; while(t--) {
llsi x, y, a0, a1, b;
std::cin >> x >> y >> a0 >> a1 >> b;
llsi t = ksm(a0 + a1, mod - 2);
a0 = a0 * t % mod; a1 = a1 * t % mod;
std::cout << f(x, y, a0, a1) << char(10);
}
return 0;
}
H. Points Selection
刚开始没仔细看题一直在想“选子集和模 \(n\) 为 \(c\)”这个限制怎么做,想来想去最优的也就是 bitset
了,遂感觉这是个不可做题,结果最后 20min 才发现题目保证数据随机
由于若 \(query(a,b,c)\) 为真,则 \(query(\ge a,\ge b,c)\) 也一定为真
因此考虑从小到大枚举 \(a\) 的值,并令 \(f_c\) 表示最小的满足 \(query(a,b,c)\) 为真的值 \(b\),有了这个后可以很容易计算答案
每次加入一个点后需要 \(O(n)\) 的时间暴力更新 \(f\) 数组,但我们可以用随机的性质来做一些分析
考虑加入 \(k\) 个点后,随机的性质会使得其 \(2^k\) 个子集和模 \(n\) 的值在 \([0,n)\) 内均匀分布,因此 \(k=O(\log n)\) 时期望就可以将所有 \(f\) 的值填满
因此 \(f\) 数组的最大值的期望值等于所有已经加入的点的纵坐标的第 \(O(\log n)\) 小值,即 \(O(\frac{n\log n}{k})\)
注意到每次加入一个点时,若它的 \(y\) 坐标 \(\ge \max(f)\) 时可以直接跳过它,因此它更新答案(即其纵坐标 \(< \max(f)\) )的概率为 \(O(\frac{\log n}{k})\)
即期望更新次数为 \(O(\sum_{k=1}^n \frac{\log n}{k})=O(\log^2 n)\),总复杂度 \(O(n\log^2n)\),常数很小可以通过
#include<cstdio>
#include<iostream>
#include<vector>
#include<utility>
#define RI register int
#define CI const int&
using namespace std;
typedef pair <int,int> pi;
const int N=500005;
int n,f[N]; vector <pi> vec[N];
unsigned long long sum,ans;
int main()
{
scanf("%d",&n);
for (RI i=1;i<=n;++i)
{
int x,y,w;
scanf("%d%d%d",&x,&y,&w);
vec[x].push_back({y,w});
}
for (RI i=0;i<n;++i) f[i]=n+1;
int maxy=n+1;
for (RI x=1;x<=n;++x)
{
for (auto [y,w]:vec[x])
{
if (y>=maxy) continue;
static int g[N]; maxy=-1; sum=0;
for (RI i=0;i<n;++i) g[i]=f[i];
for (RI i=0;i<n;++i)
g[(i+w)%n]=min(g[(i+w)%n],max(f[i],y));
g[w]=min(g[w],y);
for (RI i=0;i<n;++i)
{
f[i]=g[i]; maxy=max(maxy,f[i]);
sum+=1ull*i*(1ull*(f[i]+n)*(n-f[i]+1)/2);
}
}
ans+=1ull*x*sum;
}
return printf("%llu",ans),0;
}
I. Strange Binary
祁神开场写的神秘构造,感觉还是挺小清新的
#include<bits/stdc++.h>
using namespace std;
void solve() {
int n; cin >> n;
if (n%4==0) {
cout << "NO\n";
return ;
}
vector<int> A(32), ans(32);
for (int i=0; i<=30; ++i) {
if ((n>>i)&1) A[i] = 1;
else A[i] = 0;
}
auto find1 = [&](int pos) {
while (pos>=0) {
if (A[pos]==1) return pos;
else --pos;
}
return -1;
};
for (int i=31; i>=0; --i) {
if (1==A[i]) ans[i]=1;
else {
int pos = find1(i);
if (-1==pos) break;
ans[i] = 1;
for (int j=i-1; j>=pos; --j) ans[j]=-1;
i = pos;
}
}
cout << "YES\n";
for (int i=0; i<32; ++i) {
cout << ans[i] << (i%8==7 ? '\n' : ' ');
}
}
signed main() {
ios::sync_with_stdio(0); cin.tie(0);
int t; cin >> t; while (t--) solve();
return 0;
}
J. Stacking of Goods
很套路的题,用交换法可以证明物品 \(i\) 在 \(j\) 之前当且仅当 \(c_i\times w_j>c_j\times w_i\),改下排序的比较函数即可
#include<cstdio>
#include<iostream>
#include<algorithm>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
const int N=100005;
struct ifo
{
int w,v,c;
friend inline bool operator < (const ifo& A,const ifo& B)
{
return A.c*B.w>B.c*A.w;
}
}a[N]; int n;
signed main()
{
scanf("%lld",&n);
for (RI i=1;i<=n;++i)
scanf("%lld%lld%lld",&a[i].w,&a[i].v,&a[i].c);
sort(a+1,a+n+1); int ans=0,W=0;
for (RI i=n;i>=1;--i) ans+=a[i].v-a[i].c*W,W+=a[i].w;
return printf("%lld",ans),0;
}
K. Match
按位异或的题很容易想到从高位往低位枚举,并令 \(solve(A,B,p)\) 表示 \(A,B\) 两个集合在高 \(p\) 位匹配的答案,返回值为一个多项式
把 \(A,B\) 按当前位的权值为 \(0/1\) 分为 \(A_0,A_1,B_0,B_1\),考虑以下两种情况:
- 若 \(k\) 的第 \(p\) 位为 \(1\),此时只能异或值为 \(1\) 的两种方案进行匹配,递归算 \(solve(A_0,B_1,p-1)\) 和 \(solve(A_1,B_0,p-1)\) 即可;
- 若 \(k\) 的第 \(p\) 位为 \(0\),异或值为 \(1\) 的两种方案可以任意匹配,先求出 \(solve(A_0,B_0,p-1)\) 和 \(solve(A_1,B_1,p-1)\) 后剩下的就是个完全二分图匹配,组合 DP 转移即可;
最后总复杂度 \(O(n^4)\),实际常数极小跑的飞快
#include <bits/stdc++.h>
using llsi = long long signed int;
constexpr llsi mod = 998244353;
using poly = std::vector<llsi>;
llsi ksm(llsi a, llsi b) {
llsi c = 1;
while(b) {
if(b & 1) c = c * a % mod;
a = a * a % mod;
b >>= 1;
}
return c;
}
llsi fac[201], facinv[201];
void prep(int n = 200) {
fac[0] = 1;
for(int i = 1; i <= n; ++i) fac[i] = fac[i - 1] * i % mod;
facinv[n] = ksm(fac[n], mod - 2);
for(int i = n; i >= 1; --i) facinv[i - 1] = facinv[i] * i % mod;
return ;
}
llsi C(llsi a, llsi b) {
if(a < b || b < 0) return 0ll;
return fac[a] * facinv[b] % mod * facinv[a - b] % mod;
}
poly mult(const poly &a, const poly &b) {
poly c(a.size() + b.size() - 1, 0);
for(int i = 0; i < a.size(); ++i) for(int j = 0; j < b.size(); ++j)
c[i + j] = (c[i + j] + a[i] * b[j]) % mod;
return c;
}
poly full(int a, int b) {
poly c(std::min(a, b) + 1);
for(int i = 0; i < c.size(); ++i) c[i] = C(a, i) * C(b, i) % mod * fac[i] % mod;
return c;
}
int n;
llsi k;
poly solve(const poly &a, const poly &b, int t) {
if(a.empty() || b.empty()) return poly {1};
if(t == -1) return full(a.size(), b.size());
poly A[2], B[2];
for(auto a: a) A[a >> t & 1].emplace_back(a);
for(auto b: b) B[b >> t & 1].emplace_back(b);
if(k >> t & 1) return mult(solve(A[0], B[1], t - 1), solve(A[1], B[0], t - 1));
poly C[2];
C[0] = solve(A[0], B[0], t - 1);
C[1] = solve(A[1], B[1], t - 1);
poly res(std::min(a.size(), b.size()) + 1, 0);
for(int i = 0; i < C[0].size(); ++i) for(int j = 0; j < C[1].size(); ++j) {
llsi base = C[0][i] * C[1][j] % mod;
llsi a0res = A[0].size() - i, a1res = A[1].size() - j;
llsi b0res = B[0].size() - i, b1res = B[1].size() - j;
poly aster = mult(full(a0res, b1res), full(a1res, b0res));
for(int k = 0; k < aster.size(); ++k) res[i + j + k] = (res[i + j + k] + base * aster[k]) % mod;
}
return res;
}
int main() {
std::ios::sync_with_stdio(false);
prep(200);
std::cin >> n >> k;
poly a(n), b(n);
for(auto &a: a) std::cin >> a; for(auto &b: b) std::cin >> b;
poly ans = solve(a, b, 60);
while(ans.size() < n + 1) ans.emplace_back(0);
for(int i = 1; i <= n; ++i) std::cout << ans[i] << char(10);
return 0;
}
L. 502 Bad Gateway
徐神开场写的,做法不难想到就是均值不等式,但需要手写分数类避免精度误差
#include <bits/stdc++.h>
using llsi = long long signed int;
struct frac {
llsi a, b;
friend frac operator +(const frac &x, const frac &y) {
llsi g = std::__gcd(x.b, y.b);
return frac{ x.a * (y.b / g) + y.a * (x.b / g), x.b / g * y.b };
}
friend frac operator -(const frac &x, const frac &y) {
llsi g = std::__gcd(x.b, y.b);
return frac{ x.a * (y.b / g) - y.a * (x.b / g), x.b / g * y.b };
}
friend bool operator <(const frac &x, const frac &y) {
return x.a * y.b < x.b * y.a;
}
};
int main() {
std::ios::sync_with_stdio(false);
int t; std::cin >> t; while(t--) {
frac ans { 0x7FFFFFFF, 1 };
llsi T; std::cin >> T;
llsi cbase = static_cast<llsi>(std::sqrt(2 * T));
for(llsi c = cbase - 3; c <= cbase + 3; ++c) {
if(c <= 0 || c > T) continue;
ans = std::min(ans, frac{c * (c + 1) + 2 * T - 2 * c, 2 * c});
}
llsi g = std::__gcd(ans.a, ans.b);
std::cout << ans.a / g << " " << ans.b / g << char(10);
}
}
Postscript
网络赛也终于告一段落了,今年区域赛拿了 2+2 的名额,希望能不负众望打出点好成绩吧