THUSC2021题解

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

THUSC2021题解
T1 move
Description
给定⼀个长度为n的序列a和⼀个正整数m,现在按下述策略删除a中的数字:
选出⼀个下标序列p,使得其:
1. 单调递增
2. ∑x∈p a x≤m
3. 元素个数最多
4. 字典序是满⾜上述条件的序列中最⼤的
然后删除a中下标在p中的元素。

问按上述策略进⾏⼏次删除会使a为空。

n≤5×104,m≤109。

Solution
如果没有第4条限制,那么只需要⽤set维护所有数字,然后贪⼼选择最⼩的元素即可。

如果有第4条限制,通过这样⽅法得到的元素个数也⼀定是最多的,因此⼀定选出的下标序列⼤⼩已经确定。

考虑从前往后确定该下标序列中的元素,对于当前位置,考虑⼆分,那么元素id能放在当前位置,当且仅当下标≥id且还存在的元素中,最⼩的k 个元素之和≤m。

找到id之后,将id删掉,继续⼆分下⼀个元素。

因此需要⽀持单点修改,考虑使⽤树套树维护这个东西,外层⽤树状数组或线段树维护下标区间,内层⽤权值线段树维护当前区间的元素的权值。

单点修改时,在 log 个区间内同时修改。

查询时,将询问拆为 log 个区间,然后将这 log 个区间的权值线段树合并进⾏查询。

当然事实上你不需要合并,只需要同时维护 log 个根节点,正常权值线段树找左⼦树⼤⼩时,就将这 log 个根的左⼦树⼤⼩加起来即可。

这是树套树的⼀种常见套路,全世界⼤概只有我⼀个不会。

总复杂度为 O(n log3n)。

Code
#include<bits/stdc++.h>
using namespace std;
const int N=5e4+10,lg=16;
typedef long long ll;
const int inf=0x3f3f3f3f;
int n,m,a[N],ans,b[N],pos[N],cnt;
namespace iobuff{
const int LEN=1000000;
char in[LEN+5],out[LEN+5];
char *pin=in,*pout=out,*ed=in,*eout=out+LEN;
inline char gc(void){
#ifdef LOCAL
return getchar();
#endif
return pin==ed&&(ed=(pin=in)+fread(in,1,LEN,stdin),ed==in)?EOF:*pin++;
}
inline void pc(char c){
pout==eout&&(fwrite(out,1,LEN,stdout),pout=out);
(*pout++)=c;
}
inline void flush(){fwrite(out,1,pout-out,stdout),pout=out;}
template<typename T> inline void read(T &x){
static int f;
static char c;
c=gc(),f=1,x=0;
while(c<'0'||c>'9') f=(c=='-'?-1:1),c=gc();
while(c>='0'&&c<='9') x=10*x+c-'0',c=gc();
x*=f;
}
template<typename T> inline void putint(T x,char div){
static char s[15];
static int top;
top=0;
x<0?pc('-'),x=-x:0;
while(x) s[top++]=x%10,x/=10;
!top?pc('0'),0:0;
while(top--) pc(s[top]+'0');
pc(div);
}
}
using namespace iobuff;
int q[N];
namespace SGT{
const int M=N*lg*lg;
#define mid ((l+r)>>1)
int ls[M],rs[M],siz[M],tot;
ll sum[M];
inline void update(int &p,int x,int tp,int l=1,int r=cnt){
if(!p) p=++tot;
siz[p]+=tp;sum[p]+=tp*b[x];
if(l==r) return ;
if(x<=mid) update(ls[p],x,tp,l,mid);
else update(rs[p],x,tp,mid+1,r);
}
inline ll query(int tot,int k,int m,int l=1,int r=cnt){
int sz=0,lsiz=0;ll lsum=0;
for(int i=1;i<=tot;++i){
int p=q[i];
sz+=siz[p],lsiz+=siz[ls[p]],lsum+=sum[ls[p]];
}
if(sz<k) return m+1;
if(l==r) return 1ll*k*b[l];
if(lsiz>=k){
for(int i=1;i<=tot;++i) q[i]=ls[q[i]];
return query(tot,k,m,l,mid);
}
else{
if(lsum>m) return m+1;
for(int i=1;i<=tot;++i) q[i]=rs[q[i]];
return lsum+query(tot,k-lsiz,m-lsum,mid+1,r);
}
}
#undef mid
}
namespace BIT{
int rt[N];
inline int lowbit(int x){return x&(-x);}
inline void update(int x,int v,int tp){
for(;x;x-=lowbit(x)) SGT::update(rt[x],v,tp);
}
inline ll query(int x,int v,int m){
int tot=0;
for(;x<=n;x+=lowbit(x)) q[++tot]=rt[x];
return SGT::query(tot,v,m);
}
}
multiset<int> s;
int main(){
// freopen("1.in","r",stdin);
read(n);read(m);
for(int i=1;i<=n;++i) read(a[i]),b[i]=a[i];
sort(b+1,b+n+1);
cnt=unique(b+1,b+n+1)-b-1;
for(int i=1;i<=n;++i) pos[i]=lower_bound(b+1,b+cnt+1,a[i])-b; for(int i=1;i<=n;++i) BIT::update(i,pos[i],1),s.insert(a[i]);
while(s.size()){
vector<int> dec,rel;
int rec=m;ans++;
while(s.size()&&(*s.begin())<=rec){
dec.push_back(*s.begin());
rec-=dec.back();s.erase(s.begin());
}
int sum=dec.size();rec=m;
for(int i=1;i<=sum;++i){
int l=1,r=n-(sum-i+1)+1,ans=0;
while(l<=r){
int mid=(l+r)>>1;
if(BIT::query(mid,sum-i+1,rec)<=rec) l=mid+1,ans=mid;
else r=mid-1;
}
BIT::update(ans,pos[ans],-1);
rec-=a[ans];
rel.push_back(a[ans]);
}
for(int v:dec) s.insert(v);
for(int v:rel) s.erase(s.find(v));
}
printf("%d\n",ans);
return 0;
}
T2 watermelon
Description
给出⼀棵树,结点有点权,求所有树上简单路径的最长上升⼦序列的最⼤值,n≤105,a i≤109。

Solution
考虑DP,注意到⼀个简单路径可以被拆为向上的部分和向下的部分。

所以设f u,i表⽰u的⼦树中从u向下且第⼀项是i的 LIS 的最⼤长度,g u,i表⽰u的⼦树中u的某个⼦孙向上到u且最后⼀项是i的 LIS 的最⼤长度。

从u到⽗亲fa,转移考虑将a fa作为LIS的开头或结尾:
n
max
i=a fa+1f u,i+1→f fa,a
fa a fa−1
max
i=1g u,i+1→g fa,a
fa
于是可以对每个节点⽤线段树维护f和g,从u转移到fa只需要先将u的线段树⽤上⾯的转移⽅程进⾏修改,再直接合并到fa的线段树上即可。

统计答案时,考虑在合并u之前(此时fa的线段树维护的线段树维护的是只考虑从前⼏个⼉⼦上来的 LIS 时的f与g)更新答案,有
f u,i+max j<i
g fa,j→ans,g fa,i+max j<i g u,j→ans。

实际实现时,同时从u和fa的线段树向下⾛,每次⽤max f lson[fa]+max g rson[u]更新答案即可,这是经典的线段树合并套路。

Code
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
struct node{
int v,nxt;
}e[N<<1];
int cnt,first[N],pos[N],tot,buc[N],ans;
inline void add(int u,int v){e[++cnt].v=v;e[cnt].nxt=first[u];first[u]=cnt;}
namespace SGT{
const int M=N<<7;
#define mid ((l+r)>>1)
int rtf[N],rtg[N];
int ls[M],rs[M],mx[M],del[M],top,sum;
inline int newnode(){return top?del[top--]:++sum;}
inline void dele(int p){
ls[p]=rs[p]=mx[p]=0;
del[++top]=p;
}
inline int getmx(int p,int ql,int qr,int l=1,int r=tot){
if(ql<=l&&r<=qr) return mx[p];
if(!p) return 0;
int ans=0;
if(ql<=mid) ans=max(ans,getmx(ls[p],ql,qr,l,mid));
if(qr>mid) ans=max(ans,getmx(rs[p],ql,qr,mid+1,r));
return ans;
}
inline void update(int &p,int x,int v,int l=1,int r=tot){
if(!p) p=newnode();
if(l==r){mx[p]=max(mx[p],v);return ;}
if(x<=mid) update(ls[p],x,v,l,mid);
else update(rs[p],x,v,mid+1,r);
mx[p]=max(mx[ls[p]],mx[rs[p]]);
}
inline int merge(int p1,int p2,int l=1,int r=tot){
if(!p1||!p2) return p1+p2;
if(l==r){
mx[p1]=max(mx[p1],mx[p2]);
dele(p2);
return p1;
}
mx[p1]=max(mx[p1],mx[p2]);
ls[p1]=merge(ls[p1],ls[p2],l,mid);
rs[p1]=merge(rs[p1],rs[p2],mid+1,r);
dele(p2);
return p1;
}
inline void query(int p1,int p2,int l=1,int r=tot){
if(!p1||!p2) return ;
if(l==r) return ;
ans=max(ans,mx[ls[p1]]+mx[rs[p2]]);
query(ls[p1],ls[p2],l,mid);
query(rs[p1],rs[p2],mid+1,r);
}
#undef mid
}
using namespace SGT;
int n,a[N];
inline void work(int u,int f){
bool flag=0;
for(int i=first[u];i;i=e[i].nxt){
int v=e[i].v;
if(v==f) continue;
work(v,u);
if(flag) query(rtg[v],rtf[u]);
if(flag) query(rtg[u],rtf[v]);
update(rtg[v],pos[u],getmx(rtg[v],1,pos[u]-1)+1);
update(rtf[v],pos[u],getmx(rtf[v],pos[u]+1,tot)+1);
rtf[u]=merge(rtf[u],rtf[v]);
rtg[u]=merge(rtg[u],rtg[v]);
flag=1;
}
if(!flag) update(rtf[u],pos[u],1),update(rtg[u],pos[u],1);
ans=max(ans,mx[rtf[u]]);ans=max(ans,mx[rtg[u]]);
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;++i) scanf("%d",&a[i]),buc[i]=a[i];
sort(buc+1,buc+n+1);
tot=unique(buc+1,buc+n+1)-buc-1;
for(int i=1;i<=n;++i) pos[i]=lower_bound(buc+1,buc+tot+1,a[i])-buc;
for(int i=1,u,v;i<n;++i) scanf("%d%d",&u,&v),add(u,v),add(v,u);
work(1,0);
printf("%d\n",ans);
return 0;
}
T3 emiya
Description
有n个⼈和m种菜 , 第i个⼈对第j道菜的喜爱程度为a i,j, 如果a i,j=−1则表⽰不喜欢 .
现在你要选择⼀个菜的集合,你会获得喜欢集合中所有菜的⼈对这些菜的喜爱程度之和的权值,最⼤化这个权值,n≤20,m≤106,a i,j≤109。

Solution
考虑求出f S表⽰钦定S中的⼈喜欢所有的菜,不管其他⼈时,能获得的最⼤权值。

显然答案=max S f S。

考虑a i,j能为哪些S作出贡献,设t j为喜欢j的⼈的集合,那么会受到a i,j贡献的S应当满⾜S∈t j,i∈S。

考虑记g S表⽰对S的所有⼦集⼀起造成
的贡献,即f S=∑S∈T g T。

于是贡献就相当于g t
j +=a i,j,g t
j⊗2
i−=a i,j。

求出g后再求f,直接FWT或FMT求解即可。

Code
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=(1<<20)+20;
int n,m,a[21][N],s[N];
ll f[N];
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j){
scanf("%d",&a[i][j]);
if(a[i][j]!=-1) s[j]|=1<<i-1;
}
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j)
if(a[i][j]!=-1) f[s[j]]+=a[i][j],f[s[j]^(1<<i-1)]-=a[i][j];
for(int i=0;i<n;++i)
for(int j=0;j<(1<<n);++j) if(j&(1<<i)) f[j^(1<<i)]+=f[j];
ll ans=0;
for(int i=0;i<(1<<n);++i) ans=max(ans,f[i]);
printf("%lld\n",ans);
return 0;
}
T4 tree
Description
你需要实现两个函数encode,decode。

encode的功能是接收⼀个n个点的有根树,返回⼀个 128 位⼆进制数。

decode的功能是接收⼀个 128 位的⼆进制数,返回⼀个有根树。

题⽬会给你T颗有根树给encode,把你decode返回的⼆进制数给第⼆个函数。

你需要使得第⼆个函数返回的有根树和给你的有根树同构。

同构指的是将每个节点的⼉⼦以编号⼤⼩排序后,两棵树⽆标号有根同构。

n≤70,T≤105。

Solution
两棵树同构,其实就相当于,dfs遍历整个树时,每次从⽗亲⾛向⼉⼦在序列后添⼀个 0,从⼉⼦⾛向⽗亲在序列后添⼀个 1,两棵树得到的序列相同则这两棵树同构。

因此直接传递这个长为 2(n−1) 的序列就可以完成n=65 的情况。

注意到这个 01 序列如果把 0 看作左括号,1 看作右括号,那么原序列就变成了⼀个合法的括号序列。

⽽长为n的合法括号序列有C(n) 个,其中C是卡特兰数,⽽C(128)<2128,因此考虑求出每个序列是字典序第⼏⼩的合法括号序列,然后就能将每棵树映射为⼀个合法括号序列了。

考虑求出f i,j表⽰已经有了i个左括号,j个右括号且前半部分保证合法,接下来有多少种放括号的⽅案使得括号序列合法。

计算⼀个括号序列的字典序时,如果第i个位置是右括号,那么此前位置与序列相同,第i个位置是左括号的序列都⽐他⼩,这样的序列有f x+1,y个,其中x,y表⽰前i−1 个位置有x个左括号,y个右括号。

于是直接预处理出来每个括号长度对应的f,总复杂度为 O(n3+nT)。

Code
#include "tree.h"
#include<bits/stdc++.h>
using namespace std;
#define u128 unsigned __int128
const int N=75;
u128 f[N][N][N];
int vis[N];
inline void init(int n){
f[n][n][n]=1;
for(int sum=n<<1;sum>=1;--sum){
for(int i=min(sum,n);i>=0&&i>=sum-i;--i){
int j=sum-i;
if(!f[n][i][j]) continue;
if(i-1>=j&&i) f[n][i-1][j]+=f[n][i][j];
if(j) f[n][i][j-1]+=f[n][i][j];
}
}
}
vector<int> to[N];
int ans[N<<1],top;
inline void dfs(int u,int f){
for(int v:to[u]){
if(v==f) continue;
ans[++top]=0;dfs(v,u);
}
if(f) ans[++top]=1;
}
inline void write(u128 M){
if(M>=10) write(M/10);
putchar(M%10+'0');
}
u128 encode(int n,const int *p){
if(!vis[n-1]) vis[n-1]=1,init(n-1);
for(int i=1;i<=n;++i) to[i].clear();
for(int i=2;i<=n;++i) to[p[i]].push_back(i);
top=0;dfs(1,0);
u128 ret=0;
for(int i=1,a=0,b=0;i<=top;++i){
if(ans[i]==1) ret+=f[n-1][a+1][b],b++;
else a++;
}
return ret;
}
int dfn[N<<1];
void decode(int n,u128 M,int *p){
if(!vis[n-1]) vis[n-1]=1,init(n-1);
top=0;p[1]=0;
for(int i=1,a=0,b=0;i<=(n-1)<<1;++i){
if(M>=f[n-1][a+1][b]) M-=f[n-1][a+1][b],dfn[i]=1,b++; else dfn[i]=0,a++;
}
int now=1,cnt=1;
for(int i=1;i<=(n-1)<<1;++i){
if(!dfn[i]){
++cnt;
p[cnt]=now;now=cnt;
}else now=p[now];
}
}
Processing math: 100%。

相关文档
最新文档