2024牛客暑期多校训练营4
Preface
最赤石的一集,上来先认为 D 是个煞笔贪心提交了该题的第一发代码并喜提 WA
后面下机后祁神把 L 扔给我告诉我就是个煞笔线段树板,结果我写完过不去样例发现题意假了
值得一提的是最后发现这两题是这场过的人最少的两题,直接开局给我干的道心破碎
之后 A 题写不来带权并查集样例没过就手贱交了两发,把罚时彻底搞炸了最后只能慢慢追题数
然而科技题 E 和煞笔题 B(感觉这题榜歪了,难度其实不大)都没写出来,这下等死就完了
好在后期和祁神一起把答辩题 K 很快地写出来还一发过了,苟住了排名导致没有太难看
LCT
带权并查集腐乳闪总出列
乍一看以为是个动态维护直径的经典东西,但由于空间限制而且它保证每次询问的点都是集合的根我们就可以搞一个别的
考虑用动态维护每个联通块的根到最远的叶子的距离,每次连边 \(a\to b\) 时只要用 \(b\) 的答案加上 \(a\) 到其联通块根的距离更新贡献即可
求一个点到其联通块的路径长度是带权并查集的经典问题,但路径压缩的时候很容易写错需要注意下
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=1e6+5;
int t,n,a,b,c,fa[N],dep[N],dis[N],len[N];
inline int getfa(CI x)
{
if (x==fa[x]) return x;
int t=getfa(fa[x]);
dep[x]=dep[fa[x]]+len[x];
len[x]+=len[fa[x]];
return fa[x]=t;
}
int main()
{
for (scanf("%d",&t);t;--t)
{
RI i; for (scanf("%d",&n),i=1;i<=n;++i) fa[i]=i,dep[i]=dis[i]=len[i]=0;
for (i=1;i<n;++i)
{
scanf("%d%d%d",&a,&b,&c);
int anc=getfa(a);
dis[anc]=max(dis[anc],dep[a]+1+dis[b]);
fa[b]=a; len[b]=1;
printf("%d%c",dis[c]," \n"[i==n-1]);
}
}
return 0;
}
Pull the Box
神秘图论题,本来和祁神讨论出一个做法了但后面又被否了,后面发现好像就是对的
需要注意到路径的形式,即一定是人先走到某个与 \(1\) 相邻的位置拉动箱子;然后就一直拉着箱子不放手直到走到终点
分别考虑两段路的走法,前者只需要简单 BFS 一下即可;后者可以将人和箱子看作相邻的两个点,因此可以把原图中的边看成点来处理
当然直接暴力转化对偶图复杂度是 \(O(n^2)\) 的,但要注意到题解中讲的一个性质发现每个点会经过不超过两次,这题就有很好写的方法了
#include<cstdio>
#include<iostream>
#include<vector>
#include<queue>
#define RI register int
#define CI const int&
using namespace std;
typedef pair <int,int> pi;
const int N=1e6+5,INF=1e9;
int t,n,m,st,x,y,pos[N<<1],vis[N],dis1[N],dis2[N<<1]; vector <pi> v[N];
int main()
{
for (scanf("%d",&t);t;--t)
{
RI i; scanf("%d%d%d",&n,&m,&st);
for (i=1;i<=n;++i) v[i].clear(),vis[i]=0,dis1[i]=INF;
int idx=0; for (i=1;i<=m;++i)
{
scanf("%d%d",&x,&y);
v[x].push_back(pi(y,idx)); pos[idx++]=y;
v[y].push_back(pi(x,idx)); pos[idx++]=x;
}
for (i=0;i<idx;++i) dis2[i]=INF;
dis1[st]=0; queue <int> q; q.push(st);
while (!q.empty())
{
int now=q.front(); q.pop();
for (auto [to,id]:v[now])
if (to!=1&&dis1[to]>dis1[now]+1) dis1[to]=dis1[now]+1,q.push(to);
}
for (auto [x,id]:v[n]) dis2[id^1]=0,dis2[id]=1,q.push(id);
while (!q.empty())
{
int ne=q.front(),now=pos[ne]; q.pop();
if (++vis[now]>2) continue;
for (auto [to,id]:v[now])
{
if (id==(ne^1)) continue;
if (dis2[id]>dis2[ne]+1)
dis2[id]=dis2[ne]+1,q.push(id);
}
}
int ans=INF; for (auto [x,id]:v[1])
ans=min(ans,dis1[x]+dis2[id^1]);
if (v[n].size()<=1||ans>=INF) puts("Boring Game");
else printf("Vegetable fallleaves\n%d\n",ans);
}
return 0;
}
Sort4
考虑原排列所构成的置换环,对于长度恰为 \(3,4\) 的环可以一次排好;否则对于长度 \(len>4\) 的环可以每次排好其中三个元素,规约成 \(len-3\) 的问题
而长度恰为 \(2\) 的环就比较特殊,可以每次挑两个一起复原,因此这部分特殊处理即可
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+5;
int t, n, A[N];
bool vis[N];
int dfs(int x, int d){
if (vis[x]) return d;
else{
vis[x]=true;
return dfs(A[x], d+1);
}
}
signed main(){
ios::sync_with_stdio(0); cin.tie(0);
cin >> t;
while (t--){
cin >> n;
for (int i=1; i<=n; ++i) cin >> A[i], vis[i]=false;
int sz2=0, ans=0;
for (int i=1; i<=n; ++i) if (!vis[i]){
int res = dfs(i, 0);
// printf("t=%d i=%d res=%d\n", t, i, res);
ans += res/3;
if (res%3==2) ++sz2;
}
ans += (sz2+1)/2;
cout << ans << '\n';
}
return 0;
}
Plants vs. Zombies (Sunflower Edition)
开场以为是个煞笔贪心冲上去很快地似了,还好后面马上发现自己 trivial 了马上扔了这题没有重蹈校赛的覆辙
String God's String
多项式科技题,徐神一眼秒了这题字符串的处理部分,然后发现对于这题转化后的 0/1 背包部分做不了一点,直接坐牢到结束
由于我根本不会啥多项式科技这题就扔给徐神补了,徐神也是决定重写一遍多项式全家桶的板子以备不时之需
Good Tree
神秘乱搞构造题,当时随便写了个东西后抱着交一发试试的心态冲了一发,没想到直接就过了
考虑先构造一条有 \(d\) 个点的链,显然最大化差值的两个点选在两个端点处最优,差值初始时为 \(0\)
不难发现此时可以通过在每个点下面挂点使得差值每次增加 \(d-1,d-3,d-5,\dots\)
因此很直观地会想到 \(d\) 取 \(\sqrt x\) 左右大致是最优的,但可能需要左右微调一下
然后我就写了一个以 \(\sqrt x\) 为中心左右各取 \(5\) 个数作为链的长度的东西,然后交上去就过了
这个构造的正确性证明可以参考官方题解,这种写法就省去了繁琐的讨论部分
#include<cstdio>
#include<iostream>
#include<cmath>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
const int INF=1e18;
int t,n;
inline int calc(CI x)
{
int cnt=x,tmp=n;
while (tmp>0)
{
int y; if (tmp>=x-1) y=x-1;
else
{
y=tmp;
if (y%2!=(x-1)%2) --y;
}
if (y==0) return INF;
cnt+=tmp/y; tmp-=tmp/y*y;
}
return cnt;
}
signed main()
{
for (scanf("%lld",&t);t;--t)
{
scanf("%lld",&n); int ans=INF;
int l=max(2LL,(int)sqrt(n)-5),r=(int)sqrt(n)+5;
for (RI i=l;i<=r;++i) ans=min(ans,calc(i));
printf("%lld\n",ans);
}
return 0;
}
Horse Drinks Water
应该是个签,我题目都没看
#include <bits/stdc++.h>
using ll = long long;
long double L2(ll x1, ll y1, ll x2, ll y2) {
return sqrtl((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}
int main() {
std::ios::sync_with_stdio(false);
int t; std::cin >> t; while(t--) {
int64_t x1, y1, x2, y2;
std::cin >> x1 >> y1 >> x2 >> y2;
std::cout << std::fixed << std::setprecision(15)
<< std::min(
L2(-x1, y1, x2, y2),
L2(x1, -y1, x2, y2)
) << char(10);
}
return 0;
}
Yet Another Origami Problem
徐神开场秒的,当时我还在和 L 题的错误题意 version 搏斗,因此连这题题意都不知道
from math import gcd
T = int(input())
for t in range(T):
n = int(input())
a = list(map(int, input().split()))
min_a = min(a)
if min_a == max(a):
print(0)
continue
g = 0
for i in range(1, n):
g = gcd(g, a[i] - min_a)
print(g)
Friends
拿个 two pointers
随便搞一搞就过了
#include <bits/stdc++.h>
int main() {
std::ios::sync_with_stdio(false);
int n, m;
std::cin >> n >> m;
std::vector<std::vector<int>> fd(n + 1);
for(int i = 1, u, v; i <= m; ++i) {
std::cin >> u >> v;
fd[v].push_back(u);
}
int l = 0;
int ans = 0;
for(int i = 1; i <= n; ++i) {
auto &fs = fd[i];
fs.push_back(i);
std::sort(fs.begin(), fs.end());
int cc = 0;
for(int j = 0; j < fs.size() - 1; ++j) if(fs[j] < fs[j + 1] - 1) cc = j + 1;
ans += i - (l = std::max(l, fs[cc])) + 1;
// std::cout << l << ", " << i << char(10);
}
std::cout << ans << char(10);
return 0;
}
Zero
徐神太有实力了,直接用随机过程的方法淦过去了,我一点听不懂做法
大致思路就是求出以每个点为右端点时,连续的 \(1\) 的数量的期望,然后一波神秘拆贡献计算即可
#include <bits/stdc++.h>
using llsi = long long signed int;
constexpr llsi mod = 998244353;
constexpr llsi ksm(llsi a, llsi b) {
llsi res = 1;
while(b) {
if(b & 1) res = res * a % mod;
a = a * a % mod;
b >>= 1;
}
return res;
}
constexpr llsi inv2 = ksm(2, mod - 2);
llsi C[32][32], p[100001], e[100001][32], B[32][32];
int main() {
std::ios::sync_with_stdio(false);
int n, K; std::cin >> n >> K;
for(int i = 0; i <= 31; ++i) C[i][0] = 1;
for(int i = 1; i <= 31; ++i) for(int j = 1; j <= i; ++j)
C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % mod;
std::string s; std::cin >> s;
for(int i = 1; i <= n; ++i) {
switch(s[i - 1]) {
case '0': p[i] = 0; break;
case '1': p[i] = 1; break;
case '?': p[i] = inv2; break;
}
}
for(int i = 0; i <= n; ++i) e[i][0] = 1;
for(int i = 1; i <= n; ++i) for(int k = 1; k <= K + 1; ++k) {
for(int j = 0; j <= k; ++j) {
e[i][k] += C[k][j] * e[i - 1][j] % mod;
}
e[i][k] = e[i][k] % mod * p[i] % mod;
}
// for(int i = 1; i <= n; ++i) std::cout << e[i][K] << char(i == n ? 10 : 32);
B[0][1] = 1;
for(int k = 1; k <= K + 1; ++k) {
for(int j = 1; j <= k + 1; ++j) B[k][j] = C[k + 1][j];
for(int j = 0; j <= k - 1; ++j) {
for(int l = 0; l <= j + 1; ++l)
B[k][l] += mod - B[j][l] * C[k + 1][j] % mod;
}
for(int j = 0; j <= k + 1; ++j) B[k][j] = B[k][j] % mod * ksm(k + 1, mod - 2) % mod;
}
// for(int i = 1; i <= K + 1; ++i)
// for(int j = 0; j <= i + 1; ++j)
// std::cout << B[i][j] << char(j == i + 1 ? 10 : 32);
llsi ans = 0;
for(int i = 1; i <= n; ++i) {
for(int j = 1; j <= K + 1; ++j)
ans += e[i][j] * B[K][j] % mod;
}
std::cout << ans % mod << char(10);
return 0;
}
Calculate the Expression
究极赤石题,做法一眼就出但写起来就是纯纯的享受
这种单点修改区间查询一眼线段树维护,难点在于如何设计区间存储的状态
在和祁神一波讨论后发现大致可以分为以下三种类型存储区间:
- 含有至少一个
+
:这种区间可以规约为形如 \(a\times b+c\times d\) 的形式,其中 \(a,d\) 都是可能继续扩展的数 - 含有至少一个
*
:这种区间可以规约为形如 \(a\times b\times c\) 的形式,其中 \(a,c\) 都是可能继续扩展的数 - 否则该区间只有单纯的数字,需要特殊处理
合并时需要根据左右两个区间的类型分 \(3\times 3=9\) 种类型讨论,总复杂度 \(O(n\log n)\)
建议这题写之前在纸上手玩清楚再上机就不太容易写错,我赛时写这题的时候也是把祁神摇过来当 Debugger 全程理清思路才能一发入魂
#include<cstdio>
#include<iostream>
#include<cstring>
#define RI register int
#define CI const int&
using namespace std;
const int N=500005,mod=998244353;
int q,n,pw[N]; char s[N];
inline int S(CI x,CI y)
{
return x+y>=mod?x+y-mod:x+y;
}
inline void inc(int& x,CI y)
{
if ((x+=y)>=mod) x-=mod;
}
struct ifo
{
int l,r,a,b,c,d,p1,p2,res,add,mul;
inline ifo(CI L=0,CI R=0,CI A=0,CI B=0,CI C=0,CI D=0,CI P1=0,CI P2=0,CI Res=0,CI Add=0,CI Mul=0)
{
l=L; r=R; a=A; b=B; c=C; d=D; p1=P1; p2=P2; res=Res; add=Add; mul=Mul;
}
inline int type(void) const
{
if (add) return 1;
if (mul) return 2;
return 3;
}
inline int val(void) const
{
int tp=type();
if (tp==1) return S(res,(1LL*a*b+1LL*c*d)%mod);
if (tp==2) return S(res,1LL*a*b%mod*c%mod);
return S(res,a);
}
inline void init(const char& ch)
{
if (ch=='+') add=1,b=c=1,p1=p2=l; else
if (ch=='*') mul=1,b=1,p1=p2=l; else a=ch-'0';
}
};
inline ifo operator + (const ifo& A,const ifo& B)
{
ifo C; int tpa=A.type(),tpb=B.type();
C.l=A.l; C.r=B.r;
C.res=S(A.res,B.res);
C.add=A.add|B.add; C.mul=A.mul|B.mul;
if (tpa==1&&tpb==1)
{
int tmp=S(1LL*A.d*pw[B.p1-B.l]%mod,B.a);
tmp=1LL*tmp*A.c%mod*B.b%mod;
inc(C.res,tmp);
C.a=A.a; C.b=A.b; C.c=B.c; C.d=B.d;
C.p1=A.p1; C.p2=B.p2;
} else
if (tpa==1&&tpb==2)
{
int tmp=S(1LL*A.d*pw[B.p1-B.l]%mod,B.a);
tmp=1LL*tmp*A.c%mod*B.b%mod;
C.a=A.a; C.b=A.b; C.c=tmp; C.d=B.c;
C.p1=A.p1; C.p2=B.p2;
} else
if (tpa==1&&tpb==3)
{
int tmp=S(1LL*A.d*pw[B.r-B.l+1]%mod,B.a);
C.a=A.a; C.b=A.b; C.c=A.c; C.d=tmp;
C.p1=A.p1; C.p2=A.p2;
} else
if (tpa==2&&tpb==1)
{
int tmp=S(1LL*A.c*pw[B.p1-B.l]%mod,B.a);
tmp=1LL*tmp*A.b%mod*B.b%mod;
C.a=A.a; C.b=tmp; C.c=B.c; C.d=B.d;
C.p1=A.p1; C.p2=B.p2;
} else
if (tpa==2&&tpb==2)
{
int tmp=S(1LL*A.c*pw[B.p1-B.l]%mod,B.a);
tmp=1LL*tmp*A.b%mod*B.b%mod;
C.a=A.a; C.b=tmp; C.c=B.c;
C.p1=A.p1; C.p2=B.p2;
} else
if (tpa==2&&tpb==3)
{
int tmp=S(1LL*A.c*pw[B.r-B.l+1]%mod,B.a);
C.a=A.a; C.b=A.b; C.c=tmp;
C.p1=A.p1; C.p2=A.p2;
} else
if (tpa==3&&tpb==1)
{
int tmp=S(1LL*A.a*pw[B.p1-B.l]%mod,B.a);
C.a=tmp; C.b=B.b; C.c=B.c; C.d=B.d;
C.p1=B.p1; C.p2=B.p2;
} else
if (tpa==3&&tpb==2)
{
int tmp=S(1LL*A.a*pw[B.p1-B.l]%mod,B.a);
C.a=tmp; C.b=B.b; C.c=B.c;
C.p1=B.p1; C.p2=B.p2;
} else
if (tpa==3&&tpb==3)
{
C.a=S(1LL*A.a*pw[B.r-B.l+1]%mod,B.a);
}
return C;
}
class Segment_Tree
{
private:
ifo O[N<<2];
public:
#define TN CI now=1,CI l=1,CI r=n
#define LS now<<1,l,mid
#define RS now<<1|1,mid+1,r
inline void build(TN)
{
if (l==r)
{
O[now]=ifo(); O[now].l=O[now].r=l;
return O[now].init(s[l]);
}
int mid=l+r>>1; build(LS); build(RS);
O[now]=O[now<<1]+O[now<<1|1];
}
inline void update(CI pos,const char& ch,TN)
{
if (l==r)
{
O[now]=ifo(); O[now].l=O[now].r=l;
return O[now].init(ch);
}
int mid=l+r>>1;
if (pos<=mid) update(pos,ch,LS); else update(pos,ch,RS);
O[now]=O[now<<1]+O[now<<1|1];
}
inline ifo query(CI beg,CI end,TN)
{
if (beg<=l&&r<=end) return O[now]; int mid=l+r>>1;
if (end<=mid) return query(beg,end,LS);
if (beg>mid) return query(beg,end,RS);
return query(beg,end,LS)+query(beg,end,RS);
}
#undef TN
#undef LS
#undef RS
}SEG;
int main()
{
RI i; scanf("%d%s",&q,s+1); n=strlen(s+1);
for (pw[0]=i=1;i<=n;++i) pw[i]=10LL*pw[i-1]%mod;
for (SEG.build();q;--q)
{
int opt; scanf("%d",&opt);
if (opt==1)
{
int l,r; scanf("%d%d",&l,&r);
printf("%d\n",SEG.query(l,r).val());
} else
{
int x; char ch[5]; scanf("%d%s",&x,ch);
SEG.update(x,ch[0]);
}
}
return 0;
}
Deceleration
赛时把 第 \(x\) 秒减速 看成 第 \(x\) 个位置减速 了,然后冲了个线段树连样例都过不去,直接把罚时搞炸了
不想补题了直接白兰吧
Postscript
今天这场算是难得的后期发力了,能不能以后每场都出点像 K 这样的赤石题给我做啊,就好这一口赤石