thusc2021题解
T1 move
description
给定一个长度为 \(n\) 的序列 \(a\) 和一个正整数 \(m\) ,现在按下述策略删除 \(a\) 中的数字:
选出一个下标序列 \(p\) ,使得其:
- 单调递增
- \(\sum_\limits{x\in p}a_x\le m\)
- 元素个数最多
- 字典序是满足上述条件的序列中最大的
然后删除 \(a\) 中下标在 \(p\) 中的元素。问按上述策略进行几次删除会使 \(a\) 为空。
solution
首先,每次删除前我们可以通过贪心轻松地确定出每次操作需要删除多少个数,设为\(k\)。
为了满足字典序最大这一毒瘤条件,我们可以依次确定这\(k\) 个数的下标。具体地,如果我们已经确定了前\(t-1\) 个的位置(\(p_1,p_2,\cdots,p_{t-1}\)),现在要确定第\(t\) 个的位置。这就相当于选择一个尽量靠后下标\(x\) 满足下标在\([x,n]\) 这些数的最小的\(k-t+1\) 个数的和不超过\(m-\sum_\limits{i=1}^{t-1}a_{p_i}\) 。这显然可以二分答案,判断的话就相当于要有一个支持单点修改,查询某个后缀最小的\(p\) 个数的和的数据结构。
不难想到使用树状数组套权值线段树实现。树状数组维护后缀,每次修改时在其上\(\log n\) 个节点的权值线段树上修改,查询时则先找到后缀所对应的\(\log n\) 个区间,然后一起在权值线段树上进行查询,二者的复杂度均为\(\mathcal O(\log^2n)\) 。
总时间复杂度\(\mathcal O(n\log^3n)\) 。
code
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=50005,Log=16;
const int inf=2e9;
int qrt[N],nc,tot,tmp[N];
int rt[N],n,a[N],m;
inline void lsh()
{
for(int i=1;i<=n;++i)tmp[++tot]=a[i];
tmp[++tot]=inf;
sort(tmp+1,tmp+tot+1);
tot=unique(tmp+1,tmp+tot+1)-tmp-1;
for(int i=1;i<=n;++i)
a[i]=lower_bound(tmp+1,tmp+tot+1,a[i])-tmp;
}
namespace SGT
{
const int M=N<<8;
int cnt,ls[M],rs[M],sz[M];ll s[M];
inline void up(int rt)
{
sz[rt]=sz[ls[rt]]+sz[rs[rt]];
s[rt]=s[ls[rt]]+s[rs[rt]];
}
void update(int&rt,int l,int r,int p,int t,int v)
{
if(!rt)rt=++cnt;
if(l==r){sz[rt]+=t,s[rt]+=v*t;return;}
int mid=(l+r)>>1;
if(p<=mid)update(ls[rt],l,mid,p,t,v);
else update(rs[rt],mid+1,r,p,t,v);
return up(rt);
}
ll query(int l,int r,int t)
{
if(l==r)return 1ll*t*tmp[l];
int siz=0;ll sum=0;
for(int i=1;i<=nc;++i)
siz+=sz[ls[qrt[i]]],sum+=s[ls[qrt[i]]];
int mid=(l+r)>>1;
if(t<=siz)
{
for(int i=1;i<=nc;++i)qrt[i]=ls[qrt[i]];
return query(l,mid,t);
}
for(int i=1;i<=nc;++i)qrt[i]=rs[qrt[i]];
return sum+query(mid+1,r,t-siz);
}
}
using namespace SGT;
namespace Fenwick
{
inline int lb(int x){return x&(-x);}
inline void add(int p)
{
for(int x=p;x;x-=lb(x))
update(rt[x],1,tot,a[p],1,tmp[a[p]]);
}
inline void del(int p)
{
for(int x=p;x;x-=lb(x))
{
update(rt[x],1,tot,a[p],-1,tmp[a[p]]);
update(rt[x],1,tot,tot,1,inf);
}
}
inline ll ask(int p,int k)
{
nc=0;
for(int x=p;x<=n;x+=lb(x))qrt[++nc]=rt[x];
return query(1,tot,k);
}
}
using namespace Fenwick;
multiset<int>sq;
inline int solve(int l,int r,int num)
{
++r;
while(l+1<r)
{
int mid=(l+r)>>1;
ask(mid,num)<=m?l=mid:r=mid;
}
return l;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i)
scanf("%d",a+i),sq.insert(a[i]);
lsh();
for(int i=1;i<=n;++i)add(i);
int ans=0;
while(1)
{
int now=0;ll ss=0;
for(auto it=sq.begin();it!=sq.end();it++)
{
ss+=(*it);++now;
if(ss>m){--now;break;}
}
++ans;if(now>=sq.size())break;
int pre=0,mm=m;
for(int i=now;i;--i)
{
pre=solve(pre+1,n-i+1,i);
sq.erase(sq.find(tmp[a[pre]]));del(pre);
m-=tmp[a[pre]],a[pre]=tot;
}
m=mm;
}
printf("%d\n",ans);
return 0;
}
T2 watermelon
给出一棵树,结点有点权,求所有树上简单路径的最长上升子序列的最大值,\(1≤n≤10^5,1≤a_i≤10^9\)。
solution
将\(a_i\) 离散化。
首先有暴力\(dp\) ,记\(up_{u,i}\) 表示从\(u\) 的子树中某个节点到\(u\) 的路径上上升子序列最后一个数为\(i\) 时的最长长度,\(dn_{u,i}\) 表示从\(u\) 到其子树中的某个节点的路径上上升子序列第一个数为\(i\) 时的最长长度。那么我们有转移(其中用\(v\) 表示\(u\) 的儿子):
而答案不仅需要和\(up,dn\) 取\(\max\) ,而且需要当\(u\) 新合并进一个子树\(v\) 时更新:
稍微精细实现一下可以做到\(\mathcal O(n^2)\) 。
考虑优化。在每个节点使用线段树,每个叶子节点维护对应的\(up,dn\) 。而转移可以直接通过单点修改区间查询实现,合并更新答案时则可以线段树合并。
时间复杂度\(\mathcal O(n\log n)\) 。
code
#include<bits/stdc++.h>
using namespace std;const int N=1e5+5;
int ans,n,a[N],cnt,tmp[N],rt[N];vector<int>e[N];
inline void add(int x,int y){e[x].push_back(y);e[y].push_back(x);}
namespace SGT
{
struct val
{
int up,dn;
inline void upd(const val&x){up=max(up,x.up),dn=max(dn,x.dn);}
}t[N<<7];int ls[N<<7],rs[N<<7],tot;
void update(int&rt,int l,int r,int p,const val&now)
{
if(!rt)rt=++tot;t[rt].upd(now);
if(l==r)return;int mid=(l+r)>>1;
p<=mid?update(ls[rt],l,mid,p,now):update(rs[rt],mid+1,r,p,now);
}
val query(int rt,int l,int r,int ql,int qr)
{
if(!rt||(ql<=l&&r<=qr))return t[rt];
int mid=(l+r)>>1;val res={0,0};
if(ql<=mid)res.upd(query(ls[rt],l,mid,ql,qr));
if(mid<qr)res.upd(query(rs[rt],mid+1,r,ql,qr));
return res;
}
int merge(int x,int y)
{
if(!x||!y)return x|y;t[x].upd(t[y]);
ans=max({ans,t[ls[x]].up+t[rs[y]].dn,t[ls[y]].up+t[rs[x]].dn});
ls[x]=merge(ls[x],ls[y]);rs[x]=merge(rs[x],rs[y]);return x;
}
}
using namespace SGT;
void dfs(int u,int f)
{
update(rt[u],1,cnt,a[u],{1,1});
for(int v:e[u])
{
if(v==f)continue;dfs(v,u);
rt[u]=merge(rt[u],rt[v]);
int up=query(rt[v],1,cnt,1,a[u]-1).up,dn=query(rt[v],1,cnt,a[u]+1,cnt).dn;
update(rt[u],1,cnt,a[u],{up+1,dn+1});ans=max({ans,up+1,dn+1});
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;++i)scanf("%d",a+i),tmp[i]=a[i];
sort(tmp+1,tmp+n+1);cnt=unique(tmp+1,tmp+n+1)-tmp-1;
for(int i=1;i<=n;++i)a[i]=lower_bound(tmp+1,tmp+cnt+1,a[i])-tmp;
for(int i=1,u,v;i<n;++i)scanf("%d%d",&u,&v),add(u,v);
dfs(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\)
solution
考虑朴素的暴力。我们可以枚举会带来权值的人的集合\(S\) ,同时计算其贡献\(f(S)\) 。我们要求的相当于是\(\max f(S)\) 。
直接计算显然是复杂度爆炸的,因此我们考虑间接地求出\(f\) 。我们构造函数\(g\) 满足
求出\(g\) 是出人意料地容易的。我们对于每一个不等于\(-1\) 的\(a_{i,j}\),假设喜欢第\(j\) 道菜的人的集合为\(t_j\) ,那么
这里的\(\leftarrow\) 等价于+=
.
我们考虑证明这样做的正确性。对于每一道菜\(j\) 而言:
倘若\(S\not\subset t_j\) ,那么根据先前的式子,\(f(S)\) 从这道菜中无法得到任何贡献。
否则,那么\(f(S)\) 一定会先获得非\(-1\) 的\(a_{i,j}\) 的贡献(\(g(t_j)\))。然后,如果某个\(i\notin S\) ,则\(S\subset t_j\setminus\{i\}\) ,即会减去\(a_{i,j}\) 的贡献。
由上可知这样做的正确性。
code
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=22,M=(1<<20)+5;
int n,m,a[N][M],t[M];ll f[M];
int main()
{
scanf("%d%d",&n,&m);
for(int i=0;i<n;++i)
for(int j=1;j<=m;++j)
{
scanf("%d",a[i]+j);
if(a[i][j]>=0)t[j]+=1<<i;
}
for(int i=0;i<n;++i)
for(int j=1;j<=m;++j)
if((1<<i)&t[j])
f[t[j]]+=a[i][j],f[t[j]^(1<<i)]-=a[i][j];
for(int i=0;i<n;++i)
for(int j=0;j<(1<<n);++j)
if((1<<i)&j)f[j^(1<<i)]+=f[j];
printf("%lld\n",*max_element(f,f+(1<<n)));
return 0;
}
T4 planttree
description
本题是一道通信题。
你需要实现两个函数encode
和decode
,encode
函数接受一棵树的读入,并将一个长度为\(128\) 的\(01\) 串传输给decode
函数,要求还原出这棵树。
还原出的树和原树形态相同即可,不要求点的标号也相同。
\(n\le 70,T\le 10^6\)
solution
首先这里有暴力做法:从\(1\) 开始对根进行\(dfs\) ,如果向儿子走,则标记\(0\) ,如果向父亲走,则标记\(1\) 。最后将标记的树顺次接起来得到长度为\(2n-2\) 的\(01\) 串,可以通过\(n\le 65\) 的数据。
但其实可以发现有些\(01\) 串是根本不肯能出现的。具体观察一下,对于这个串的任意一个前缀,\(0\) 的 个数一定不小于\(1\) 的个数。我们可以将\(0\) 看作\((\) ,\(1\) 看作\()\) ,则这个串一定是合法的括号序列。
根据经典结论,长为\(2n\) 的合法括号序列有\(C_n\) 个(卡特兰数)。注意到\(C_{69}<2^{128}\) ,即我们现在应该构造一种括号序列和\([0,2^{128})\) 中的数的一一映射方式。注意到这个括号序列的字典序就是一种合适的选择。
编码过程即计算有多少括号序列字典序比当前的小。对于每一个\()\) 的位置,我们可以通过组合数(类似卡特兰数的组合数)计算出如果这个位置填\((\) 有多少合法括号序列,累加到答案中,最后传输过去。
解码也是类似的。计算如果当前位置填\((\) 有多少括号序列,如果这个数不超过我们所需,则这一位填\()\) ,否则填\((\) 。这样就完成了。
code
#include "tree.h"
#include<bits/stdc++.h>
using namespace std;
typedef unsigned __int128 Uint;
const int N=75;
vector<bool>res,ret;vector<int>e[N];
bool flag=0;Uint C[N<<1][N<<1];
inline void pre()
{
int n=140;
C[0][0]=1;
for(int i=1;i<=n;++i)
{
C[i][0]=1;
for(int j=1;j<=i;++j)
C[i][j]=C[i-1][j]+C[i-1][j-1];
}
}
void dfs(int u)
{
for(int v:e[u])
res.push_back(0),dfs(v);
if(u>1)res.push_back(1);
}
inline Uint calc(int n,int x,int y)
{
int p=n-x,q=n-y;
return C[p+q][p]-C[p+q][q+1];
}
unsigned __int128 encode(int n,const int*fa)
{
if(n<=2)return 0;
if(!flag)pre(),flag=1;
for(int i=1;i<=n;++i)e[i].clear();
for(int i=2;i<=n;++i)e[fa[i]].push_back(i);
res.clear();dfs(1);
Uint ans=0;int ct0=0,ct1=0;
for(int i=0;i<res.size();++i)
{
if(res[i])++ct1;
else{++ct0;continue;}
ans+=calc(n-1,ct0+1,ct1-1);
}
return ans;
}
int tot;
void gans(int u,int p,int*fa)
{
if(p>=ret.size())return;
if(!ret[p])
{
int nd=++tot;
fa[nd]=u;gans(nd,p+1,fa);
}
else gans(fa[u],p+1,fa);
}
void decode(int n,unsigned __int128 M, int*fa)
{
if(n==1){fa[1]=0;return;}
if(n==2){fa[1]=0,fa[2]=1;return;}
if(!flag)pre(),flag=1;
ret.clear();int ct0=0,ct1=0;
for(int i=1;i<=2*n-2;++i)
{
Uint now=calc(n-1,ct0+1,ct1);
if(M<now)ret.push_back(0),++ct0;
else M-=now,ret.push_back(1),++ct1;
}
tot=1;gans(1,0,fa);fa[1]=0;
}