The 2023 ICPC Asia Hangzhou Regional Contest (The 2nd Universal Cup. Stage 22: Hangzhou)
Preface
时隔许久终于搞了场组队训练,虽然大家都有点小唐但好在最后会的题还是过的七七八八了,最后7题收场
比较可惜的是如果手速快点留够时间去写大模拟A,或者想清楚B题的实现的话兴许有机会8题冲击出线区
A. Submissions
由于这题实在太经典了,之前杭州站结束就知道这场有个毒瘤模拟了
本来打算扔给徐神写的,结果因为徐神被E卡常关了一整场所以最后也就不了了之,感觉要写出来至少得要2h
坐等徐神赛后补题了
B. Festival Decorating
我愿称之为bitset
仙人题,感觉需要对bitset
有雄厚的理解才能做出这题
做法一是通过这题输出有容错的性质分成$\log $段来处理,然后每一段用FFT优化来做
但这题还有一个大力bitset
直接求答案的做法,完全不需要利用容错的性质就可以通过
考虑先把所有询问离线,然后按编号从小到大枚举每个灯\(i\)作为左端点
可以用bitset
维护除了当前这种颜色的灯外其它灯的位置,由于题目保证任意两盏灯位置不同,因此我们可以那所有灯对应的bitset
异或上当前颜色的灯对应的bitset
但这里如果直接给每种颜色都开bitset
的话内存就会爆炸,我们可以利用一种根号分治的优化
不妨设题目中所有范围的数量级都是\(N=250000\),令\(S=\sqrt N\),对于每种颜色:
- 其对应的灯数量\(\le S\),我们可以直接暴力遍历该颜色对应的灯的位置来清除其贡献
- 其对应的灯数量\(> S\),不难发现这样的颜色总数是\(\le S\),我们对于这些颜色每个开一个
bitset
来维护出现位置即可
不难发现这部分的复杂度是\(O(N\times(\sqrt N+\frac{N}{\omega}))\)的,相对于后面的\(O(\frac{N^2}{\omega})\)我们可以忽略掉前项的\(O(N\sqrt N)\)
然后我们考虑将此时还未得到答案的询问的\(k_i\)值也用bitset
存起来,考虑此时需要得到的等式为\(x_i+k_j=x'\),其中\(k_j\)和\(x'\)都是用bitset
来维护的
我们移个项后就可以轻松算出此时这个灯能影响到的询问值了,直接把这些询问的答案设为\(i\)然后删除这些询问即可
今天写这题的时候上网搜了才知道,原来bitset
是有诸如找某个位置开始的下一个1
的位置的函数的,具体实现可以看下面的代码
最后总复杂度\(O(\frac{N^2}{\omega})\)
#include<cstdio>
#include<iostream>
#include<bitset>
#include<cctype>
#include<vector>
#define RI register int
#define CI const int&
using namespace std;
const int N=250005,B=505;
int n,q,k[N],x[N],c[N],rst[N],idx,ans[N];
vector <int> vc[N]; bitset <N> ques,ext[B],all;
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++)
#define pc(ch) (Ftop!=Fend?*Ftop++=ch:(fwrite(Fout,1,S,stdout),*(Ftop=Fout)++=ch))
char Fin[S],Fout[S],*A,*B,*Ftop,*Fend; int pt[20];
public:
inline FileInputOutput(void) { Ftop=Fout; Fend=Fout+S; }
inline void read(int& x)
{
x=0; char ch; while (!isdigit(ch=tc()));
while (x=(x<<3)+(x<<1)+(ch&15),isdigit(ch=tc()));
}
inline void write(int x,const char& ch='\n')
{
if (!x) return (void)(pc('0'),pc(ch));
RI ptop=0; while (x) pt[++ptop]=x%10,x/=10;
while (ptop) pc(pt[ptop--]+48); pc(ch);
}
inline void flush(void)
{
fwrite(Fout,1,Ftop-Fout,stdout);
}
}F;
int main()
{
//freopen("B.in","r",stdin); freopen("B.out","w",stdout);
RI i,j; for (F.read(n),F.read(q),i=1;i<=n;++i)
F.read(x[i]),F.read(c[i]),vc[c[i]].push_back(x[i]),all.set(x[i]);
for (i=1;i<=250000;++i) if (vc[i].size()>B)
{
rst[i]=++idx; for (auto x:vc[i]) ext[idx].set(x);
}
for (i=1;i<=q;++i) F.read(k[i]),ques.set(k[i]);
for (i=1;i<=n;++i)
{
bitset <N> cur=all,tmp=ques;
if (vc[c[i]].size()<=B)
{
for (auto x:vc[c[i]]) cur.reset(x);
} else cur^=ext[rst[c[i]]];
tmp&=(cur>>x[i]);
for (int x=tmp._Find_first();x!=tmp.size();x=tmp._Find_next(x)) ans[x]=i,ques.reset(x);
}
for (i=1;i<=q;++i) F.write(ans[k[i]]);
return F.flush(),0;
}
C. Yet Another Shortest Path Query
到现在CF上也没一个人过这个题,害怕.jpg
D. Operator Precedence
小清新构造题
对于\(\prod\)的式子,为了尽量控制它的结果我们很容易想到将因子都设为\(0/1\),由于这题不允许设为\(0\)因此考虑全部凑成\(1\)
尝试着用-1 2
这样的pair去凑中间的乘积项,不妨设\(a_1=x,a_{2n}=y\),则现在序列形如:
然后列出题目要求的等式得到:\(xy=-x-2(n-2)+2y\),不难发现设\(y=1\)即可解出\(x=3-n\),然后这题就做完了
注意由于题目不允许使用\(0\),因此要特判\(n=3\)的Case(直接把样例抄上去即可)
#include<cstdio>
#include<iostream>,
#define RI register int
#define CI const int&
using namespace std;
int t,n;
int main()
{
for (scanf("%d",&t);t;--t)
{
if (scanf("%d",&n),n==3) { puts("1 -10 6 6 -10 1"); continue; }
printf("%d ",3-n); for (RI i=1;i<n;++i) printf("-1 2 "); printf("1\n");
}
return 0;
}
E. Period of a String
徐神写的,我题意都没看
其实这题初代version早就写好了,然后T在了41个点
后面和徐神一起卡常搞了半天最后卡在比赛结束前15min过了这个题
#include <bits/stdc++.h>
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++)
#define pc(ch) (Ftop!=Fend?*Ftop++=ch:(fwrite(Fout,1,S,stdout),*(Ftop=Fout)++=ch))
char Fin[S],Fout[S],*A,*B,*Ftop,*Fend;
public:
inline FileInputOutput(void) { Ftop=Fout; Fend=Fout+S; }
inline void read(int& x)
{
x=0; char ch; while (!isdigit(ch=tc()));
while (x=(x<<3)+(x<<1)+(ch&15),isdigit(ch=tc()));
}
inline void read_string(std::string& s)
{
char ch; while (isalpha(ch=tc())) s+=ch;
}
inline void write_string(const std::string& s)
{
for (auto ch:s) pc(ch);
}
inline void flush(void)
{
fwrite(Fout,1,Ftop-Fout,stdout);
}
}F;
struct vivi {
int a[26];
inline int& operator[](const size_t i) { return a[i]; }
vivi () { memset(a, 0, sizeof(a)); }
// inline vivi& operator += (const vivi& b) const { for(int i = 0; i < 26; ++i) a[i] += b[i]; }
// inline vivi& operator -= (const vivi& b) const { for(int i = 0; i < 26; ++i) a[i] -= b[i]; }
// inline vivi& operator %= (const vivi& b) const { for(int i = 0; i < 26; ++i) a[i] %= b[i]; }
// inline bool less(const vivi& b) const {
// for(int i = 0; i < 26; ++i) if(a[i] < b[i]) return true;
// return true;
// }
};
std::vector<vivi> hkr;
std::vector<int> len;
std::vector<std::vector<std::pair<int, int>>> trim;
struct pqnode_t {
int inner_value;
inline bool operator <(const pqnode_t &b) const {
return len[inner_value] < len[b.inner_value];
}
};
std::priority_queue<pqnode_t> pq;
std::vector<pqnode_t> pqv;
std::vector<int> ending;
void work() {
#define NO() ({ F.write_string("NO\n"); return; })
int n; F.read(n);
hkr.assign(n, vivi());
len.assign(n, 0);
trim.assign(n, {});
std::vector<std::string> ans(n);
for(int i = 0; i < n; ++i) {
F.read_string(ans[i]); len[i] = ans[i].size();
for(auto c: ans[i]) hkr[i][c - 'a'] += 1;
}
while(pq.size()) pq.pop();
pq.push({ n - 1 });
for(int i = n - 2; i >= 0; --i) {
while(pq.size()) {
auto [top] = pq.top();
if(len[top] < len[i]) break;
pq.pop();
const int uu = len[top] / len[i];
if(ans[i].size() >= 26) {
for(int j = 0; j < 26; ++j) hkr[top][j] -= hkr[i][j] * uu;
for(int j = 0; j < 26; ++j) if(hkr[top][j] < 0) NO();
} else {
for(auto c: ans[i]) if((hkr[top][c - 'a'] -= uu) < 0) NO();
}
len[top] -= uu * len[i];
trim[top].push_back({i, uu});
if(len[top]) pq.push({ top });
}
pq.push({ i });
}
pqv.clear();
pqv.reserve(pq.size());
while(pq.size()) pqv.push_back(pq.top()), pq.pop();
std::string curAns = "";
vivi curVivi;
ending.assign(n, 0);
std::reverse(pqv.begin(), pqv.end());
for(auto [v]: pqv) {
for(int i = 0; i < 26; ++i) {
if(hkr[v][i] < curVivi[i]) NO();
while(hkr[v][i] > curVivi[i]) {
curAns += char('a' + i);
curVivi[i] += 1;
}
}
ending[v] = curAns.size();
}
F.write_string("YES\n");
for(int i = 0; i < n; ++i) {
ans[i].clear();
for(auto [a, b]: trim[i]) while(b--) ans[i] += ans[a];
for(int j = 0; j < ending[i]; ++j) ans[i] += curAns[j];
F.write_string(ans[i]);
F.write_string("\n");
}
#undef NO
}
int main(void) {
// std::ios::sync_with_stdio(false);
int t; F.read(t);
while(t--) work();
return F.flush(),0;
}
F. Top Cluster
感觉这题算是中档题ABEF中最合我胃口的一个了,而且出题人在题目中提示了做法因此也不难想到
首先不妨将点按照\(w_i\)排序,找到第一个没有出现的数\(x\),不难发现答案只可能在\([0,x]\)中
对于MEX问题一个经典的思路就是二分,我们不妨二分\(mid\),判断所有\(w_i\le mid\)的点是否都在\(\operatorname{dist}(x,i)\le k\)的范围内
那么问题等价于找是否存在某个\(w_i\le mid\)的点在\(\operatorname{dist}(x,i)> k\)的范围内,即等价于找\(w_i\le mid\)的点到\(x\)距离的最大值
根据经典结论,我们求出\(w_i\le mid\)的点在树上构成的直径,则\(x\)到这些点距离的最大值等于到直径两个端点距离的较大值
而求某段前缀点集构成的直径也很简单,只要讨论新加入的点和原来直径的两个端点是否能构成新的直径即可
用欧拉序+ST表实现\(O(1)\)查询LCA,总复杂度\(O((n+q)\log n)\)
#include<cstdio>
#include<iostream>
#include<vector>
#include<cctype>
#include<utility>
#include<algorithm>
#define int long long
#define RI register int
#define CI const int&
#define fi first
#define se second
using namespace std;
typedef pair <int,int> pi;
const int N=1e6+5;
int n,q,x,y,z,u[N],v[N]; pi w[N]; vector <pi> E[N];
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++)
#define pc(ch) (Ftop!=Fend?*Ftop++=ch:(fwrite(Fout,1,S,stdout),*(Ftop=Fout)++=ch))
char Fin[S],Fout[S],*A,*B,*Ftop,*Fend; int pt[30];
public:
inline FileInputOutput(void) { Ftop=Fout; Fend=Fout+S; }
inline void read(int& x)
{
x=0; char ch; while (!isdigit(ch=tc()));
while (x=(x<<3)+(x<<1)+(ch&15),isdigit(ch=tc()));
}
inline void write(int x,const char& ch='\n')
{
if (!x) return (void)(pc('0'),pc(ch));
RI ptop=0; while (x) pt[++ptop]=x%10,x/=10;
while (ptop) pc(pt[ptop--]+48); pc(ch);
}
inline void flush(void)
{
fwrite(Fout,1,Ftop-Fout,stdout);
}
}F;
namespace LCA_Solver
{
int dep[N],dis[N],seq[N],fir[N],f[N][20],_log[N],idx;
inline void DFS(CI now=1,CI fa=0)
{
seq[++idx]=now; fir[now]=idx; dep[now]=dep[fa]+1;
for (auto [to,w]:E[now]) if (to!=fa) dis[to]=dis[now]+w,DFS(to,now),seq[++idx]=now;
}
inline int mindep(CI x,CI y)
{
return dep[x]<dep[y]?x:y;
}
inline void init(void)
{
RI i,j; for (_log[0]=-1,i=1;i<=idx;++i) _log[i]=_log[i>>1]+1;
for (i=1;i<=idx;++i) f[i][0]=seq[i];
for (j=1;j<20;++j) for (i=1;i+(1<<j)-1<=idx;++i)
f[i][j]=mindep(f[i][j-1],f[i+(1<<j-1)][j-1]);
}
inline int LCA(int x,int y)
{
x=fir[x]; y=fir[y]; if (x>y) swap(x,y);
int k=_log[y-x+1]; return mindep(f[x][k],f[y-(1<<k)+1][k]);
}
inline int getdis(CI x,CI y)
{
if (x==0||y==0) return 0;
return dis[x]+dis[y]-2LL*dis[LCA(x,y)];
}
};
using namespace LCA_Solver;
signed main()
{
//freopen("F.in","r",stdin); freopen("F.out","w",stdout);
RI i; for (F.read(n),F.read(q),i=1;i<=n;++i) F.read(w[i].fi),w[i].se=i;
for (i=1;i<n;++i) F.read(x),F.read(y),F.read(z),E[x].push_back(pi(y,z)),E[y].push_back(pi(x,z));
DFS(); init(); int pos=n+1; for (sort(w+1,w+n+1),i=1;i<=n;++i) if (w[i].fi!=i-1) { pos=i; break; }
--pos; u[1]=u[2]=w[1].se; v[2]=w[2].se;
for (i=3;i<=pos;++i)
{
int x=w[i].se,d0=getdis(u[i-1],v[i-1]),d1=getdis(u[i-1],x),d2=getdis(v[i-1],x);
if (d0>=d1&&d0>=d2) u[i]=u[i-1],v[i]=v[i-1]; else
if (d1>=d0&&d1>=d2) u[i]=u[i-1],v[i]=x; else u[i]=x,v[i]=v[i-1];
}
//for (i=1;i<=pos;++i) printf("%lld %lld %lld\n",w[i].se,u[i],v[i]);
for (i=1;i<=q;++i)
{
F.read(x); F.read(y);
auto check=[&](CI k)
{
return max(getdis(x,u[k+1]),getdis(x,v[k+1]))<=y;
};
int l=0,r=pos-1,mid,ret=pos; while (l<=r)
if (check(mid=l+r>>1)) l=mid+1; else ret=mid,r=mid-1;
F.write(ret);
}
return F.flush(),0;
}
G. Snake Move
首先不难发现我们只要关注蛇头的移动即可,除了初始时就存在的蛇身外,后面移动蛇头产生的蛇身是不会影响答案的
具体地,我们可以把每一步的操作看成以下两个步骤:
- 将蛇尾删除
- 将蛇头移动一个位置(或不动)
因此可以直接在图上跑一个单源BFS求蛇头到每个点的最短路,只不过对于初始时的某段蛇身(设其为第\(i\)段),到它的最短路要和\(k-i+1\)取\(\max\)
如果直接用Dijkstra去写的话可能会T,这里我们可以用deque
维护正常走到的空地,用优先队列来维护被松弛到的蛇身
每次判断能否将优先队列中的元素放到deque
前面即可,由于优先队列是\(O(1)\)查询最小值的,因此总复杂度为\(O(nm+k\log k)\)
#include<cstdio>
#include<iostream>
#include<queue>
#include<deque>
#include<utility>
#include<cstring>
#define RI register int
#define CI const int&
#define fi first
#define se second
using namespace std;
typedef pair <int,int> pi;
const int N=3005,K=100005,dx[4]={0,1,0,-1},dy[4]={1,0,-1,0};
int n,m,k,u[K],v[K],f[N][N],rk[N][N],vis[N][N]; char a[N][N];
int main()
{
RI i,j; for (scanf("%d%d%d",&n,&m,&k),i=1;i<=k;++i) scanf("%d%d",&u[i],&v[i]);
for (i=1;i<=n;++i) scanf("%s",a[i]+1); memset(f,-1,sizeof(f));
for (i=2;i<=k;++i) a[u[i]][v[i]]='b',rk[u[i]][v[i]]=i;
deque <pi> q; q.push_back(pi(u[1],v[1])); f[u[1]][v[1]]=0;
priority_queue <pi,vector <pi>,greater <pi>> hp;
while (!q.empty())
{
auto [x,y]=q.front(); q.pop_front();
for (i=0;i<4;++i)
{
int nx=x+dx[i],ny=y+dy[i];
if (nx<1||nx>n||ny<1||ny>m) continue;
if (a[nx][ny]=='#') continue;
if (a[nx][ny]=='.'&&f[nx][ny]==-1)
f[nx][ny]=f[x][y]+1,q.push_back(pi(nx,ny));
if (a[nx][ny]=='b'&&!vis[nx][ny])
vis[nx][ny]=1,hp.push(pi(max(f[x][y]+1,k-rk[nx][ny]+1),rk[nx][ny]));
}
if (q.empty()&&!hp.empty())
{
auto [dis,id]=hp.top(); hp.pop();
f[u[id]][v[id]]=dis; q.push_front(pi(u[id],v[id]));
} else
{
auto [tx,ty]=q.front();
if (!hp.empty()&&hp.top().fi<=f[tx][ty])
{
auto [dis,id]=hp.top(); hp.pop();
f[u[id]][v[id]]=dis; q.push_front(pi(u[id],v[id]));
}
}
}
//for (i=1;i<=n;++i) for (j=1;j<=m;++j) printf("%d%c",f[i][j]," \n"[j==m]);
unsigned long long ans=0;
for (i=1;i<=n;++i) for (j=1;j<=m;++j)
{
if (f[i][j]==-1) f[i][j]=0;
ans+=1ull*f[i][j]*f[i][j];
}
return printf("%llu",ans),0;
}
H. Sugar Sweet II
这题本来我和祁神一起想的然后中间我滚去写G题了,后面祁神和徐神就讨论出来怎么做了
好像大致思路就是暴力连\(b_i\to i\)的边,然后如果\(a_i\ge a_{b_i}+w_{b_i}\)就可以直接删掉这条边,这样得到的图就一定没有环了
然后考虑从某个必然发生的事件到某个点\(i\)的路径长度\(d_i\),则\(i\)增加\(w_i\)的概率为\(\frac{1}{d_i!}\),因此随便搞一下求出每个点的\(d_i\)即可
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int MOD = (int)1e9+7;
void add(int &x, int a){if ((x+=a)>=MOD) x-=MOD;}
void mul(int &x, int a){x=x*a%MOD;}
int powe(int x, int p){
int res=1;
while (p>0){if (p&1) mul(res, x); mul(x, x); p>>=1;}
return res;
}
const int N = 5e5+5;
int t, n, A[N], B[N], wt[N], dis[N], ans[N];
int fac[N];
vector<int> G[N];
void dfs(int x){
for (int v : G[x]){
dis[v]=dis[x]+1;
dfs(v);
}
}
signed main(){
ios::sync_with_stdio(0); cin.tie(0);
fac[0]=1;
for (int i=1; i<N; ++i) fac[i]=fac[i-1]*i%MOD;
cin >> t;
while (t--){
cin >> n;
// printf("n=%lld\n", n);
for (int i=1; i<=n; ++i) cin >> A[i];
for (int i=1; i<=n; ++i) cin >> B[i];
for (int i=1; i<=n; ++i) cin >> wt[i];
for (int i=1; i<=n; ++i) dis[i]=0, G[i].clear();
vector<int> vec;
for (int i=1; i<=n; ++i){
if (A[B[i]] > A[i]) dis[i]=1, vec.push_back(i);
else if (A[B[i]]+wt[B[i]] <= A[i]) dis[i]=0, vec.push_back(i);
else G[B[i]].push_back(i);
}
for (int x : vec){
if (dis[x]>0) dfs(x);
}
// printf("dis:"); for (int i=1; i<=n; ++i) printf("%lld ", dis[i]); puts("");
for (int i=1; i<=n; ++i){
if (dis[i]==0) ans[i]=A[i];
else ans[i] = (A[i] + wt[i]*powe(fac[dis[i]], MOD-2)%MOD)%MOD;
cout << ans[i] << ' ';
}
cout << '\n';
}
return 0;
}
I. Dreamy Putata
经典没人过的题,弃疗
J. Mysterious Tree
小清新交互题
不妨先询问\(\lceil\frac{n}{2}\rceil\)个二元组\((1,2),(3,4),\cdots,(n-1,n)\),这样可以保证每个点都被询问至少一次,如果这些边都不在树中,那么此时树必然不可能是菊花图
否则对于任意一条存在的边,设其端点为\(u,v\),我们依次检验这两个点是否可能是菊花图的中心即可,稍微讨论下会发现刚好需要\(3\)次操作
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
int t,n;
inline int ask(CI x,CI y)
{
printf("? %d %d\n\n",x,y); fflush(stdout);
int z; scanf("%d",&z); return z;
}
inline void answer(CI tp)
{
printf("! %d\n\n",tp); fflush(stdout);
}
int main()
{
for (scanf("%d",&t);t;--t)
{
RI i; scanf("%d",&n); int u=-1,v=-1;
for (i=1;i+1<=n;i+=2)
{
int tmp=ask(i,i+1);
if (tmp) u=i,v=i+1;
}
if (n%2==1)
{
int tmp=ask(n-1,n);
if (tmp) u=n-1,v=n;
}
if (u==-1) { answer(1); continue; }
int x,y; for (i=1;i<=n;++i) if (i!=u&&i!=v) { x=i; break; }
for (i=1;i<=n;++i) if (i!=u&&i!=v&&i!=x) { y=i; break; }
int t1=ask(u,x);
if (t1)
{
int t2=ask(u,y);
if (t2) { answer(2); continue; }
else { answer(1); continue; }
} else
{
int t2=ask(v,x);
if (!t2) { answer(1); continue; }
int t3=ask(v,y);
if (t3) { answer(2); continue; }
else { answer(1); continue; }
}
}
return 0;
}
K. Card Game
貌似是Hard题中唯一一个有人过的,但我完全不会做
L. Master of Both V
又是一个没人过的题,所以感觉这场后期的题难度有点偏高了的说
M. V-Diagram
签到题,我题意都不知道
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 3e5+5;
int t, n, A[N];
signed main(){
ios::sync_with_stdio(0); cin.tie(0);
cout << setiosflags(ios::fixed) << setprecision(10);
cin >> t;
while (t--){
cin >> n;
int mn=1;
for (int i=1; i<=n; ++i){
cin >> A[i];
if (A[i]<A[mn]) mn=i;
}
int sum=0, sumL=0, sumR=0;
for (int i=1; i<=mn+1; ++i) sumL+=A[i];
for (int i=mn-1; i<=n; ++i) sumR+=A[i];
for (int i=1; i<=n; ++i) sum+=A[i];
cout << max({1.0L*sum/n, 1.0L*sumL/(mn+1), 1.0L*sumR/(n-mn+2)}) << '\n';
}
return 0;
}
Postscript
徐神还是太猛了,带飞我们轻松摄金