THUSC2021 游记&Day1题解
Day -2
以为去不了,结果给批下来了。可以时隔两年再去一次杭州了。
Day 0
在火车站等了2个小时然后坐了1个小时火车之后到了杭州。午饭湘菜辣的我怀疑我是不是在浙江。
得知系统是Ubuntu觉得根本用不惯,大概看了一下怎么编译和运行,应该不会爆零了。
晚上用VS Code练了一下手,打了两个模板还打挂了。
Day 1
拿到牌子发现在紫金港校区上机,上午别人试机我们干等非常无聊,把能找到的远古THUSC的题简单看了一下觉得还行。
一试
读题
快速读了一遍题,T1看着比较简单,感觉按照题意贪心即可。T2看着有限眼熟,T3只会暴力,T4神仙通信题。从来没做过通信题的我一直没怎么认真看T4。
T1 move
以为是简单贪心,想了各种贪心方法不是正确性假了就是复杂度假了。有一瞬间想到过正解,就是二分之后找\(k\)小值之和,但是不知道为什么以为复杂度是错的就给否掉了。在想贪心上花了太多的时间。
写了个\(O(n^2\log n)\)的做法跑路了,因为没法单步调试还调了好久。而且中间还无数次觉得难写调不出来,想先去写后面的再回来调,然而后面的题也没啥好思路,就又滚回来写了。
期望得分\(60\ pts\),pretest得分\(100\ pts\)。可能是因为我的\(O(n^2\log n)\)一般卡不太满。
这时考试已经过去了2h
T2 watermelon
看着有点像联合省选Day2T1,结果发现完全不一样。想着T1花的时间太多,没想多久就赶紧写了个\(O(n^2\log n)\)的LIS和各种奇怪部分分跑了
期望得分\(60\ pts\),pretest得分\(60\ pts\)。
此时考试已经过去了3h
T3 emiya
今天我死在 Emiya 家,明天我也死在 Emiya 家——whx1003
也没什么思路,想了一会就打了个暴力。考完了发现我是个傻子,还有特别好写的\(20\ pts\)部分分我没写
期望得分\(20\ pts\),pretest得分\(20\ pts\)。
此时考试已经过去了3h45min
T4 planttree
想着前面的题已经尽力了,决定乱搞一下T4,看看能不能搞出什么好东西。
开始连暴力都想不出来,乱搞了几个作答做法之后想出了\(20\ pts\)做法。交互库用不来,就只能人脑判断树同构。
期望得分\(20\ pts\),pretest得分\(20\ pts\)。
出考场之后听大家说pretest没卡满,\(70\ pts\)的\(n\le 65\),我开始怀疑我写挂了
总分:期望得分\(160\ pts\),pretest得分\(200\ pts\)
考后
感觉我考场上脑子非常不清醒,不然分数本可以更高(发挥好一点至少也该有\(220\ pts\)吧)
晚上因为有人没上车等了很久才回西溪校区,晚饭吃的不错
Day2
二试
从来没见过工程题,拿到题的时候一脸懵。考场前预言用不到论文,结果果然没用到。
T1 bitmap
可能看了半个小时终于搞懂了它要干什么,但是什么输出格式和怎么控制输出的字节完全不会。尝试了很多种输出方式比如printf,翻cppreferences,都完全不对。本来想放弃,用官方converter去做剩下的题,结果没有权限用不起。而后面所有的题都必须用转化器,我以为我要爆零了。然后回来继续乱试,然后对着下发文件的逆函数看,流下了不会fwrite和fprintf的眼泪。
终于看到了公告上解决没有权限的问题,然后把图片用Vim打开看到了一堆乱码,终于对着这一对乱码搞懂了该怎么输出。终于拿到了\(40\ pts\),没有爆零。
T2 triangle
留给我的时间已经不多了,而且这道题是后面所有题的基础,就只能做这道了。
花了一点时间在math&geometry.pdf里找到了教程,想按照它给的式子求,然后发现求平面解析式还要高斯消元。好不容易写完了结果编译不过,有个在library.pdf里的函数我调用不了,时间也所剩无几,我就放弃了。
出了考场听说library.pdf有一个求直线与平面交点的函数,我还在那自己手写,简直血亏。
T3\(\sim\)T8
完全没时间看
考后
第一次接触这种题,已经不能再比这手忙脚乱了,希望下次遇到能好一点,这次就当体验生活了。对fwrite和fprintf不常用函数不够了解,对Ubuntu极其不熟悉,考场上遇到权限问题和编译问题没有提问(以为不是技术问题会不予解答)……
剩下的时间
学校的午饭除了有点贵都不错,还有虾和扇贝可以吃。
回去西溪校区的比较晚就只听到了Day1T4和Day2的讲评。因为闭幕式推迟了就先坐大巴会宁波镇海了。大巴上得知我是优秀,感觉不可思议的离谱。
THUSC 2021 Day1 题解
T1 move
题意
给定数列\(a\),按照如下规则删去数字:
- 数字之和不超过\(m\)
- 删去的数字最多
- 在有多个满足前两个条件的位置集合中,选择对位置排序之后字典序最大的一个
求删多少次之和能把所有数删完
数据范围:\(n\le 5\times 10^4,a_i\le m\le 10^9\)
solution
首先贪心地选择全局最小的数,这样可以得到当前这一轮需要的个数,记为\(k\)
我们需要得到最大的,满足后缀\(k\)小值之和\(\le m\)的最大的位置\(p\)。位置\(p\)可以二分得到,在线动态求后缀\(k\)小值之和用树套树(树状数组套权值线段树)。得到这一位之后一次类似地求出后面的每一位。
将\(a_i\)离散化,时间复杂度\(O(n\log^3 n)\)
const int N=5e4+5,M=1e7+3e6;
#define ll long long
const ll inf=1e15;
int buc[N],w,idbuc[N],a[N],b[N],n,m;
inline void lsh(){
for(int i=1;i<=n;++i)buc[i]=a[i];
sort(buc+1,buc+1+n);w=unique(buc+1,buc+1+n)-buc-1;
for(int i=1;i<=n;++i)b[i]=lower_bound(buc+1,buc+1+w,a[i])-buc;
}
namespace SGT{
int tot,sz[M],ls[M],rs[M];ll sum[M];
#define lid ls[id]
#define rid rs[id]
#define mid ((l+r)>>1)
void modify(int &id,int l,int r,int pos,int val){
if(!id)id=++tot;
sz[id]+=val;sum[id]+=buc[pos]*val;
if(l==r)return;
if(pos<=mid)modify(lid,l,mid,pos,val);
else modify(rid,mid+1,r,pos,val);
}
ll query(int tot1,int l,int r,int k){
int cnt=0;
for(int i=1;i<=tot1;++i)cnt+=sz[idbuc[i]];
if(cnt<k)return inf;
if(l==r)return 1ll*buc[l]*k;
int lcnt=0;ll lsum=0;
for(int i=1;i<=tot1;++i)lsum+=sum[ls[idbuc[i]]],lcnt+=sz[ls[idbuc[i]]];
if(lcnt>=k){
for(int i=1;i<=tot1;++i)idbuc[i]=ls[idbuc[i]];
return query(tot1,l,mid,k);
}else{
for(int i=1;i<=tot1;++i)idbuc[i]=rs[idbuc[i]];
return lsum+query(tot1,mid+1,r,k-lcnt);
}
}
int query1(int id,int l,int r,int k){
if(!id)return 0;
if(l==r)return min(sz[id],k/buc[l]);
if(sum[lid]>=k)return query1(lid,l,mid,k);
else return sz[lid]+query1(rid,mid+1,r,k-sum[lid]);
}
}
using namespace SGT;
int rt[N];
namespace BIT{
inline int lb(int x){return x&(-x);}
inline void update(int x,int pos,int val){
for(;x;x-=lb(x))modify(rt[x],1,w,pos,val);
}
inline ll ask(int x,int k){
int tot1=0;
for(;x<=n;x+=lb(x))idbuc[++tot1]=rt[x];
return query(tot1,1,w,k);
}
}
using namespace BIT;
int main(){
n=read();m=read();
for(int i=1;i<=n;++i)a[i]=read();
lsh();
for(int i=1;i<=n;++i)update(i,b[i],1),modify(rt[0],1,w,b[i],1);
int ans=0,tot1=n;
while(tot1){
int cnt=query1(rt[0],1,w,m);
tot1-=cnt;++ans;
int l=1,r=n,ps;ll cur,curm=m;
for(int i=1;i<=cnt;++i){
while(l<=r){
cur=ask(mid,cnt-i+1);
if(cur<=curm)ps=mid,l=mid+1;
else r=mid-1;
}
update(ps,b[ps],-1),modify(rt[0],1,w,b[ps],-1);
l=ps+1;r=n;curm-=a[ps];
}
}
printf("%d\n",ans);
return 0;
}
T2 watermelon
题意
选择树上的一条链,使得这条链上的最长严格上升子序列长度最大。求这个最大值。
数据范围:\(n\le 10^5,a_i\le 10^9\)
solution
考虑dp,\(f_{u,i}\)表示\(u\)的子树中,从下往上的最长上升子序列以\(i\)结尾的最长长度,\(g_{u,i}\)表示\(u\)的子树中,从上往下的最长上升子序列以\(i\)开头的最长长度
那么两颗子树合并的时候,用\(\max\limits_{l<r}(f_{x,l}+g_{y,r},f_{y,l}+g_{x,r})\)更新答案
子树往根合并时,用\(\max\limits_{j=1}^{i-1}f_{v,j}+1\)更新\(f_{u,i}\),用\(\max\limits_{j=i+1}^{a_v}g_{v,j}+1\)更新\(g_{u,i}\)
现在我们得到了\(O(n^2)\)DP
实际上我们用线段树合并维护子树的合并,在合并的过程中更新答案即可
复杂度\(O(n\log n)\)
const int N=1e5+5;
struct Edge{int to,next;}e[N<<1];
int head[N],ecnt;
inline void adde(int u,int v){e[++ecnt]=(Edge){v,head[u]};head[u]=ecnt;}
int n,w,a[N],b[N],buc[N];
inline void lsh(){
for(int i=1;i<=n;++i)buc[i]=a[i];
sort(buc+1,buc+1+n);w=unique(buc+1,buc+1+n)-buc-1;
for(int i=1;i<=n;++i)b[i]=lower_bound(buc+1,buc+1+w,a[i])-buc;
}
struct Node{int f,g;};
inline Node Max(Node a1,Node b1){return (Node){max(a1.f,b1.f),max(a1.g,b1.g)};}
int ans=0;
namespace SGT{
int tot,ls[N<<7],rs[N<<7];
Node tr[N<<7];
#define lid ls[id]
#define rid rs[id]
#define mid ((l+r)>>1)
void modify(int &id,int l,int r,int pos,Node val){
if(!id)id=++tot;
tr[id]=Max(tr[id],val);
if(l==r)return;
if(pos<=mid)modify(lid,l,mid,pos,val);
else modify(rid,mid+1,r,pos,val);
}
Node query(int id,int l,int r,int L,int R){
if(!id||L>R)return (Node){0,0};
if(L<=l&&r<=R)return tr[id];
Node ret=(Node){0,0};
if(L<=mid)ret=Max(ret,query(lid,l,mid,L,R));
if(R>mid)ret=Max(ret,query(rid,mid+1,r,L,R));
return ret;
}
int merge(int x,int y){
if(!x||!y)return x+y;
tr[x]=Max(tr[x],tr[y]);
ans=max(ans,max(tr[ls[x]].f+tr[rs[y]].g,tr[ls[y]].f+tr[rs[x]].g));
ls[x]=merge(ls[x],ls[y]);
rs[x]=merge(rs[x],rs[y]);
return x;
}
}
using namespace SGT;
int rt[N];
void dfs(int u,int fa){
SGT::modify(rt[u],1,w,b[u],(Node){1,1});
for(int i=head[u],v;i;i=e[i].next){
v=e[i].to;if(v==fa)continue;
dfs(v,u);
Node cur=(Node){query(rt[v],1,w,1,b[u]-1).f,query(rt[v],1,w,b[u]+1,w).g};
rt[u]=merge(rt[u],rt[v]);
ans=max(ans,max(cur.f+1,cur.g+1));
modify(rt[u],1,w,b[u],(Node){cur.f+1,cur.g+1});
}
}
int main(){
n=read();
for(int i=1;i<=n;++i)a[i]=read();
lsh();
for(int i=1,u,v;i<n;++i)u=read(),v=read(),adde(u,v),adde(v,u);
dfs(1,0);
printf("%d",ans);
return 0;
}
T3 emiya
题意
有\(n\)个人\(m\)道菜,每个人对每道菜的喜爱度为\(a_{i,j}\)。如果餐桌上有自己不喜欢的菜(\(a_{i,j}=-1\)),他会愤怒的离席而去,并且一分钱也不留下;否则对于每一道菜他都会留下\(a_{i,j}\)的奖金
选择做一些菜,使你得到的奖金最大
数据范围:\(n\le 20,m\le 10^6\)
solution
设每道菜喜爱的人点集合为\(s_i\)
设选择集合为\(S\)的人的答案为\(f(S)\),构造\(g(T)\)满足
类似差分后缀和,对于\(a_{i,j}\ne -1\),
然后求和即可,求和可以递推
const int N=20,M=1e6+5;
int n,m,a[N][M],s[M];
#define ll long long
ll f[1<<N],ans;
int main(){
ll ans=0;
n=read();m=read();for(int i=1;i<=n;++i)for(int j=1;j<=m;++j)a[i][j]=read();
for(int i=1;i<=m;++i)
for(int j=1;j<=n;++j)
if(a[j][i]>=0)
s[i]|=1<<(j-1);
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j)
if(a[i][j]>=0){
f[s[j]]+=a[i][j];
f[s[j]^(1<<(i-1))]-=a[i][j];
}
for(int i=1;i<=n;++i)
for(int j=1;j<(1<<n);++j)
if(j&(1<<(i-1)))f[j^(1<<(i-1))]+=f[j];
for(int i=1;i<(1<<n);++i)ans=max(ans,f[i]);
printf("%lld",ans);
return 0;
}
T4 planttree
题意
通信题
给你一棵树,用一个\(128\)位二进制数表示这棵树(编码),并在只知道节点数和你计算出的\(128\)位二进制数的情况下还原这棵树(树同构即可)。多组数据
数据范围:\(n\le 70,T\le 10^5\)
solution
首先考虑\(n\le 65\)的部分,递归地表示这棵树:如果往儿子走,则当前位填\(1\),往父亲走填\(0\)。还原时也递归地还原这棵树即可。然后因为\(1\)一定往儿子走,不会往父亲走,前后两位可以去掉,可以将长度控制在\(2n-2\)以内
考虑优化
上述过程是一个括号匹配的过程,填\(1\)则为左括号,填\(0\)为右括号
而我们有可以省去最靠前的一个\((\)和最靠后的一个\()\),这样的合法括号序列个数为\(Catalan(n-1)\)个
而\(Catalan(69)\)刚好比\(2^{128}-1\)小
也就是我们现在要将一个括号序列与一个数字一一对应
我们计算这个括号序列的字典序是所有合法括号序列的第几项,也就是对于每一个填\(1\)的位,求出这一位填\(0\),前面所有位都一样的合法括号序列数,可以用组合数计算。这样我们就完成了编码。
解码时就是上述过程的逆过程。如果当前位填\(0\)之后,合法括号序列的个数小于我们所需的,那么这一位必须填\(1\),否则必须填\(0\)。这样我们完成了解码
#include <bits/stdc++.h>
#include "tree.h"
using namespace std;
#define u128 unsigned __int128
const int N=300;
namespace Encoder{
u128 C[N][N];
inline u128 calc(int x,int y){return C[x+y][x]-C[x+y][x+1];}
bool flag=0;
struct Edge{int to,next;}e[N];
int head[N],ecnt;
inline void adde(int u,int v){e[++ecnt]=(Edge){v,head[u]};head[u]=ecnt;}
u128 ret;
inline void init(int n){
memset(head+1,0,n<<2);
ecnt=0;ret=0;
if(!flag){
C[0][0]=1;
for(int i=1;i<=145;++i){
C[i][0]=1;C[i][i]=1;
for(int j=1;j<i;++j)C[i][j]=C[i-1][j-1]+C[i-1][j];
}
flag=1;
}
}
int x,y;
void dfs(int u){
if(u>1)ret+=calc(x-1,y),--y;
for(int i=head[u];i;i=e[i].next)dfs(e[i].to);
if(u>1)--x;
}
inline u128 work(int n,const int *p){
init(n);
for(int i=n;i>=2;--i)adde(p[i],i);
x=n-1;y=n-1;dfs(1);
return ret;
}
}
u128 encode(int n,const int *fa){return Encoder::work(n,fa);}
namespace Decoder{
u128 C[N][N];
inline u128 calc(int x,int y){return C[x+y][x]-C[x+y][x+1];}
bool flag=0;
int tot=0;
inline void init(int n){
tot=1;
if(!flag){
C[0][0]=1;
for(int i=1;i<=145;++i){
C[i][0]=1;C[i][i]=1;
for(int j=1;j<i;++j)C[i][j]=C[i-1][j-1]+C[i-1][j];
}
flag=1;
}
}
int stac[N],a[N],top;
void work(int n,u128 cur,int *p){
init(n);
for(int i=1,x=n-1,y=n-1;i<=n*2-2;++i){
if(cur>=calc(x-1, y)) cur-=calc(x-1, y), a[i]=1, --y;
else --x,a[i]=0;
}
p[1]=0;stac[top=1]=1;tot=1;
for(int i=1;i<=n*2-2;++i){
if(a[i])p[++tot]=stac[top],stac[++top]=tot;
else --top;
}
}
}
void decode(int n,u128 tr,int *p){Decoder::work(n,tr,p);}