THUSC2021 题解
T1 move
Description
给定一个长度为 \(n\) 的序列 \(a\) 和一个正整数 \(m\),现在按下述策略删除 \(a\) 中的数字:
选出一个下标序列 \(p\),使得其:
- 单调递增
- \(\sum_{x\in p}a_x \le m\)
- 元素个数最多
- 字典序是满足上述条件的序列中最大的
然后删除 \(a\) 中下标在 \(p\) 中的元素。问按上述策略进行几次删除会使 \(a\) 为空。
\(n\le 5\times 10^4,m\le 10^9\)。
Solution
如果没有第4条限制,那么只需要用 \(set\) 维护所有数字,然后贪心选择最小的元素即可。如果有第4条限制,通过这样方法得到的元素个数也一定是最多的,因此一定选出的下标序列大小已经确定。
考虑从前往后确定该下标序列中的元素,对于当前位置,考虑二分,那么元素 \(id\) 能放在当前位置,当且仅当下标 \(\ge id\) 且还存在的元素中,最小的 \(k\) 个元素之和 \(\le m\)。 找到 \(id\) 之后,将 \(id\) 删掉,继续二分下一个元素。因此需要支持单点修改,考虑使用树套树维护这个东西,外层用树状数组或线段树维护下标区间,内层用权值线段树维护当前区间的元素的权值。
单点修改时,在 \(\log\) 个区间内同时修改。查询时,将询问拆为 \(\log\) 个区间,然后将这 \(\log\) 个区间的权值线段树合并进行查询。当然事实上你不需要合并,只需要同时维护 \(\log\) 个根节点,正常权值线段树找左子树大小时,就将这 \(\log\) 个根的左子树大小加起来即可。这是树套树的一种常见套路,全世界大概只有我一个不会。
总复杂度为 \(\mathcal O(n\log^3 n)\)。
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\le 10^5,a_i\le 10^9\)。
Solution
考虑 \(DP\),注意到一个简单路径可以被拆为向上的部分和向下的部分。所以设 \(f_{u,i}\) 表示 \(u\) 的子树中从 \(u\) 向下且第一项是 \(i\) 的 LIS 的最大长度,\(g_{u,i}\) 表示 \(u\) 的子树中 \(u\) 的某个子孙向上到 \(u\) 且最后一项是 \(i\) 的 LIS 的最大长度。
从 \(u\) 到父亲 \(fa\) ,转移考虑将 \(a_{fa}\) 作为 \(LIS\) 的开头或结尾:
于是可以对每个节点用线段树维护 \(f\) 和 \(g\),从 \(u\) 转移到 \(fa\) 只需要先将 \(u\) 的线段树用上面的转移方程进行修改,再直接合并到 \(fa\) 的线段树上即可。统计答案时,考虑在合并 \(u\) 之前(此时 \(fa\) 的线段树维护的线段树维护的是只考虑从前几个儿子上来的 LIS 时的 \(f\) 与 \(g\) )更新答案,有 \(f_{u,i}+\max_{j<i} g_{fa,j}\rightarrow ans\),\(g_{fa,i}+\max_{j<i} g_{u,j}\rightarrow 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\le 20,m\le 10^6,a_{i,j}\le 10^9\)。
Solution
考虑求出 \(f_S\) 表示钦定 \(S\) 中的人喜欢所有的菜,不管其他人时,能获得的最大权值。显然答案 \(=\max_S f_S\)。
考虑 \(a_{i,j}\) 能为哪些 \(S\) 作出贡献,设 \(t_j\) 为喜欢 \(j\) 的人的集合,那么会受到 \(a_{i,j}\) 贡献的 \(S\) 应当满足 \(S\in t_j,i\in S\)。考虑记 \(g_S\) 表示对 \(S\) 的所有子集一起造成的贡献,即 \(f_S=\sum_{S\in T}g_{T}\)。于是贡献就相当于 \(g_{t_j}+=a_{i,j},g_{t_j\otimes 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\le 70,T\le 10^5\)。
Solution
两棵树同构,其实就相当于,\(dfs\) 遍历整个树时,每次从父亲走向儿子在序列后添一个 \(0\),从儿子走向父亲在序列后添一个 \(1\),两棵树得到的序列相同则这两棵树同构。因此直接传递这个长为 \(2(n-1)\) 的序列就可以完成 \(n=65\) 的情况。
注意到这个 \(01\) 序列如果把 \(0\) 看作左括号,\(1\) 看作右括号,那么原序列就变成了一个合法的括号序列。而长为 \(n\) 的合法括号序列有 \(C(n)\) 个,其中 \(C\) 是卡特兰数,而 \(C(128)<2^{128}\),因此考虑求出每个序列是字典序第几小的合法括号序列,然后就能将每棵树映射为一个合法括号序列了。
考虑求出 \(f_{i,j}\) 表示已经有了 \(i\) 个左括号,\(j\) 个右括号且前半部分保证合法,接下来有多少种放括号的方案使得括号序列合法。计算一个括号序列的字典序时,如果第 \(i\) 个位置是右括号,那么此前位置与序列相同,第 \(i\) 个位置是左括号的序列都比他小,这样的序列有 \(f_{x+1,y}\) 个,其中 \(x,y\) 表示前 \(i-1\) 个位置有 \(x\) 个左括号,\(y\) 个右括号。
于是直接预处理出来每个括号长度对应的 \(f\),总复杂度为 \(\mathcal O(n^3+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];
}
}